Inspiration
Studying for the BJCP (Beer Judge Certification Program) exam means memorizing 125 beer styles — each with vital-stat ranges (OG, FG, IBU, SRM, ABV), aroma/flavor/mouthfeel profiles, history, and commercial examples. The usual tools are a 100-page PDF and a stack of flashcards, which is brutal and easy to quit. I wanted studying to feel like collecting: every style you master should become a bottle on your own shelf.
## What it does It turns the BJCP 2021 guidelines into a game. The home screen is a 술장 (liquor cabinet) — a shelf per beer sub-family, a bottle per style, grayscale until you clear it and full color once mastered. You climb a tech-tree of families → sub-families → styles through bite-size quizzes (multiple choice, SRM color-matching, style duels, off-flavor ID, an interactive flavor wheel, map quizzes), earning XP, levels, streaks, badges, and hop shards, and finish each family with a unit exam. It ships as an offline-capable web PWA and a native SwiftUI iOS/macOS app that share one cloud-synced account.
## How I built it
- Content pipeline: parsed the BJCP 2021 guidelines from DOCX (
python-docx) into a normalized JSON model — 125 styles, 35 categories, vital stats, and ~900 questions. - Two clients, one brain: a Next.js 16 / React / Tailwind v4 web app and a SwiftUI app consume the same content and the same pure engine layer (progression, gating, recommendation, XP, streaks, spaced repetition).
- Local-first + sync: web stores in IndexedDB (Dexie), iOS in SwiftData; a byte-identical merge layer syncs to Supabase (Postgres + auth). Deployed on Vercel as a PWA, with email reminders via Resend and analytics via PostHog.
- The cabinet: an image-compositing system lays per-style pixel-art bottle sprites onto empty-cabinet art (normalized by a small Python script), with reveal animations — bottles literally pour from grayscale to color the first time you collect them.
Vital stats are the heart of the content — a style's strength is roughly $\mathrm{ABV}\% \approx (\mathrm{OG} - \mathrm{FG}) \times 131.25$ — and the adaptive quiz engine keeps every session in the learning sweet spot by targeting a predicted accuracy $\hat{p} \in [0.8,\ 0.9]$.
## What I learned
- Curriculum as a graph. Encoding an entire certification syllabus as a navigable, gate-locked tech-tree (with free vs. premium tiers) — without ever creating a dead-end path — is its own design problem.
- Conflict-free offline sync. Additive state like the hop-shard wallet must be modeled as an immutable set of grant events merged by union with $\max$ on conflicts, so two devices always converge — never as a mutable scalar you can double-count.
- Framework footguns. In Tailwind v4,
@theme inlinebakes tokens at build time, so runtime per-family theming has to stay plain CSS variables — a subtle bug that cost real hours.
## Challenges I faced
- Fitting a real curriculum onto a metaphor: variable style counts across fixed shelves, and distinguishing the shelf-ladder lock (for families with no internal gates) from the tech-tree's own chain-gates — so a bottle the tree has unlocked never gets re-locked by the shelf.
- Web ↔ iOS parity: keeping two native stacks feature-identical, including porting the sync merge logic byte-for-byte.
- Motion without regressions: entrance, "pour," and scroll reveals that honor
prefers-reduced-motionand never break the Playwright + Vitest suites. - PDF parsing failed twice; switching to DOCX finally unlocked clean, bilingual content extraction.
Built With
- dexie.js
- framer-motion
- indexeddb
- ios
- macos
- next.js
- playwright
- postgresql
- posthog
- pwa
- python
- python-docx
- react
- supabase
- swift
- swiftdata
- swiftui
- tailwind-css
- typescript
- vercel
- vitest
Log in or sign up for Devpost to join the conversation.