|
@@ -1,11 +1,9 @@
|
|
|
'use client'
|
|
'use client'
|
|
|
|
|
|
|
|
import { useEffect, useState, useCallback } from 'react'
|
|
import { useEffect, useState, useCallback } from 'react'
|
|
|
-import dynamic from 'next/dynamic'
|
|
|
|
|
-import { api, ApiRequestError } from '@/lib/api'
|
|
|
|
|
-import type { Place, Tag, Feature } from '@/types'
|
|
|
|
|
-
|
|
|
|
|
-const MapPicker = dynamic(() => import('@/components/MapPicker'), { ssr: false })
|
|
|
|
|
|
|
+import { useRouter } from 'next/navigation'
|
|
|
|
|
+import { api } from '@/lib/api'
|
|
|
|
|
+import type { Place } from '@/types'
|
|
|
|
|
|
|
|
const STATUS_LABELS: Record<string, { label: string; color: string }> = {
|
|
const STATUS_LABELS: Record<string, { label: string; color: string }> = {
|
|
|
published: { label: 'Опубликовано', color: 'text-green-400' },
|
|
published: { label: 'Опубликовано', color: 'text-green-400' },
|
|
@@ -15,239 +13,6 @@ const STATUS_LABELS: Record<string, { label: string; color: string }> = {
|
|
|
draft: { label: 'Черновик', color: 'text-white/40' },
|
|
draft: { label: 'Черновик', color: 'text-white/40' },
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function PlaceEditModal({ placeId, onClose, onSaved }: { placeId: string; onClose: () => void; onSaved: () => void }) {
|
|
|
|
|
- const [place, setPlace] = useState<Place | null>(null)
|
|
|
|
|
- const [loadingPlace, setLoadingPlace] = useState(true)
|
|
|
|
|
- const [tags, setTags] = useState<Tag[]>([])
|
|
|
|
|
- const [features, setFeatures] = useState<Feature[]>([])
|
|
|
|
|
- const [saving, setSaving] = useState(false)
|
|
|
|
|
- const [error, setError] = useState('')
|
|
|
|
|
-
|
|
|
|
|
- const [title, setTitle] = useState('')
|
|
|
|
|
- const [description, setDescription] = useState('')
|
|
|
|
|
- const [address, setAddress] = useState('')
|
|
|
|
|
- const [accessInfo, setAccessInfo] = useState('')
|
|
|
|
|
- const [selectedTags, setSelectedTags] = useState<string[]>([])
|
|
|
|
|
- const [selectedFeatures, setSelectedFeatures] = useState<string[]>([])
|
|
|
|
|
- const [lat, setLat] = useState('')
|
|
|
|
|
- const [lng, setLng] = useState('')
|
|
|
|
|
- const [coverFile, setCoverFile] = useState<File | null>(null)
|
|
|
|
|
- const [coverPreview, setCoverPreview] = useState('')
|
|
|
|
|
- const [hourlyRate, setHourlyRate] = useState('')
|
|
|
|
|
- const [minHours, setMinHours] = useState('1')
|
|
|
|
|
- const [showMapPicker, setShowMapPicker] = useState(false)
|
|
|
|
|
-
|
|
|
|
|
- useEffect(() => {
|
|
|
|
|
- api.get<Place>(`/places/${placeId}`)
|
|
|
|
|
- .then((p) => {
|
|
|
|
|
- setPlace(p)
|
|
|
|
|
- setTitle(p.title || '')
|
|
|
|
|
- setDescription(p.description || '')
|
|
|
|
|
- setAddress(p.address || '')
|
|
|
|
|
- setAccessInfo(p.access_info || '')
|
|
|
|
|
- setSelectedTags(p.tags?.map((t) => t.id) || [])
|
|
|
|
|
- setSelectedFeatures(p.features?.map((f) => f.id) || [])
|
|
|
|
|
- setLat(p.lat?.toString() || '')
|
|
|
|
|
- setLng(p.lng?.toString() || '')
|
|
|
|
|
- setCoverPreview(p.cover_image || '')
|
|
|
|
|
- setHourlyRate(p.hourly_rate?.toString() || '')
|
|
|
|
|
- setMinHours(p.min_hours?.toString() || '1')
|
|
|
|
|
- })
|
|
|
|
|
- .catch(() => onClose())
|
|
|
|
|
- .finally(() => setLoadingPlace(false))
|
|
|
|
|
-
|
|
|
|
|
- api.get<Tag[]>('/tags').then(setTags).catch(() => setError('Ошибка загрузки тегов'))
|
|
|
|
|
- api.get<Feature[]>('/features').then(setFeatures).catch(() => setError('Ошибка загрузки характеристик'))
|
|
|
|
|
- }, [placeId, onClose])
|
|
|
|
|
-
|
|
|
|
|
- const handleMapPick = (pickLat: number, pickLng: number, pickAddress?: string) => {
|
|
|
|
|
- setLat(pickLat.toFixed(6))
|
|
|
|
|
- setLng(pickLng.toFixed(6))
|
|
|
|
|
- if (pickAddress) setAddress(pickAddress)
|
|
|
|
|
- setShowMapPicker(false)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const handleSubmit = async (e: React.FormEvent) => {
|
|
|
|
|
- e.preventDefault()
|
|
|
|
|
- setSaving(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 body: Record<string, unknown> = {
|
|
|
|
|
- title,
|
|
|
|
|
- description: description || undefined,
|
|
|
|
|
- address: address || undefined,
|
|
|
|
|
- lat: parseFloat(lat),
|
|
|
|
|
- lng: parseFloat(lng),
|
|
|
|
|
- access_info: accessInfo || undefined,
|
|
|
|
|
- tags: selectedTags,
|
|
|
|
|
- features: selectedFeatures,
|
|
|
|
|
- cover_image: coverImage,
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (place?.type === 'studio') {
|
|
|
|
|
- body.hourly_rate = hourlyRate ? parseInt(hourlyRate) : undefined
|
|
|
|
|
- body.min_hours = parseInt(minHours) || 1
|
|
|
|
|
- body.currency = 'RUB'
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- await api.patch(`/places/${placeId}`, body)
|
|
|
|
|
- onSaved()
|
|
|
|
|
- } catch (err: any) {
|
|
|
|
|
- setError(err instanceof ApiRequestError ? err.message : 'Ошибка при сохранении')
|
|
|
|
|
- } finally {
|
|
|
|
|
- setSaving(false)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- 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]
|
|
|
|
|
- )
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return (
|
|
|
|
|
- <>
|
|
|
|
|
- <div className="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-black/60 pt-10 pb-10" onClick={onClose}>
|
|
|
|
|
- <div className="w-full max-w-2xl rounded-xl bg-[#1e293b] shadow-xl" onClick={(e) => e.stopPropagation()}>
|
|
|
|
|
- {loadingPlace ? (
|
|
|
|
|
- <div className="flex items-center justify-center p-8">
|
|
|
|
|
- <p className="text-white/40">Загрузка...</p>
|
|
|
|
|
- </div>
|
|
|
|
|
- ) : (
|
|
|
|
|
- <form onSubmit={handleSubmit} className="space-y-5 p-6">
|
|
|
|
|
- <h2 className="text-2xl font-bold text-white">Редактировать</h2>
|
|
|
|
|
-
|
|
|
|
|
- {error && <p className="rounded-lg bg-red-500/10 p-3 text-sm text-red-400">{error}</p>}
|
|
|
|
|
-
|
|
|
|
|
- <div>
|
|
|
|
|
- <label className="mb-1 block text-sm text-white/60">Название *</label>
|
|
|
|
|
- <input value={title} onChange={(e) => 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 />
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div>
|
|
|
|
|
- <label className="mb-1 block text-sm text-white/60">Описание</label>
|
|
|
|
|
- <textarea value={description} onChange={(e) => setDescription(e.target.value)}
|
|
|
|
|
- rows={3} className="w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white outline-none focus:border-green-500" />
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div>
|
|
|
|
|
- <label className="mb-1 block text-sm text-white/60">Адрес</label>
|
|
|
|
|
- <input value={address} onChange={(e) => setAddress(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" />
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div className="grid grid-cols-5 gap-4">
|
|
|
|
|
- <div className="col-span-2">
|
|
|
|
|
- <label className="mb-1 block text-sm text-white/60">Широта</label>
|
|
|
|
|
- <input type="number" step="any" value={lat} onChange={(e) => setLat(e.target.value)}
|
|
|
|
|
- className="w-full rounded-lg border border-white/10 bg-white/5 px-3 py-3 text-white outline-none focus:border-green-500" />
|
|
|
|
|
- </div>
|
|
|
|
|
- <div className="col-span-2">
|
|
|
|
|
- <label className="mb-1 block text-sm text-white/60">Долгота</label>
|
|
|
|
|
- <input type="number" step="any" value={lng} onChange={(e) => setLng(e.target.value)}
|
|
|
|
|
- className="w-full rounded-lg border border-white/10 bg-white/5 px-3 py-3 text-white outline-none focus:border-green-500" />
|
|
|
|
|
- </div>
|
|
|
|
|
- <div className="col-span-1 flex items-end">
|
|
|
|
|
- <button type="button" onClick={() => setShowMapPicker(true)}
|
|
|
|
|
- className="h-[50px] w-auto rounded-lg border border-white/10 bg-white/5 px-3 py-1.5 text-sm text-white/80 hover:bg-white/10 transition text-center leading-tight"
|
|
|
|
|
- >Указать<br />на карте</button>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div>
|
|
|
|
|
- <label className="mb-1 block text-sm text-white/60">Информация о доступе</label>
|
|
|
|
|
- <textarea value={accessInfo} onChange={(e) => setAccessInfo(e.target.value)}
|
|
|
|
|
- rows={2} className="w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white outline-none focus:border-green-500"
|
|
|
|
|
- placeholder="Как добраться, парковка..." />
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div>
|
|
|
|
|
- <label className="mb-1 block text-sm text-white/60">Обложка</label>
|
|
|
|
|
- <input type="file" accept="image/*" onChange={(e) => {
|
|
|
|
|
- const file = e.target.files?.[0]
|
|
|
|
|
- if (file) {
|
|
|
|
|
- setCoverFile(file)
|
|
|
|
|
- if (coverPreview) URL.revokeObjectURL(coverPreview)
|
|
|
|
|
- setCoverPreview(URL.createObjectURL(file))
|
|
|
|
|
- }
|
|
|
|
|
- }}
|
|
|
|
|
- className="w-full text-sm text-white/60 file:mr-4 file:rounded-lg file:border-0 file:bg-green-600 file:px-4 file:py-2 file:text-white file:hover:bg-green-500" />
|
|
|
|
|
- {coverPreview && (
|
|
|
|
|
- <img src={coverPreview} alt="preview" className="mt-2 h-32 rounded-lg object-cover" />
|
|
|
|
|
- )}
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- {place?.type === 'studio' && (
|
|
|
|
|
- <div className="grid grid-cols-2 gap-4">
|
|
|
|
|
- <div>
|
|
|
|
|
- <label className="mb-1 block text-sm text-white/60">Цена за час (₽)</label>
|
|
|
|
|
- <input type="number" value={hourlyRate} onChange={(e) => setHourlyRate(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" />
|
|
|
|
|
- </div>
|
|
|
|
|
- <div>
|
|
|
|
|
- <label className="mb-1 block text-sm text-white/60">Мин. часов</label>
|
|
|
|
|
- <input type="number" min="1" value={minHours} onChange={(e) => setMinHours(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" />
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- )}
|
|
|
|
|
-
|
|
|
|
|
- <div>
|
|
|
|
|
- <label className="mb-2 block text-sm text-white/60">Теги (стили съёмки)</label>
|
|
|
|
|
- <div className="flex flex-wrap gap-2">
|
|
|
|
|
- {tags.map((tag) => (
|
|
|
|
|
- <button key={tag.id} type="button" onClick={() => toggleTag(tag.id)}
|
|
|
|
|
- className={`rounded-full px-3 py-1.5 text-sm transition ${selectedTags.includes(tag.id) ? 'bg-green-600 text-white' : 'bg-white/10 text-white/60 hover:bg-white/20'}`}
|
|
|
|
|
- >{tag.name}</button>
|
|
|
|
|
- ))}
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div>
|
|
|
|
|
- <label className="mb-2 block text-sm text-white/60">Характеристики</label>
|
|
|
|
|
- <div className="flex flex-wrap gap-2">
|
|
|
|
|
- {features.map((feat) => (
|
|
|
|
|
- <button key={feat.id} type="button" onClick={() => toggleFeature(feat.id)}
|
|
|
|
|
- className={`rounded-full px-3 py-1.5 text-sm transition ${selectedFeatures.includes(feat.id) ? 'bg-green-600 text-white' : 'bg-white/10 text-white/60 hover:bg-white/20'}`}
|
|
|
|
|
- >{feat.name}</button>
|
|
|
|
|
- ))}
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div className="flex items-center gap-3 pt-2">
|
|
|
|
|
- <button type="button" onClick={onClose}
|
|
|
|
|
- className="rounded-lg bg-white/10 px-6 py-3 text-white hover:bg-white/20 transition"
|
|
|
|
|
- >Отмена</button>
|
|
|
|
|
- <button type="submit" disabled={saving}
|
|
|
|
|
- className="flex-1 rounded-lg bg-green-600 px-6 py-3 font-medium text-white hover:bg-green-500 transition disabled:opacity-50"
|
|
|
|
|
- >{saving ? 'Сохранение...' : 'Сохранить'}</button>
|
|
|
|
|
- </div>
|
|
|
|
|
- </form>
|
|
|
|
|
- )}
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- {showMapPicker && <MapPicker onSelect={handleMapPick} onClose={() => setShowMapPicker(false)} />}
|
|
|
|
|
- </>
|
|
|
|
|
- )
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
const FILTERS: { key: string; label: string }[] = [
|
|
const FILTERS: { key: string; label: string }[] = [
|
|
|
{ key: 'all', label: 'Все' },
|
|
{ key: 'all', label: 'Все' },
|
|
|
{ key: 'draft', label: 'Черновики' },
|
|
{ key: 'draft', label: 'Черновики' },
|
|
@@ -321,9 +86,9 @@ function PlaceCard({ place, onEdit, onDeleted }: { place: Place; onEdit: () => v
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
export default function MyPlacesPage() {
|
|
export default function MyPlacesPage() {
|
|
|
|
|
+ const router = useRouter()
|
|
|
const [places, setPlaces] = useState<Place[]>([])
|
|
const [places, setPlaces] = useState<Place[]>([])
|
|
|
const [loading, setLoading] = useState(true)
|
|
const [loading, setLoading] = useState(true)
|
|
|
- const [editId, setEditId] = useState<string | null>(null)
|
|
|
|
|
const [filter, setFilter] = useState('all')
|
|
const [filter, setFilter] = useState('all')
|
|
|
const [toast, setToast] = useState('')
|
|
const [toast, setToast] = useState('')
|
|
|
|
|
|
|
@@ -381,7 +146,7 @@ export default function MyPlacesPage() {
|
|
|
<PlaceCard
|
|
<PlaceCard
|
|
|
key={place.id}
|
|
key={place.id}
|
|
|
place={place}
|
|
place={place}
|
|
|
- onEdit={() => setEditId(place.id)}
|
|
|
|
|
|
|
+ onEdit={() => router.push('/places/edit/' + place.id)}
|
|
|
onDeleted={() => {
|
|
onDeleted={() => {
|
|
|
setPlaces((prev) => prev.filter((p) => p.id !== place.id))
|
|
setPlaces((prev) => prev.filter((p) => p.id !== place.id))
|
|
|
setToast('Место удалено')
|
|
setToast('Место удалено')
|
|
@@ -393,10 +158,6 @@ export default function MyPlacesPage() {
|
|
|
)
|
|
)
|
|
|
})()}
|
|
})()}
|
|
|
</div>
|
|
</div>
|
|
|
-
|
|
|
|
|
- {editId && (
|
|
|
|
|
- <PlaceEditModal placeId={editId} onClose={() => setEditId(null)} onSaved={() => { setEditId(null); setToast('Сохранено'); setTimeout(() => setToast(''), 3000); fetchPlaces() }} />
|
|
|
|
|
- )}
|
|
|
|
|
</div>
|
|
</div>
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|