SkateSpots/src/components/AddPhotoToSpot.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

234 lines
7.6 KiB
TypeScript

import { useState } from 'react';
import { Camera, Upload, Loader2, X } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { Card, CardContent } from '@/components/ui/card';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useNostrPublish } from '@/hooks/useNostrPublish';
import { useUploadFile } from '@/hooks/useUploadFile';
import { useToast } from '@/hooks/useToast';
import { useQueryClient } from '@tanstack/react-query';
import { PHOTO_CONTRIBUTION_KIND } from '@/hooks/useSpotContributions';
import { SKATE_SPOT_KIND } from '@/lib/types';
import { LoginArea } from '@/components/auth/LoginArea';
interface AddPhotoToSpotProps {
spotPubkey: string;
spotIdentifier: string;
spotTitle: string;
}
export function AddPhotoToSpot({ spotPubkey, spotIdentifier, spotTitle }: AddPhotoToSpotProps) {
const { user } = useCurrentUser();
const { mutateAsync: publishEvent, isPending: isPublishing } = useNostrPublish();
const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();
const { toast } = useToast();
const queryClient = useQueryClient();
const [open, setOpen] = useState(false);
const [caption, setCaption] = useState('');
const [uploadedImage, setUploadedImage] = useState<{ url: string; mimeType?: string } | null>(null);
const spotRef = `${SKATE_SPOT_KIND}:${spotPubkey}:${spotIdentifier}`;
const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (!files || files.length === 0) return;
try {
const file = files[0];
const tags = await uploadFile(file);
const url = tags.find(([name]) => name === 'url')?.[1];
const mimeType = tags.find(([name]) => name === 'm')?.[1];
if (url) {
setUploadedImage({ url, mimeType });
toast({
title: 'Photo uploaded',
description: 'Add a caption and submit to contribute!',
});
}
} catch (error) {
toast({
title: 'Upload failed',
description: error instanceof Error ? error.message : 'Failed to upload photo',
variant: 'destructive',
});
}
e.target.value = '';
};
const handleSubmit = async () => {
if (!uploadedImage) {
toast({
title: 'No photo',
description: 'Please upload a photo first.',
variant: 'destructive',
});
return;
}
try {
// Create kind 1063 file metadata event
const eventTags: string[][] = [
['url', uploadedImage.url],
['a', spotRef],
['alt', `Community photo for ${spotTitle}`],
];
if (uploadedImage.mimeType) {
eventTags.push(['m', uploadedImage.mimeType]);
}
await publishEvent({
kind: PHOTO_CONTRIBUTION_KIND,
content: caption.trim(),
tags: eventTags,
});
toast({
title: 'Photo contributed!',
description: 'Your photo has been added to this spot.',
});
// Reset and close
setCaption('');
setUploadedImage(null);
setOpen(false);
// Invalidate the photos query
queryClient.invalidateQueries({ queryKey: ['spot-photos', spotPubkey, spotIdentifier] });
} catch (error) {
toast({
title: 'Failed to contribute',
description: error instanceof Error ? error.message : 'Something went wrong',
variant: 'destructive',
});
}
};
const handleClose = () => {
setOpen(false);
setCaption('');
setUploadedImage(null);
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="outline" size="sm" className="gap-2">
<Camera className="h-4 w-4" />
Add Photo
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Camera className="h-5 w-5 text-[#ff0040]" />
Add Photo to Spot
</DialogTitle>
<DialogDescription>
Contribute a photo to help others discover this spot.
</DialogDescription>
</DialogHeader>
{!user ? (
<div className="py-6 text-center">
<p className="text-muted-foreground mb-4">Log in to contribute photos</p>
<LoginArea className="justify-center" />
</div>
) : (
<div className="space-y-4">
{/* Photo upload area */}
{uploadedImage ? (
<div className="relative">
<img
src={uploadedImage.url}
alt="Uploaded"
className="w-full aspect-video object-cover rounded-lg"
/>
<Button
variant="destructive"
size="icon"
className="absolute top-2 right-2 h-8 w-8"
onClick={() => setUploadedImage(null)}
>
<X className="h-4 w-4" />
</Button>
</div>
) : (
<Card className="border-dashed">
<CardContent className="py-8">
<label className="flex flex-col items-center cursor-pointer">
<input
type="file"
accept="image/*"
className="hidden"
onChange={handleImageUpload}
disabled={isUploading}
/>
{isUploading ? (
<>
<Loader2 className="h-10 w-10 animate-spin text-muted-foreground mb-2" />
<span className="text-sm text-muted-foreground">Uploading...</span>
</>
) : (
<>
<Upload className="h-10 w-10 text-muted-foreground mb-2" />
<span className="text-sm text-muted-foreground">Click to upload a photo</span>
<span className="text-xs text-muted-foreground mt-1">JPG, PNG, WebP</span>
</>
)}
</label>
</CardContent>
</Card>
)}
{/* Caption */}
<div className="space-y-2">
<Textarea
placeholder="Add a caption (optional) - describe what's in the photo, when it was taken, etc."
value={caption}
onChange={(e) => setCaption(e.target.value)}
rows={3}
/>
</div>
{/* Actions */}
<div className="flex gap-2 justify-end">
<Button variant="outline" onClick={handleClose}>
Cancel
</Button>
<Button
onClick={handleSubmit}
disabled={!uploadedImage || isPublishing}
className="bg-[#ff0040] hover:bg-[#ff0040]/90"
>
{isPublishing ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Contributing...
</>
) : (
<>
<Camera className="h-4 w-4 mr-2" />
Contribute Photo
</>
)}
</Button>
</div>
</div>
)}
</DialogContent>
</Dialog>
);
}