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

198 lines
7.1 KiB
TypeScript

import { useState, useMemo } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent } from '@/components/ui/card';
import { useSkateSpots } from '@/hooks/useSkateSpots';
import { MapPin, Search, X, Plus } from 'lucide-react';
import { cn } from '@/lib/utils';
interface SpotPickerProps {
selectedSpots: string[]; // Array of "pubkey:identifier" strings
onSpotsChange: (spots: string[]) => void;
maxSpots?: number;
label?: string;
description?: string;
}
export function SpotPicker({
selectedSpots,
onSpotsChange,
maxSpots = 5,
label = 'Spots Where You\'re Available',
description = 'Select spots where you offer your services',
}: SpotPickerProps) {
const { data: allSpots } = useSkateSpots(100);
const [searchQuery, setSearchQuery] = useState('');
const [isExpanded, setIsExpanded] = useState(false);
// Filter spots for search
const filteredSpots = useMemo(() => {
if (!allSpots) return [];
if (!searchQuery) return allSpots.slice(0, 10);
const query = searchQuery.toLowerCase();
return allSpots
.filter(s =>
s.title.toLowerCase().includes(query) ||
s.location?.toLowerCase().includes(query)
)
.slice(0, 10);
}, [allSpots, searchQuery]);
// Get selected spot details
const selectedSpotDetails = useMemo(() => {
if (!allSpots) return [];
return selectedSpots
.map(spotId => {
const [pubkey, identifier] = spotId.split(':');
return allSpots.find(s => s.pubkey === pubkey && s.identifier === identifier);
})
.filter(Boolean);
}, [allSpots, selectedSpots]);
const toggleSpot = (pubkey: string, identifier: string) => {
const spotId = `${pubkey}:${identifier}`;
if (selectedSpots.includes(spotId)) {
onSpotsChange(selectedSpots.filter(id => id !== spotId));
} else if (selectedSpots.length < maxSpots) {
onSpotsChange([...selectedSpots, spotId]);
}
};
const removeSpot = (spotId: string) => {
onSpotsChange(selectedSpots.filter(id => id !== spotId));
};
return (
<div className="space-y-3">
<div className="flex items-center justify-between">
<div>
<Label className="text-base font-bold">{label}</Label>
<p className="text-sm text-muted-foreground mt-0.5">{description}</p>
</div>
<Badge variant="outline" className="text-xs">
{selectedSpots.length}/{maxSpots}
</Badge>
</div>
{/* Selected spots */}
{selectedSpotDetails.length > 0 && (
<div className="flex flex-wrap gap-2">
{selectedSpotDetails.map((spot) => spot && (
<Badge
key={`${spot.pubkey}:${spot.identifier}`}
variant="secondary"
className="gap-1.5 py-1.5 px-3 bg-[#ff0040]/10 border border-[#ff0040]/30 text-[#ff0040]"
>
<MapPin className="h-3 w-3" />
{spot.title}
<button
type="button"
onClick={() => removeSpot(`${spot.pubkey}:${spot.identifier}`)}
className="ml-1 hover:bg-[#ff0040]/20 rounded-full p-0.5"
>
<X className="h-3 w-3" />
</button>
</Badge>
))}
</div>
)}
{/* Add spot button / expanded picker */}
{selectedSpots.length < maxSpots && (
<>
{!isExpanded ? (
<Button
type="button"
variant="outline"
size="sm"
className="gap-2 border-dashed"
onClick={() => setIsExpanded(true)}
>
<Plus className="h-4 w-4" />
Add Spot
</Button>
) : (
<Card className="bg-gray-900/50 border-gray-700">
<CardContent className="p-3 space-y-3">
<div className="flex items-center gap-2">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-500" />
<Input
placeholder="Search spots..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="bg-gray-800 border-gray-700 pl-9 h-9"
/>
</div>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => {
setIsExpanded(false);
setSearchQuery('');
}}
>
<X className="h-4 w-4" />
</Button>
</div>
<div className="max-h-48 overflow-y-auto space-y-1.5">
{filteredSpots.map((spot) => {
const spotId = `${spot.pubkey}:${spot.identifier}`;
const isSelected = selectedSpots.includes(spotId);
return (
<button
key={spotId}
type="button"
className={cn(
'w-full flex items-center gap-2 p-2 rounded-lg text-left transition-colors text-sm',
isSelected
? 'bg-[#ff0040]/20 border border-[#ff0040]/50'
: 'hover:bg-gray-800 border border-transparent'
)}
onClick={() => toggleSpot(spot.pubkey, spot.identifier)}
disabled={!isSelected && selectedSpots.length >= maxSpots}
>
<MapPin className={cn(
'h-4 w-4 flex-shrink-0',
isSelected ? 'text-[#ff0040]' : 'text-gray-500'
)} />
<div className="flex-1 min-w-0">
<div className="font-medium truncate">{spot.title}</div>
{spot.location && (
<div className="text-xs text-gray-400 truncate">{spot.location}</div>
)}
</div>
{isSelected && (
<Badge variant="outline" className="text-[10px] border-[#ff0040]/50 text-[#ff0040]">
Selected
</Badge>
)}
</button>
);
})}
{filteredSpots.length === 0 && (
<p className="text-sm text-gray-400 text-center py-4">
No spots found. Try a different search.
</p>
)}
</div>
</CardContent>
</Card>
)}
</>
)}
{selectedSpots.length === 0 && (
<p className="text-xs text-gray-400 italic">
Adding spots helps users find you when browsing specific locations
</p>
)}
</div>
);
}