Skip to content

Latest commit

 

History

History
722 lines (641 loc) · 39.9 KB

File metadata and controls

722 lines (641 loc) · 39.9 KB

BeanMap - Project Tasks

An interactive world map showcasing coffee beans from around the world, their origins, flavor profiles, and recommended brewing methods.

Tech Stack: Next.js 14+ (App Router) | TypeScript | react-map-gl + Mapbox GL JS | Tailwind CSS + shadcn/ui | Zustand | Framer Motion | Recharts + D3 | Fuse.js | MDX


Phase 1: Foundation

Goal: A working interactive map with clickable bean markers, profile panels, and dark/light mode.

1.1 Project Scaffolding

  • Initialize Next.js project with create-next-app (TypeScript, Tailwind, App Router, src/ dir)
  • Install core dependencies: react-map-gl, mapbox-gl, framer-motion, zustand, lucide-react, zod, next-themes
  • Initialize shadcn/ui (npx shadcn-ui@latest init) and install base components: Sheet, Dialog, Button, Toggle, Slider, Command, NavigationMenu
  • Configure Tailwind theme with coffee color palette:
    • cream: #FAF6F1, parchment: #F0E8DC, tan: #D4C4A8
    • roast-light: #A67C52, roast-medium: #6F4E37, roast-dark: #3B2314, espresso: #1A0F09
    • cherry-red: #C1440E, leaf-green: #4A7C59, water-blue: #5B8FA8
  • Set up fonts via next/font/google: DM Serif Display (headings), Inter (body), JetBrains Mono (parameters)
  • Configure ESLint + Prettier with prettier-plugin-tailwindcss
  • Create project directory structure:
    src/
      app/          -- pages and layouts
      components/   -- map/, bean/, brewing/, filter/, compare/, visualization/, shared/, layout/
      lib/          -- schemas, utils, data loading, map helpers, search config
      store/        -- Zustand store
      types/        -- TypeScript interfaces
      content/      -- MDX articles (Phase 3)
      data/         -- JSON seed data + GeoJSON
    public/
      images/       -- beans/, methods/, icons/
    

1.2 Mapbox Setup

  • Create Mapbox account and generate access token
  • Store token in .env.local as NEXT_PUBLIC_MAPBOX_TOKEN
  • Design custom light map style in Mapbox Studio (warm terrain tones, desaturated non-bean-belt regions)
  • Design custom dark map style (espresso-toned background, reduced saturation accents)
  • Add .env.local to .gitignore, create .env.example with placeholder

1.3 Seed Data - Coffee Beans (Batch 1)

  • Create /data/beans.json with 30 well-researched bean profiles:
    • Africa: Ethiopia (Yirgacheffe, Sidamo, Guji), Kenya (Nyeri, Kirinyaga), Rwanda (Nyamasheke), Tanzania (Kilimanjaro)
    • Central America: Guatemala (Antigua, Huehuetenango), Costa Rica (Tarrazu), Panama (Boquete/Gesha), Honduras (Copan)
    • South America: Colombia (Huila, Narino, Tolima), Brazil (Cerrado, Sul de Minas), Peru (Cajamarca)
    • Asia-Pacific: Indonesia (Sumatra Mandheling, Java), Yemen (Haraz), India (Malabar)
  • Each bean includes: id, slug, name, country, countryCode, region, coordinates [lng, lat], altitude range, varieties, processing method, roast recommendation, 6-axis flavor profile (1-10), flavor notes array, harvest months, description, related bean IDs
  • Validate all coordinates are accurate for the specific growing regions
  • Cross-reference flavor profiles against SCA cupping standards and specialty roaster notes

1.4 Seed Data - Brewing Methods & Recommendations

  • Create /data/brewing-methods.json with 8 methods:
    • Pour-Over V60, Chemex, Kalita Wave, Espresso, French Press, AeroPress, Cold Brew, Moka Pot
    • Each: id, name, category, description, icon name, equipment list, default parameters
  • Add brewingRecommendations array to each bean in beans.json:
    • Per method: grind size + microns, water temp (C), coffee:water ratio, bloom time, brew time, pour stages (pour-over), difficulty (1-5), affinity score (1-10), tasting notes
    • Tailor recommendations to each bean's characteristics (e.g., light-roast Ethiopian gets higher V60 affinity, lower French Press)

1.5 Seed Data - Regions GeoJSON

  • Create /data/regions.geojson with simplified polygons for the 30 origin regions
  • Source boundaries from Natural Earth / GADM datasets
  • Simplify polygons with mapshaper (tolerance ~0.01) to keep total file under 500KB
  • Include properties: regionId, country, name, altitudeRange

1.6 TypeScript Types & Zod Schemas

  • Define interfaces in /types/index.ts:
    • CoffeeBean, BrewRecommendation, BrewingMethod, CoffeeRegion, FlavorNote, PourStage
    • Type literals: ProcessingMethod, RoastLevel, GrindSize
  • Create Zod schemas in /lib/schemas.ts mirroring all interfaces
  • Write build-time validation script that parses all JSON data through Zod on next build
  • Create /lib/data.ts with typed data loading functions: getBeans(), getBeanBySlug(), getBrewingMethods(), getRegions()

1.7 Zustand Store

  • Create /store/index.ts with slices:
    • mapState: viewport (lat, lng, zoom, bearing, pitch), selectedBeanId
    • filterState: regions, processingMethods, altitudeRange, roastLevels, flavorRanges (acidity, body, sweetness, bitterness min/max)
    • comparisonState: selectedBeanIds (max 3), isComparisonOpen
    • uiState: theme, isBeanPanelOpen, isFilterPanelOpen
  • Add actions: selectBean(), clearSelection(), toggleFilter(), resetFilters(), addToComparison(), removeFromComparison()
  • Add computed selectors: filteredBeans() that applies all active filters to the bean dataset

1.8 Interactive Map Component

  • Build src/components/map/CoffeeMap.tsx (Client Component)
  • Render full-bleed map with react-map-gl Map component
  • Enable globe projection at low zoom levels
  • Add GeoJSON Source with bean coordinates, enable clustering (cluster: true, clusterMaxZoom: 14, clusterRadius: 50)
  • Render cluster Layer (circle with count label) and individual marker Layer (custom coffee bean icon)
  • Handle marker click: flyTo the bean coordinates (zoom 10), set selectedBeanId in store, open bean panel
  • Handle cluster click: flyTo cluster bounds to expand it
  • Sync map viewport to Zustand store for URL state (Phase 2)
  • Add atmosphere/fog for globe view aesthetic
  • Handle loading state with skeleton placeholder

1.9 Region Polygon Highlight

  • Build src/components/map/RegionHighlight.tsx
  • Add regions GeoJSON as a map Source
  • On marker hover, set a hoveredRegionId filter on a fill Layer (semi-transparent warm overlay)
  • Fade in/out with paint-transition properties
  • Clear highlight on mouse leave

1.10 Bean Profile Panel

  • Build src/components/bean/BeanPanel.tsx using shadcn Sheet component
  • Layout sections:
    • Header: Country flag emoji, bean name, region, altitude range
    • Flavor Profile: Display 6-axis values as labeled bars (radar chart comes in Phase 3)
    • Tasting Notes: Chip/badge list of flavor notes
    • Details: Varieties, processing method, roast recommendation, harvest season (month names)
    • Brewing: Preview cards for top 3 recommended methods (by affinity score)
    • Similar Beans: 2-3 related bean cards
    • Description: Short overview paragraph
  • Wire up: selectedBeanId from store -> fetch bean data -> render panel
  • Close panel: clear selection, panel slides out
  • Add "View Full Profile" link to /bean/[slug] page

1.11 Top Navigation

  • Build src/components/layout/TopNav.tsx
  • Fixed position, 56px height, backdrop blur (backdrop-blur-md)
  • Left: BeanMap logo (text + coffee bean icon from Lucide)
  • Center: Search button placeholder (Cmd+K hint) -- functional search in Phase 2
  • Right: "Explore" link (map), "Learn" link (placeholder), theme toggle button
  • Mobile: Hamburger menu for nav links, search icon

1.12 Dark/Light Mode

  • Install and configure next-themes with ThemeProvider in root layout
  • Build src/components/shared/ThemeToggle.tsx (Sun/Moon icon toggle)
  • System preference detection with manual override
  • Map: switch Mapbox style ID between light and dark variants on theme change
  • All components use Tailwind dark: variants
  • Persist preference in localStorage

1.13 Responsive Layout

  • Mobile (< 640px): Map fills viewport, bean panel opens as bottom sheet (shadcn Sheet side="bottom")
  • Tablet (640-1024px): Bean panel as right sheet (50% width)
  • Desktop (> 1024px): Bean panel as right sheet (420px fixed width)
  • Ensure map controls (zoom +/-, compass) don't overlap with panels
  • Test touch interactions on map (pinch zoom, two-finger pan)

1.14 Bean Detail Page (SSR)

  • Create src/app/bean/[slug]/page.tsx as a Server Component
  • Fetch bean data by slug using getBeanBySlug()
  • Full-page bean profile with all details (expanded version of panel)
  • Generate metadata with Open Graph tags: title, description, image (static placeholder for now)
  • generateStaticParams() to pre-render all bean pages at build time
  • 404 handling for invalid slugs
  • "View on Map" button that links back to /?bean=[slug]

Phase 1 Verification

  • npm run build succeeds with no errors
  • Map loads with 30 markers and clustering at low zoom
  • Clicking a marker flies to it and opens the bean panel with correct data
  • Region highlights on marker hover
  • Dark/light mode toggles map style and all UI
  • Responsive: mobile bottom sheet, desktop side panel
  • Bean detail page renders at /bean/ethiopian-yirgacheffe with correct OG tags
  • Deploy to Vercel and verify production build

Phase 2: Core Features

Goal: Full filtering, search, brewing recommendations with calculator, expanded data, and URL state sync.

2.1 Filter Panel

  • Build src/components/filter/FilterPanel.tsx
  • Desktop: Collapsible left sidebar (280px), toggle button on map edge
  • Mobile: Full-screen bottom sheet triggered by filter icon button
  • Sections:
    • Region: Multi-select checkboxes (Africa, Central America, South America, Asia-Pacific) with country sub-groups
    • Processing Method: Checkboxes (Washed, Natural, Honey, Anaerobic, Wet-Hulled)
    • Roast Level: Toggle chips (Light, Medium-Light, Medium, Medium-Dark, Dark)
    • Altitude: Dual-handle range slider (500-2500 masl)
  • "Reset Filters" button, active filter count badge on toggle button
  • Wire all values to Zustand filterState

2.2 Flavor Profile Sliders

  • Build src/components/filter/FlavorSliders.tsx
  • Dual-handle range sliders for: Acidity, Body, Sweetness, Bitterness (range 1-10)
  • Use shadcn Slider component (customized for dual handles)
  • Real-time preview: show count of matching beans as sliders adjust
  • Add to FilterPanel as a collapsible "Flavor Profile" section

2.3 Real-Time Map Filtering

  • When filterState changes, compute filteredBeans via Zustand selector
  • Update GeoJSON source data: set filtered: false property on non-matching beans
  • Map paint expression: matching markers full opacity, non-matching at 0.15 opacity
  • Animate opacity transition (200ms ease)
  • Update cluster counts to only include matching beans
  • Show "X of Y beans" counter on the filter panel

2.4 Search (Cmd+K)

  • Build src/components/shared/SearchCommand.tsx using shadcn Command (cmdk)
  • Configure Fuse.js index in /lib/search.ts:
    • Search fields: name, country, region, flavorNotes (weighted: name > region > flavorNotes)
    • Threshold: 0.3 for fuzzy matching
  • Keyboard shortcut: Cmd+K (Mac) / Ctrl+K (Windows) opens dialog
  • Results show: bean name, country flag, region, top 2 flavor notes
  • Fixed during Phase 5: searching by flavor note returned nothing. Fuse matched correctly, but cmdk then re-filtered the results against each item's value (name + country + region only) and discarded them — two search algorithms stacked on top of each other. The <CommandDialog> now passes shouldFilter={false} so Fuse alone ranks. Verified: "blackcurrant" returns 3 beans (was 0).
  • Selecting a result: close dialog, fly to bean on map, open bean panel
  • "No results" state with suggestion to adjust search
  • Recent searches stored in localStorage (last 5)

2.5 Brewing Recommendation Cards

  • Build src/components/brewing/BrewCard.tsx
  • Display: method icon, method name, affinity score (bar or dots), grind size, water temp, ratio, brew time, difficulty stars
  • Horizontal scrolling row in the bean panel (snap scrolling on mobile)
  • Cards sorted by affinity score (highest first)
  • Visual indicator for "Best Match" on highest affinity method
  • Click card to open brew detail modal

2.6 Brew Detail Modal

  • Build src/components/brewing/BrewDetailModal.tsx using shadcn Dialog
  • Full parameter display:
    • Grind size with visual particle-size reference (illustrated scale from extra-fine to extra-coarse)
    • Water temperature (C and F toggle)
    • Coffee:water ratio with gram amounts
    • Bloom time + total brew time
    • Pour stages with water amounts and instructions (pour-over methods)
    • Equipment list
    • Difficulty rating with description
  • "Why this works" explanation paragraph
  • Link to full brewing guide (Phase 3)
  • Integrated dose calculator (see 2.7)

2.7 Dose Calculator

  • Build src/components/brewing/BrewCalculator.tsx
  • Input: desired output (ml or cups, with cup size selector: 200ml, 250ml, 300ml, 350ml)
  • Auto-calculates: coffee grams, water ml, maintaining the bean-specific ratio
  • Persist preferred cup size in localStorage
  • Clean number display (round to 0.1g for coffee, whole ml for water)
  • Embedded within BrewDetailModal and also usable standalone

2.8 Seed Data - Beans (Batch 2)

  • Expand /data/beans.json to 150-200 profiles covering:
    • Colombia: Cauca, Santander, Sierra Nevada, Antioquia, Quindio
    • Brazil: Mogiana, Bahia, Espirito Santo, Chapada Diamantina
    • East Africa: Burundi (Kayanza), Tanzania (Mbeya), DRC (Kivu), Uganda (Mt. Elgon), Malawi (Misuku)
    • Central America: Honduras (Marcala, Comayagua), El Salvador (Apaneca), Nicaragua (Jinotega, Matagalpa), Mexico (Chiapas, Oaxaca)
    • Asia: Myanmar (Shan State), Papua New Guinea (Eastern Highlands), China (Yunnan), Vietnam (Da Lat specialty)
    • Islands: Hawaii (Kona), Jamaica (Blue Mountain), Reunion (Bourbon origin)
  • Add brewing recommendations for all new beans
  • Ensure no duplicate coordinates (offset overlapping markers slightly)

2.9 Flavor Notes Data

  • Create /data/flavor-notes.json following SCA flavor wheel hierarchy:
    • ~15 top-level categories (Fruity, Floral, Sweet, Nutty/Cocoa, Spices, Roasted, Cereal, etc.)
    • ~40 subcategories
    • ~100 specific notes with hex color for wheel visualization
  • Tag each bean in beans.json with specific flavor note IDs (replacing plain-text notes)
  • Update FlavorNote type and Zod schema

2.10 Bean List Page

  • Create src/app/beans/page.tsx
  • Grid view (default): Cards showing bean name, country, key flavor notes, altitude, processing
  • Table view (toggle): Sortable columns for all key attributes
  • Reuse filter components from the map view
  • Each card/row links to bean detail page AND has "Show on Map" button
  • Pagination or infinite scroll for 150+ beans

2.11 Similar Beans Section

  • Add "Similar Beans" section to BeanPanel and bean detail page
  • Algorithm: compute Euclidean distance across the 6-axis flavor profile
  • Show 3 closest beans (excluding same country to encourage exploration)
  • Display as mini cards with name, country, top 2 flavor notes
  • Click navigates to that bean on the map

2.12 URL State Sync

  • Install nuqs for type-safe URL search parameter management
  • Sync to URL: selected bean (?bean=slug), map viewport (lat, lng, zoom), active filters (region, processing, altitude, roast)
  • On page load, restore state from URL params
  • Shareable URLs: bean-map.vercel.app/?bean=ethiopian-yirgacheffe&region=africa&acidity=7-10
  • Update URL on state change without full page navigation (shallow routing)

Phase 2 Verification

  • Filters narrow visible markers in real-time with opacity fade
  • All filter types work: region, processing, altitude range, roast, flavor sliders
  • Cmd+K search finds beans by name, region, and flavor notes
  • Brew cards display correctly sorted by affinity in bean panel
  • Brew detail modal shows all parameters with correct C/F toggle
  • Dose calculator scales correctly for different cup sizes
  • Bean list page renders 150+ beans with grid/table toggle
  • URL state persists: copy URL, open in new tab, same view loads
  • Mobile: all features accessible via bottom sheets and modals

Phase 3: Enhanced UX

Goal: Rich data visualizations, bean comparison, polished animations, and educational content.

3.1 Flavor Radar Chart

  • Build src/components/visualization/FlavorRadar.tsx (pure SVG, no Recharts dep)
  • 6 axes: Acidity, Body, Sweetness, Bitterness, Complexity, Fruitiness
  • Filled polygon with semi-transparent brand color fill
  • Animated draw-on-enter (CSS keyframes, respects prefers-reduced-motion)
  • Replace the text-based flavor display in BeanPanel with this chart
  • Support overlay mode: render 2-3 polygons for comparison (different colors)

3.2 Interactive Flavor Wheel

  • Build src/components/visualization/FlavorWheel.tsx
  • D3 sunburst layout using d3-hierarchy and d3-shape
  • 3 concentric rings: category -> subcategory -> specific note
  • Color-coded segments matching the SCA wheel colors
  • Hover: highlight segment + show tooltip with note name and bean count
  • Click a segment: filter beans on map to only those with that flavor note
  • Smooth arc transitions on hover/click
  • Accessible: include screen-reader-only table listing all notes
  • Lazy-loaded (dynamic import) to avoid D3 in initial bundle
  • Place on map view as a toggleable overlay or on a dedicated /explore/flavors page

3.3 Altitude Chart

  • Build src/components/visualization/AltitudeChart.tsx (pure SVG/CSS bars)
  • Shows altitude ranges for currently visible/filtered beans
  • Sorted highest to lowest (by midpoint)
  • Color gradient from leaf-green (low) to roast-dark (high)
  • Click a bar to select that bean on the map
  • Lives on /explore/insights page

3.4 Harvest Calendar

  • Build src/components/visualization/SeasonalChart.tsx
  • Gantt-style grid: months as columns (Jan-Dec), beans as rows
  • Colored cells for harvest months, highlight current month (cherry-red ring)
  • Group rows by country for easy scanning
  • Click a row to navigate to that bean
  • Useful for finding "what's in season now"

3.5 Comparison Feature

  • Build src/components/compare/ComparisonTray.tsx
    • Sticky bottom bar that collapses to a peek state, slides up from bottom
    • Shows 1-3 selected bean mini cards with X to remove
    • "Share link" button -> /compare?beans=...
    • "Compare" button opens full comparison view in a Dialog
  • Build src/components/compare/ComparisonView.tsx
    • Side-by-side bean cards (2-3 columns)
    • Overlaid radar chart (all beans on one chart, different colors)
    • Parameter comparison table: altitude, processing, roast, top flavor notes
    • Brewing recommendation comparison with method picker
    • "Best for [method]" highlight (Trophy icon on highest affinity)
  • Add "Compare" toggle button to BeanCard and BeanPanel
  • Store comparison state in Zustand, max 3 beans (already wired pre-Phase-3)

3.6 Comparison Page

  • Create src/app/compare/page.tsx
  • Read bean slugs from URL params: /compare?beans=slug1,slug2,slug3
  • Render full ComparisonView as a standalone page
  • Shareable URL for bean comparisons (dynamic metadata: "X vs Y · Compare on BeanMap")
  • Empty state with CTA to explore beans

3.7 Framer Motion Polish

  • BeanPanel: AnimatePresence slide-in from right (desktop) / up (mobile via bottom sheet), staggered children entrance
  • Bean cards in lists: staggered fade-up on scroll into view (whileInView, row-capped delay)
  • Comparison tray: layout + AnimatePresence — cards fly in/out and reflow as beans are added/removed
  • Filter chips: entrance/exit animation when toggled (scale+fade AnimatePresence)
  • Hero image in bean panel: subtle parallax on scroll (per-bean gradient generated from the bean's flavor profile via src/lib/flavor-gradient.ts — dominant flavor-note category colors drive the hue, numeric profile drives the geometry; no external imagery)
  • Page transitions: fade between map and list views (app/template.tsx)
  • Loading skeletons: shimmer animation on data loading states (.skeleton, Skeleton component, map + /beans loading)
  • prefers-reduced-motion: disable all animations, instant transitions (global rule in globals.css; chart animations gated on motion-safe:)

3.8 Educational Content - Processing Methods

  • Set up MDX rendering pipeline with next-mdx-remote
  • Create src/app/learn/page.tsx (Learn hub with article grid)
  • Create src/app/learn/processing/[slug]/page.tsx (article renderer)
  • Write 5 MDX articles in /content/processing/:
    • washed.mdx - Washed/wet processing
    • natural.mdx - Natural/dry processing
    • honey.mdx - Honey processing (yellow, red, black)
    • anaerobic.mdx - Anaerobic fermentation
    • wet-hulled.mdx - Wet-hulled (Giling Basah)
  • Each article: overview, step-by-step process, impact on flavor, origin regions that use it, embedded SVG diagrams
  • Link from bean profiles to relevant processing article

3.9 Educational Content - Brewing Guides

  • Create src/app/learn/brewing/[slug]/page.tsx (article renderer)
  • Write 8 MDX guides in /content/brewing/:
    • v60.mdx
    • chemex.mdx, kalita-wave.mdx, french-press.mdx
    • aeropress.mdx, espresso.mdx, cold-brew.mdx, moka-pot.mdx
  • Each guide: equipment list, step-by-step instructions with timing, common mistakes, tips for different beans
  • Embed interactive BrewTimer component within guides
  • Link from brew detail modal to relevant guide

3.10 Interactive Brew Timer

  • Build src/components/brewing/BrewTimer.tsx
  • Countdown/count-up timer with start/pause/reset
  • Pour stage alerts for pour-over methods (visual + optional sound)
  • Configurable stages based on the brewing method's pour schedule
  • Circular progress indicator with elapsed/remaining time
  • requestAnimationFrame for smooth rendering
  • Embeddable in MDX articles and brew detail modal

3.11 Altitude Heatmap Layer

  • Add toggleable terrain/altitude layer to the map
  • Use Mapbox terrain-rgb raster tiles
  • Style with color ramp showing elevation gradient (greens -> browns -> whites)
  • Only display within coffee-growing regions (mask with bean-belt bounds)
  • Toggle button on map controls
  • Opacity slider for blending with base map

3.12 Mobile Bottom Sheet

  • Build src/components/layout/MobileBottomSheet.tsx
  • Replace simple mobile Sheet with a draggable bottom sheet
  • Three snap points: peek (25% - shows bean name + key stats), half (50%), full (90%)
  • Gesture-based: drag handle, flick to expand/collapse
  • Use @use-gesture/react for touch handling
  • Smooth spring animation between snap points
  • Backdrop: map dims slightly when sheet is at half or full

Phase 3 Verification

  • Radar chart renders correctly for each bean with animated entrance
  • Flavor wheel displays full SCA hierarchy, clicking filters beans on map
  • Altitude chart and harvest calendar show correct data for filtered beans
  • Comparison tray: add up to 3 beans, overlaid radar chart renders, parameter table is accurate
  • Comparison URL is shareable and loads correctly
  • Animations are smooth (60fps), respect reduced-motion preference
  • All 13 MDX articles render with embedded components (timer, diagrams)
  • Brew timer counts down accurately with pour stage alerts
  • Mobile bottom sheet drags smoothly between snap points

Phase 4: Social & Community

Goal: Sharing capabilities, persistent favorites, optional authentication, and community features.

4.1 Share Functionality

  • Build src/components/shared/ShareButton.tsx
  • Mobile: Web Share API (navigator.share()) with title, text, URL
  • Desktop fallback: Copy-to-clipboard with "Copied!" toast notification
  • Add ShareButton to: BeanPanel, bean detail page, ComparisonView, BrewDetailModal
  • Share text format: "Check out [Bean Name] from [Region] on BeanMap! [URL]"

4.2 OG Image Generation

  • Create src/app/api/og/route.tsx using @vercel/og (Satori)
  • Dynamic OG images for bean pages: bean name, origin, radar chart preview, key flavor notes, warm coffee-themed background
  • Dynamic OG images for comparison pages: 2-3 bean names, "Compare on BeanMap"
  • Update all metadata exports to use dynamic OG image URLs
  • Test with social media debuggers (Twitter Card Validator, Facebook Sharing Debugger)

4.3 Shareable Brew Recipe Cards

  • Build src/components/brewing/ShareRecipeCard.tsx
  • "Share Recipe" button in BrewDetailModal
  • Generate a visually appealing card showing: bean name, brewing method, key parameters (grind, temp, ratio, time)
  • Option 1: Generate as OG-powered link (dynamic OG image at /api/og/recipe?bean=X&method=Y)
  • Option 2: Generate as downloadable PNG using html-to-image library
  • Card design: clean layout with coffee color palette, BeanMap branding

4.4 Local Favorites

  • Build src/components/shared/FavoriteButton.tsx (heart icon toggle)
  • Zustand persist middleware to save favorited bean IDs in localStorage
  • Add FavoriteButton to: BeanPanel, BeanCard, bean detail page
  • Create src/app/favorites/page.tsx:
    • Grid of favorited bean cards
    • Sort by: date added, name, region
    • Empty state with CTA to explore
    • Export favorites list (optional: JSON download)

4.5 Database Setup

  • Provision Neon Postgres database (free tier)
  • Install Drizzle ORM + drizzle-kit
  • Define schemas in /drizzle/schema.ts:
    • users table: id, email, name, image, provider, createdAt
    • favorites table: id, userId, beanSlug, createdAt
    • brewNotes table: id, userId, beanSlug, methodId, note, rating, createdAt, updatedAt
  • Generate and run initial migration
  • Create /lib/db.ts with Drizzle client configuration
  • Add database URL to .env.local

4.6 Authentication

  • Install NextAuth.js v5 (next-auth@beta)
  • Configure providers: Google OAuth, GitHub OAuth
  • Set up JWT session strategy (no database sessions needed)
  • Create auth API route at src/app/api/auth/[...nextauth]/route.ts
  • Build sign-in/sign-out UI components
  • Add user avatar/sign-in button to TopNav
  • Create user record in Postgres on first login
  • Protect authenticated routes with middleware

4.7 Synced Favorites

  • Create API routes:
    • GET /api/favorites - list user's favorites
    • POST /api/favorites - add a favorite
    • DELETE /api/favorites/[beanSlug] - remove a favorite
  • On login: merge localStorage favorites with server favorites (union, no duplicates)
  • After merge: clear localStorage favorites, use server as source of truth
  • FavoriteButton: check auth state, use local or server accordingly
  • Optimistic updates with rollback on error

4.8 Personal Brew Notes

  • Build src/components/brewing/BrewNoteForm.tsx
  • Authenticated users can add notes to a bean:
    • Brewing method used
    • Free-text tasting notes
    • Rating (1-5 stars)
    • Date brewed
  • Create API routes for CRUD operations on brew notes
  • Display as a timeline on the bean profile (private, only visible to the author)
  • Build src/app/notes/page.tsx - personal brewing journal listing all notes

4.9 Discover Feed

  • Create src/app/discover/page.tsx
  • Sections:
    • In Season Now: Beans currently in harvest (based on current month)
    • Popular This Week: Most favorited beans (requires tracking, or curate manually)
    • Editor's Picks: Curated rotating selection (stored in JSON or CMS)
    • New Additions: Recently added beans
  • Responsive grid layout with section headers
  • Link to bean profiles and map view

Phase 4 Verification

  • Share button works on mobile (Web Share API) and desktop (clipboard)
  • OG images generate correctly for bean pages and comparisons
  • Brew recipe card generates and downloads as PNG
  • Favorites persist in localStorage for unauthenticated users
  • Auth flow works: sign in with Google/GitHub, user created in DB
  • Favorites sync: local favorites merge on first login, subsequent favorites save to DB
  • Brew notes: create, read, update, delete all work correctly
  • Discover page shows correct seasonal beans based on current month

Phase 5: Polish & Launch

Goal: Performance optimization, SEO hardening, accessibility audit, and production readiness.

5.1 Performance Audit

  • Run Lighthouse on key pages: home/map, bean detail, bean list, learn articles
  • Target scores: Performance 90+, Accessibility 95+, Best Practices 95+, SEO 95+
  • Measure Core Web Vitals: LCP < 2.5s, FID < 100ms, CLS < 0.1
  • Identify and address largest contentful paint bottleneck (likely Mapbox JS)
  • Profile React rendering with React DevTools Profiler

5.2 Bundle Optimization

  • Analyze output — via npm run analyze (next build --experimental-analyze). NOT @next/bundle-analyzer: that plugin configures webpack, which this project no longer uses (Next 16 builds with Turbopack), so it would be a silent no-op. Next's built-in Turbopack analyzer replaces it.
  • Dynamic imports for heavy components:
    • CoffeeMap (Mapbox GL JS — measured 466KB gzipped, kept out of initial load)
    • FlavorWheel (D3 subset, via FlavorWheelLazy)
    • ComparisonView (lazy via ComparisonTray)
    • BrewTimer (lazy via BeanPanel)
    • SearchCommandDialog (cmdk, mounted only after first open)
  • Removed Framer Motion from the universal route wrapper — template.tsx used motion.div for the page fade, which pulled the whole animation engine (~43KB gz) into the shared bundle of every page, including fully static Learn articles. Now a CSS keyframe animation (.page-fade). Framer now loads only on pages that actually animate (map, beans list).
  • Slimmed the root-layout search payload: <SearchCommand> received full CoffeeBean[] + FlavorNotesData, which serialized into the RSC payload of every page. Now a SearchableBean projection built server-side. Learn-page HTML 216KB -> 77KB raw (29KB -> 17KB gz).
  • Tree-shake D3: only d3-hierarchy and d3-shape are imported
  • Verified no lodash/moment imports
  • Target: initial JS bundle < 200KB gzipped — not yet met. Measured (gzipped, initial JS per page): | page | before | after | modern browsers* | |---|---|---|---| | /en (map) | 402KB | 360KB | 321KB | | /en/beans | 370KB | 327KB | 289KB | | /en/bean/[slug] | 310KB | 267KB | 229KB | | /en/learn | 305KB | 262KB | 224KB | * excludes the 38.5KB legacy polyfill chunk, which is served noModule and never fetched by browsers that support ES modules. Remaining floor is framework: react-dom (69KB) + Next runtime (47KB) + base-ui/sonner/nuqs/next-intl/next-auth (~65KB combined). Next levers: drop SessionProvider from the root layout (only auth-dependent UI needs it), and lazy-load sonner.

5.3 Image Optimization

Largely N/A as built — the site ships essentially no raster imagery. Bean "hero images" are per-bean CSS gradients generated from the flavor profile (src/lib/flavor-gradient.ts, decided in 3.7), flavor icons are SVG, and the visualizations are inline SVG. Audited rather than implemented:

  • No bean hero images exist to convert — nothing to do
  • next/image used for the only raster asset in the UI (the 96x96, 12KB TopNav logo). The one raw <img> is the OAuth avatar in UserMenu, which is correct: it's a remote provider URL rendered in a fixed-size box, so it causes no layout shift.
  • Total image weight in the initial viewport is ~12KB, far under the 500KB budget
  • src/app/icon.png is 258KB for a 512x512 icon — recompress (not on the critical path, but it's the single largest static asset)

5.4 Map Performance

  • Measure GeoJSON file sizes — public/data/regions.geojson is 363KB, well under the 1MB budget. No further simplification needed.
  • Vector tilesets not needed at this size (see above); revisit only if the region set grows past ~1MB.
  • Implement useDeferredValue for filter state to prevent map jank during rapid filter changes
  • Debounce viewport sync to URL (300ms) to avoid excessive history entries
  • Fixed whole-store subscriptions. CoffeeMap and six other components called useBeanMap() with no selector, subscribing to every store change. Since onMove writes viewport on each animation frame and onMouseMove writes hoveredRegionId on each pointer move, dragging the map forced a full React re-render of the map, its sources and layers — and of the whole filter UI — ~60x/second. All seven now use useShallow selectors, and the map reads its initial camera once via getState() instead of subscribing.
  • Test on low-end devices (throttled CPU/network in DevTools)

5.5 SEO

  • Generate dynamic sitemap.xml at build time (src/app/sitemap.ts) — 148 URLs: 6 static + 55 beans + 13 learn articles, x2 locales. Every entry carries the full hreflang alternate set plus x-default.
  • Create robots.txt (src/app/robots.ts) — allows all, disallows /api/, points to the sitemap. Deliberately does not Disallow the thin/private routes: a disallowed URL is never fetched, so the crawler would never see their noindex or canonical tags. Those use per-page noindex, follow.
  • Add JSON-LD structured data (src/lib/structured-data.ts + <JsonLd>):
    • Bean pages: Article about a Thing (origin attributes as additionalProperty, plus GeoCoordinates) + BreadcrumbList. Modelled as Article, not Product: BeanMap sells nothing, and a Product with no offers/review/aggregateRating earns no rich result and reports missing required fields in the Rich Results Test.
    • Home: Organization + WebSite graph
    • /beans: CollectionPage + ItemList + BreadcrumbList
    • Learn articles: Article + BreadcrumbList
  • Unique meta title and description for every page
  • Canonical URLs on all pages, plus hreflang alternates for en / zh-TW / x-default (src/lib/seo.ts — every route is locale-prefixed, so the translations must be declared as alternates rather than duplicates)
  • Indexation policy for thin / combinatorial routes (noindex, follow): /favorites and /notes (per-visitor content), /compare?beans=… (any 2-3 of 55 beans = tens of thousands of near-identical URLs), and the per-method recipe pages (~440 per locale, canonical -> parent bean page). All still render full OG cards, since they exist to be shared.
  • Submit to Google Search Console, verify indexing (needs production deploy)
  • Test with Google Rich Results Test (needs a public URL)

5.6 Accessibility Audit

  • Full keyboard navigation audit: every interactive element reachable and operable via keyboard
  • Screen reader testing with VoiceOver (macOS) and NVDA (Windows)
  • Run axe-core automated audit on all pages, fix all violations
  • Map accessibility: provide a hidden bean list as an equivalent non-visual navigation method
  • All images have descriptive alt text
  • Radar charts and flavor wheel include screen-reader-only data tables
  • Color contrast: verify all text meets WCAG 2.1 AA (4.5:1 body, 3:1 large)
  • Focus indicators: visible ring on all interactive elements
  • prefers-reduced-motion disables all animations
  • Skip-to-content link for keyboard users
  • Aria labels on all icon-only buttons

5.7 Error Handling

  • Add React Error Boundaries around: Map component, visualization components, auth-dependent sections
  • Graceful fallback UI for map loading failures (Mapbox token issues, network errors)
  • Custom 404 page for invalid bean slugs and routes
  • Custom 500 page for server errors
  • API route error responses with proper status codes and messages
  • Toast notifications for user-facing errors (favoriting fails, share fails, etc.)

5.8 Analytics

  • Add Vercel Analytics for Web Vitals monitoring
  • Add Plausible Analytics (or Vercel Web Analytics) for page views (privacy-friendly, no cookies)
  • Track key events: bean viewed, brew method selected, filter applied, comparison made, favorite added, share clicked
  • Dashboard for monitoring engagement and popular beans

5.9 PWA Basics

  • Create manifest with app name, icons, theme color, display: standalone (src/app/manifest.ts -> /manifest.webmanifest). start_url is /en rather than /, so an installed app doesn't round-trip the i18n proxy on every launch.
  • Add to root layout <head> — Next links the manifest automatically; added a matching viewport.themeColor (light/dark) alongside it
  • Configure service worker with @ducanh2912/next-pwa or next-pwa
  • Cache strategies:
    • Bean data JSON: cache-first (update in background)
    • Images: cache-first with stale-while-revalidate
    • Educational content: cache-first
    • Map tiles: network-first (Mapbox handles its own caching)
  • Offline fallback page for when network is unavailable
  • Test add-to-homescreen on iOS and Android

5.10 End-to-End Tests

  • Set up Playwright with test fixtures
  • Critical flow tests:
    • Land on map -> markers visible -> click marker -> bean panel opens with correct data
    • Apply filters -> markers fade -> clear filters -> all markers return
    • Cmd+K search -> type bean name -> select result -> map flies to bean
    • Open brew recommendation -> view full details -> use dose calculator
    • Add 2 beans to comparison -> open comparison view -> radar chart overlays
    • Toggle dark/light mode -> map and UI update
    • Navigate to bean detail page -> all sections render -> "View on Map" works
    • Mobile: bottom sheet opens and drags between snap points
  • Run in CI (GitHub Actions) on push to main
  • Visual regression tests for key components (optional: Playwright screenshots)

5.11 Documentation

  • Write README.md:
    • Project overview and screenshots
    • Tech stack summary
    • Local development setup instructions
    • Environment variables guide
    • Project structure overview
    • Data contribution guide (how to add new beans to beans.json)
    • Deployment instructions (Vercel)
  • Add CONTRIBUTING.md with code style, PR process, data quality guidelines
  • Add LICENSE (MIT or similar)

5.12 Production Launch

  • Final deploy to Vercel production environment
  • Set up custom domain (e.g., beanmap.coffee or bean-map.vercel.app)
  • Verify all pages load correctly in production
  • Verify OG images, sitemap, robots.txt in production
  • Test on real mobile devices (iOS Safari, Android Chrome)
  • Submit sitemap to Google Search Console
  • Create launch announcement / social media posts

Phase 5 Verification

  • Lighthouse scores: Performance 90+, Accessibility 95+, Best Practices 95+, SEO 95+
  • Core Web Vitals pass in Vercel Analytics
  • All Playwright E2E tests pass in CI
  • Full keyboard navigation works across all pages
  • VoiceOver reads all content meaningfully
  • PWA installs and works offline (cached content)
  • No console errors or warnings in production build
  • Custom domain resolves and HTTPS works
  • OG images render correctly when shared on Twitter/Facebook/LinkedIn