From 8b1aafbc1bbbe8df170c088b1953ee4cb017fe20 Mon Sep 17 00:00:00 2001 From: Caleb Bae Date: Wed, 30 Sep 2026 10:29:20 -0500 Subject: [PATCH] feat: track logo drag and drop --- .../portal/src/pages/admin/sponsors/store.ts | 8 +- .../tracks/components/TrackFormDialog.tsx | 125 +++++++++++------- client/portal/src/pages/admin/tracks/store.ts | 8 +- 3 files changed, 94 insertions(+), 47 deletions(-) diff --git a/client/portal/src/pages/admin/sponsors/store.ts b/client/portal/src/pages/admin/sponsors/store.ts index f1e2a6200..a387182c2 100644 --- a/client/portal/src/pages/admin/sponsors/store.ts +++ b/client/portal/src/pages/admin/sponsors/store.ts @@ -94,7 +94,13 @@ export const useSponsorsStore = create((set) => ({ }, uploadLogo: async (sponsorId: string, file: File) => { - const logo = await prepareLogoForUpload(file); + let logo; + try { + logo = await prepareLogoForUpload(file); + } catch { + toast.error("Couldn't read that image. Try a different file."); + return null; + } if (logo.byteLength > MAX_LOGO_BYTES) { toast.error("Logo is still too large after compression."); return null; diff --git a/client/portal/src/pages/admin/tracks/components/TrackFormDialog.tsx b/client/portal/src/pages/admin/tracks/components/TrackFormDialog.tsx index 45b33e60e..83a5947c2 100644 --- a/client/portal/src/pages/admin/tracks/components/TrackFormDialog.tsx +++ b/client/portal/src/pages/admin/tracks/components/TrackFormDialog.tsx @@ -51,12 +51,10 @@ function TrackForm({ const [displayOrder, setDisplayOrder] = useState(track?.display_order ?? 0); const [logoFile, setLogoFile] = useState(null); const [logoPreview, setLogoPreview] = useState(logoPreviewFor(track)); + const [isDraggingLogo, setIsDraggingLogo] = useState(false); const logoInputRef = useRef(null); - const handleLogoChange = (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - if (!file) return; - + const acceptLogoFile = (file: File) => { if (!ALLOWED_LOGO_TYPES.includes(file.type)) { toast.error("Unsupported file type. Use PNG, JPEG, WebP, or GIF."); return; @@ -73,6 +71,31 @@ function TrackForm({ reader.readAsDataURL(file); }; + const handleLogoChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) acceptLogoFile(file); + }; + + const handleLogoDragOver = (e: React.DragEvent) => { + if (!e.dataTransfer.types.includes("Files")) return; + e.preventDefault(); + e.dataTransfer.dropEffect = "copy"; + setIsDraggingLogo(true); + }; + + const handleLogoDragLeave = (e: React.DragEvent) => { + // dragleave also fires when moving onto a child; only reset on real exit. + if (e.currentTarget.contains(e.relatedTarget as Node | null)) return; + setIsDraggingLogo(false); + }; + + const handleLogoDrop = (e: React.DragEvent) => { + e.preventDefault(); + setIsDraggingLogo(false); + const file = e.dataTransfer.files[0]; + if (file) acceptLogoFile(file); + }; + const clearLogo = () => { setLogoFile(null); setLogoPreview(logoPreviewFor(track)); @@ -124,52 +147,64 @@ function TrackForm({
-
- {logoPreview ? ( - Logo preview - ) : ( -
- -
- )} -
- + +
+ + {logoFile && ( - {logoFile && ( - - )} -
+ )}
-

- PNG, JPEG, WebP, or GIF (max 750KB) -

diff --git a/client/portal/src/pages/admin/tracks/store.ts b/client/portal/src/pages/admin/tracks/store.ts index 7a54818e4..4c1307a50 100644 --- a/client/portal/src/pages/admin/tracks/store.ts +++ b/client/portal/src/pages/admin/tracks/store.ts @@ -111,7 +111,13 @@ export const useTracksStore = create((set) => ({ }, uploadLogo: async (trackId: string, file: File) => { - const logo = await prepareLogoForUpload(file); + let logo; + try { + logo = await prepareLogoForUpload(file); + } catch { + toast.error("Couldn't read that image. Try a different file."); + return null; + } if (logo.byteLength > MAX_LOGO_BYTES) { toast.error("Logo is still too large after compression."); return null;