Inspiration

Every day, millions of people face the same exhausting question: "What should I eat today?" Decision fatigue is real—studies show adults make over 200 food-related decisions daily. I wanted to transform this daily anxiety into a moment of genuine delight. The idea struck me while watching a lottery draw on TV: the spinning balls, the suspense, the reveal. Why not apply that same dopamine hit to meal selection? Thus, "What to Eat Today" was born—a slot-machine-style web app that makes choosing meals feel like unwrapping a gift, complete with hidden cultural surprises and smart nutrition logic under the hood.

What I Learned

Building this project deepened my understanding of several key concepts:

  • Probabilistic recommendation systems: I learned how to design a weighted random algorithm that balances short-term variety (no repeats within 3 days) with long-term fairness (every dish gets equal exposure over time). The weight calculation uses inverse frequency weighting:

$$W_i = \frac{1}{C_i + 1}$$

where $W_i$ is the selection weight for dish $i$, and $C_i$ is its cumulative selection count. The $+1$ prevents division by zero and ensures cold-start dishes have maximum initial weight.

  • State machine design in UX: The app has a nuanced flow—idle, spinning, revealed, partially re-rolling, and confirmed/locked. Modeling this as a finite state machine was critical to avoiding edge-case bugs like double confirmations or inconsistent history.

  • Animation performance optimization: Building the slot-machine reel effect with requestAnimationFrame and transform: translateY instead of CSS animations gave me fine-grained control over easing curves and stop positions. I used a deceleration function:

$$v(t) = v_0 \cdot e^{-kt}$$

where $v_0$ is the initial velocity and $k$ is the friction coefficient, producing a natural-looking slow-down.

  • How cultural context enhances UX: Adding dialect greetings (e.g., "您几位里边儿请!" for Beijing duck) required structuring the dish database with optional dialect fields and a matching system that surfaces them at just the right moment—after the reveal, before the confirmation. These micro-interactions turned functional feedback into memorable moments.

How I Built It

The project uses a deliberately minimal stack to maximize portability and zero-config deployment:

Frontend: A single index.html file containing all four views (landing page, onboarding guide, main slot-machine interface, and settings panel). Page switching uses a hash-based router. All CSS is inline, styled with a warm food-inspired palette (amber, cream, orange) and rounded fonts for a playful feel. The slot-machine animation is powered by a custom JavaScript engine that maps reel position offsets to final dish selections, ensuring visual stops align precisely with backend results.

Backend: Node.js with Express serves both the static frontend and a REST API. Endpoints handle:

  • POST /api/generate — full meal randomization
  • POST /api/regenerate — partial re-roll (user picks which categories to replace)
  • POST /api/confirm — lock today's choice into history
  • GET /api/today — fetch locked recommendation
  • GET/PUT /api/settings — dietary preferences management

Data Layer: Three JSON files act as a lightweight database: dishes.json (60+ items across 4 categories with tags and dialect strings), history.json (rolling 7-day confirmed meal log), and settings.json (allergies, vegetarian mode, dessert toggle). No external database means zero setup—just clone and run.

Smart Recommendation Engine: The core algorithm filters dishes by user dietary restrictions, removes items seen in the last 3 days (with a soft fallback if all candidates are exhausted), then applies weighted random selection. The weight for each candidate dish $i$ is:

$$P(i) = \frac{\frac{1}{C_i + 1}}{\sum_{j \in S} \frac{1}{C_j + 1}}$$

where $S$ is the set of eligible dishes in that category, and $C_i$ is the historical selection count. This ensures both recency-based diversity and long-term uniformity.

Challenges Faced

  1. The Partial Re-roll Constraint Problem: Allowing users to replace only specific categories (e.g., "just redo the protein") while keeping others fixed introduced a non-trivial constraint satisfaction problem. The regenerated category must not only respect dietary filters and the 3-day recency rule, but also avoid creating an exact duplicate of any meal from the past 7 days. I solved this by querying history for partial matches, then filtering candidates against those known combinations before applying the weighted random selection.

  2. Synchronizing Animation Endpoints with Backend Results: The slot-machine reels visually spin before the server even responds. I needed the animation to stop exactly on the dish the backend selected. The solution: the frontend pre-generates a random "spin index" per reel and sends it alongside the request. The backend uses that index modulo the candidate list length to deterministically pick the dish. The animation then decelerates to land on the corresponding visual position—perfect sync, no race conditions.

  3. State Persistence Without a Database: Users expect their "locked" daily meal to survive page refreshes and server restarts. With only JSON files, I timestamp every confirmed entry and implemented a GET /api/today endpoint that checks if any entry matches today's date. On page load, the frontend calls this first—if it returns a result, the UI skips straight to the confirmed state, bypassing the spin button entirely.

  4. Cold-Start Dish Selection: On first use with empty history, all dishes have equal cumulative counts ($C_i = 0$ for all $i$). Pure random selection could lead to unbalanced long-term distributions. I introduced a small $\epsilon$-greedy exploration factor: 90% of the time, use the inverse-frequency weighted random described above; 10% of the time, pick completely at random from the eligible pool to ensure the system continuously explores the full menu space. Over time, the weighted component naturally dominates as counts accumulate.

  5. Balancing Fun with Nutrition:The app must recommend meals that are genuinely balanced (staple + protein + vegetable) while still feeling serendipitous. I structured dishes.json with explicit category tags and enforced cross-category completeness in every generation cycle—the algorithm never outputs an incomplete plate. The dessert category is optional and toggleable, preserving nutritional integrity even when users opt into sweets.

This project taught me that great UX isn't just about functionality—it's about injecting personality, anticipation, and cultural warmth into everyday decisions. What started as a simple meal picker became a lesson in probabilistic algorithms, state machines, and the art of delightful micro-interactions.

Built With

  • deepseek
  • medo
Share this project:

Updates