SkateSpots/src/components/ReportDialog.tsx
shakespeare.diy e29d35f1e4 fix: restore full skate-spots source tree from misplaced src/src/ copy
The entire skate-spots codebase was accidentally copied into src/src/ and
public/public/ instead of src/ and public/ directly. This left the real
src/ directory as blank MKStack template stubs, explaining why the landing
page showed the default template instead of SkateSpots.

Changes made:
- Copied all files from src/src/ up to src/ (App.tsx, AppRouter.tsx, main.tsx,
  all pages, components, hooks, contexts, lib, i18n, test)
- Copied all files from public/public/ up to public/ (images, manifest, etc.)
- Deleted src/src/ and public/public/ nested directories
- Removed HelloWorld.tsx template stub from pages/
- Retained the previously fixed index.css (Tailwind v3 directives)

Full skate-spots feature set is now active:
- All 30+ routes (map, events, spots, coaches, marketplace, wallet, etc.)
- NWCProvider, LanguageProvider, PWAInstallPrompt, AmberCallbackHandler
- Skate community relays (goskatespots.nostr1.com, wotr.relatr.xyz, etc.)
- Outfit Variable font, Amber pre-check in main.tsx
- All hooks (useSkateSpots, useCoaches, useZaps, etc.)
- i18n support (en/es)

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-15 22:18:28 +07:00

314 lines
9.6 KiB
TypeScript

import { useState } from 'react';
import { useNostrPublish } from '@/hooks/useNostrPublish';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useToast } from '@/hooks/useToast';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { Loader2, Flag, AlertTriangle, CheckCircle2 } from 'lucide-react';
import { cn } from '@/lib/utils';
// NIP-56 Report types with skate-specific labels
const REPORT_REASONS = [
{
id: 'spam',
label: 'Fake/Non-existent spot',
description: 'This spot does not exist or location is wrong',
icon: '📍',
},
{
id: 'other',
label: 'Stolen/Fake photos',
description: 'Photos are stock images or stolen from elsewhere',
icon: '📸',
},
{
id: 'spam',
label: 'Spam or advertising',
description: 'Promotional content or duplicate listings',
icon: '🗑️',
subtype: 'spam-ad',
},
{
id: 'illegal',
label: 'Dangerous location',
description: 'Private property, unsafe, or illegal to access',
icon: '⚠️',
},
{
id: 'nudity',
label: 'Inappropriate content',
description: 'Contains offensive or adult content',
icon: '🔞',
},
{
id: 'impersonation',
label: 'Impersonation',
description: 'Someone pretending to be someone else',
icon: '👤',
},
{
id: 'other',
label: 'Other',
description: 'Something else not listed above',
icon: '📝',
subtype: 'other-general',
},
] as const;
interface ReportDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
// What we're reporting
targetType: 'spot' | 'event' | 'tour' | 'shop' | 'user' | 'listing' | 'coach' | 'filmer';
targetPubkey: string;
targetEventId?: string;
targetTitle?: string;
// Callback after report
onReported?: () => void;
}
export function ReportDialog({
open,
onOpenChange,
targetType,
targetPubkey,
targetEventId,
targetTitle,
onReported,
}: ReportDialogProps) {
const { user } = useCurrentUser();
const { mutateAsync: publishEvent, isPending } = useNostrPublish();
const { toast } = useToast();
const [selectedReason, setSelectedReason] = useState<string>('');
const [additionalDetails, setAdditionalDetails] = useState('');
const [step, setStep] = useState<'form' | 'success'>('form');
const handleSubmit = async () => {
if (!user || !selectedReason) return;
const reason = REPORT_REASONS.find(
(r) => r.id === selectedReason || r.subtype === selectedReason
);
if (!reason) return;
try {
// Build NIP-56 report tags
const tags: string[][] = [];
// Always include the pubkey being reported
if (targetEventId) {
// Reporting an event (spot, tour, etc.)
tags.push(['e', targetEventId, reason.id]);
tags.push(['p', targetPubkey]);
} else {
// Reporting just a user/profile
tags.push(['p', targetPubkey, reason.id]);
}
// Build content with context
let content = '';
if (additionalDetails.trim()) {
content = additionalDetails.trim();
}
if (targetTitle) {
content = content
? `${content}\n\nReported: ${targetTitle} (${targetType})`
: `Reported: ${targetTitle} (${targetType})`;
}
await publishEvent({
kind: 1984, // NIP-56 Report
content,
tags,
});
setStep('success');
onReported?.();
toast({
title: 'Report submitted',
description: 'Thank you for helping keep SkateSpots safe.',
});
} catch (error) {
toast({
title: 'Failed to submit report',
description: error instanceof Error ? error.message : 'Please try again',
variant: 'destructive',
});
}
};
const handleClose = () => {
onOpenChange(false);
// Reset form after animation
setTimeout(() => {
setSelectedReason('');
setAdditionalDetails('');
setStep('form');
}, 200);
};
const getReasonKey = (reason: typeof REPORT_REASONS[number]) => {
return reason.subtype || reason.id;
};
if (!user) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Flag className="h-5 w-5 text-red-500" />
Report Content
</DialogTitle>
</DialogHeader>
<div className="text-center py-6">
<AlertTriangle className="h-12 w-12 mx-auto mb-4 text-yellow-500" />
<p className="text-muted-foreground">
You need to be logged in to report content.
</p>
</div>
</DialogContent>
</Dialog>
);
}
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Flag className="h-5 w-5 text-red-500" />
Report Content
</DialogTitle>
{step === 'form' && (
<DialogDescription>
Help keep the community safe by reporting inappropriate content.
</DialogDescription>
)}
</DialogHeader>
{step === 'form' ? (
<div className="space-y-4">
{/* Target info */}
{targetTitle && (
<div className="p-3 rounded-lg bg-muted/50 border">
<p className="text-xs text-muted-foreground uppercase mb-1">
Reporting {targetType}
</p>
<p className="font-medium text-sm truncate">{targetTitle}</p>
</div>
)}
{/* Reason selection */}
<div className="space-y-2">
<Label>Why are you reporting this?</Label>
<RadioGroup
value={selectedReason}
onValueChange={setSelectedReason}
className="space-y-2"
>
{REPORT_REASONS.map((reason) => {
const key = getReasonKey(reason);
return (
<div
key={key}
className={cn(
'flex items-start space-x-3 p-3 rounded-lg border transition-colors cursor-pointer',
selectedReason === key
? 'border-red-500/50 bg-red-500/10'
: 'border-border hover:bg-muted/50'
)}
onClick={() => setSelectedReason(key)}
>
<RadioGroupItem value={key} id={key} className="mt-0.5" />
<div className="flex-1 min-w-0">
<Label
htmlFor={key}
className="flex items-center gap-2 cursor-pointer font-medium"
>
<span>{reason.icon}</span>
{reason.label}
</Label>
<p className="text-xs text-muted-foreground mt-0.5">
{reason.description}
</p>
</div>
</div>
);
})}
</RadioGroup>
</div>
{/* Additional details */}
<div className="space-y-2">
<Label htmlFor="details">Additional details (optional)</Label>
<Textarea
id="details"
placeholder="Provide any additional context that might help..."
value={additionalDetails}
onChange={(e) => setAdditionalDetails(e.target.value)}
rows={3}
className="resize-none"
/>
</div>
{/* Submit */}
<div className="flex gap-2 pt-2">
<Button
variant="outline"
onClick={handleClose}
className="flex-1"
disabled={isPending}
>
Cancel
</Button>
<Button
onClick={handleSubmit}
disabled={!selectedReason || isPending}
className="flex-1 bg-red-500 hover:bg-red-600"
>
{isPending ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Submitting...
</>
) : (
<>
<Flag className="h-4 w-4 mr-2" />
Submit Report
</>
)}
</Button>
</div>
</div>
) : (
/* Success state */
<div className="text-center py-6">
<div className="w-16 h-16 rounded-full bg-green-500/20 flex items-center justify-center mx-auto mb-4">
<CheckCircle2 className="h-8 w-8 text-green-500" />
</div>
<h3 className="font-bold text-lg mb-2">Report Submitted</h3>
<p className="text-muted-foreground text-sm mb-6">
Thanks for helping keep SkateSpots safe and accurate. Your report
will be reviewed by trusted community members.
</p>
<Button onClick={handleClose} className="w-full">
Done
</Button>
</div>
)}
</DialogContent>
</Dialog>
);
}