feat: delete photo from server on remove + fix file drag-and-drop

- DELETE /api/upload?url= endpoint removes file from disk (with path traversal protection)
- api.upload.deletePhoto() helper in frontend
- removePhoto() now calls deletePhoto for cdn.hotelsync.ru URLs (best-effort)
- handleDragOver only highlights drop zone for external file drags (not thumbnail DnD)
- <img> in preview gets draggable={false} + pointer-events-none to not interfere with drops

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-23 13:39:10 +03:00
parent 036885dd24
commit 5d17ad0579
3 changed files with 70 additions and 6 deletions

View File

@@ -81,12 +81,28 @@ function CategoryForm({ category, onClose, onSave }: CategoryFormProps) {
const handleDrop = (e: React.DragEvent) => {
e.preventDefault()
setDragging(false)
uploadFiles(e.dataTransfer.files)
// Only handle external file drops (not internal thumbnail DnD)
if (e.dataTransfer.files.length > 0) {
uploadFiles(e.dataTransfer.files)
}
}
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault()
// Only show drop highlight for external files, not internal thumbnail reorder
if (e.dataTransfer.types.includes('Files')) {
setDragging(true)
}
}
const removePhoto = (i: number) => {
const url = photos[i]
setPhotos(prev => prev.filter((_, idx) => idx !== i))
setPhotoIdx(p => Math.max(0, p - 1))
// Delete from server (best-effort, don't block UI)
if (url.startsWith('https://cdn.hotelsync.ru/')) {
api.upload.deletePhoto(url).catch(console.error)
}
}
const handleSave = async () => {
@@ -224,11 +240,11 @@ function CategoryForm({ category, onClose, onSave }: CategoryFormProps) {
dragging && 'ring-2 ring-brand-500 ring-offset-2',
)}
style={{ height: 220 }}
onDragOver={e => { e.preventDefault(); setDragging(true) }}
onDragOver={handleDragOver}
onDragLeave={() => setDragging(false)}
onDrop={handleDrop}
>
<img src={photos[photoIdx]} alt="" className="w-full h-full object-cover" />
<img src={photos[photoIdx]} alt="" draggable={false} className="w-full h-full object-cover pointer-events-none" />
{dragging && (
<div className="absolute inset-0 bg-brand-600/40 flex items-center justify-center">
<p className="text-white font-semibold text-sm">Отпустите для добавления</p>
@@ -290,7 +306,7 @@ function CategoryForm({ category, onClose, onSave }: CategoryFormProps) {
: 'border-slate-300 dark:border-slate-600 text-slate-400 hover:border-slate-400',
)}
onClick={() => fileRef.current?.click()}
onDragOver={e => { e.preventDefault(); setDragging(true) }}
onDragOver={handleDragOver}
onDragLeave={() => setDragging(false)}
onDrop={handleDrop}
>