From 5540d0cdfa1e12dab9f29bfe3e70645f363b7ec9 Mon Sep 17 00:00:00 2001 From: HotelSync Date: Mon, 23 Mar 2026 12:17:53 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20category=20form=20=E2=80=94=20async=20sa?= =?UTF-8?q?ve=20with=20error=20display,=20drag-and-drop=20photos,=2020MB?= =?UTF-8?q?=20body=20limit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/app.ts | 1 + src/pages/RoomCategoriesPage.tsx | 99 ++++++++++++++++++++++++-------- 2 files changed, 77 insertions(+), 23 deletions(-) diff --git a/backend/src/app.ts b/backend/src/app.ts index 3c69828..83284e2 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -31,6 +31,7 @@ export async function buildApp() { transport: { target: 'pino-pretty', options: { colorize: true } }, }), }, + bodyLimit: 20 * 1024 * 1024, // 20MB — for base64 photo uploads }) // ── Security ─────────────────────────────────────────────────────────────── diff --git a/src/pages/RoomCategoriesPage.tsx b/src/pages/RoomCategoriesPage.tsx index 92fb6c7..82ef339 100644 --- a/src/pages/RoomCategoriesPage.tsx +++ b/src/pages/RoomCategoriesPage.tsx @@ -31,7 +31,7 @@ function fromApi(c: CategoryApi): RoomCategory { interface CategoryFormProps { category?: RoomCategory onClose: () => void - onSave: (cat: RoomCategory) => void + onSave: (cat: RoomCategory) => Promise } function CategoryForm({ category, onClose, onSave }: CategoryFormProps) { @@ -44,13 +44,17 @@ function CategoryForm({ category, onClose, onSave }: CategoryFormProps) { const [amenities, setAmenities] = useState(category?.amenities ?? []) const [photos, setPhotos] = useState(category?.photos ?? []) const [photoIdx, setPhotoIdx] = useState(0) + const [saving, setSaving] = useState(false) + const [saveError, setSaveError] = useState('') + const [dragging, setDragging] = useState(false) const fileRef = useRef(null) const toggleAmenity = (a: string) => setAmenities(prev => prev.includes(a) ? prev.filter(x => x !== a) : [...prev, a]) - const handlePhotoUpload = (e: React.ChangeEvent) => { - Array.from(e.target.files ?? []).forEach(file => { + 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 @@ -58,25 +62,42 @@ function CategoryForm({ category, onClose, onSave }: CategoryFormProps) { } reader.readAsDataURL(file) }) + } + + const handlePhotoUpload = (e: React.ChangeEvent) => { + readFiles(e.target.files) e.target.value = '' } + const handleDrop = (e: React.DragEvent) => { + e.preventDefault() + setDragging(false) + readFiles(e.dataTransfer.files) + } + const removePhoto = (i: number) => { setPhotos(prev => prev.filter((_, idx) => idx !== i)) setPhotoIdx(p => Math.max(0, p - 1)) } - const handleSave = () => { - if (!name.trim()) return - onSave({ - id: category?.id ?? `cat-${Date.now()}`, - hotelId: category?.hotelId ?? 'hotel-1', - name: name.trim(), - description, - color, - amenities, - photos, - }) + const handleSave = async () => { + if (!name.trim() || saving) return + setSaving(true) + setSaveError('') + try { + await onSave({ + id: category?.id ?? `cat-${Date.now()}`, + hotelId: category?.hotelId ?? '', + name: name.trim(), + description, + color, + amenities, + photos, + }) + } catch (err) { + setSaveError(err instanceof Error ? err.message : 'Ошибка сохранения') + setSaving(false) + } } return ( @@ -188,8 +209,22 @@ function CategoryForm({ category, onClose, onSave }: CategoryFormProps) { <> {photos.length > 0 ? (
-
+
{ e.preventDefault(); setDragging(true) }} + onDragLeave={() => setDragging(false)} + onDrop={handleDrop} + > + {dragging && ( +
+

Отпустите для добавления

+
+ )}
) : ( -
- -

Фото категории не загружены

+
fileRef.current?.click()} + onDragOver={e => { e.preventDefault(); setDragging(true) }} + onDragLeave={() => setDragging(false)} + onDrop={handleDrop} + > + +

{dragging ? 'Отпустите файлы' : 'Перетащите фото или нажмите'}

+

JPG, PNG, WEBP

)} @@ -225,11 +272,17 @@ function CategoryForm({ category, onClose, onSave }: CategoryFormProps) {
{/* Footer */} -
- - +
+ {saveError && ( +

{saveError}

+ )} +
+ + +