import { useState, useEffect } from 'react';
import { useSeoMeta } from '@unhead/react';
import { useNavigate, useParams } from 'react-router-dom';
import ngeohash from 'ngeohash';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import { Skeleton } from '@/components/ui/skeleton';
import { useNostrPublish } from '@/hooks/useNostrPublish';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { Nip05HandlesEditor } from '@/components/Nip05HandlesEditor';
import { useUploadFile } from '@/hooks/useUploadFile';
import { useToast } from '@/hooks/useToast';
import { useShop } from '@/hooks/useShops';
import { LoginArea } from '@/components/auth/LoginArea';
import { SKATE_SHOP_KIND, isValidNoffer } from '@/lib/types';
import { normalizeClinkCode } from '@/lib/clink';
import { reverseGeocodeRegions, pushRegionTags } from '@/lib/geocodeRegions';
import { NofferField } from '@/components/NofferField';
import {
Store,
Upload,
X,
Loader2,
ArrowLeft,
MapPin,
AlertTriangle,
} from 'lucide-react';
import { Link } from 'react-router-dom';
import { useTranslation } from '@/contexts/language';
import NotFound from './NotFound';
function EditShopSkeleton() {
return (
<>
>
);
}
export default function EditShopPage() {
const { t } = useTranslation();
const { pubkey, identifier } = useParams<{ pubkey: string; identifier: string }>();
const navigate = useNavigate();
const { user } = useCurrentUser();
const { data: shop, isLoading, error } = useShop(identifier || '', pubkey);
const { mutateAsync: createEvent, isPending: isPublishing } = useNostrPublish();
const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();
const { toast } = useToast();
// Form state
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [location, setLocation] = useState('');
const [latitude, setLatitude] = useState('');
const [longitude, setLongitude] = useState('');
const [phone, setPhone] = useState('');
const [email, setEmail] = useState('');
const [website, setWebsite] = useState('');
const [instagram, setInstagram] = useState('');
const [hours, setHours] = useState('');
const [noffer, setNoffer] = useState('');
const [nip05s, setNip05s] = useState([]);
const [images, setImages] = useState([]);
const [isGettingLocation, setIsGettingLocation] = useState(false);
const [initialized, setInitialized] = useState(false);
useSeoMeta({
title: shop ? `Edit ${shop.name} - SkateSpots` : 'Edit Shop - SkateSpots',
description: t('seoUi.editShopDesc'),
});
// Initialize form with existing data
useEffect(() => {
if (shop && !initialized) {
setName(shop.name);
setDescription(shop.description || '');
setLocation(shop.location || '');
setPhone(shop.phone || '');
setEmail(shop.email || '');
setWebsite(shop.website || '');
setInstagram(shop.instagram || '');
setHours(shop.hours || '');
setNoffer(shop.noffer || '');
setNip05s(shop.nip05s ?? []);
setImages(shop.images);
// Decode geohash to get coordinates
if (shop.geohash) {
const { latitude: lat, longitude: lng } = ngeohash.decode(shop.geohash);
setLatitude(lat.toFixed(6));
setLongitude(lng.toFixed(6));
}
setInitialized(true);
}
}, [shop, initialized]);
const getCurrentLocation = () => {
if (!navigator.geolocation) {
toast({
title: t('placesUi.createEvent.geoNotSupported'),
description: t('placesUi.createEvent.geoNotSupportedDesc'),
variant: 'destructive',
});
return;
}
setIsGettingLocation(true);
navigator.geolocation.getCurrentPosition(
(position) => {
setLatitude(position.coords.latitude.toFixed(6));
setLongitude(position.coords.longitude.toFixed(6));
setIsGettingLocation(false);
toast({
title: t('placesUi.createEvent.locationFound'),
description: t('placesUi.createEvent.locationFoundDesc'),
});
},
(error) => {
setIsGettingLocation(false);
toast({
title: t('placesUi.createEvent.locationError'),
description: error.message,
variant: 'destructive',
});
}
);
};
const handleImageUpload = async (e: React.ChangeEvent) => {
const files = e.target.files;
if (!files || files.length === 0) return;
try {
const file = files[0];
const tags = await uploadFile(file);
const url = tags[0]?.[1];
if (url) {
setImages((prev) => [...prev, url]);
toast({
title: t('placesUi.createEvent.imageUploaded'),
description: t('placesUi.createEvent.imageUploadedDesc'),
});
}
} catch (error) {
toast({
title: t('placesUi.createEvent.uploadFailed'),
description: error instanceof Error ? error.message : t('placesUi.createEvent.uploadFailedDesc'),
variant: 'destructive',
});
}
e.target.value = '';
};
const removeImage = (url: string) => {
setImages((prev) => prev.filter((img) => img !== url));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!shop) return;
if (!name.trim()) {
toast({
title: t('placesUi.createShop.nameRequired'),
description: t('placesUi.createShop.nameRequiredDesc'),
variant: 'destructive',
});
return;
}
if (!location.trim()) {
toast({
title: t('placesUi.createShop.addressRequired'),
description: t('placesUi.createShop.addressRequiredDesc'),
variant: 'destructive',
});
return;
}
if (!latitude || !longitude) {
toast({
title: t('placesUi.createShop.locationRequired'),
description: t('placesUi.createShop.locationRequiredDesc'),
variant: 'destructive',
});
return;
}
const lat = parseFloat(latitude);
const lng = parseFloat(longitude);
if (isNaN(lat) || isNaN(lng) || lat < -90 || lat > 90 || lng < -180 || lng > 180) {
toast({
title: t('placesUi.createShop.invalidCoords'),
description: t('placesUi.createShop.invalidCoordsDesc'),
variant: 'destructive',
});
return;
}
try {
// Generate geohash from coordinates (7 chars = ~153m precision)
const geohash = ngeohash.encode(lat, lng, 7);
// Best-effort reverse geocode → city/country/country_code (same helper as
// spot/shop creation; never throws). Re-runs on edit so a moved shop's
// region tags stay correct.
const geo = await reverseGeocodeRegions(lat, lng);
// Build tags - use the same identifier to update the addressable event
const tags: string[][] = [
['d', shop.identifier],
['name', name.trim()],
['g', geohash],
['t', 'skateshop'],
['lat', lat.toString()],
['lon', lng.toString()],
['location', location.trim()],
['alt', `Skate shop: ${name.trim()}`],
];
pushRegionTags(tags, geo);
if (phone.trim()) {
tags.push(['phone', phone.trim()]);
}
if (email.trim()) {
tags.push(['email', email.trim()]);
}
if (website.trim()) {
tags.push(['website', website.trim()]);
}
if (instagram.trim()) {
tags.push(['instagram', instagram.trim()]);
}
if (hours.trim()) {
tags.push(['hours', hours.trim()]);
}
if (isValidNoffer(noffer.trim())) {
tags.push(['noffer', normalizeClinkCode(noffer)]);
}
for (const handle of nip05s) tags.push(['nip05', handle]);
for (const imageUrl of images) {
// Use imeta tag for NIP-92 compatibility
tags.push(['imeta', `url ${imageUrl}`, 'm image/jpeg']);
}
// Carry through any tag this form doesn't manage, untouched. A shop is an
// addressable NIP-15 stall, so re-publishing REPLACES the whole tag set —
// rebuilding only the fields on this form would silently strip anything
// else the stall carries (e.g. currency/shipping from another client, or
// future tags), just like the event-edit fix. `t` is special: this form
// owns the 'skateshop' marker, so keep every OTHER topic tag.
const MANAGED_TAGS = new Set([
'd', 'name', 'g', 'lat', 'lon', 'location', 'alt',
'phone', 'email', 'website', 'instagram', 'hours', 'noffer', 'imeta', 'nip05',
]);
for (const tag of shop.event.tags) {
const [tagName, tagValue] = tag;
if (MANAGED_TAGS.has(tagName)) continue;
if (tagName === 't') {
if (tagValue !== 'skateshop') tags.push(tag); // keep foreign topics
continue;
}
tags.push(tag);
}
await createEvent({
kind: SKATE_SHOP_KIND,
content: description.trim(),
tags,
});
toast({
title: t('placesUi.editShop.updated'),
description: t('placesUi.editEvent.updatedDesc'),
});
navigate(`/shop/${user?.pubkey}/${shop.identifier}`);
} catch (error) {
toast({
title: t('placesUi.editEvent.updateFailed'),
description: error instanceof Error ? error.message : t('placesUi.createEvent.somethingWrong'),
variant: 'destructive',
});
}
};
if (isLoading) {
return ;
}
if (error || !shop) {
return ;
}
// Check if user is the owner
if (!user) {
return (
<>
{t('errors.loginRequired')}
{t('placesUi.editShop.loginToEdit')}
>
);
}
if (user.pubkey !== shop.pubkey) {
return (
<>
{t('placesUi.editEvent.accessDenied')}
{t('placesUi.editShop.accessDeniedDesc')}
>
);
}
return (
<>
{t('placesUi.editShop.backToShop')}
{t('shops.editShop')}
{t('placesUi.editShop.updateDetails', { name: shop.name })}
>
);
}