From f16d280f9a129185a62aafd145dd5f2f477690be Mon Sep 17 00:00:00 2001 From: Hein Date: Sun, 23 Aug 2026 10:33:50 +0200 Subject: [PATCH] feat(api): add endpoint to list guest photos * implement handleListPhotos to retrieve uploaded photos * create PhotoFileRecord struct for photo data * add fetchGuestPhotos function in the UI * display guest photos in the photo upload page * remove index.html as it is no longer needed * add calendar functionality to download wedding event --- .gitignore | 3 +- server/internal/api/handlers.go | 27 +++++-- server/internal/store/store.go | 30 +++++++ server/web/dist/index.html | 1 - ui/src/lib/api.ts | 12 +++ ui/src/lib/calendar.ts | 33 ++++++++ ui/src/lib/components/PhotoGallery.svelte | 25 ++++++ ui/src/routes/details/+page.svelte | 18 +++++ ui/src/routes/photos/+page.svelte | 95 ++++++++++++++--------- 9 files changed, 198 insertions(+), 46 deletions(-) delete mode 100644 server/web/dist/index.html create mode 100644 ui/src/lib/calendar.ts create mode 100644 ui/src/lib/components/PhotoGallery.svelte diff --git a/.gitignore b/.gitignore index d32251c..a125335 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,4 @@ ui/node_modules/ ui/build/ ui/.svelte-kit/ server/wedding-server -server/web/dist/* -!server/web/dist/index.html +server/web/dist/ diff --git a/server/internal/api/handlers.go b/server/internal/api/handlers.go index 6b7791a..89280ea 100644 --- a/server/internal/api/handlers.go +++ b/server/internal/api/handlers.go @@ -36,6 +36,7 @@ func (s *Server) Routes(mux *http.ServeMux) { mux.HandleFunc("GET /api/config", s.handleConfig) mux.HandleFunc("POST /api/rsvp", s.handleRSVP) mux.HandleFunc("GET /api/guests", s.handleGuestList) + mux.HandleFunc("GET /api/photos", s.handleListPhotos) mux.HandleFunc("POST /api/photos/upload", s.handlePhotoUpload) } @@ -146,6 +147,27 @@ func (s *Server) handleGuestList(w http.ResponseWriter, r *http.Request) { }) } +type photoResponse struct { + URL string `json:"url"` + UploaderName string `json:"uploaderName"` +} + +func (s *Server) handleListPhotos(w http.ResponseWriter, r *http.Request) { + files, err := s.store.ListPhotoFiles() + if err != nil { + log.Printf("photos: list failed: %v", err) + writeError(w, http.StatusInternalServerError, "could not load photos") + return + } + + out := make([]photoResponse, 0, len(files)) + for _, f := range files { + out = append(out, photoResponse{URL: f.URL, UploaderName: f.UploaderName}) + } + + writeJSON(w, http.StatusOK, map[string]any{"photos": out}) +} + var allowedImageTypes = map[string]bool{ "image/jpeg": true, "image/png": true, @@ -155,11 +177,6 @@ var allowedImageTypes = map[string]bool{ } func (s *Server) handlePhotoUpload(w http.ResponseWriter, r *http.Request) { - if time.Now().Before(s.cfg.Wedding.Date) { - writeError(w, http.StatusForbidden, "photo uploads open on the wedding day") - return - } - maxTotalBytes := int64(s.cfg.Upload.MaxSizeMB) * int64(s.cfg.Upload.MaxFiles) * 1024 * 1024 r.Body = http.MaxBytesReader(w, r.Body, maxTotalBytes+1<<20) diff --git a/server/internal/store/store.go b/server/internal/store/store.go index 74d6fcb..aceacb8 100644 --- a/server/internal/store/store.go +++ b/server/internal/store/store.go @@ -159,6 +159,36 @@ func (s *Store) CreatePhotoUpload(u PhotoUpload) (int64, error) { return res.LastInsertId() } +type PhotoFileRecord struct { + URL string + UploaderName string +} + +// ListPhotoFiles returns every uploaded photo, most recent first. +func (s *Store) ListPhotoFiles() ([]PhotoFileRecord, error) { + rows, err := s.db.Query( + `SELECT f.url, u.name + FROM photo_files f + JOIN photo_uploads u ON u.id = f.upload_id + ORDER BY f.created_at DESC, f.id DESC + LIMIT 200`, + ) + if err != nil { + return nil, err + } + defer rows.Close() + + var out []PhotoFileRecord + for rows.Next() { + var rec PhotoFileRecord + if err := rows.Scan(&rec.URL, &rec.UploaderName); err != nil { + return nil, err + } + out = append(out, rec) + } + return out, rows.Err() +} + func (s *Store) AddPhotoFiles(uploadID int64, files []PhotoFile) error { tx, err := s.db.Begin() if err != nil { diff --git a/server/web/dist/index.html b/server/web/dist/index.html deleted file mode 100644 index 14b1a81..0000000 --- a/server/web/dist/index.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/ui/src/lib/api.ts b/ui/src/lib/api.ts index a0a6079..bfe0141 100644 --- a/ui/src/lib/api.ts +++ b/ui/src/lib/api.ts @@ -20,6 +20,11 @@ export interface RsvpPayload { message?: string; } +export interface GuestPhoto { + url: string; + uploaderName: string; +} + export interface PhotoUploadPayload { name: string; message?: string; @@ -80,6 +85,13 @@ export async function fetchGuests(fetchImpl: typeof fetch = fetch): Promise { + const response = await fetchImpl('/api/photos'); + if (!response.ok) throw new ApiError(await parseError(response), response.status); + const body = await response.json(); + return body.photos ?? []; +} + export async function uploadPhotos(payload: PhotoUploadPayload): Promise { const form = new FormData(); form.set('name', payload.name); diff --git a/ui/src/lib/calendar.ts b/ui/src/lib/calendar.ts new file mode 100644 index 0000000..f5b29c1 --- /dev/null +++ b/ui/src/lib/calendar.ts @@ -0,0 +1,33 @@ +function toICSDate(date: Date): string { + return date.toISOString().replace(/[-:]/g, "").split(".")[0] + "Z"; +} + +function escapeICSText(text: string): string { + return text.replace(/([,;])/g, "\\$1"); +} + +export function buildIcsDataUrl(event: { + start: Date; + end: Date; + summary: string; + location: string; + description: string; +}): string { + const lines = [ + "BEGIN:VCALENDAR", + "VERSION:2.0", + "PRODID:-//Zoe & Shaldon Wedding//EN", + "BEGIN:VEVENT", + `UID:${event.start.getTime()}@zoeshaldon.warky.info`, + `DTSTAMP:${toICSDate(new Date())}`, + `DTSTART:${toICSDate(event.start)}`, + `DTEND:${toICSDate(event.end)}`, + `SUMMARY:${escapeICSText(event.summary)}`, + `LOCATION:${escapeICSText(event.location)}`, + `DESCRIPTION:${escapeICSText(event.description)}`, + "END:VEVENT", + "END:VCALENDAR", + ].join("\r\n"); + + return `data:text/calendar;charset=utf8,${encodeURIComponent(lines)}`; +} diff --git a/ui/src/lib/components/PhotoGallery.svelte b/ui/src/lib/components/PhotoGallery.svelte new file mode 100644 index 0000000..cdb5905 --- /dev/null +++ b/ui/src/lib/components/PhotoGallery.svelte @@ -0,0 +1,25 @@ + + +{#if photos.length > 0} +
+

A Few of Our Favourites

+
+ {#each photos as src (src)} + + {/each} +
+
+{/if} diff --git a/ui/src/routes/details/+page.svelte b/ui/src/routes/details/+page.svelte index 1c47523..b86610a 100644 --- a/ui/src/routes/details/+page.svelte +++ b/ui/src/routes/details/+page.svelte @@ -1,8 +1,19 @@
- {#if !isOpen} -
-

Photo uploads open on the wedding day

-

Come back on {formatFullDate(wedding.date)} to share your photos with Zoé & Shaldon.

-
- {:else if status === 'done'} -
- Thank you — your photos have been uploaded! -
- {:else} -
+
+ + + {#if status === 'done'} +
+ Thank you — your photos have been uploaded! +
+ {:else}

Share your photos

- Upload photos from the day — up to {MAX_FILES} at a time, {MAX_SIZE_MB}MB each. + Upload your photos of Zoé & Shaldon — up to {MAX_FILES} at a time, {MAX_SIZE_MB}MB each.

@@ -90,16 +95,6 @@ > - - - -