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

267 lines
10 KiB
TypeScript

import { useState } from 'react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Skeleton } from '@/components/ui/skeleton';
import { useToast } from '@/hooks/useToast';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import {
useMySpotCollections,
useCreateSpotCollection,
useAddSpotToCollection,
} from '@/hooks/useSpotCollections';
import { SKATE_SPOT_KIND, type SkateSpot, type SpotCollection } from '@/lib/types';
import { FolderPlus, Plus, Check, Loader2, FolderOpen, MapPin } from 'lucide-react';
import { cn } from '@/lib/utils';
interface AddToCollectionDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
spot: SkateSpot;
}
export function AddToCollectionDialog({ open, onOpenChange, spot }: AddToCollectionDialogProps) {
const { user } = useCurrentUser();
const { data: collections, isLoading } = useMySpotCollections();
const { createCollection, isPending: isCreating } = useCreateSpotCollection();
const { addSpotToCollection, isPending: isAdding } = useAddSpotToCollection();
const { toast } = useToast();
const [showNewForm, setShowNewForm] = useState(false);
const [newTitle, setNewTitle] = useState('');
const [newDescription, setNewDescription] = useState('');
const [addingToId, setAddingToId] = useState<string | null>(null);
const spotRef = `${SKATE_SPOT_KIND}:${spot.pubkey}:${spot.identifier}`;
const handleAddToCollection = async (collection: SpotCollection) => {
if (collection.spotRefs.includes(spotRef)) {
toast({
title: 'Already saved',
description: `"${spot.title}" is already in "${collection.title}"`,
});
return;
}
setAddingToId(collection.id);
try {
await addSpotToCollection({
collection,
spotPubkey: spot.pubkey,
spotIdentifier: spot.identifier,
});
toast({
title: 'Spot saved!',
description: `Added "${spot.title}" to "${collection.title}"`,
});
onOpenChange(false);
} catch (error) {
toast({
title: 'Failed to save',
description: error instanceof Error ? error.message : 'Something went wrong',
variant: 'destructive',
});
} finally {
setAddingToId(null);
}
};
const handleCreateCollection = async (e: React.FormEvent) => {
e.preventDefault();
if (!newTitle.trim()) return;
try {
await createCollection({
title: newTitle.trim(),
description: newDescription.trim() || undefined,
spotRefs: [spotRef], // Add the current spot to the new collection
});
toast({
title: 'Collection created!',
description: `"${spot.title}" added to your new collection "${newTitle}"`,
});
setNewTitle('');
setNewDescription('');
setShowNewForm(false);
onOpenChange(false);
} catch (error) {
toast({
title: 'Failed to create collection',
description: error instanceof Error ? error.message : 'Something went wrong',
variant: 'destructive',
});
}
};
if (!user) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="bg-black border-2 border-[#ff0040]/50">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-white">
<FolderPlus className="h-5 w-5 text-[#ccff00]" />
Save to Collection
</DialogTitle>
</DialogHeader>
<div className="py-8 text-center">
<p className="text-gray-400">Please log in to save spots to collections</p>
</div>
</DialogContent>
</Dialog>
);
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="bg-black border-2 border-[#ff0040]/50 mx-4 max-w-[calc(100vw-2rem)] sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-white">
<FolderPlus className="h-5 w-5 text-[#ccff00]" />
Save to Collection
</DialogTitle>
<DialogDescription className="text-gray-400 text-sm">
Save "{spot.title}" to one of your collections
</DialogDescription>
</DialogHeader>
<div className="space-y-4 mt-4">
{/* Existing Collections */}
{isLoading ? (
<div className="space-y-2">
{Array.from({ length: 3 }).map((_, i) => (
<Skeleton key={i} className="h-16 w-full" />
))}
</div>
) : collections && collections.length > 0 ? (
<div className="space-y-2 max-h-48 sm:max-h-64 overflow-y-auto pr-1 sm:pr-2 -mr-1 sm:-mr-2">
{collections.map((collection) => {
const isInCollection = collection.spotRefs.includes(spotRef);
const isAddingThis = addingToId === collection.id;
return (
<button
key={collection.id}
onClick={() => handleAddToCollection(collection)}
disabled={isAdding || isInCollection}
className={cn(
'w-full p-2.5 sm:p-3 rounded-lg border-2 transition-all text-left flex items-center gap-2.5 sm:gap-3 touch-manipulation active:scale-[0.98]',
isInCollection
? 'border-[#ccff00]/50 bg-[#ccff00]/10 cursor-default'
: 'border-gray-700 hover:border-[#ccff00]/50 hover:bg-[#ccff00]/5'
)}
>
<div className={cn(
'w-9 h-9 sm:w-10 sm:h-10 rounded-lg flex items-center justify-center flex-shrink-0',
isInCollection ? 'bg-[#ccff00]/20' : 'bg-gray-800'
)}>
{isAddingThis ? (
<Loader2 className="h-4 w-4 sm:h-5 sm:w-5 animate-spin text-[#ccff00]" />
) : isInCollection ? (
<Check className="h-4 w-4 sm:h-5 sm:w-5 text-[#ccff00]" />
) : (
<FolderOpen className="h-4 w-4 sm:h-5 sm:w-5 text-gray-400" />
)}
</div>
<div className="flex-1 min-w-0">
<p className={cn(
'font-bold truncate text-sm sm:text-base',
isInCollection ? 'text-[#ccff00]' : 'text-white'
)}>
{collection.title}
</p>
<p className="text-[10px] sm:text-xs text-gray-500 flex items-center gap-1">
<MapPin className="h-2.5 w-2.5 sm:h-3 sm:w-3" />
{collection.spotRefs.length} spot{collection.spotRefs.length !== 1 ? 's' : ''}
{isInCollection && ' · Already saved'}
</p>
</div>
</button>
);
})}
</div>
) : !showNewForm ? (
<div className="text-center py-4 sm:py-6 text-gray-400">
<FolderOpen className="h-10 w-10 sm:h-12 sm:w-12 mx-auto mb-2 sm:mb-3 opacity-50" />
<p className="text-sm sm:text-base">You don't have any collections yet</p>
<p className="text-xs sm:text-sm">Create your first one below!</p>
</div>
) : null}
{/* Create New Collection Form */}
{showNewForm ? (
<form onSubmit={handleCreateCollection} className="space-y-3 sm:space-y-4 pt-3 sm:pt-4 border-t border-gray-800">
<div className="space-y-1.5 sm:space-y-2">
<Label htmlFor="title" className="text-white text-sm">Collection Name</Label>
<Input
id="title"
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)}
placeholder="Barcelona Trip 2024"
className="bg-black border-gray-700 text-white h-11"
autoFocus
/>
</div>
<div className="space-y-1.5 sm:space-y-2">
<Label htmlFor="description" className="text-white text-sm">Description (optional)</Label>
<Textarea
id="description"
value={newDescription}
onChange={(e) => setNewDescription(e.target.value)}
placeholder="My favorite spots from my Barcelona trip..."
className="bg-black border-gray-700 text-white resize-none"
rows={2}
/>
</div>
<div className="flex gap-2">
<Button
type="button"
variant="outline"
onClick={() => setShowNewForm(false)}
className="flex-1 border-gray-700 h-11"
>
Cancel
</Button>
<Button
type="submit"
disabled={!newTitle.trim() || isCreating}
className="flex-1 bg-[#ccff00] text-black hover:bg-[#ccff00]/90 font-bold h-11"
>
{isCreating ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
<span className="hidden sm:inline">Creating...</span>
<span className="sm:hidden">...</span>
</>
) : (
<>
<Plus className="h-4 w-4 sm:mr-2" />
<span className="hidden sm:inline">Create & Save</span>
<span className="sm:hidden">Create</span>
</>
)}
</Button>
</div>
</form>
) : (
<Button
onClick={() => setShowNewForm(true)}
variant="outline"
className="w-full border-dashed border-gray-600 hover:border-[#ccff00]/50 hover:bg-[#ccff00]/5 h-11"
>
<Plus className="h-4 w-4 mr-2" />
Create New Collection
</Button>
)}
</div>
</DialogContent>
</Dialog>
);
}