'use client'; import { useEffect, useMemo, useState } from 'react'; import Link from 'next/link'; import { ListingStatus } from '@prisma/client'; import { useI18n } from '../../../components/I18nProvider'; import type { Locale } from '../../../../lib/i18n'; type ImageInput = { data?: string; url?: string; mimeType?: string; altText?: string }; type SelectedImage = { id?: string; name: string; size: number; mimeType: string; dataUrl: string; isExisting?: boolean; }; const MAX_IMAGES = 6; const MAX_IMAGE_BYTES = 5 * 1024 * 1024; // 5 MB per image const SUPPORTED_LOCALES: Locale[] = ['en', 'fi', 'sv']; type LocaleFields = { title: string; description: string; teaser: string }; export default function EditListingPage({ params }: { params: { id: string } }) { const { t, locale: uiLocale } = useI18n(); const [slug, setSlug] = useState(''); const [currentLocale, setCurrentLocale] = useState(uiLocale as Locale); const [translations, setTranslations] = useState>({ en: { title: '', description: '', teaser: '' }, fi: { title: '', description: '', teaser: '' }, sv: { title: '', description: '', teaser: '' }, }); const [suggestedSlugs, setSuggestedSlugs] = useState>({ en: '', fi: '', sv: '' }); const [country, setCountry] = useState('Finland'); const [region, setRegion] = useState(''); const [city, setCity] = useState(''); const [streetAddress, setStreetAddress] = useState(''); const [addressNote, setAddressNote] = useState(''); const [latitude, setLatitude] = useState(''); const [longitude, setLongitude] = useState(''); const [contactName, setContactName] = useState(''); const [contactEmail, setContactEmail] = useState(''); const [maxGuests, setMaxGuests] = useState(''); const [bedrooms, setBedrooms] = useState(''); const [beds, setBeds] = useState(''); const [bathrooms, setBathrooms] = useState(''); const [priceWeekday, setPriceWeekday] = useState(''); const [priceWeekend, setPriceWeekend] = useState(''); const [hasSauna, setHasSauna] = useState(true); const [hasFireplace, setHasFireplace] = useState(true); const [hasWifi, setHasWifi] = useState(true); const [petsAllowed, setPetsAllowed] = useState(false); const [byTheLake, setByTheLake] = useState(false); const [hasAirConditioning, setHasAirConditioning] = useState(false); const [hasKitchen, setHasKitchen] = useState(true); const [hasDishwasher, setHasDishwasher] = useState(false); const [hasWashingMachine, setHasWashingMachine] = useState(false); const [hasBarbecue, setHasBarbecue] = useState(false); const [hasMicrowave, setHasMicrowave] = useState(false); const [hasFreeParking, setHasFreeParking] = useState(false); const [hasSkiPass, setHasSkiPass] = useState(false); const [evChargingAvailable, setEvChargingAvailable] = useState(false); const [evChargingOnSite, setEvChargingOnSite] = useState(false); const [wheelchairAccessible, setWheelchairAccessible] = useState(false); const [calendarUrls, setCalendarUrls] = useState(''); const [selectedImages, setSelectedImages] = useState([]); const [coverImageIndex, setCoverImageIndex] = useState(1); const [message, setMessage] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); const [slugStatus, setSlugStatus] = useState<'idle' | 'checking' | 'available' | 'taken' | 'error'>('idle'); const [aiResponse, setAiResponse] = useState(''); const [copyStatus, setCopyStatus] = useState<'idle' | 'copied' | 'error'>('idle'); const [aiLoading, setAiLoading] = useState(false); const [showManualAi, setShowManualAi] = useState(false); const [initialStatus, setInitialStatus] = useState(null); const [loadingListing, setLoadingListing] = useState(true); useEffect(() => { setCurrentLocale(uiLocale as Locale); }, [uiLocale]); useEffect(() => { async function loadListing() { setLoadingListing(true); try { const res = await fetch(`/api/listings/${params.id}`, { cache: 'no-store' }); const data = await res.json(); if (!res.ok) { throw new Error(data.error || 'Failed to load listing'); } const listing = data.listing; setInitialStatus(listing.status as ListingStatus); const translationMap: Record = { en: { title: '', description: '', teaser: '' }, fi: { title: '', description: '', teaser: '' }, sv: { title: '', description: '', teaser: '' }, }; const slugMap: Record = { en: '', fi: '', sv: '' }; SUPPORTED_LOCALES.forEach((loc) => { const found = listing.translations.find((t: any) => t.locale === loc); if (found) { translationMap[loc] = { title: found.title || '', description: found.description || '', teaser: found.teaser || '', }; slugMap[loc] = found.slug || ''; } }); setTranslations(translationMap); setSuggestedSlugs(slugMap); const primarySlug = slugMap[currentLocale] || slugMap.en || slugMap.fi || slugMap.sv || listing.translations[0]?.slug || ''; setSlug(primarySlug); setCountry(listing.country || ''); setRegion(listing.region || ''); setCity(listing.city || ''); setStreetAddress(listing.streetAddress || ''); setAddressNote(listing.addressNote || ''); setLatitude(listing.latitude ?? ''); setLongitude(listing.longitude ?? ''); setContactName(listing.contactName || ''); setContactEmail(listing.contactEmail || ''); setMaxGuests(listing.maxGuests ?? ''); setBedrooms(listing.bedrooms ?? ''); setBeds(listing.beds ?? ''); setBathrooms(listing.bathrooms ?? ''); setPriceWeekday(listing.priceWeekdayEuros ?? ''); setPriceWeekend(listing.priceWeekendEuros ?? ''); setHasSauna(listing.hasSauna); setHasFireplace(listing.hasFireplace); setHasWifi(listing.hasWifi); setPetsAllowed(listing.petsAllowed); setByTheLake(listing.byTheLake); setHasAirConditioning(listing.hasAirConditioning); setHasKitchen(listing.hasKitchen); setHasDishwasher(listing.hasDishwasher); setHasWashingMachine(listing.hasWashingMachine); setHasBarbecue(listing.hasBarbecue); setHasMicrowave(listing.hasMicrowave); setHasFreeParking(listing.hasFreeParking); setHasSkiPass(listing.hasSkiPass); setEvChargingAvailable(listing.evChargingAvailable); setEvChargingOnSite(Boolean(listing.evChargingOnSite)); setWheelchairAccessible(Boolean(listing.wheelchairAccessible)); setCalendarUrls((listing.calendarUrls || []).join('\n')); if (listing.images?.length) { const coverIdx = listing.images.find((img: any) => img.isCover)?.order ?? 1; setCoverImageIndex(coverIdx); setSelectedImages( listing.images.map((img: any) => ({ id: img.id, name: img.altText || img.url || `image-${img.id}`, size: img.size || 0, mimeType: img.mimeType || 'image/jpeg', dataUrl: img.url || `/api/images/${img.id}`, isExisting: true, })), ); } } catch (err: any) { setError(err.message || 'Failed to load listing'); } finally { setLoadingListing(false); } } loadListing(); }, [params.id]); function localeStatus(locale: Locale) { const { title, description } = translations[locale]; if (!title && !description) return 'missing'; if (title && description) return 'ready'; return 'partial'; } const aiPrompt = useMemo(() => { const payload = { task: 'Translate this localization file for a holiday rental listing.', instructions: [ 'Preserve meaning, tone, numbers, and any markup.', 'Return valid JSON only with the same keys.', 'Fill missing translations; keep existing text unchanged.', 'Suggest localized slugs based on the title/description; keep them URL-friendly (kebab-case).', 'If teaser or slug is empty, propose one; otherwise keep the existing value.', ], sourceLocale: currentLocale, targetLocales: SUPPORTED_LOCALES.filter((loc) => loc !== currentLocale), locales: SUPPORTED_LOCALES.reduce( (acc, loc) => ({ ...acc, [loc]: { title: translations[loc].title, teaser: translations[loc].teaser, description: translations[loc].description, slug: suggestedSlugs[loc] || slug, }, }), {} as Record, ), }; return JSON.stringify(payload, null, 2); }, [translations, currentLocale, suggestedSlugs, slug]); async function readFileAsDataUrl(file: File): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { resolve({ name: file.name, size: file.size, mimeType: file.type || 'image/jpeg', dataUrl: String(reader.result), isExisting: false, }); }; reader.onerror = reject; reader.readAsDataURL(file); }); } function parseImages(): ImageInput[] { return selectedImages.map((img, idx) => { const isData = img.dataUrl.startsWith('data:'); const base: ImageInput = { mimeType: img.mimeType, altText: img.name.replace(/[-_]/g, ' '), }; if (isData) { base.data = img.dataUrl; } else { base.url = img.dataUrl; } return base; }); } async function removeImageAt(index: number) { const img = selectedImages[index]; setError(null); setMessage(null); const remainingExisting = selectedImages.filter((i) => i.isExisting).length; const hasNewImages = selectedImages.some((i, idx) => idx !== index && !i.isExisting); if (img?.isExisting && remainingExisting <= 1 && !hasNewImages) { setError(t('imageRemoveLastError')); return; } if (img?.isExisting && img.id && remainingExisting > 1) { try { const res = await fetch(`/api/listings/${params.id}/images/${img.id}`, { method: 'DELETE' }); const data = await res.json(); if (!res.ok) { setError(data.error || t('imageRemoveFailed')); return; } } catch (err) { setError(t('imageRemoveFailed')); return; } } const next = selectedImages.filter((_, idx) => idx !== index); setSelectedImages(next); setCoverImageIndex(next.length ? Math.min(coverImageIndex, next.length) : 1); } async function handleFileChange(e: React.ChangeEvent) { const files = Array.from(e.target.files ?? []); setError(null); setMessage(null); if (!files.length) return; if (files.length > MAX_IMAGES) { setError(t('imagesTooMany', { count: MAX_IMAGES })); return; } const tooLarge = files.find((f) => f.size > MAX_IMAGE_BYTES); if (tooLarge) { setError(t('imagesTooLarge', { sizeMb: Math.floor(MAX_IMAGE_BYTES / 1024 / 1024) })); return; } try { const parsed = await Promise.all(files.map(readFileAsDataUrl)); setSelectedImages(parsed); setCoverImageIndex(1); } catch (err) { setError(t('imagesReadFailed')); } } function toggleEvChargingNearby(next: boolean) { setEvChargingAvailable(next); if (!next) { setEvChargingOnSite(false); } } function toggleEvChargingOnSite(next: boolean) { setEvChargingOnSite(next); if (next) { setEvChargingAvailable(true); } } const amenityOptions = [ { key: 'sauna', label: t('amenitySauna'), icon: '๐Ÿง–', checked: hasSauna, toggle: setHasSauna }, { key: 'fireplace', label: t('amenityFireplace'), icon: '๐Ÿ”ฅ', checked: hasFireplace, toggle: setHasFireplace }, { key: 'wifi', label: t('amenityWifi'), icon: '๐Ÿ“ถ', checked: hasWifi, toggle: setHasWifi }, { key: 'pets', label: t('amenityPets'), icon: '๐Ÿพ', checked: petsAllowed, toggle: setPetsAllowed }, { key: 'lake', label: t('amenityLake'), icon: '๐ŸŒŠ', checked: byTheLake, toggle: setByTheLake }, { key: 'ac', label: t('amenityAirConditioning'), icon: 'โ„๏ธ', checked: hasAirConditioning, toggle: setHasAirConditioning }, { key: 'kitchen', label: t('amenityKitchen'), icon: '๐Ÿฝ๏ธ', checked: hasKitchen, toggle: setHasKitchen }, { key: 'dishwasher', label: t('amenityDishwasher'), icon: '๐Ÿงผ', checked: hasDishwasher, toggle: setHasDishwasher }, { key: 'washer', label: t('amenityWashingMachine'), icon: '๐Ÿงบ', checked: hasWashingMachine, toggle: setHasWashingMachine }, { key: 'barbecue', label: t('amenityBarbecue'), icon: '๐Ÿ–', checked: hasBarbecue, toggle: setHasBarbecue }, { key: 'microwave', label: t('amenityMicrowave'), icon: '๐Ÿฒ', checked: hasMicrowave, toggle: setHasMicrowave }, { key: 'parking', label: t('amenityFreeParking'), icon: '๐Ÿ…ฟ๏ธ', checked: hasFreeParking, toggle: setHasFreeParking }, { key: 'ski', label: t('amenitySkiPass'), icon: 'โ›ท๏ธ', checked: hasSkiPass, toggle: setHasSkiPass }, { key: 'ev', label: t('amenityEvNearby'), icon: 'โšก', checked: evChargingAvailable, toggle: toggleEvChargingNearby }, { key: 'ev-onsite', label: t('amenityEvOnSite'), icon: '๐Ÿ”Œ', checked: evChargingOnSite, toggle: toggleEvChargingOnSite }, { key: 'accessible', label: t('amenityWheelchairAccessible'), icon: 'โ™ฟ', checked: wheelchairAccessible, toggle: setWheelchairAccessible }, ]; async function checkSlugAvailability() { const value = slug.trim().toLowerCase(); if (!value) { setSlugStatus('idle'); return; } setSlugStatus('checking'); try { const res = await fetch(`/api/listings/check-slug?slug=${encodeURIComponent(value)}`, { cache: 'no-store' }); const data = await res.json(); if (!res.ok || typeof data.available !== 'boolean') { throw new Error('bad response'); } setSlugStatus(data.available ? 'available' : 'taken'); } catch (err) { setSlugStatus('error'); } } async function autoTranslate() { if (aiLoading) return; setMessage(null); setError(null); setAiLoading(true); try { const res = await fetch('/api/listings/translate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ translations, currentLocale }), }); const data = await res.json(); if (!res.ok || !data.translations) { throw new Error('bad response'); } const incoming = data.translations as Record; setTranslations((prev) => { const next = { ...prev }; SUPPORTED_LOCALES.forEach((loc) => { const found = incoming[loc]; if (found) { next[loc] = { title: found.title ?? prev[loc].title, description: found.description ?? prev[loc].description, teaser: found.teaser ?? prev[loc].teaser, }; setSuggestedSlugs((s) => ({ ...s, [loc]: found.slug || s[loc] || slug })); } }); return next; }); setMessage(t('aiAutoTranslate')); } catch (err) { setError(t('aiApplyError')); } finally { setAiLoading(false); } } function applyAiResponse() { try { const parsed = JSON.parse(aiResponse); if (!parsed?.locales) throw new Error('missing locales'); setTranslations((prev) => { const next = { ...prev }; SUPPORTED_LOCALES.forEach((loc) => { const incoming = parsed.locales[loc]; if (incoming) { next[loc] = { title: incoming.title ?? prev[loc].title, description: incoming.description ?? prev[loc].description, teaser: incoming.teaser ?? prev[loc].teaser, }; if (incoming.slug) { setSuggestedSlugs((s) => ({ ...s, [loc]: incoming.slug })); } } }); return next; }); setMessage(t('aiApplySuccess')); } catch (err) { setError(t('aiApplyError')); } } async function copyAiPrompt() { try { if (!navigator?.clipboard) throw new Error('clipboard unavailable'); await navigator.clipboard.writeText(aiPrompt); setCopyStatus('copied'); setTimeout(() => setCopyStatus('idle'), 1500); } catch (err) { setCopyStatus('error'); setTimeout(() => setCopyStatus('idle'), 2000); } } function buildTranslationEntries() { return SUPPORTED_LOCALES.map((loc) => ({ locale: loc, title: translations[loc].title.trim(), description: translations[loc].description.trim(), teaser: translations[loc].teaser.trim(), slug: (suggestedSlugs[loc] || slug).trim().toLowerCase(), })).filter((t) => t.title && t.description); } async function submitListing(saveDraft: boolean, e?: React.FormEvent) { if (e) e.preventDefault(); setMessage(null); setError(null); setLoading(true); try { const translationEntries = buildTranslationEntries(); const missing: string[] = []; if (!slug.trim()) missing.push(t('slugLabel')); if (!saveDraft && translationEntries.length === 0) missing.push(t('translationMissing')); if (!saveDraft && !country.trim()) missing.push(t('countryLabel')); if (!saveDraft && !region.trim()) missing.push(t('regionLabel')); if (!saveDraft && !city.trim()) missing.push(t('cityLabel')); if (!saveDraft && !streetAddress.trim()) missing.push(t('streetAddressLabel')); if (!saveDraft && !contactName.trim()) missing.push(t('contactNameLabel')); if (!saveDraft && !contactEmail.trim()) missing.push(t('contactEmailLabel')); if (missing.length) { setError(t('missingFields', { fields: missing.join(', ') })); setLoading(false); return; } const res = await fetch(`/api/listings/${params.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ saveDraft, slug, translations: translationEntries.map((t) => ({ ...t, teaser: t.teaser || null, })), country, region, city, streetAddress, addressNote, latitude: latitude === '' ? null : latitude, longitude: longitude === '' ? null : longitude, contactName, contactEmail, maxGuests, bedrooms, beds, bathrooms, priceWeekdayEuros: priceWeekday === '' ? null : Math.round(Number(priceWeekday)), priceWeekendEuros: priceWeekend === '' ? null : Math.round(Number(priceWeekend)), hasSauna, hasFireplace, hasWifi, petsAllowed, byTheLake, hasAirConditioning, hasKitchen, hasDishwasher, hasWashingMachine, hasBarbecue, hasMicrowave, hasFreeParking, hasSkiPass, evChargingAvailable, evChargingOnSite, wheelchairAccessible, coverImageIndex, images: selectedImages.length ? parseImages() : undefined, calendarUrls, }), }); const data = await res.json(); if (!res.ok) { setError(data.error || 'Failed to update listing'); } else { setMessage(saveDraft ? t('createListingSuccess', { id: data.listing.id, status: 'DRAFT' }) : t('createListingSuccess', { id: data.listing.id, status: data.listing.status })); setInitialStatus(data.listing.status); } } catch (err) { setError('Failed to update listing'); } finally { setLoading(false); } } if (loadingListing) { return (

{t('createListingTitle')}

{t('loading')}

); } return (

{t('createListingTitle')}

{t('myListingsTitle')}
{initialStatus && initialStatus !== ListingStatus.PUBLISHED ? (
{t('statusLabel')}: {initialStatus}
) : null}
submitListing(false, e)} style={{ display: 'grid', gap: 10 }}>
{t('languageTabsLabel')} {t('languageTabsHint')}
{SUPPORTED_LOCALES.map((loc) => { const status = localeStatus(loc); const badge = status === 'ready' ? t('localeReady') : status === 'partial' ? t('localePartial') : t('localeMissing'); return ( ); })}

{t('localeSectionTitle')}