Music showcase for Bohns — albums and tracks streamed from Supabase, rebuilt on Next.js (App Router) with a full redesign of the Home and Album pages.
- Next.js 16 (App Router, TypeScript, CSS Modules)
- Supabase — public content (
albums,track_overviewview,trackslyrics) plus Google sign-in and per-user likes (@supabase/ssr) - lucide-react icons
- Fonts: Gloock (display) + Space Grotesk via
next/font
- The site header renders with
mix-blend-mode: color-dodgeover the dark mauve background (#11090C) — its soft-grey content composites to pink. - Each album has a color palette theme (
light/accent/deep) applied through CSS custom properties. Palettes are hardcoded insrc/lib/palettes.tsfor now (no Supabase model change, step 1). - Track durations are not stored in the database; they resolve client-side
from audio metadata (
preload="metadata").
/— artist header (brand, bio, social links) then two tabbed sections: a Tracks section of standalone tracks (the cover doubles as the play/pause control) under Popular / Latests tabs, and an Albums section of album cards filtered by genre. Popularity ranks a track bylikes × 10 + plays ÷ 10(play totals come from thetrack_play_countsview)/albums/[id]— compact header, album switcher (rail on desktop, horizontal strip on mobile) and album detail with track descriptions and a lyrics sheet/artist/albums— artist-only: rank the albums shown in the home Albums section. Reached from the account menu's Artist group (only rendered for artist members). Moves are staged locally and saved as one full ranking via thereorder_albumsRPC, which re-checks membership before writing; the order lives inalbums.position(seesupabase/migrations/20260716000000_album_position.sql) and albums without one sort last, newest first
Playing a track queues its whole album into a global player bar: waveform
timeline (wavesurfer.js) recolored per album palette, play/pause,
next/previous, repeat (all by default), scrubbing, and MediaSession
metadata + handlers for lock-screen controls on iOS/Android. Playback
survives page navigation (the player lives above the router).
- Sign in with Google from the account button (top-right on every page).
OAuth runs through Supabase Auth: the browser client starts the flow, Google
redirects back to
/auth/callback, and the route handler exchanges the code for a cookie session.middleware.tskeeps the session fresh. - Likes are per-user and persisted in the
track_likestable. The heart on each track toggles a row via the browser client (writes are guarded by row-level security), with an optimistic count and rollback on failure. The public catalog stays statically cached (ISR) — only the like state is resolved client-side, so pages don't become per-user dynamic. /profileshows the signed-in user and a sign-out control;/likeslists the tracks the user has liked, most recent first.- Artist members (rows in
artist_members) additionally get inline album editing and the/artist/albumsordering page.
Supabase clients live in src/lib/supabase/: anon.ts (public read-only
content), client.ts (browser, auth + likes) and server.ts (cookie-bound, for
the callback route and the protected pages). The track_likes schema + RLS are
in supabase/migrations/20260710_track_likes.sql.
The hosted project must have Google enabled as an auth provider and
<site>/auth/callbackin its redirect allow-list. No extra environment variables are needed beyond the two below.
cp .env.example .env.local # fill in the Supabase URL + anon key
npm install
npm run devRequired environment variables (also needed on Vercel):
| Variable | Description |
|---|---|
NEXT_PUBLIC_SUPABASE_URL |
Supabase project URL |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
Supabase anonymous key |
This project is licensed under the Creative Commons Attribution-NonCommercial 4.0 International License.
You are free to share, adapt, and build upon this work for non-commercial purposes only, as long as you provide appropriate credit.