Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion client/portal/src/pages/admin/sponsors/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,13 @@ export const useSponsorsStore = create<SponsorsState>((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;
Expand Down
125 changes: 80 additions & 45 deletions client/portal/src/pages/admin/tracks/components/TrackFormDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,10 @@ function TrackForm({
const [displayOrder, setDisplayOrder] = useState(track?.display_order ?? 0);
const [logoFile, setLogoFile] = useState<File | null>(null);
const [logoPreview, setLogoPreview] = useState<string>(logoPreviewFor(track));
const [isDraggingLogo, setIsDraggingLogo] = useState(false);
const logoInputRef = useRef<HTMLInputElement>(null);

const handleLogoChange = (e: React.ChangeEvent<HTMLInputElement>) => {
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;
Expand All @@ -73,6 +71,31 @@ function TrackForm({
reader.readAsDataURL(file);
};

const handleLogoChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) acceptLogoFile(file);
};

const handleLogoDragOver = (e: React.DragEvent<HTMLButtonElement>) => {
if (!e.dataTransfer.types.includes("Files")) return;
e.preventDefault();
e.dataTransfer.dropEffect = "copy";
setIsDraggingLogo(true);
};

const handleLogoDragLeave = (e: React.DragEvent<HTMLButtonElement>) => {
// 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<HTMLButtonElement>) => {
e.preventDefault();
setIsDraggingLogo(false);
const file = e.dataTransfer.files[0];
if (file) acceptLogoFile(file);
};

const clearLogo = () => {
setLogoFile(null);
setLogoPreview(logoPreviewFor(track));
Expand Down Expand Up @@ -124,52 +147,64 @@ function TrackForm({
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label>Logo</Label>
<div className="flex items-center gap-3">
{logoPreview ? (
<img
src={logoPreview}
alt="Logo preview"
className="size-12 rounded object-contain border"
/>
) : (
<div className="size-12 rounded border border-dashed flex items-center justify-center text-muted-foreground">
<ImagePlus className="size-5" />
</div>
)}
<div className="flex items-center gap-2">
<input
ref={logoInputRef}
type="file"
accept={ALLOWED_LOGO_TYPES.join(",")}
className="hidden"
onChange={handleLogoChange}
/>
<input
ref={logoInputRef}
type="file"
accept={ALLOWED_LOGO_TYPES.join(",")}
className="hidden"
onChange={handleLogoChange}
/>
<div className="relative">
<button
type="button"
onClick={() => logoInputRef.current?.click()}
onDragOver={handleLogoDragOver}
onDragLeave={handleLogoDragLeave}
onDrop={handleLogoDrop}
className={`flex h-32 w-full cursor-pointer flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed px-4 text-center transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${
isDraggingLogo
? "border-primary bg-primary/5"
: "border-muted-foreground/25 hover:border-muted-foreground/50 hover:bg-muted/50"
}`}
>
{logoPreview ? (
<>
<img
src={logoPreview}
alt="Logo preview"
className="h-16 max-w-full object-contain"
/>
<span className="text-xs text-muted-foreground">
Drop or click to replace
</span>
</>
) : (
<>
<ImagePlus className="size-4 text-muted-foreground" />
<span className="text-sm font-medium">
{isDraggingLogo
? "Drop to upload"
: "Drop logo here or click to browse"}
</span>
<span className="text-xs text-muted-foreground">
PNG, JPEG, WebP, or GIF · max 10MB
</span>
</>
)}
</button>
{logoFile && (
<Button
type="button"
variant="outline"
size="sm"
onClick={() => logoInputRef.current?.click()}
className="cursor-pointer"
variant="ghost"
size="icon-sm"
onClick={clearLogo}
aria-label="Remove selected logo"
className="absolute right-1.5 top-1.5 cursor-pointer text-muted-foreground hover:text-foreground"
>
<ImagePlus className="mr-1 size-3" />
{logoPreview ? "Replace" : "Choose file"}
<X className="size-4" />
</Button>
{logoFile && (
<Button
type="button"
variant="ghost"
size="icon-sm"
onClick={clearLogo}
className="cursor-pointer text-muted-foreground hover:text-foreground"
>
<X className="size-4" />
</Button>
)}
</div>
)}
</div>
<p className="text-xs text-muted-foreground">
PNG, JPEG, WebP, or GIF (max 750KB)
</p>
</div>

<div className="space-y-2">
Expand Down
8 changes: 7 additions & 1 deletion client/portal/src/pages/admin/tracks/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,13 @@ export const useTracksStore = create<TracksState>((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;
Expand Down
Loading