import { useMemo, useState } from 'react'; import { useSeoMeta } from '@unhead/react'; import { useParams, Link, useNavigate } from 'react-router-dom'; import { useQueryClient } from '@tanstack/react-query'; import ngeohash from 'ngeohash'; import { Layout } from '@/components/Layout'; import { SpotMap } from '@/components/SpotMap'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Skeleton } from '@/components/ui/skeleton'; import { Button } from '@/components/ui/button'; import { ZapButton } from '@/components/ZapButton'; import { CommentsSection } from '@/components/comments/CommentsSection'; import { sanitizeUrl, sanitizeTel, sanitizeEmail, urlDomain } from '@/lib/sanitizeUrl'; import { OfficialBadge } from '@/components/OfficialBadge'; import { useShop } from '@/hooks/useShops'; import { useAuthor } from '@/hooks/useAuthor'; import { useNostrPublish } from '@/hooks/useNostrPublish'; import { useToast } from '@/hooks/useToast'; import { genUserName } from '@/lib/genUserName'; import NotFound from './NotFound'; import { Store, ArrowLeft, Share2, Navigation, Calendar, Pencil, Trash2, AlertTriangle, Shield, Phone, Mail, Globe, Instagram, Clock, CheckCircle2, } from 'lucide-react'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; function ShopDetailSkeleton() { return (
); } export default function ShopDetailPage() { const { pubkey, identifier } = useParams<{ pubkey: string; identifier: string }>(); const { data: shop, isLoading, error } = useShop(identifier || '', pubkey); const author = useAuthor(shop?.pubkey); const { user } = useCurrentUser(); const { mutateAsync: publishEvent } = useNostrPublish(); const { toast } = useToast(); const navigate = useNavigate(); const queryClient = useQueryClient(); const [showDeleteDialog, setShowDeleteDialog] = useState(false); const isOwner = user && shop && user.pubkey === shop.pubkey; const hasNip05 = user?.metadata?.nip05; useSeoMeta({ title: shop ? `${shop.name} - SkateSpots` : 'Skate Shop - SkateSpots', description: shop?.description || 'View this skate shop on SkateSpots.', }); const metadata = author.data?.metadata; const displayName = metadata?.name ?? genUserName(shop?.pubkey || ''); const coordinates = useMemo(() => { if (!shop?.geohash) return null; const { latitude, longitude } = ngeohash.decode(shop.geohash); return { lat: latitude, lng: longitude }; }, [shop?.geohash]); // Check if shop owner's NIP-05 domain matches website domain const isVerifiedOwner = useMemo(() => { if (!metadata?.nip05 || !shop?.website) return false; const nip05Domain = metadata.nip05.split('@')[1]; const websiteDomain = shop.website.replace(/^https?:\/\/(www\.)?/, '').split('/')[0]; return nip05Domain.toLowerCase() === websiteDomain.toLowerCase(); }, [metadata?.nip05, shop?.website]); const handleShare = async () => { if (navigator.share) { try { await navigator.share({ title: shop?.name, text: shop?.description || `Check out this skate shop: ${shop?.name}`, url: window.location.href, }); } catch { // User cancelled or share failed } } else { navigator.clipboard.writeText(window.location.href); toast({ title: 'Link copied!', description: 'Shop link copied to clipboard.', }); } }; const handleDirections = () => { if (coordinates) { window.open( `https://www.google.com/maps/dir/?api=1&destination=${coordinates.lat},${coordinates.lng}`, '_blank' ); } }; const handleDelete = async () => { if (!shop || !user) return; try { await publishEvent({ kind: 5, content: 'Deleted shop', tags: [ ['e', shop.id], ['a', `${shop.event.kind}:${shop.pubkey}:${shop.identifier}`], ], }); setShowDeleteDialog(false); toast({ title: 'Shop deleted', description: 'Deletion request sent to relays. Most relays will remove this content.', }); queryClient.invalidateQueries({ queryKey: ['skate-shops'] }); queryClient.invalidateQueries({ queryKey: ['skate-shop', shop.identifier, shop.pubkey] }); queryClient.invalidateQueries({ queryKey: ['user-skate-shops', user.pubkey] }); setTimeout(() => navigate('/map'), 1000); } catch (error) { toast({ title: 'Delete failed', description: error instanceof Error ? error.message : 'Something went wrong', variant: 'destructive', }); setShowDeleteDialog(false); } }; if (isLoading) { return ; } if (error || !shop) { return ; } // Create a fake SkateSpot for the map component const spotForMap = { ...shop, title: shop.name, types: ['shop'], accessType: undefined, contactInfo: undefined, }; return (
{/* Back link */} Back to Map
{/* Main Content */}
{/* Image Gallery */} {shop.images.length > 0 ? (
{shop.name}
) : (
)} {/* Additional images */} {shop.images.length > 1 && (
{shop.images.slice(1, 5).map((img, idx) => (
{`${shop.name}
))}
)} {/* Shop Info */}

{shop.name}

{isVerifiedOwner && ( Verified )}
{shop.location && (

{shop.location}

)}
{isOwner && ( <> )}
{/* Hours */} {shop.hours && (

Hours

{shop.hours}

)} {/* Claim Shop Notice */} {!isOwner && user && (
🏪

Are You the Shop Owner?

If you own or manage this shop, you can claim it to verify ownership using NIP-05.

{hasNip05 ? (

To claim this shop, add a NIP-05 identifier that matches the shop's website domain. For example, if the shop website is supremenewyork.com, your NIP-05 should be you@supremenewyork.com

) : (

⚠️ NIP-05 verification required to claim shops

)}
)} {/* Description */} {shop.description && (

{shop.description}

)} {/* Comments Section */} {shop.event && }
{/* Sidebar */}
{/* Contact Card */} {(shop.phone || shop.email || shop.website || shop.instagram) && ( Contact Shop {sanitizeTel(shop.phone) && ( {shop.phone} )} {sanitizeEmail(shop.email) && ( {shop.email} )} {sanitizeUrl(shop.website) && ( {urlDomain(shop.website!)} )} {shop.instagram && ( @{shop.instagram.replace('@', '')} )} )} {/* Map */} Location
{coordinates && ( )}
{/* Added By Card with Tip the Mapper */} ⚡ Tip the Mapper
{displayName.slice(0, 2).toUpperCase()}

{displayName}

{new Date(shop.createdAt * 1000).toLocaleDateString()}

This shop was added by a community member. Zap them to say thanks!

{/* Delete Confirmation Dialog */} Delete Shop? This will permanently delete "{shop.name}" from all relays. This action cannot be undone. Cancel Delete Shop
); }