'use client'; import { useEffect, useState } from 'react'; import { useI18n } from '../../components/I18nProvider'; import type { Locale } from '../../../lib/i18n'; type ImageInput = { data: string; mimeType: string; altText?: string }; type SelectedImage = { name: string; size: number; mimeType: string; dataUrl: string; }; 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 NewListingPage() { 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 [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(4); const [bedrooms, setBedrooms] = useState(2); const [beds, setBeds] = useState(3); const [bathrooms, setBathrooms] = useState(1); const [price, setPrice] = 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 [evCharging, setEvCharging] = useState<'NONE' | 'FREE' | 'PAID'>('NONE'); 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 [isAuthed, setIsAuthed] = useState(false); const [aiLoading, setAiLoading] = useState(false); const [slugStatus, setSlugStatus] = useState<'idle' | 'checking' | 'available' | 'taken' | 'error'>('idle'); useEffect(() => { setCurrentLocale(uiLocale as Locale); }, [uiLocale]); useEffect(() => { // simple check if session exists fetch('/api/auth/me', { cache: 'no-store' }) .then((res) => res.json()) .then((data) => setIsAuthed(Boolean(data.user))) .catch(() => setIsAuthed(false)); }, []); 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), }); }; reader.onerror = reject; reader.readAsDataURL(file); }); } 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')); } } 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 }, ]; function updateTranslation(locale: Locale, field: keyof LocaleFields, value: string) { setTranslations((prev) => ({ ...prev, [locale]: { ...prev[locale], [field]: value }, })); } function localeStatus(locale: Locale) { const { title, description } = translations[locale]; if (!title && !description) return 'missing'; if (title && description) return 'ready'; return 'partial'; } 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 aiLoc = incoming?.[loc]; next[loc] = { title: prev[loc].title || aiLoc?.title || '', teaser: prev[loc].teaser || aiLoc?.teaser || '', description: prev[loc].description || aiLoc?.description || '', }; }); return next; }); setMessage(t('aiAutoSuccess')); } catch (err) { setError(t('aiAutoError')); } finally { setAiLoading(false); } } function parseImages(): ImageInput[] { return selectedImages.map((img) => ({ data: img.dataUrl, mimeType: img.mimeType, altText: img.name.replace(/[-_]/g, ' '), })); } 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 onSubmit(e: React.FormEvent) { e.preventDefault(); setMessage(null); setError(null); setLoading(true); try { const translationEntries = SUPPORTED_LOCALES.map((loc) => ({ locale: loc, title: translations[loc].title.trim(), description: translations[loc].description.trim(), teaser: translations[loc].teaser.trim(), })).filter((t) => t.title && t.description); if (translationEntries.length === 0) { setError(t('translationMissing')); setLoading(false); return; } if (selectedImages.length === 0) { setError(t('imagesRequired')); setLoading(false); return; } const res = await fetch('/api/listings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ slug, translations: translationEntries.map((t) => ({ ...t, slug, teaser: t.teaser || null, })), country, region, city, streetAddress, addressNote, latitude: latitude === '' ? null : latitude, longitude: longitude === '' ? null : longitude, contactName, contactEmail, maxGuests, bedrooms, beds, bathrooms, priceHintPerNightEuros: price === '' ? null : Math.round(Number(price)), hasSauna, hasFireplace, hasWifi, petsAllowed, byTheLake, hasAirConditioning, hasKitchen, hasDishwasher, hasWashingMachine, hasBarbecue, evCharging, coverImageIndex, images: parseImages(), calendarUrls, }), }); const data = await res.json(); if (!res.ok) { setError(data.error || 'Failed to create listing'); } else { setMessage(t('createListingSuccess', { id: data.listing.id, status: data.listing.status })); setSlug(''); setTranslations({ en: { title: '', description: '', teaser: '' }, fi: { title: '', description: '', teaser: '' }, sv: { title: '', description: '', teaser: '' }, }); setRegion(''); setCity(''); setStreetAddress(''); setAddressNote(''); setLatitude(''); setLongitude(''); setContactName(''); setContactEmail(''); setCalendarUrls(''); setSelectedImages([]); setCoverImageIndex(1); } } catch (err) { setError('Failed to create listing'); } finally { setLoading(false); } } if (!isAuthed) { return (

{t('createListingTitle')}

{t('loginToCreate')}

); } return (

{t('createListingTitle')}

{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 ( ); })}