'use client' import { useState, useEffect } from 'react' import dynamic from 'next/dynamic' import { api } from '@/lib/api' import type { Tag, Feature } from '@/types' const MapPicker = dynamic(() => import('./MapPicker'), { ssr: false }) /** Пропсы компонента формы создания/редактирования места/студии */ interface PlaceFormProps { /** Тип объекта: место или студия */ type: 'place' | 'studio' /** Колбэк после успешного сохранения */ onSuccess?: () => void /** Данные места для редактирования */ place?: import('@/types').Place } /** * Форма создания нового места или студии. * Собирает название, описание, адрес, координаты, теги, характеристики и обложку. * Для студий дополнительно запрашивает цену за час и минимальное количество часов. * Перед сохранением загружает обложку через presigned URL. * @param type - Тип объекта: 'place' — место, 'studio' — студия */ export default function PlaceForm({ type, onSuccess, place }: PlaceFormProps) { const isEdit = !!place const [tags, setTags] = useState([]) const [features, setFeatures] = useState([]) const [loading, setLoading] = useState(false) const [error, setError] = useState('') const [title, setTitle] = useState(place?.title || '') const [description, setDescription] = useState(place?.description || '') const [address, setAddress] = useState(place?.address || '') const [accessInfo, setAccessInfo] = useState(place?.access_info || '') const [selectedTags, setSelectedTags] = useState(place?.tags?.map((t) => t.id) || []) const [selectedFeatures, setSelectedFeatures] = useState(place?.features?.map((f) => f.id) || []) const [lat, setLat] = useState(place?.lat?.toString() || '55.7558') const [lng, setLng] = useState(place?.lng?.toString() || '37.6173') const [hourlyRate, setHourlyRate] = useState(place?.hourly_rate?.toString() || '') const [minHours, setMinHours] = useState(place?.min_hours?.toString() || '1') const [coverFile, setCoverFile] = useState(null) const [coverPreview, setCoverPreview] = useState(place?.cover_image || '') const [showMapPicker, setShowMapPicker] = useState(false) const [saved, setSaved] = useState<'draft' | 'submitted' | null>(null) useEffect(() => { api.get('/tags').then(setTags).catch(() => setError('Ошибка загрузки тегов')) api.get('/features').then(setFeatures).catch(() => setError('Ошибка загрузки характеристик')) }, []) /** Освобождает blob-URL обложки при размонтировании или смене preview */ useEffect(() => { return () => { if (coverPreview) { URL.revokeObjectURL(coverPreview) } } }, [coverPreview]) const handleMapPick = (pickLat: number, pickLng: number, pickAddress?: string) => { setLat(pickLat.toFixed(6)) setLng(pickLng.toFixed(6)) if (pickAddress) setAddress(pickAddress) setShowMapPicker(false) } const submitPlace = async (status?: string) => { setLoading(true) setError('') try { let coverImage: string | undefined if (coverFile) { const formData = new FormData() formData.append('file', coverFile) const res = await api.upload<{ file_url: string }>('/upload', formData) coverImage = res.file_url } const parsedLat = parseFloat(lat) const parsedLng = parseFloat(lng) if (isNaN(parsedLat) || isNaN(parsedLng)) { setError('Укажите корректные координаты') return } const body: Record = { title, description: description || undefined, address: address || undefined, lat: parsedLat, lng: parsedLng, access_info: accessInfo || undefined, tags: selectedTags, features: selectedFeatures, } if (!isEdit) body.type = type if (coverImage) body.cover_image = coverImage if (type === 'studio') { body.hourly_rate = hourlyRate !== '' ? parseInt(hourlyRate) : undefined const parsedMinHours = parseInt(minHours) body.min_hours = isNaN(parsedMinHours) ? 1 : parsedMinHours body.currency = 'RUB' } if (status) body.status = status if (isEdit) { await api.patch(`/places/${place.id}`, body) if (onSuccess) onSuccess() } else { await api.post('/places', body) setSaved(status === 'draft' ? 'draft' : 'submitted') } } catch (err: any) { setError(err.message || 'Ошибка при создании') } finally { setLoading(false) } } const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); submitPlace() } const handleSaveDraft = () => submitPlace('draft') const toggleTag = (id: string) => { setSelectedTags((prev) => prev.includes(id) ? prev.filter((t) => t !== id) : [...prev, id] ) } const toggleFeature = (id: string) => { setSelectedFeatures((prev) => prev.includes(id) ? prev.filter((f) => f !== id) : [...prev, id] ) } if (saved) { return (

{saved === 'draft' ? 'Черновик сохранён' : 'Место отправлено на модерацию'}

Добавить ещё
) } return (

{isEdit ? 'Редактировать' : type === 'studio' ? 'Добавить студию' : 'Добавить место'}

{error &&

{error}

}
setTitle(e.target.value)} className="w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white outline-none focus:border-green-500" required />