feat: real file upload to /opt/hotelsync/uploads + serve via /uploads/ on API

This commit is contained in:
2026-03-23 12:23:13 +03:00
parent 5540d0cdfa
commit 3ef7f8265a
5 changed files with 94 additions and 15 deletions

View File

@@ -344,6 +344,23 @@ export const api = {
req<void>('DELETE', `/api/hotels/${slug}/rental-bookings/${id}`),
},
// ── File Upload ───────────────────────────────────────────────────────────
upload: {
photo: async (file: File): Promise<string> => {
const token = localStorage.getItem('access_token')
const fd = new FormData()
fd.append('file', file)
const res = await fetch(`${BASE}/api/upload`, {
method: 'POST',
headers: token ? { Authorization: `Bearer ${token}` } : {},
body: fd,
})
if (!res.ok) throw new Error(`Upload failed: ${res.statusText}`)
const data = await res.json() as { url: string }
return data.url
},
},
// ── Categories ────────────────────────────────────────────────────────────
categories: {
list: (slug: string) =>

View File

@@ -52,27 +52,35 @@ function CategoryForm({ category, onClose, onSave }: CategoryFormProps) {
const toggleAmenity = (a: string) =>
setAmenities(prev => prev.includes(a) ? prev.filter(x => x !== a) : [...prev, a])
const readFiles = (files: FileList | null) => {
Array.from(files ?? []).forEach(file => {
if (!file.type.startsWith('image/')) return
const reader = new FileReader()
reader.onload = ev => {
const url = ev.target?.result as string
setPhotos(prev => { const next = [...prev, url]; setPhotoIdx(next.length - 1); return next })
}
reader.readAsDataURL(file)
})
const [uploading, setUploading] = useState(false)
const uploadFiles = async (files: FileList | null) => {
const list = Array.from(files ?? []).filter(f => f.type.startsWith('image/'))
if (!list.length) return
setUploading(true)
try {
const urls = await Promise.all(list.map(f => api.upload.photo(f)))
setPhotos(prev => {
const next = [...prev, ...urls]
setPhotoIdx(next.length - 1)
return next
})
} catch {
setSaveError('Ошибка загрузки фото')
} finally {
setUploading(false)
}
}
const handlePhotoUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
readFiles(e.target.files)
uploadFiles(e.target.files)
e.target.value = ''
}
const handleDrop = (e: React.DragEvent) => {
e.preventDefault()
setDragging(false)
readFiles(e.dataTransfer.files)
uploadFiles(e.dataTransfer.files)
}
const removePhoto = (i: number) => {
@@ -263,9 +271,9 @@ function CategoryForm({ category, onClose, onSave }: CategoryFormProps) {
</div>
)}
<input ref={fileRef} type="file" accept="image/*" multiple className="hidden" onChange={handlePhotoUpload} />
<button onClick={() => fileRef.current?.click()} className="btn-secondary w-full justify-center">
<ImagePlus size={14} />
Загрузить фото
<button onClick={() => fileRef.current?.click()} className="btn-secondary w-full justify-center" disabled={uploading}>
{uploading ? <Loader2 size={14} className="animate-spin" /> : <ImagePlus size={14} />}
{uploading ? 'Загрузка...' : 'Загрузить фото'}
</button>
</>
)}