Skip to content

Commit 9f47be5

Browse files
feat: banner upload and deletion complete
1 parent 5436613 commit 9f47be5

16 files changed

Lines changed: 225 additions & 81 deletions

File tree

apps/api/internal/api/api.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ func (api *API) setupRoutes(mw *mw.Middleware) {
106106
// Admin-only
107107
r.With(ensureEventAdmin).Patch("/", api.Handlers.Event.UpdateEventById)
108108
r.With(ensureEventAdmin).Post("/banner", api.Handlers.Event.UploadEventBanner)
109+
r.With(ensureEventAdmin).Delete("/banner", api.Handlers.Event.DeleteBanner)
109110
r.With(ensureEventAdmin).Get("/staff", api.Handlers.Event.GetEventStaffUsers)
110111
r.With(ensureEventAdmin).Post("/roles", api.Handlers.Event.AssignEventRole)
111112

apps/api/internal/api/handlers/events.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,3 +486,24 @@ func (h *EventHandler) UploadEventBanner(w http.ResponseWriter, r *http.Request)
486486
})
487487

488488
}
489+
490+
func (h *EventHandler) DeleteBanner(w http.ResponseWriter, r *http.Request) {
491+
eventIdStr := chi.URLParam(r, "eventId")
492+
if eventIdStr == "" {
493+
res.SendError(w, http.StatusBadRequest, res.NewError("missing_event_id", "The event ID is missing from the URL!"))
494+
return
495+
}
496+
eventId, err := uuid.Parse(eventIdStr)
497+
if err != nil {
498+
res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not a valid UUID"))
499+
return
500+
}
501+
502+
err = h.eventService.DeleteBanner(r.Context(), eventId)
503+
if err != nil {
504+
res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went wrong on our end"))
505+
return
506+
}
507+
508+
w.WriteHeader(http.StatusOK)
509+
}

apps/api/internal/services/events.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,8 @@ func (s *EventService) UploadBanner(ctx context.Context, eventId uuid.UUID, bann
219219
fileName := header.Filename
220220
fileExt := strings.ToLower(filepath.Ext(fileName))
221221

222+
s.logger.Info().Str("Filetype", fileExt).Msg("The file type")
223+
222224
switch fileExt {
223225
case ".jpg", ".png", ".jpeg":
224226
// Do nothing
@@ -243,8 +245,8 @@ func (s *EventService) UploadBanner(ctx context.Context, eventId uuid.UUID, bann
243245
return nil, ErrFailedToUploadBanner
244246
}
245247

246-
// Reconstrust URL
247-
url := fmt.Sprintf("%s/%s", s.buckets.EventAssetsBaseUrl, uploadKey)
248+
// Reconstrust URL with cache buster
249+
url := fmt.Sprintf("%s/%s?t=%d", s.buckets.EventAssetsBaseUrl, uploadKey, time.Now().Unix())
248250

249251
err = s.eventRepo.UpdateEventById(ctx, sqlc.UpdateEventByIdParams{
250252
ID: eventId,
@@ -258,3 +260,12 @@ func (s *EventService) UploadBanner(ctx context.Context, eventId uuid.UUID, bann
258260
return &url, nil
259261

260262
}
263+
264+
func (s *EventService) DeleteBanner(ctx context.Context, eventId uuid.UUID) error {
265+
// For now its a soft delete, not actually deleting banner is easiest, just set to null
266+
return s.eventRepo.UpdateEventById(ctx, sqlc.UpdateEventByIdParams{
267+
ID: eventId,
268+
BannerDoUpdate: true,
269+
Banner: nil,
270+
})
271+
}

apps/web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"@tanstack/react-table": "^8.21.3",
3737
"@uidotdev/usehooks": "^2.4.1",
3838
"axios": "^1.9.0",
39+
"browser-image-compression": "^2.0.2",
3940
"clsx": "^2.1.1",
4041
"date-fns": "^4.1.0",
4142
"js-cookie": "^3.0.5",

apps/web/pnpm-lock.yaml

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/web/src/features/Event/components/EventBannerUploader.tsx

Lines changed: 92 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { Button } from "@/components/ui/Button";
22
import { useState } from "react";
33
import { Button as RAC_Button, FileTrigger } from "react-aria-components";
4-
import { useUploadEventBanner } from "../hooks/useUploadEventBanner";
4+
import { useEventBannerActions } from "../hooks/useEventBannerActions";
55
import { toast } from "react-toastify";
6+
import imageCompression, { type Options } from "browser-image-compression";
67

78
interface Props {
89
bannerUrl: string | null;
@@ -12,41 +13,106 @@ interface Props {
1213
const EventBannerUploader = ({ bannerUrl, eventId }: Props) => {
1314
// State to hold the selected file
1415
const [file, setFile] = useState<File | null | undefined>(undefined); // File and null can be uploaded, undefined means no change
15-
const { mutateAsync } = useUploadEventBanner(eventId);
16+
const [currentBanner, setCurrentBanner] = useState<string | null>(bannerUrl);
17+
const [uploadProgress, setUploadProgress] = useState(0); // 0-100
18+
const [isUploading, setIsUploading] = useState(false);
1619

17-
const handleFileSelection = (e: FileList | null) => {
18-
let files = e ? Array.from(e) : [];
19-
setFile(files[0] || null);
20+
const { upload, remove } = useEventBannerActions(eventId);
21+
22+
const handleFileSelection = async (e: FileList | null) => {
23+
const files = e ? Array.from(e) : [];
24+
25+
if (!files[0]) {
26+
setFile(null);
27+
return;
28+
}
29+
30+
const selectedFile = files[0];
31+
32+
const options = {
33+
maxSizeMB: 0.5,
34+
maxWidthOrHeight: 1920,
35+
useWebWorker: true,
36+
} as Options;
37+
38+
// Compress the image file
39+
const compressedFileBlob = await imageCompression(selectedFile, options);
40+
41+
// Create a new File object from the compressed Blob to preserve the file name and type
42+
// This is needed so the server can correctly identify the file type
43+
const compressedFile = new File([compressedFileBlob], selectedFile.name, {
44+
type: compressedFileBlob.type,
45+
});
46+
47+
setFile(compressedFile);
2048
};
2149

2250
const handleUpload = async () => {
2351
if (file) {
24-
console.log("Uploading file:", file);
25-
await mutateAsync(file, {
52+
setIsUploading(true);
53+
setUploadProgress(0);
54+
55+
// Fake progress bar until upload completes
56+
let progress = 0;
57+
const interval = setInterval(() => {
58+
progress += Math.random() * 3 + 1; // Random increment between 1-4%
59+
if (progress >= 80) progress = 80; // Cap at 80% until upload finishes
60+
setUploadProgress(progress);
61+
}, 10); // Every 10ms to fake a smooth progress
62+
63+
await upload.mutateAsync(file, {
2664
onSuccess: (data) => {
27-
bannerUrl = data.banner_url;
28-
setFile(undefined); // Reset file state after successful upload
65+
clearInterval(interval);
66+
setUploadProgress(100);
67+
setTimeout(() => {
68+
setIsUploading(false);
69+
setUploadProgress(0);
70+
setCurrentBanner(data.banner_url);
71+
setFile(undefined);
72+
}, 500);
2973
toast.success("Banner uploaded successfully!", {
3074
position: "bottom-right",
3175
});
3276
},
3377
onError: () => {
78+
clearInterval(interval);
79+
setIsUploading(false);
80+
setUploadProgress(0);
3481
toast.error("Failed to upload banner. Please try again.", {
3582
position: "bottom-right",
3683
});
3784
},
3885
});
3986
} else {
40-
toast.info("No file selected for upload.", {
41-
position: "bottom-right",
87+
await remove.mutateAsync(undefined, {
88+
onSuccess: () => {
89+
setCurrentBanner(null);
90+
toast.success("Banner removed successfully!", {
91+
position: "bottom-right",
92+
});
93+
},
94+
onError: () => {
95+
toast.error("Failed to remove banner. Please try again.", {
96+
position: "bottom-right",
97+
});
98+
},
4299
});
43100
}
44101
};
45102

46103
return (
47104
<div className="max-w-lg flex flex-col gap-2">
48105
<p>Banner</p>
49-
{bannerUrl || file ? (
106+
{file === null || (file === undefined && !currentBanner) ? (
107+
<FileTrigger
108+
acceptedFileTypes={["image/*"]}
109+
onSelect={handleFileSelection}
110+
>
111+
<RAC_Button className="w-full h-58 flex items-center justify-center bg-neutral-100 dark:bg-neutral-800 border border-dashed border-neutral-300 dark:border-neutral-700 rounded-sm cursor-pointer select-none hover:dark:bg-neutral-700 hover:bg-neutral-200 transition-colors">
112+
<span className="text-neutral-500">No Banner Uploaded</span>
113+
</RAC_Button>
114+
</FileTrigger>
115+
) : (
50116
<>
51117
<FileTrigger
52118
acceptedFileTypes={["image/png", "image/jpeg", "image/jpg"]}
@@ -55,9 +121,9 @@ const EventBannerUploader = ({ bannerUrl, eventId }: Props) => {
55121
>
56122
<RAC_Button className="relative w-full h-58 group p-0 overflow-hidden rounded-sm cursor-pointer">
57123
<img
58-
src={bannerUrl || (file && URL.createObjectURL(file)) || ""}
124+
src={file ? URL.createObjectURL(file) : currentBanner || ""}
59125
alt="Event Banner"
60-
className="object-cover w-full h-full rounded-sm border border-neutral-300 dark:border-neutral-700"
126+
className="w-full h-58 object-cover rounded-sm"
61127
/>
62128
{/* Overlay */}
63129
<div className="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity flex flex-row justify-center items-center">
@@ -74,18 +140,8 @@ const EventBannerUploader = ({ bannerUrl, eventId }: Props) => {
74140
Remove Banner
75141
</Button>
76142
</>
77-
) : (
78-
<FileTrigger
79-
acceptedFileTypes={["image/*"]}
80-
onSelect={handleFileSelection}
81-
>
82-
<RAC_Button
83-
className="w-full h-58 flex items-center justify-center bg-neutral-100 dark:bg-neutral-800 border border-dashed border-neutral-300 dark:border-neutral-700 rounded-sm cursor-pointer select-none hover:dark:bg-neutral-700 hover:bg-neutral-200 transition-colors"
84-
>
85-
<span className="text-neutral-500">No Banner Uploaded</span>
86-
</RAC_Button>
87-
</FileTrigger>
88143
)}
144+
89145
{file !== undefined && (
90146
<Button
91147
variant="primary"
@@ -96,10 +152,21 @@ const EventBannerUploader = ({ bannerUrl, eventId }: Props) => {
96152
Save Banner Changes
97153
</Button>
98154
)}
155+
99156
<p className="text-sm text-neutral-500">
100157
Recommended dimensions: 1200x300px. Max file size: 5MB. Supported
101158
formats: JPG, JPEG, PNG.
102159
</p>
160+
161+
{/* Progress bar for uploads */}
162+
{isUploading && (
163+
<div className="absolute bottom-0 left-0 w-full h-1 bg-neutral-200 dark:bg-neutral-600 rounded-t overflow-hidden">
164+
<div
165+
className="h-1 bg-cyan-700 transition-[width] duration-300 ease-out"
166+
style={{ width: `${uploadProgress}%` }}
167+
/>
168+
</div>
169+
)}
103170
</div>
104171
);
105172
};

apps/web/src/features/Event/components/EventCard.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export interface EventCardProps {
1717
description: string;
1818
date: string;
1919
location: string;
20+
banner: string | null;
2021
}
2122

2223
const EventCard = ({
@@ -26,13 +27,14 @@ const EventCard = ({
2627
description,
2728
date,
2829
location,
30+
banner,
2931
}: EventCardProps) => {
3032
return (
3133
<Card className="border">
3234
<div className="w-full">
3335
<img
3436
className="w-full h-40 object-cover rounded-t-md"
35-
src={imageFile}
37+
src={banner ?? imageFile}
3638
alt={`${title} Image`}
3739
/>
3840
</div>

apps/web/src/features/Event/components/EventSettingsForm.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,9 @@ const EventSettingsForm = ({ event }: Props) => {
246246
isSelected={field.state.value}
247247
onChange={field.handleChange}
248248
>
249-
{field.state.value ? "Event is published" : "Event is unpublished"}
249+
{field.state.value
250+
? "Event is published"
251+
: "Event is unpublished"}
250252
</Switch>
251253
)}
252254
</form.Field>

apps/web/src/features/Event/hooks/useEvent.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ export function useEvent(eventId: string) {
66
const fetchEvent = async () => {
77
const data = await getEventById(eventId);
88

9+
console.log("Fetched event data:", data);
10+
911
return EventSchema.parse(data);
1012
};
1113

0 commit comments

Comments
 (0)