"use client"; import { useEffect, useMemo, 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< Record >({ 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 [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 [isAuthed, setIsAuthed] = useState(false); const [aiResponse, setAiResponse] = useState(""); const [copyStatus, setCopyStatus] = useState<"idle" | "copied" | "error">( "idle", ); const [aiLoading, setAiLoading] = useState(false); const [showManualAi, setShowManualAi] = useState(false); const [slugStatus, setSlugStatus] = useState< "idle" | "checking" | "available" | "taken" | "error" >("idle"); const [suggestedSlugs, setSuggestedSlugs] = useState>({ en: "", fi: "", sv: "", }); 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")); } } 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, }, ]; 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"; } 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 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< Locale, LocaleFields & { slug?: string } >; 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; }); setSuggestedSlugs((prev) => { const next = { ...prev }; SUPPORTED_LOCALES.forEach((loc) => { if (incoming?.[loc]?.slug) { next[loc] = incoming[loc].slug as string; } }); return next; }); setMessage(t("aiAutoSuccess")); setShowManualAi(false); } catch (err) { setError(t("aiAutoError")); setShowManualAi(true); } finally { setAiLoading(false); } } function applyAiResponse() { setMessage(null); setError(null); try { const parsed = JSON.parse(aiResponse); const locales = parsed?.locales || parsed; if (!locales || typeof locales !== "object") { throw new Error("Missing locales"); } const next = { ...translations }; SUPPORTED_LOCALES.forEach((loc) => { if (locales[loc]) { const incoming = locales[loc]; next[loc] = { title: typeof incoming.title === "string" && incoming.title ? incoming.title : next[loc].title, teaser: typeof incoming.teaser === "string" && incoming.teaser ? incoming.teaser : next[loc].teaser, description: typeof incoming.description === "string" && incoming.description ? incoming.description : next[loc].description, }; } }); setSuggestedSlugs((prev) => { const next = { ...prev }; SUPPORTED_LOCALES.forEach((loc) => { const incoming = locales[loc]; if (incoming && typeof incoming.slug === "string" && incoming.slug) { next[loc] = incoming.slug; } }); return next; }); setTranslations(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 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 submitListing(saveDraft: boolean, e?: React.FormEvent) { if (e) 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); 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 (!saveDraft && selectedImages.length === 0) missing.push(t("imagesLabel")); if (missing.length) { setError(t("missingFields", { fields: missing.join(", ") })); setLoading(false); return; } const res = await fetch("/api/listings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ saveDraft, 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, 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: parseImages(), calendarUrls, }), }); const data = await res.json(); if (!res.ok) { setError(data.error || "Failed to create listing"); } else { setMessage( saveDraft ? t("createListingSuccess", { id: data.listing.id, status: "DRAFT", }) : t("createListingSuccess", { id: data.listing.id, status: data.listing.status, }), ); if (!saveDraft) { setSlug(""); setTranslations({ en: { title: "", description: "", teaser: "" }, fi: { title: "", description: "", teaser: "" }, sv: { title: "", description: "", teaser: "" }, }); setMaxGuests(4); setBedrooms(2); setBeds(3); setBathrooms(1); setPriceWeekday(""); setPriceWeekend(""); setHasSauna(true); setHasFireplace(true); setHasWifi(true); setPetsAllowed(false); setByTheLake(false); setHasAirConditioning(false); setHasKitchen(true); setHasDishwasher(false); setHasWashingMachine(false); setHasBarbecue(false); setHasMicrowave(false); setHasFreeParking(false); setHasSkiPass(false); setEvChargingAvailable(false); setEvChargingOnSite(false); setWheelchairAccessible(false); 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")}

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")}