a lofi radio player + pixel arts ❤️
- Live lofi channels — stream a curated set of YouTube live radio channels, with shuffle, next/previous, and auto-switching when a channel goes down.
- Ambient sound mixer — layer up to 5 looping ambient sounds (rain, crickets, café, etc.) over the music, each with its own volume. The mixer button shows a small badge with the number of currently active sounds, plus reset and randomize controls.
- Live chat — a single global, anonymous chatroom backed by Supabase realtime, with an online-listener counter.
- Keyboard shortcuts —
Spaceplay/pause,Rrandom channel,←/→previous/next channel,↑/↓volume,Ffullscreen,Cchat,Msound mixer. - Deep links — append
#<n>to the URL (e.g.#3) to open a specific channel. - Animated pixel art backgrounds on a deterministic, shared rotation, with a static-frame fallback when paused. Installable as a PWA.
React 19 (+ React Compiler) · TypeScript · Vite · Zustand · CSS Modules · Supabase (realtime chat + presence) · Cloudflare Workers · Vitest.
pnpm install # install dependencies
pnpm dev # start the dev server
pnpm build # type-check + production build
pnpm test # run the test suiteThe live chat and online-listener counter talk to Supabase. Create a .env file in the
project root with your project's credentials (both are exposed to the client, so use the
anon key, never the service-role key):
VITE_SUPABASE_URL=https://<your-project>.supabase.co
VITE_SUPABASE_ANON_KEY=<your-anon-key>The app is served as a static SPA on Cloudflare Workers via Wrangler. Preview a production build locally, or deploy:
pnpm preview # build + run locally with wrangler dev
pnpm deploy # build + wrangler deployWorker configuration lives in wrangler.jsonc.
The app uses animated GIFs/WebPs for backgrounds when playing and static JPGs when stopped. To generate static frames from your animated assets, ensure you have ImageMagick installed, then run:
pnpm generate-staticThis will extract the first frame of every asset in src/assets/gifs and save it to src/assets/static.
The live chat stores messages in a Supabase messages table. To keep the database within free-tier size limits, a Postgres trigger automatically trims the table to the newest 500 messages on every insert.
The trigger lives in supabase/migrations/20260602_trim_messages_to_500.sql. It is not applied automatically from this repo — apply it once to your Supabase project, either by pasting the file's contents into the Supabase SQL editor, or with the Supabase CLI:
supabase db pushPull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
Copyright © Late Night Vibes. All rights reserved.
This project is proprietary. The source is published for reference only; it is not open source. No license is granted to use, copy, modify, or distribute this software or its assets without prior written permission.
