d95ccf9dc7cc5a9782032d6333091435a0944d8f
Adds a reusable shadcn-style Spinner (lucide loader-circle icon, localized aria-label) and wires it into every button that has a pending state: comment submit, delete, tag save, profile save, admin saves, upload, login and register. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
BRETT
An imageboard-style upload and discussion platform built with Nuxt 4 and shadcn-vue. Users can upload files or URLs, comment in threads (including file attachments and voice messages), respond with Discord-style reactions, subscribe to threads, and pick from over 20 themes.
Features
- Uploads: Drag & drop (page dropzone + upload popover), file or URL upload, max. 200 MB. Types: images (generated thumbnails), videos (embedded Plyr player), audio (MP3/FLAC with embedded cover art and music metadata via
music-metadata, dedicated player dialog), YouTube and Twitch embeds, plus link preview cards - Threads & comments: Every upload is a thread with comments. Comments support file attachments and voice messages recorded in the browser (converted to MP3 via
ffmpeg-static). Discord-style reactions on both comments and uploads (toggle, counters, tooltips, optimistic updates, burst animation), @mentions with autocomplete suggestions and profile hover cards — all with live WebSocket updates - Thread subscriptions & notifications: Creating, commenting, favoriting, or reacting to a thread automatically subscribes you (
thread_subscriptionstable). Can be toggled per thread via a bell button (a disabled subscription stays disabled even on later participation and also suppresses mentions in that thread). Notifications are grouped per thread (no spam from multiple comments), there are no self-notifications, and the bell shows live events plus toasts - Announcement (MOTD): Admin-editable slim bar below the header (
MotdBanner.vue), publicly readable and updated live via WebSocket - Tags & filters: Uploads can be tagged; filter popover by type, uploader, time range, and tag (URL query–driven)
- Command palette:
Ctrl/Cmd + Kopens a global search across uploads/users/actions - Favorites: Favorite uploads, visible on the profile page
- Auth & roles: Session-cookie login (30 days); the first registered user automatically becomes admin. Admin panel with user list, role management, and per-role configurable permissions (
admin.access,roles.manage,users.manage,site.manage,comment.delete,upload.delete,tag.delete) — theadminrole is an implicit wildcard and can do everything;moderatoris a system role with moderation rights - Profiles & personalization: Profile pages with own uploads, comments, and stats; user overview page. In settings: avatar (max. 5 MB), name color, card effects (WebGL shaders via
ogl— Aurora, Particles, Plasma, Silk, Dither, Scanner and more as hover-card backgrounds), border effects, and vignette - Themes: 21 themes (Standard, Discord, Cyberpunk, Spring, Terminal, Infoseer, Homebrew, Florense, Kawi Green, Clo.nz, Clawmorphism, Realmorphism, Amber, Sunny Sprout, Spotify, Nordic Theme, SS, Amethyst Haze, Deep Wine, myClaude, Neo), switched via
data-themeon<html>, persisted via cookie — SSR-safe without flash. The imported TweakCN themes are included as dark variants only - Layouts: Switchable Bento/uniform grid for the upload overview
Tech stack
| Area | Technology |
|---|---|
| Framework | Nuxt 4 / Nitro, Vue 3 (<script setup> + TS) |
| UI | shadcn-vue (shadcn-nuxt), Reka UI, Tailwind CSS v4, Phosphor/Simple Icons via @nuxt/icon (local SVG bundle mode), vue-sonner |
| Database | MariaDB 11 + Drizzle ORM (mysql2) |
| Realtime | Nitro WebSocket (crossws) |
| Media | @nuxt/image, Plyr, music-metadata, ffmpeg-static, ogl (shader effects) |
| Data/Utils | @tanstack/vue-table, Zod, Unovis charts, dnd-kit-vue |
| State | Nuxt useState composables, VueUse |
Project structure
app/
assets/css/tailwind.css # Tailwind v4 entry + all theme tokens ([data-theme="…"])
components/ # App components (header, dropzone, tiles, comments,
# MotdBanner, CommandPalette, ThreadSubscriptionButton, …)
effects/ # WebGL card effects + vignette/border
ui/ # shadcn-vue base components
composables/ # useAuth, useTheme, useCommandPalette, useCommentReactions,
# useNotifications, useCardShader, …
layouts/ middleware/ plugins/ pages/
index.vue # Upload grid with filters / homepage
thread/[id].vue # Thread detail page with comments, attachments, voice recording
admin.vue # Admin panel (users, roles, permissions, MOTD)
settings.vue # Profile settings (avatar, color, card effects)
users.vue user/[id].vue login.vue register.vue
server/
api/ # Nitro endpoints (auth, uploads, comments, notifications,
# users, tags, link-preview, motd, admin/…)
db/schema.ts # Drizzle schema (users, sessions, uploads, comments,
# comment_attachments, comment_reactions,
# upload_reactions, favorites, thread_subscriptions,
# link_previews, notifications, roles, user_roles,
# permissions, role_permissions, tags, upload_tags,
# settings)
db/migrations/ # Generated SQL migrations
plugins/migrate.ts # Runs migrations on server start (dev)
routes/_ws.ts # WebSocket route for live events (broadcastToAll)
utils/ # auth (requirePermission), db, roles, uploads,
# notifications, threadSubscriptions, linkPreview
scripts/migrate.ts # Manual migration runner (npm run db:migrate)
shared/utils/ # Shared utilities — incl. permissions.ts (catalog + seeding)
public/uploads/ # Upload target, incl. uploads/avatars/ (gitignored)
docker-compose.yml # MariaDB container
Setup
Requirements: Node.js ≥ 20, Docker (for the bundled MariaDB) or your own MySQL/MariaDB instance.
# 1. Start the database
docker compose up -d
# 2. Configure environment
cp .env.example .env # contains DATABASE_URL, matches the Docker setup
# 3. Install dependencies
npm install
# 4. Fetch the ffmpeg binary (needed for voice messages → MP3;
# the download runs in ffmpeg-static's postinstall script, which may be
# blocked depending on your npm configuration)
node node_modules/ffmpeg-static/install.js
# 5. Start the dev server (migrations + permission seeding run automatically)
npm run dev
The app runs on http://localhost:3000. The first registered account automatically gets the admin role and can then create additional roles, assign permissions, and set the announcement in the admin panel.
Without Docker
Provide your own MariaDB/MySQL instance and adjust DATABASE_URL in .env:
DATABASE_URL=mysql://user:pass@host:3306/database
NPM scripts
| Command | Purpose |
|---|---|
npm run dev |
Dev server with HMR, auto-migrations |
npm run build |
Production build |
npm run preview |
Run the built app locally |
npm run db:generate |
Generate a new Drizzle migration from a schema change |
npm run db:migrate |
Apply migrations manually |
npm run db:studio |
Drizzle Studio (DB GUI) |
Development
- Change the schema: Edit
server/db/schema.ts→npm run db:generate→ commit the migration. In dev mode,server/plugins/migrate.tsapplies it automatically on start. - Add a new permission: Add an entry to
PERMISSIONSinshared/utils/permissions.ts→ seeded on next start viaseedPermissions(); check server-side withrequirePermission(event, key), client-side withcan(key)fromuseAuth. - Live events: The server broadcasts via
broadcastToAllinserver/routes/_ws.ts; the client handles events inapp/plugins/notifications.client.ts. - Add a new theme: Create a
[data-theme="…"]block with the CSS variables inapp/assets/css/tailwind.css, add the name toTHEME_NAMESinapp/composables/useTheme.ts, and add an entry inapp/components/ThemeSelector.vue. Themes can also override theshadow-*tokens (glow, offset, or flat shadows) andfont-sans/font-heading/font-mono;--font-logointentionally stays theme-independent. - Conventions: Semantic tokens instead of hardcoded colors (
primary,accent,muted, …);rounded-md/rounded-smscale via--radiuswith the theme (neois square:--radius: 0). More details inAGENTS.md. - Health check:
GET /api/healthreports the DB connection status.
Languages
Vue
66.8%
TypeScript
26.2%
CSS
7%
