- routeUi.* (123 keys): RouteDialog (incl. follow-mode banner, transit time picker + mode filters, itinerary legs), StopDepartures, TripItineraryCard. - sasquatchUi.* (81 keys): SasquatchGuide, SasquatchProDialog, SasquatchActionCard, SasquatchAiStatusCard. LLM prompts + chat bodies deliberately left English. - collectionUi.* / bootUi.*: AddToCollectionDialog example placeholders, SplashScreen rotating status lines + aria labels. - New src/i18n/batch3/* fragment files, spread into en/es so parallel passes never edit the big literal.
269 lines
11 KiB
TypeScript
269 lines
11 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 { useTranslation } from '@/contexts/language';
|
|
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 { t } = useTranslation();
|
|
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: t('walletUi.addToCollection.alreadySaved'),
|
|
description: t('walletUi.addToCollection.alreadySavedDesc', { spot: spot.title, collection: collection.title }),
|
|
});
|
|
return;
|
|
}
|
|
|
|
setAddingToId(collection.id);
|
|
try {
|
|
await addSpotToCollection({
|
|
collection,
|
|
spotPubkey: spot.pubkey,
|
|
spotIdentifier: spot.identifier,
|
|
});
|
|
toast({
|
|
title: t('walletUi.addToCollection.saved'),
|
|
description: t('walletUi.addToCollection.savedDesc', { spot: spot.title, collection: collection.title }),
|
|
});
|
|
onOpenChange(false);
|
|
} catch (error) {
|
|
toast({
|
|
title: t('walletUi.addToCollection.saveFailed'),
|
|
description: error instanceof Error ? error.message : t('walletUi.addToCollection.somethingWrong'),
|
|
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: t('walletUi.addToCollection.created'),
|
|
description: t('walletUi.addToCollection.createdDesc', { spot: spot.title, collection: newTitle }),
|
|
});
|
|
setNewTitle('');
|
|
setNewDescription('');
|
|
setShowNewForm(false);
|
|
onOpenChange(false);
|
|
} catch (error) {
|
|
toast({
|
|
title: t('walletUi.addToCollection.createFailed'),
|
|
description: error instanceof Error ? error.message : t('walletUi.addToCollection.somethingWrong'),
|
|
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]" />
|
|
{t('walletUi.addToCollection.title')}
|
|
</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="py-8 text-center">
|
|
<p className="text-gray-400">{t('walletUi.addToCollection.loginRequired')}</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]" />
|
|
{t('walletUi.addToCollection.title')}
|
|
</DialogTitle>
|
|
<DialogDescription className="text-gray-400 text-sm">
|
|
{t('walletUi.addToCollection.desc', { spot: spot.title })}
|
|
</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" />
|
|
{t('walletUi.addToCollection.spotCount', { count: collection.spotRefs.length })}
|
|
{isInCollection && ` · ${t('walletUi.addToCollection.alreadySavedTag')}`}
|
|
</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">{t('walletUi.addToCollection.noCollections')}</p>
|
|
<p className="text-xs sm:text-sm">{t('walletUi.addToCollection.createFirst')}</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">{t('walletUi.addToCollection.name')}</Label>
|
|
<Input
|
|
id="title"
|
|
value={newTitle}
|
|
onChange={(e) => setNewTitle(e.target.value)}
|
|
placeholder={t('collectionUi.namePlaceholder')}
|
|
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">{t('walletUi.addToCollection.descriptionOptional')}</Label>
|
|
<Textarea
|
|
id="description"
|
|
value={newDescription}
|
|
onChange={(e) => setNewDescription(e.target.value)}
|
|
placeholder={t('collectionUi.descriptionPlaceholder')}
|
|
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"
|
|
>
|
|
{t('common.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">{t('walletUi.addToCollection.creating')}</span>
|
|
<span className="sm:hidden">...</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Plus className="h-4 w-4 sm:mr-2" />
|
|
<span className="hidden sm:inline">{t('walletUi.addToCollection.createAndSave')}</span>
|
|
<span className="sm:hidden">{t('common.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" />
|
|
{t('walletUi.addToCollection.createNew')}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|