Skip to content

Namecard photo resize backend#125

Open
AndrewHUNGNguyen wants to merge 3 commits into
DEVxNetwork:mainfrom
AndrewHUNGNguyen:#61-Add-Profile_Photos-Table
Open

Namecard photo resize backend#125
AndrewHUNGNguyen wants to merge 3 commits into
DEVxNetwork:mainfrom
AndrewHUNGNguyen:#61-Add-Profile_Photos-Table

Conversation

@AndrewHUNGNguyen

Copy link
Copy Markdown
Contributor

Summary

Moves nametag profile photo uploads from direct client-to-Supabase storage to a server-side upload API that resizes and normalizes images before saving them.

This PR adds a backend processing step using Sharp so uploaded avatars are:

  • validated (JPG/PNG only, max 4MB input)
  • auto-oriented (EXIF rotation)
  • resized to a max dimension of 512px while preserving aspect ratio
  • converted to WebP for smaller storage size

Frontend upload flows on /setup and /whois now call a shared uploadAvatar() helper instead of uploading directly to Supabase Storage.

New files

  • lib/avatarImageConfig.ts
    Centralizes avatar image constants (max dimension, max input size, allowed formats, WebP quality) so validation and processing stay in sync.

  • lib/processAvatarImage.ts
    Core Sharp processing logic. Validates input, rotates based on EXIF, resizes with fit: "inside" and withoutEnlargement: true, and outputs WebP. Throws typed AvatarImageErrors for API responses.

  • lib/supabaseServer.ts
    Server-side Supabase helpers for API routes. Creates a user-scoped Supabase client from the request bearer token and verifies the user is authenticated before allowing uploads.

  • lib/uploadAvatar.ts
    Client-side helper used by setup/profile pages. Sends the selected file to the new API route with the user's session token and returns the processed image's public URL.

  • app/api/avatars/upload/route.ts
    New authenticated upload endpoint. Accepts multipart form data, runs validation + Sharp processing, uploads the processed .webp file to the avatars bucket, and returns the public URL.

Modified files

  • package.json
    Adds sharp as a dependency for server-side image processing.

  • app/setup/page.tsx
    Replaces direct Supabase Storage upload logic with uploadAvatar(file) so onboarding uses the new resize pipeline.

  • app/whois/ProfileDisplay.tsx
    Same refactor as setup page — profile photo edits now go through the shared upload helper/API.

  • app/components/Nametag.tsx
    Changes nametag avatar display from object-fit: cover to object-fit: fill so the full processed image is shown in the square photo frame without cropping (with the tradeoff that non-square images may stretch).

Resize behavior

Sharp uses:

  • fit: "inside" — scales image down to fit within 512×512 without cropping
  • withoutEnlargement: true — does not upscale images smaller than 512px
  • .webp({ quality: 85 }) — converts output to WebP

Example:

  • Input: 1200×1482 PNG
  • Output: 415×512 WebP

Test plan

  • Run supabase start and point .env.local at local Supabase
  • Run bun install and bun run dev
  • Sign up / log in and upload a profile photo on /setup
  • Upload a new profile photo on /whois
  • Confirm uploaded file in Supabase Studio → Storage → avatars is <uuid>.webp
  • Confirm output dimensions are ≤512px on longest side
  • Reject file >4MB (413)
  • Reject unsupported format like GIF/SVG (415)
  • Reject upload when logged out (401)

Resize verification results from terminal

% bun -e 'import sharp from "sharp"; import { processAvatarImage } from "./lib/processAvatarImage.ts";
const p="{some_image}";
const input = await sharp(p).toBuffer();
const before = await sharp(input).metadata();
const output = await processAvatarImage(input);
const after = await sharp(output).metadata();
console.log({ before: ${before.width}x${before.height} ${before.format}, after: ${after.width}x${after.height} ${after.format}, outputBytes: output.length });'
{
before: "600x500 jpeg",
after: "512x427 webp",
outputBytes: 31204,
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant