A mobile-first personal tracker where one beer fills each letter A–Z. Local-only data, no accounts, no backend.
Screens
Home (/)
- Header "Beer Alphabet"
- Progress line: "12 / 26 letters" plus "46% complete"; when all 26 filled, a simple "Alphabet complete" line
- 4-column grid of 26 square letter cards
- Empty card: big letter + "Empty" label, subtle grey border
- Filled card: beer photo as background with the letter overlaid clearly (dark scrim behind letter for legibility)
- Tap empty letter → add form prefilled with that letter; tap filled letter → detail screen
Add / Edit (/letter/$letter/edit)
- "Add Beer" action opens the photo picker: one input for camera capture, one for gallery
- Fields: beer name, brewery, country, date added (defaults to today), optional notes
- Letter auto-derives from the first letter of the beer name as it is typed, and stays editable via a letter dropdown
- If the chosen letter is taken, show the existing beer inline with "Replace" / "Cancel" — replacing overwrites that letter's entry
- Save returns to the letter detail
Beer detail (/letter/$letter)
- Large photo, name, brewery, country, letter, date added, notes
- Edit and Delete buttons (delete asks for confirmation)
Collection (/collection)
- Vertical list of filled entries sorted A–Z: letter, photo thumbnail, name, brewery
- Empty state when nothing added yet
Settings (/settings)
- Progress summary, "Clear all data" with confirmation, short note that data lives only on this device
- Brief line that the app is for cataloguing beers already encountered
Navigation
- Fixed bottom tab bar: Home, Collection, Settings
Design
Off-white background, black text, hairline grey borders, one accent-free palette. Large clear type (bold condensed-free sans), generous spacing, square cards, no gradients, no animation beyond instant state changes. All colors as tokens in src/styles.css.
Technical notes
- Storage: IndexedDB via a small wrapper (
idb), one record per letter { letter, name, brewery, country, dateAdded, notes, photo } with the photo kept as a Blob so it stays bound to its entry and survives app restarts. Photos rendered through URL.createObjectURL, revoked on unmount.
- A
useCollection hook exposes entries, save, delete, clear; reads happen client-side only (ClientOnly/effect-gated) so SSR has no storage access.
- Routes:
index.tsx, collection.tsx, settings.tsx, letter.$letter.tsx, letter.$letter.edit.tsx; each gets its own head() metadata.
- Images are downscaled to ~1200px max on save via canvas to keep storage small.
- No Lovable Cloud, no network calls.