A full-stack music festival discovery and scheduling portal. Browse festivals, explore stage lineups, favorite artists, build a personal set-time plan, and get real-time conflict detection when sets overlap.
| Technology | Version | Purpose |
|---|---|---|
| React | ^19 | UI framework |
| TypeScript | ^5 | Static typing |
| Vite | ^6 | Build tooling & dev server |
| React Router v7 | ^7 | Client-side routing + protected routes |
| TailwindCSS | v4 | Utility-first styling |
| shadcn/ui | latest | Accessible UI component library (Radix UI primitives) |
| Framer Motion | ^11 | Page transitions & UI animations |
| Lucide React | ^0.400 | Icon system |
| Sonner | ^1 | Toast notifications |
| Zustand | ^5 | Client state (theme) |
| TanStack Query | ^5 | Server state, data fetching & caching |
| React Hook Form | ^7 | Form state management |
| Zod | ^3 | Schema validation (forms + API) |
| date-fns | ^3 | Date formatting & conflict detection |
| clsx + tailwind-merge | latest | Conditional classname utility |
| Technology | Version | Purpose |
|---|---|---|
| Hono | ^4 | API server (edge-native, TypeScript-first) |
| Neon | latest | Serverless PostgreSQL database |
| Drizzle ORM | ^0.30 | Type-safe query builder + migrations |
| Better Auth | ^1 | Authentication (email/password, sessions) |
| Zod | ^3 | Request/response validation |
| Technology | Purpose |
|---|---|
| Zod schemas | Shared validation between frontend & backend |
| TypeScript types | Shared entity types (Festival, Stage, Artist, Set, etc.) |
| Tool | Purpose |
|---|---|
| pnpm workspaces | Monorepo package management |
| TypeScript project references | Cross-package type safety |
festival-planner/
├── apps/
│ ├── web/ # Vite + React frontend
│ │ ├── src/
│ │ │ ├── components/
│ │ │ │ ├── layout/
│ │ │ │ │ └── ThemeToggle.tsx
│ │ │ │ └── plan/
│ │ │ │ └── PlanSidebar.tsx
│ │ │ ├── hooks/
│ │ │ │ ├── useFestivals.ts
│ │ │ │ ├── useFestival.ts
│ │ │ │ ├── useFavorites.ts
│ │ │ │ ├── usePlan.ts
│ │ │ │ ├── useAdminFestivals.ts
│ │ │ │ ├── useAdminArtists.ts
│ │ │ │ └── useAdminSchedule.ts
│ │ │ ├── lib/
│ │ │ │ ├── auth-client.ts
│ │ │ │ ├── queryClient.ts
│ │ │ │ └── utils.ts
│ │ │ ├── pages/
│ │ │ │ ├── LandingPage.tsx
│ │ │ │ ├── FestivalDetailPage.tsx
│ │ │ │ ├── LoginPage.tsx
│ │ │ │ ├── RegisterPage.tsx
│ │ │ │ └── admin/
│ │ │ │ ├── AdminLayout.tsx
│ │ │ │ ├── FestivalsAdminPage.tsx
│ │ │ │ ├── FestivalFormPage.tsx
│ │ │ │ ├── ArtistsAdminPage.tsx
│ │ │ │ ├── ArtistFormPage.tsx
│ │ │ │ └── ScheduleAdminPage.tsx
│ │ │ ├── routes/
│ │ │ │ ├── index.tsx
│ │ │ │ └── ProtectedRoute.tsx
│ │ │ ├── store/
│ │ │ │ └── themeStore.ts
│ │ │ ├── styles/
│ │ │ │ └── theme.css
│ │ │ ├── App.tsx
│ │ │ └── main.tsx
│ │ ├── index.html
│ │ ├── vite.config.ts
│ │ ├── components.json
│ │ └── tsconfig.json
│ │
│ └── api/ # Hono API server
│ ├── src/
│ │ ├── db/
│ │ │ ├── schema.ts
│ │ │ ├── client.ts
│ │ │ ├── seed.ts
│ │ │ └── migrations/
│ │ ├── routes/
│ │ │ ├── auth.ts
│ │ │ ├── festivals.ts
│ │ │ ├── stages.ts
│ │ │ ├── artists.ts
│ │ │ ├── sets.ts
│ │ │ ├── favorites.ts
│ │ │ └── plans.ts
│ │ ├── middleware/
│ │ │ ├── auth.ts
│ │ │ └── adminOnly.ts
│ │ └── index.ts
│ ├── drizzle.config.ts
│ └── tsconfig.json
│
├── packages/
│ └── shared/
│ └── src/
│ ├── schemas/
│ │ ├── festival.schema.ts
│ │ ├── artist.schema.ts
│ │ └── set.schema.ts
│ └── types/
│ └── index.ts
│
├── package.json
├── pnpm-workspace.yaml
└── tsconfig.base.json
festivals stages artists
───────── ────── ───────
id (uuid) id (uuid) id (uuid)
slug festival_id name
name name image_url
description order genre
short_desc bio
start_date sets created_at
end_date ────
location id (uuid) users (Better Auth)
image_url festival_id ──────────────────
hero_image_url stage_id id
created_at artist_id name
start_time email
end_time email_verified
day role
image
user_favorites user_plan_items created_at
────────────── ───────────────
user_id id
artist_id user_id
festival_id
set_id
added_at
| Route | Component | Notes |
|---|---|---|
/login |
LoginPage |
Better Auth email/password |
/register |
RegisterPage |
New account creation |
| Route | Component | Notes |
|---|---|---|
/ |
LandingPage |
Festival grid, filterable by year |
/festival/:slug |
FestivalDetailPage |
Hero, stage tabs, day selector, plan sidebar |
| Route | Component | Notes |
|---|---|---|
/admin |
AdminLayout |
Redirects to /admin/festivals |
/admin/festivals |
FestivalsAdminPage |
All festivals table |
/admin/festivals/new |
FestivalFormPage |
Create festival |
/admin/festivals/:id/edit |
FestivalFormPage |
Edit festival |
/admin/festivals/:id/schedule |
ScheduleAdminPage |
Manage stages + sets |
/admin/artists |
ArtistsAdminPage |
All artists table |
/admin/artists/new |
ArtistFormPage |
Create artist |
/admin/artists/:id/edit |
ArtistFormPage |
Edit artist |
GET /api/festivals List all (filterable: ?year=2026)
GET /api/festivals/:slug Festival + stages + sets
POST /api/festivals Create (admin)
PUT /api/festivals/:id Update (admin)
DELETE /api/festivals/:id Delete (admin)
GET /api/stages?festivalId= List stages for festival
POST /api/stages Create stage (admin)
PUT /api/stages/:id Update stage (admin)
DELETE /api/stages/:id Delete stage (admin)
GET /api/artists List all artists
GET /api/artists/:id Single artist
POST /api/artists Create (admin)
PUT /api/artists/:id Update (admin)
DELETE /api/artists/:id Delete (admin)
GET /api/sets?festivalId= All sets for a festival
POST /api/sets Create set (admin)
PUT /api/sets/:id Update set (admin)
DELETE /api/sets/:id Delete set (admin)
GET /api/me/favorites User's favorited artists
POST /api/me/favorites Add favorite { artistId }
DELETE /api/me/favorites/:artistId Remove favorite
GET /api/me/plans/:festivalId User's plan for a festival
POST /api/me/plans Add set to plan { festivalId, setId }
DELETE /api/me/plans/:setId Remove set from plan
POST /api/auth/sign-up Register new user
POST /api/auth/sign-in/email Email + password login
POST /api/auth/sign-out End session
GET /api/auth/get-session Current session
Two sets conflict when they are on different stages, on the same day, and their time windows overlap. Detection runs client-side using date-fns interval comparison. Conflicts are surfaced on the set card (red border + warning badge), in the plan sidebar (conflict banner + per-item overlap indicator), and via Sonner toast on add.
The landing page derives available years from festival.start_date values in the database. A year selector pill group appears automatically when festivals span multiple years. No hardcoded year values anywhere in the codebase.
Dark/light mode is class-based (.dark on <html>) using a shadcn/ui-compatible CSS variable token system. Persisted to localStorage via Zustand. Defaults to dark mode. Amber brand accent (#f59e0b) extends the base theme as a custom --brand token mapped to Tailwind utilities.
- Desktop (≥1024px): Fixed right sidebar, 320px wide
- Mobile (<1024px): Bottom sheet drawer with rounded top corners + backdrop
- Plan state is per-user and persisted to the database via
user_plan_items
Full-bleed festival crowd hero image with frosted glass form card overlay. Animated gradient wordmark. Browser autofill styled to match the glassmorphism aesthetic.
Role-gated to admin users. Left sidebar navigation. Festivals and artists management with inline delete confirmation. Schedule page with split stages/sets layout, day tab selector, and inline create forms for both stages and sets.
node --version # 20+ required
pnpm --version # 9+ requiredgit clone https://github.com/tworoniak/setlist
cd festival-planner
pnpm installCreate apps/api/.env:
DATABASE_URL=postgresql://...
BETTER_AUTH_SECRET=your_secret_here
BETTER_AUTH_URL=http://localhost:3001
WEB_URL=http://localhost:3000Create apps/web/src/.env.local:
VITE_API_URL=http://localhost:3001cd apps/api
pnpm db:migrate
pnpm db:seed # optional — loads 3 sample festivals# From monorepo root
pnpm dev # starts web (:3000) and api (:3001) in parallelAfter registering, open Drizzle Studio and set your user's role to admin:
cd apps/api
pnpm db:studio| Phase | Scope | Status |
|---|---|---|
| Phase 1 | Scaffold, theme, routing, Zustand stores | ✅ Complete |
| Phase 2 | Neon DB, Drizzle schema, Hono API, Better Auth | ✅ Complete |
| Phase 3 | LandingPage, FestivalDetailPage, plan sidebar, dark mode | ✅ Complete |
| Phase 4 | Persist favorites + plan to database per user | ✅ Complete |
| Phase 5 | Admin CRUD — festivals, stages, artists, sets | ✅ Complete |
| Phase 6 | Framer Motion transitions, mobile polish, a11y audit | 🔲 Pending |
- Font pairing: Bebas Neue (display) + DM Sans (body/UI)
- Brand accent:
#f59e0bamber —--brand/bg-brand/text-brandTailwind utilities - Animated gradient: CSS keyframe utility class
animated-gradient-textused on the SetList wordmark - Component library: shadcn/ui (Radix UI primitives)
- Icon system: Lucide React throughout
- Radius base:
0.625remwith sm/md/lg/xl scale - Auth pages: Glassmorphism card (
bg-white/10 backdrop-blur-md) over full-bleed hero