A NIP-05 name maps to one key, so "who owns it" decides where it lives: - Crew: repeatable nip05 tags (up to 5), verified against the founder key. - Shop: same on kind 30017, verified against the owner key (new field). - Profile: kind-0 has ONE standard nip05; extra personal handles live in a separate NIP-78 record (kind 30078, d=skatespots:profile-handles) so another client re-saving the profile can't wipe them. Shared pieces: - components/Nip05Badges.tsx — generic verify-and-render chip row (any owner). - components/Nip05HandlesEditor.tsx — shared add/remove editor with live verified/mismatch status; used by profile, crew and shop forms. - CrewNip05Badge now delegates to Nip05Badges; CrewForm uses the shared editor. - Profile page shows primary + extra handles, and each crew/shop chips its own verified handle. - lib/profileHandles.ts + hooks/useProfileHandles.ts (read/publish the record). - types: SkateShop gains nip05/nip05s; parseSkateShop parses repeatable nip05s. - NIP.md + AGENTS.md + i18n (EN/ES) updated.
563 lines
23 KiB
TypeScript
563 lines
23 KiB
TypeScript
import { dateLocaleCode } from '@/lib/dateFnsI18n';
|
|
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 { PageError } from '@/components/ErrorState';
|
|
import { ImageWithFallback } from '@/components/ImageWithFallback';
|
|
import { ImageLightbox } from '@/components/ImageLightbox';
|
|
import { SpotMap } from '@/components/SpotMap';
|
|
import { RouteDialog } from '@/components/RouteDialog';
|
|
import { SKATE_SHOP_KIND, type SkateSpot } from '@/lib/types';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
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 { ClinkPayButton } from '@/components/ClinkPayButton';
|
|
import { CommentsSection } from '@/components/comments/CommentsSection';
|
|
import { sanitizeUrl, sanitizeTel, sanitizeEmail, urlDomain } from '@/lib/sanitizeUrl';
|
|
import { OfficialBadge } from '@/components/OfficialBadge';
|
|
import { ControllerBadge } from '@/components/ControllerBadge';
|
|
import { Nip05Badges } from '@/components/Nip05Badges';
|
|
import { VenueAccessCard } from '@/components/VenueAccessCard';
|
|
import { useShop } from '@/hooks/useShops';
|
|
import { useAuthor } from '@/hooks/useAuthor';
|
|
import { useDeleteEntity } from '@/hooks/useDeleteEntity';
|
|
import { isAdminPubkey } from '@/lib/admins';
|
|
import { cn } from '@/lib/utils';
|
|
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,
|
|
} from 'lucide-react';
|
|
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
|
import { useTranslation } from '@/contexts/language';
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
} from '@/components/ui/alert-dialog';
|
|
|
|
function ShopDetailSkeleton() {
|
|
return (
|
|
<>
|
|
<div className="container px-4 py-8">
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
<div className="lg:col-span-2 space-y-6 min-w-0">
|
|
<Skeleton className="aspect-video w-full rounded-xl" />
|
|
<div className="space-y-4">
|
|
<Skeleton className="h-10 w-3/4" />
|
|
<Skeleton className="h-6 w-1/2" />
|
|
<Skeleton className="h-24 w-full" />
|
|
</div>
|
|
</div>
|
|
<div className="space-y-6 min-w-0">
|
|
<Skeleton className="h-64 w-full rounded-xl" />
|
|
<Skeleton className="h-32 w-full rounded-xl" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
export default function ShopDetailPage() {
|
|
const { t } = useTranslation();
|
|
const { pubkey, identifier } = useParams<{ pubkey: string; identifier: string }>();
|
|
const { data: shop, isLoading, error, refetch } = useShop(identifier || '', pubkey);
|
|
const author = useAuthor(shop?.pubkey);
|
|
const { user, metadata: userMetadata } = useCurrentUser();
|
|
const { mutateAsync: deleteEntity } = useDeleteEntity();
|
|
const { toast } = useToast();
|
|
const navigate = useNavigate();
|
|
const queryClient = useQueryClient();
|
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
|
const [showRouteDialog, setShowRouteDialog] = useState(false);
|
|
const [lightboxIndex, setLightboxIndex] = useState<number | null>(null);
|
|
|
|
const isOwner = user && shop && user.pubkey === shop.pubkey;
|
|
const isAdmin = isAdminPubkey(user?.pubkey);
|
|
// Owner can delete their own shop; a SkateSpots admin can take down any shop.
|
|
const canDelete = !!user && !!shop && (isOwner || isAdmin);
|
|
const adminTakedown = isAdmin && !isOwner;
|
|
const hasNip05 = userMetadata?.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: t('placesUi.shop.linkCopied'),
|
|
description: t('placesUi.shop.linkCopiedDesc'),
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleDirections = () => {
|
|
if (coordinates) {
|
|
// In-app routing — nothing leaves to Google/Apple Maps.
|
|
setShowRouteDialog(true);
|
|
}
|
|
};
|
|
|
|
const handleDelete = async () => {
|
|
if (!shop || !user) return;
|
|
|
|
try {
|
|
// Routes through useDeleteEntity: owner → NIP-09 delete; admin → NIP-09
|
|
// delete + a moderation label so the shop is hidden across the app.
|
|
await deleteEntity({
|
|
event: shop.event,
|
|
reason: adminTakedown ? t('placesUi.shop.adminRemovedReason', { name: shop.name }) : t('placesUi.shop.deletedReason'),
|
|
});
|
|
|
|
setShowDeleteDialog(false);
|
|
toast({
|
|
title: adminTakedown ? t('placesUi.shop.removed') : t('placesUi.shop.deleted'),
|
|
description: adminTakedown
|
|
? t('placesUi.shop.removedDesc')
|
|
: t('placesUi.shop.deletedDesc'),
|
|
});
|
|
|
|
queryClient.invalidateQueries({ queryKey: ['skate-shops'] });
|
|
queryClient.invalidateQueries({ queryKey: ['skate-shop', shop.identifier, shop.pubkey] });
|
|
queryClient.invalidateQueries({ queryKey: ['user-skate-shops', user.pubkey] });
|
|
// The legacy directory hook (`useShops`) keeps its own cache namespace.
|
|
queryClient.invalidateQueries({ queryKey: ['shop-directory'] });
|
|
|
|
setTimeout(() => navigate('/map'), 1000);
|
|
} catch (error) {
|
|
toast({
|
|
title: t('placesUi.shop.deleteFailed'),
|
|
description: error instanceof Error ? error.message : t('placesUi.createEvent.somethingWrong'),
|
|
variant: 'destructive',
|
|
});
|
|
setShowDeleteDialog(false);
|
|
}
|
|
};
|
|
|
|
if (isLoading) {
|
|
return <ShopDetailSkeleton />;
|
|
}
|
|
|
|
if (error) {
|
|
return <PageError onRetry={() => refetch()} />;
|
|
}
|
|
|
|
if (!shop) {
|
|
return <NotFound />;
|
|
}
|
|
|
|
// Create a fake SkateSpot for the map component
|
|
const spotForMap = {
|
|
...shop,
|
|
title: shop.name,
|
|
types: ['shop'],
|
|
accessType: undefined,
|
|
contactInfo: undefined,
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div className="container px-4 py-8">
|
|
{/* Back link */}
|
|
<Link
|
|
to="/map"
|
|
className="inline-flex items-center text-muted-foreground hover:text-foreground transition-colors mb-6"
|
|
>
|
|
<ArrowLeft className="h-4 w-4 mr-2" />
|
|
{t('placesUi.shop.backToMap')}
|
|
</Link>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
{/* Main Content */}
|
|
<div className="lg:col-span-2 space-y-6 min-w-0">
|
|
{/* Image Gallery — click any photo to view full size */}
|
|
{shop.images.length > 0 ? (
|
|
<button
|
|
type="button"
|
|
onClick={() => setLightboxIndex(0)}
|
|
className="group relative aspect-video w-full cursor-zoom-in overflow-hidden rounded-xl bg-muted"
|
|
aria-label={t('placesUi.shop.viewPhotoFull')}
|
|
>
|
|
<ImageWithFallback
|
|
src={shop.images[0]}
|
|
alt={shop.name}
|
|
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-[1.02]"
|
|
/>
|
|
</button>
|
|
) : (
|
|
<div className="aspect-video rounded-xl bg-gradient-to-br from-[#ccff00]/20 to-[#00ff88]/20 flex items-center justify-center">
|
|
<Store className="h-16 w-16 text-[#ccff00]/40" />
|
|
</div>
|
|
)}
|
|
|
|
{/* Additional images */}
|
|
{shop.images.length > 1 && (
|
|
<div className="grid grid-cols-4 gap-2">
|
|
{shop.images.slice(1, 5).map((img, idx) => {
|
|
const realIndex = idx + 1;
|
|
const remaining = shop.images.length - 5;
|
|
return (
|
|
<button
|
|
key={`${img}-${realIndex}`}
|
|
type="button"
|
|
onClick={() => setLightboxIndex(realIndex)}
|
|
className="relative aspect-square cursor-zoom-in overflow-hidden rounded-lg bg-muted"
|
|
aria-label={t('placesUi.shop.viewPhotoOf', { index: realIndex + 1, total: shop.images.length })}
|
|
>
|
|
<ImageWithFallback src={img} alt={`${shop.name} ${realIndex + 1}`} className="w-full h-full object-cover" />
|
|
{idx === 3 && remaining > 0 && (
|
|
<div className="absolute inset-0 flex items-center justify-center bg-black/70 text-sm font-bold text-white">
|
|
+{remaining}
|
|
</div>
|
|
)}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
{/* Shop Info */}
|
|
<div className="space-y-4">
|
|
<div className="flex items-start justify-between gap-4">
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-3 mb-2">
|
|
<h1 className="text-3xl font-black uppercase neon-lime" style={{
|
|
fontFamily: 'Impact, "Arial Black", sans-serif'
|
|
}}>{shop.name}</h1>
|
|
{isVerifiedOwner && <ControllerBadge role="verifiedOwner" />}
|
|
<OfficialBadge pubkey={shop.pubkey} size="md" />
|
|
</div>
|
|
{shop.nip05s.length > 0 && (
|
|
<Nip05Badges handles={shop.nip05s} pubkey={shop.pubkey} className="mb-1.5" />
|
|
)}
|
|
{shop.location && (
|
|
<p className="text-muted-foreground flex items-center gap-2">
|
|
<Store className="h-4 w-4" />
|
|
{shop.location}
|
|
</p>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{isOwner && (
|
|
<Link to={`/edit-shop/${shop.pubkey}/${shop.identifier}`}>
|
|
<Button variant="outline" size="icon">
|
|
<Pencil className="h-4 w-4" />
|
|
</Button>
|
|
</Link>
|
|
)}
|
|
{canDelete && (
|
|
<Button
|
|
variant="outline"
|
|
size="icon"
|
|
onClick={() => setShowDeleteDialog(true)}
|
|
title={adminTakedown ? t('placesUi.shop.removeAdmin') : t('common.delete')}
|
|
className={cn(
|
|
'hover:bg-destructive hover:text-destructive-foreground',
|
|
adminTakedown && 'border-red-500/50 text-red-400',
|
|
)}
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</Button>
|
|
)}
|
|
<Button variant="outline" size="icon" onClick={handleShare}>
|
|
<Share2 className="h-4 w-4" />
|
|
</Button>
|
|
<Button variant="outline" size="icon" onClick={handleDirections}>
|
|
<Navigation className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Hours */}
|
|
{shop.hours && (
|
|
<Card className="bg-black/90 border-2 border-gray-700">
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center gap-3">
|
|
<Clock className="h-5 w-5 text-[#ccff00]" />
|
|
<div>
|
|
<p className="text-sm text-gray-400">{t('shops.hours')}</p>
|
|
<p className="text-white font-medium">{shop.hours}</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Owner: issue &/or review access passes for this venue */}
|
|
<VenueAccessCard coord={`${SKATE_SHOP_KIND}:${shop.pubkey}:${shop.identifier}`} isOwner={!!isOwner} />
|
|
|
|
{/* Claim Shop Notice */}
|
|
{!isOwner && user && (
|
|
<Card className="bg-[#ccff00]/10 border-2 border-[#ccff00]/50">
|
|
<CardContent className="p-6">
|
|
<div className="flex items-start gap-3">
|
|
<div className="text-2xl">🏪</div>
|
|
<div className="flex-1">
|
|
<h3 className="font-bold text-[#ccff00] mb-2 uppercase">{t('placesUi.shop.areYouOwner')}</h3>
|
|
<p className="text-sm text-gray-300 mb-4">
|
|
{t('placesUi.shop.claimDesc')}
|
|
</p>
|
|
{hasNip05 ? (
|
|
<div>
|
|
<p className="text-xs text-gray-400 mb-3">
|
|
{t('placesUi.shop.claimNip05Intro')}{' '}
|
|
<code className="bg-black/50 px-1 rounded">supremenewyork.com</code>,{' '}
|
|
{t('placesUi.shop.claimNip05Your')} <code className="bg-black/50 px-1 rounded">you@supremenewyork.com</code>
|
|
</p>
|
|
<Link to="/settings">
|
|
<Button className="gap-2 bg-[#ccff00] text-black hover:bg-[#ccff00]/90 font-bold uppercase w-full">
|
|
<Shield className="h-4 w-4" />
|
|
{t('placesUi.shop.updateNip05')}
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
) : (
|
|
<div>
|
|
<p className="text-xs text-yellow-400 mb-2">{t('placesUi.shop.nip05Required')}</p>
|
|
<Link to="/settings">
|
|
<Button variant="outline" size="sm" className="border-yellow-500/50 hover:bg-yellow-500/10 text-yellow-400 font-bold">
|
|
{t('placesUi.shop.addNip05Settings')}
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Description */}
|
|
{shop.description && (
|
|
<Card className="bg-black/90 border-2 border-gray-700">
|
|
<CardContent className="pt-6">
|
|
<p className="whitespace-pre-wrap text-gray-300">{shop.description}</p>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Comments Section */}
|
|
{shop.event && <CommentsSection root={shop.event} />}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Sidebar */}
|
|
<div className="space-y-6 min-w-0">
|
|
{/* Contact Card */}
|
|
{(shop.phone || shop.email || shop.website || shop.instagram) && (
|
|
<Card className="bg-[#ccff00]/10 border-2 border-[#ccff00]/50">
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="text-lg text-[#ccff00] uppercase font-black flex items-center gap-2">
|
|
<Phone className="h-5 w-5" />
|
|
{t('placesUi.shop.contactShop')}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-2">
|
|
{sanitizeTel(shop.phone) && (
|
|
<a href={sanitizeTel(shop.phone)!} className="flex items-center gap-3 p-3 bg-black/50 rounded-lg hover:bg-black/70 transition-colors">
|
|
<Phone className="h-4 w-4 text-[#ccff00]" />
|
|
<span className="text-gray-300">{shop.phone}</span>
|
|
</a>
|
|
)}
|
|
{sanitizeEmail(shop.email) && (
|
|
<a href={sanitizeEmail(shop.email)!} className="flex items-center gap-3 p-3 bg-black/50 rounded-lg hover:bg-black/70 transition-colors">
|
|
<Mail className="h-4 w-4 text-[#ccff00]" />
|
|
<span className="text-gray-300 truncate">{shop.email}</span>
|
|
</a>
|
|
)}
|
|
{sanitizeUrl(shop.website) && (
|
|
<a href={sanitizeUrl(shop.website)!} target="_blank" rel="noopener noreferrer" className="flex items-center gap-3 p-3 bg-black/50 rounded-lg hover:bg-black/70 transition-colors" title={t('placesUi.shop.opens', { domain: urlDomain(shop.website!) })}>
|
|
<Globe className="h-4 w-4 text-[#ccff00]" />
|
|
<span className="text-gray-300 truncate">{urlDomain(shop.website!)}</span>
|
|
</a>
|
|
)}
|
|
{shop.instagram && (
|
|
<a href={`https://instagram.com/${shop.instagram.replace('@', '')}`} target="_blank" rel="noopener noreferrer" className="flex items-center gap-3 p-3 bg-black/50 rounded-lg hover:bg-black/70 transition-colors">
|
|
<Instagram className="h-4 w-4 text-[#ccff00]" />
|
|
<span className="text-gray-300">@{shop.instagram.replace('@', '')}</span>
|
|
</a>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Map */}
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="text-lg flex items-center gap-2">
|
|
<Store className="h-5 w-5 text-[#ccff00]" />
|
|
{t('placesUi.eventDetail.location')}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="aspect-square rounded-lg overflow-hidden">
|
|
<SpotMap
|
|
spots={[spotForMap as unknown as SkateSpot]}
|
|
center={coordinates ? [coordinates.lat, coordinates.lng] : undefined}
|
|
zoom={15}
|
|
className="h-full"
|
|
/>
|
|
</div>
|
|
{coordinates && (
|
|
<Button
|
|
variant="outline"
|
|
className="w-full mt-4 gap-2"
|
|
onClick={handleDirections}
|
|
>
|
|
<Navigation className="h-4 w-4" />
|
|
{t('placesUi.eventDetail.getDirections')}
|
|
</Button>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Added By Card with Tip the Mapper */}
|
|
<Card className="bg-gradient-to-br from-[#ccff00]/5 to-orange-500/5 border-2 border-[#ccff00]/30">
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="text-lg flex items-center gap-2">
|
|
<span className="text-[#ccff00]">⚡</span>
|
|
{t('placesUi.shop.tipMapper')}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="flex items-center gap-3 mb-4">
|
|
<Avatar className="h-12 w-12 border-2 border-[#ccff00]/30">
|
|
<AvatarImage src={metadata?.picture} alt={displayName} />
|
|
<AvatarFallback className="bg-[#ccff00]/10 text-[#ccff00]">
|
|
{displayName.slice(0, 2).toUpperCase()}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="font-medium truncate">{displayName}</p>
|
|
<p className="text-sm text-muted-foreground flex items-center gap-1">
|
|
<Calendar className="h-3 w-3" />
|
|
{new Date(shop.createdAt * 1000).toLocaleDateString(dateLocaleCode())}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<p className="text-xs text-gray-400 mb-3">
|
|
{t('placesUi.shop.tipMapperDesc')}
|
|
</p>
|
|
|
|
<ZapButton
|
|
target={shop.event}
|
|
className="w-full justify-center bg-[#ccff00] text-black hover:bg-[#ccff00]/90 font-bold"
|
|
/>
|
|
|
|
{/* CLINK self-custodial payment (only when the shop published a valid noffer) */}
|
|
{shop.noffer && (
|
|
<div className="mt-3">
|
|
<ClinkPayButton
|
|
noffer={shop.noffer}
|
|
recipientName={shop.name}
|
|
className="w-full justify-center"
|
|
/>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Delete Confirmation Dialog */}
|
|
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
|
<AlertDialogContent className="bg-black border-2 border-red-500/50">
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle className="text-white flex items-center gap-2">
|
|
<AlertTriangle className="h-5 w-5 text-red-500" />
|
|
{adminTakedown ? t('placesUi.shop.removeDialogTitle') : t('placesUi.shop.deleteDialogTitle')}
|
|
</AlertDialogTitle>
|
|
<AlertDialogDescription className="text-gray-300">
|
|
{adminTakedown
|
|
? t('placesUi.shop.removeDialogDesc', { name: shop.name })
|
|
: t('placesUi.shop.deleteDialogDesc', { name: shop.name })}
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel className="border-gray-600">{t('common.cancel')}</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={handleDelete}
|
|
className="bg-red-500 hover:bg-red-600 text-white"
|
|
>
|
|
{adminTakedown ? t('placesUi.shop.removeShop') : t('placesUi.shop.deleteShop')}
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
|
|
{/* In-app route (no external maps) */}
|
|
<RouteDialog
|
|
open={showRouteDialog}
|
|
onOpenChange={setShowRouteDialog}
|
|
destination={coordinates}
|
|
destinationLabel={shop?.name ?? 'Shop'}
|
|
/>
|
|
|
|
{/* Full-screen photo viewer */}
|
|
<ImageLightbox
|
|
images={shop.images.map((url) => ({ url, isOwner: true }))}
|
|
index={lightboxIndex}
|
|
onIndexChange={setLightboxIndex}
|
|
onClose={() => setLightboxIndex(null)}
|
|
/>
|
|
</>
|
|
);
|
|
}
|