feat: wire photo upload on player profile edit #43
No reviewers
Labels
No labels
domain:backend
domain:devops
domain:frontend
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
forgejo_admin/westside-app!43
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "42-wire-photo-upload-on-player-profile-edit"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Connects the existing file input on the player profile page to the backend
POST /upload/photoendpoint. On successful upload, saves the photo URL to the player record and displays the uploaded photo in the avatar circle immediately.Changes
src/lib/api.js: ExportAPI_BASEfor constructing full photo URLs. AddapiUpload()helper for multipart/form-data uploads (omitsContent-Typeheader so the browser sets the multipart boundary automatically).src/routes/players/[id]/+page.svelte: Wireonchangehandler on the file input that uploads viaapiUpload('/upload/photo'), then PUTs the returnedphoto_urlto/players/{id}. Shows an<img>tag with the photo whenplayer.photo_urlexists, falling back to initials. Includes loading indicator on the edit overlay and error message on failure.src/app.css: Add.profile-avatar-img(object-fit: cover for uploaded photos) and.photo-errorstyles.Test Plan
/players/{id}as an admin or ownerphoto_urlis setnpm run checkpasses — no new errors introduced (2 pre-existing type warnings unrelated to this change)Review Checklist
Related
Add apiUpload helper in api.js for multipart/form-data uploads (no Content-Type header so the browser sets the multipart boundary). Export API_BASE for constructing full photo URLs. On the player profile page, the file input now calls handlePhotoUpload which POSTs to /upload/photo, saves the returned photo_url to the player via PUT /players/{id}, and updates local state so the avatar shows the uploaded image immediately. Includes loading and error states. Closes #42 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>Review-fix loop: PASS -- no issues found.
Reviewed all 3 changed files (96 additions, 6 deletions):
src/lib/api.js--apiUploadcorrectly omitsContent-Typefor multipart boundary, follows same auth pattern asapiFetch, proper error handling.src/routes/players/[id]/+page.svelte-- Upload flow is correct: POST file, PUT photo_url, update local state, reset input, handle errors. Template correctly branches onplayer.photo_urlfor both editable and read-only avatar views. Loading indicator on overlay.src/app.css--object-fit: coveron.profile-avatar-imgworks with existing 96x96 circular avatar. Error style is clean.npm run checkpasses with no new errors (2 pre-existing type warnings in unrelated files).PR #43 Review
DOMAIN REVIEW
Tech stack: SvelteKit SPA (adapter-static) + vanilla CSS + client-side JS API layer. Domain checklist: SvelteKit/TypeScript/CSS.
API layer (
src/lib/api.js)DRY concern between
apiFetchandapiUpload: Both functions duplicate the token-fetch, header-build, fetch-call, error-check, and JSON-parse pattern. The only difference is: (a)apiUploadomitsContent-Type, (b) always uses POST, and (c) usesFormDatabody. This is a minor DRY issue but not in an auth/security path -- both delegate togetToken()the same way, so divergence risk is low. Non-blocking.API_BASEexport is correct. Previously private, now exported so the Svelte component can construct full image URLs. Reasonable change -- the constant was already hardcoded and used by both functions.Error message in
apiUploaddiffers fromapiFetch:apiFetchthrowsAPI ${res.status}: ${res.statusText}whileapiUploadthrowsUpload failed: ${res.status} ${res.statusText}. Inconsistent but non-blocking -- the consumer catches and displays a user-friendly message anyway.Component (
src/routes/players/[id]/+page.svelte)No client-side file validation before upload. The
accept="image/*"attribute on the input is advisory only (browser hint). A user could bypass it. There is no check for file size or MIME type before sending to the server. The backend endpoint (POST /upload/photo) should enforce these limits, but a client-side size check (e.g., max 5MB) would improve UX by avoiding a wasted upload + cryptic error. Non-blocking -- server-side validation is the real gate.Photo URL construction:
{API_BASE}{player.photo_url}assumesphoto_urlis a relative path starting with/. If the backend ever returns a full URL, this would double-prefix. Given the decision to use MinIO (per plan-wkq), the backend currently returns paths like/uploads/photos/filename.jpg, so this works. But it is fragile. Non-blocking.Duplicated avatar rendering block: The photo-or-initials logic appears twice in the template (lines 262-266 for editable view, lines 274-278 for non-editable view). This is a minor template DRY issue. A Svelte snippet (
{#snippet}) could eliminate the duplication. Non-blocking.Loading state UX is good. The overlay shows
...during upload, the file input is disabled during upload, and the input is reset after completion to allow re-upload of the same file. Error state is displayed and cleared on retry.Accessibility: The
<label for="photo-upload">wraps the avatar and provides click-to-upload. The<img>tag has a properaltattribute. The error message uses a visible.photo-errordiv. However, the error is not announced to screen readers (norole="alert"oraria-live). Non-blocking nit.CSS (
src/app.css).profile-avatar-imgusesobject-fit: cover-- correct for avatar images. Applied alongside existing.profile-avatarclass which sets width/height/border-radius, so the image will be properly clipped to a circle. Clean..photo-errorusesvar(--color-red)-- consistent with design token usage elsewhere in the file. No magic numbers.BLOCKERS
No blockers found.
Test coverage: This repo has zero tests (confirmed: no
.test.*or.spec.*files outsidenode_modules). This is a pre-existing condition tracked in the plan-wkq epilogue (PR #37, item 2: "Zero test coverage -- no unit or integration tests for any SPA route"). The PR is additive frontend wiring of an existing file input to an existing backend endpoint -- it does not introduce new architectural patterns or security-critical logic. The test gap is a repo-wide issue, not specific to this PR.No secrets committed.
API_BASEis a Tailscale hostname, not a secret.No unvalidated user input risk. The photo file is sent to the backend as
FormData; thephoto_urlreturned by the backend is stored and displayed. Theplayer.nameused inaltattributes comes from the API, not user input in this flow.No DRY violation in auth/security paths. Both
apiFetchandapiUploaduse the samegetToken()call.NITS
Client-side file size check. Add a guard like
if (file.size > 5 * 1024 * 1024) { photoError = 'File too large (max 5MB)'; return; }before uploading. Prevents wasted bandwidth on oversized files.aria-live="polite"on.photo-errorfor screen reader announcement of upload failures.Template DRY: The photo-or-initials rendering could use a Svelte
{#snippet}to avoid the duplicated{#if player.photo_url}block in the editable and non-editable branches.getInitials()still duplicated across 7 files (tracked in plan-wkq epilogue, PR #37 item 1). This PR does not make it worse but is a reminder the extraction to$lib/utils.jsis still pending.Inconsistent error message format between
apiFetch(API ${status}) andapiUpload(Upload failed: ${status}). Minor -- consider aligning.SOP COMPLIANCE
42-wire-photo-upload-on-player-profile-editmatches issue #42plan-wkqreference. Only hasCloses #42.PROCESS OBSERVATIONS
plan-wkqfor traceability. This is a process gap, not a code gap.VERDICT: APPROVED