lomavuokraus/app/listings/new/page.tsx
Tero Halla-aho 0b5ca0a190
Some checks failed
CI / checks (push) Has been cancelled
CI / checks (pull_request) Has been cancelled
Fix draft listing save/reset and allow viewing drafts
2025-12-15 21:23:25 +02:00

749 lines
31 KiB
TypeScript

'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<Locale>(uiLocale as Locale);
const [translations, setTranslations] = useState<Record<Locale, LocaleFields>>({
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<number | ''>('');
const [longitude, setLongitude] = useState<number | ''>('');
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<number | ''>('');
const [priceWeekend, setPriceWeekend] = useState<number | ''>('');
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<boolean>(false);
const [calendarUrls, setCalendarUrls] = useState('');
const [selectedImages, setSelectedImages] = useState<SelectedImage[]>([]);
const [coverImageIndex, setCoverImageIndex] = useState(1);
const [message, setMessage] = useState<string | null>(null);
const [error, setError] = useState<string | null>(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<Record<Locale, string>>({ 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<SelectedImage> {
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<HTMLInputElement>) {
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 },
{ 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('amenityEvAvailable'), icon: '⚡', checked: evChargingAvailable, toggle: setEvChargingAvailable },
];
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<Locale, LocaleFields & { slug?: string }>
),
};
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,
evChargingAvailable,
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);
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 (
<main className="panel" style={{ maxWidth: 720, margin: '40px auto' }}>
<h1>{t('createListingTitle')}</h1>
<p>{t('loginToCreate')}</p>
</main>
);
}
return (
<main className="panel" style={{ maxWidth: 1100, margin: '40px auto' }}>
<h1>{t('createListingTitle')}</h1>
<form onSubmit={(e) => submitListing(false, e)} style={{ display: 'grid', gap: 10 }}>
<div>
<div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap', marginBottom: 6 }}>
<strong>{t('languageTabsLabel')}</strong>
<span style={{ color: '#cbd5e1', fontSize: 13 }}>{t('languageTabsHint')}</span>
</div>
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
{SUPPORTED_LOCALES.map((loc) => {
const status = localeStatus(loc);
const badge =
status === 'ready' ? t('localeReady') : status === 'partial' ? t('localePartial') : t('localeMissing');
return (
<button
key={loc}
type="button"
onClick={() => setCurrentLocale(loc)}
className={`button ${currentLocale === loc ? '' : 'secondary'}`}
>
{loc.toUpperCase()} · {badge}
</button>
);
})}
</div>
</div>
<div
style={{
display: 'grid',
gap: 12,
gridTemplateColumns: 'repeat(auto-fit, minmax(340px, 1fr))',
alignItems: 'start',
}}
>
<div className="panel" style={{ display: 'grid', gap: 10, border: '1px solid rgba(148,163,184,0.3)', background: 'rgba(255,255,255,0.02)' }}>
<h3 style={{ margin: 0 }}>{t('localeSectionTitle')}</h3>
<label>
{t('titleLabel')}
<input value={translations[currentLocale].title} onChange={(e) => updateTranslation(currentLocale, 'title', e.target.value)} />
</label>
<label>
{t('descriptionLabel')}
<textarea value={translations[currentLocale].description} onChange={(e) => updateTranslation(currentLocale, 'description', e.target.value)} rows={6} />
</label>
<label>
{t('teaserLabel')}
<input
value={translations[currentLocale].teaser}
onChange={(e) => updateTranslation(currentLocale, 'teaser', e.target.value)}
placeholder={t('teaserHelp')}
/>
<div style={{ color: '#cbd5e1', fontSize: 12 }}>{t('aiOptionalHint')}</div>
</label>
<label>
{t('slugLabel')}
<input
value={suggestedSlugs[currentLocale] || slug}
onChange={(e) => {
const val = e.target.value;
setSlug(val);
setSuggestedSlugs((prev) => ({ ...prev, [currentLocale]: val }));
}}
onBlur={checkSlugAvailability}
/>
<div style={{ color: '#cbd5e1', fontSize: 12, marginTop: 4 }}>{t('slugPreview', { url: `${process.env.NEXT_PUBLIC_SITE_URL ?? 'https://lomavuokraus.fi'}/${suggestedSlugs[currentLocale] || slug || 'your-slug-here'}` })}</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 4, flexWrap: 'wrap' }}>
<span style={{ color: '#cbd5e1', fontSize: 12 }}>{t('slugHelp')}</span>
<span style={{ color: '#cbd5e1', fontSize: 12 }}>{t('aiOptionalHint')}</span>
{slugStatus === 'checking' ? <span style={{ color: '#cbd5e1', fontSize: 12 }}>{t('slugChecking')}</span> : null}
{slugStatus === 'available' ? <span style={{ color: '#34d399', fontSize: 12 }}>{t('slugAvailable')}</span> : null}
{slugStatus === 'taken' ? <span style={{ color: '#f87171', fontSize: 12 }}>{t('slugTaken')}</span> : null}
{slugStatus === 'error' ? <span style={{ color: '#facc15', fontSize: 12 }}>{t('slugCheckError')}</span> : null}
</div>
</label>
</div>
<div className="panel" style={{ border: '1px solid rgba(148,163,184,0.3)', background: 'rgba(255,255,255,0.02)' }}>
<h3 style={{ marginTop: 0 }}>{t('aiHelperTitle')}</h3>
<p style={{ color: '#cbd5e1', marginTop: 4 }}>{t('aiAutoExplain')}</p>
<div style={{ display: 'flex', gap: 10, marginTop: 8, flexWrap: 'wrap', alignItems: 'center' }}>
<button type="button" className="button secondary" onClick={autoTranslate} disabled={aiLoading}>
{aiLoading ? t('aiAutoTranslating') : t('aiAutoTranslate')}
</button>
<span style={{ color: '#cbd5e1', fontSize: 13 }}>{t('aiHelperNote')}</span>
</div>
<details open={showManualAi} style={{ marginTop: 12 }}>
<summary style={{ cursor: 'pointer', color: '#cbd5e1' }}>
{showManualAi ? t('aiManualLead') : t('aiManualLead')}
</summary>
<div style={{ marginTop: 8, display: 'grid', gap: 6 }}>
<div style={{ display: 'grid', gap: 6 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, flexWrap: 'wrap' }}>
<span>{t('aiPromptLabel')}</span>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
{copyStatus === 'copied' ? (
<span style={{ color: '#34d399', fontSize: 13 }}>{t('aiPromptCopied')}</span>
) : null}
{copyStatus === 'error' ? <span style={{ color: '#f87171', fontSize: 13 }}>{t('aiCopyError')}</span> : null}
<button
type="button"
className="button secondary"
onClick={copyAiPrompt}
style={{ minHeight: 0, padding: '8px 12px' }}
>
{t('aiCopyPrompt')}
</button>
</div>
</div>
<textarea value={aiPrompt} readOnly rows={10} style={{ fontFamily: 'monospace' }} />
</div>
<label style={{ display: 'grid', gap: 6 }}>
<span>{t('aiResponseLabel')}</span>
<textarea
value={aiResponse}
onChange={(e) => setAiResponse(e.target.value)}
rows={6}
placeholder='{"locales":{"fi":{"title":"..."}}}'
style={{ fontFamily: 'monospace' }}
/>
</label>
<div style={{ display: 'flex', gap: 10, marginTop: 4, flexWrap: 'wrap' }}>
<button type="button" className="button secondary" onClick={() => applyAiResponse()}>
{t('aiApply')}
</button>
</div>
</div>
</details>
</div>
</div>
<div style={{ display: 'grid', gap: 8, gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))' }}>
<label>
{t('countryLabel')}
<input value={country} onChange={(e) => setCountry(e.target.value)} />
</label>
<label>
{t('regionLabel')}
<input value={region} onChange={(e) => setRegion(e.target.value)} />
</label>
<label>
{t('cityLabel')}
<input value={city} onChange={(e) => setCity(e.target.value)} />
</label>
</div>
<label>
{t('streetAddressLabel')}
<input value={streetAddress} onChange={(e) => setStreetAddress(e.target.value)} />
</label>
<label>
{t('addressNoteLabel')}
<input value={addressNote} onChange={(e) => setAddressNote(e.target.value)} placeholder={t('addressNotePlaceholder')} />
</label>
<label>
{t('contactNameLabel')}
<input value={contactName} onChange={(e) => setContactName(e.target.value)} />
</label>
<label>
{t('contactEmailLabel')}
<input type="email" value={contactEmail} onChange={(e) => setContactEmail(e.target.value)} />
</label>
<div style={{ display: 'grid', gap: 8, gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))' }}>
<label>
{t('maxGuestsLabel')}
<select value={maxGuests} onChange={(e) => setMaxGuests(Number(e.target.value))}>
{Array.from({ length: 30 }, (_, i) => i + 1).map((n) => (
<option key={n} value={n}>
{n}
</option>
))}
</select>
</label>
<label>
{t('bedroomsLabel')}
<select value={bedrooms} onChange={(e) => setBedrooms(Number(e.target.value))}>
{[0, 1, 2, 3, 4, 5, 6].map((n) => (
<option key={n} value={n}>
{n}
</option>
))}
</select>
</label>
<label>
{t('bedsLabel')}
<select value={beds} onChange={(e) => setBeds(Number(e.target.value))}>
{[1, 2, 3, 4, 5, 6, 8, 10].map((n) => (
<option key={n} value={n}>
{n}
</option>
))}
</select>
</label>
<label>
{t('bathroomsLabel')}
<select value={bathrooms} onChange={(e) => setBathrooms(Number(e.target.value))}>
{[1, 2, 3, 4].map((n) => (
<option key={n} value={n}>
{n}
</option>
))}
</select>
</label>
</div>
<div style={{ display: 'grid', gap: 8, gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))' }}>
<label>
{t('priceWeekdayLabel')}
<input
type="number"
value={priceWeekday}
onChange={(e) => setPriceWeekday(e.target.value === '' ? '' : Number(e.target.value))}
min={0}
step="10"
placeholder="120"
/>
</label>
<label>
{t('priceWeekendLabel')}
<input
type="number"
value={priceWeekend}
onChange={(e) => setPriceWeekend(e.target.value === '' ? '' : Number(e.target.value))}
min={0}
step="10"
placeholder="140"
/>
</label>
</div>
<div style={{ color: '#cbd5e1', fontSize: 12 }}>{t('priceHintHelp')}</div>
<label style={{ gridColumn: '1 / -1' }}>
{t('calendarUrlsLabel')}
<textarea
value={calendarUrls}
onChange={(e) => setCalendarUrls(e.target.value)}
placeholder="https://example.com/calendar.ics"
rows={3}
/>
<div style={{ color: '#cbd5e1', fontSize: 12 }}>{t('calendarUrlsHelp')}</div>
</label>
<div style={{ display: 'grid', gap: 8, gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))' }}>
<label>
{t('latitudeLabel')}
<input type="number" value={latitude} onChange={(e) => setLatitude(e.target.value === '' ? '' : Number(e.target.value))} step="0.000001" />
</label>
<label>
{t('longitudeLabel')}
<input type="number" value={longitude} onChange={(e) => setLongitude(e.target.value === '' ? '' : Number(e.target.value))} step="0.000001" />
</label>
</div>
<div className="amenity-grid">
{amenityOptions.map((option) => (
<button
key={option.key}
type="button"
className={`amenity-option ${option.checked ? 'selected' : ''}`}
aria-pressed={option.checked}
onClick={() => option.toggle(!option.checked)}
>
<div className="amenity-option-meta">
<span aria-hidden className="amenity-emoji">
{option.icon}
</span>
<span className="amenity-name">{option.label}</span>
</div>
<span className="amenity-check" aria-hidden>
{option.checked ? '✓' : ''}
</span>
</button>
))}
</div>
<div style={{ display: 'grid', gap: 8, gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))' }}>
<label>
{t('imagesLabel')}
<input type="file" accept="image/*" multiple onChange={handleFileChange} />
<div style={{ color: '#cbd5e1', fontSize: 12, marginTop: 4 }}>{t('imagesHelp', { count: MAX_IMAGES, sizeMb: Math.floor(MAX_IMAGE_BYTES / 1024 / 1024) })}</div>
</label>
<label>
{t('coverImageLabel')}
<input type="number" min={1} max={selectedImages.length || 1} value={coverImageIndex} onChange={(e) => setCoverImageIndex(Number(e.target.value))} />
<div style={{ color: '#cbd5e1', fontSize: 12, marginTop: 4 }}>{t('coverImageHelp')}</div>
</label>
</div>
{selectedImages.length > 0 ? (
<div style={{ display: 'grid', gap: 8, gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))' }}>
{selectedImages.map((img, idx) => (
<div key={img.name + idx} style={{ border: '1px solid rgba(148,163,184,0.3)', padding: 8, borderRadius: 8 }}>
<div style={{ fontWeight: 600 }}>{img.name}</div>
<div style={{ fontSize: 12, color: '#cbd5e1' }}>
{(img.size / 1024).toFixed(0)} KB · {img.mimeType || 'image/jpeg'}
</div>
<div style={{ fontSize: 12, marginTop: 4 }}>{t('coverChoice', { index: idx + 1 })}</div>
</div>
))}
</div>
) : null}
<div style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
<button className="button secondary" type="button" disabled={loading} onClick={(e) => submitListing(true, e)}>
{loading ? t('saving') : t('saveDraft')}
</button>
<button className="button" type="submit" disabled={loading}>
{loading ? t('submittingListing') : t('submitListing')}
</button>
</div>
</form>
{message ? <p style={{ marginTop: 12, color: 'green' }}>{message}</p> : null}
{error ? <p style={{ marginTop: 12, color: 'red' }}>{error}</p> : null}
</main>
);
}