Skip to content

feat: expand food database + remove + suffix from counters - #20

Merged
rekabytes merged 5 commits into
mainfrom
dev
Dec 26, 2025
Merged

feat: expand food database + remove + suffix from counters#20
rekabytes merged 5 commits into
mainfrom
dev

Conversation

@rekabytes

@rekabytes rekabytes commented Dec 26, 2025

Copy link
Copy Markdown
Owner

Foods (230 items):

  • Added 34 more Malaysian foods: kuih, regional dishes, street food, drinks

Halal Brands (118 items, 5 new brands for 2025):

  • Saji: instant noodles, condiments
  • A&W: fast food, beverages
  • Marigold: dairy, juices
  • Yeo's: Asian beverages
  • 100 Plus: sports drinks

Added 54 international foods (Japanese, Thai, Vietnamese, Korean, Chinese, Western, Healthy)
Removed + suffix from food count displays
Total: 284 foods + 118 halal = 402 items

📝 Description

Brief description of what this PR does.

🔗 Related Issue

Fixes #(issue number)

🏷️ Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • 📚 Documentation update
  • 🧹 Code refactoring (no functional changes)
  • 🧪 Test update (adding or updating tests)

✅ Checklist

  • I have read the Contributing Guidelines
  • My branch is created from dev (not main)
  • I have run pnpm lint:fix
  • I have run pnpm typecheck
  • I have tested my changes locally
  • My code follows the project's coding standards
  • I have updated documentation (if applicable)

📸 Screenshots (if applicable)

Add screenshots to help explain your changes.

🧪 How to Test

Steps to test this PR:

  1. ...
  2. ...
  3. ...

📝 Additional Notes

Any additional information reviewers should know.

Summary by CodeRabbit

  • New Features
    • Landing page hero section now displays animated statistics fetched live from the server, replacing static hardcoded values.
    • Database expanded significantly with hundreds of additional food items across multiple categories including regional dishes, street food, international cuisines, desserts, and branded products.
    • Enhanced statistics visualization with smooth number animation effects.

✏️ Tip: You can customize this high-level summary in your review settings.

Foods (230 items):
- Added 34 more Malaysian foods: kuih, regional dishes, street food, drinks

Halal Brands (118 items, 5 new brands for 2025):
- Saji: instant noodles, condiments
- A&W: fast food, beverages
- Marigold: dairy, juices
- Yeo's: Asian beverages
- 100 Plus: sports drinks

Total: 230 foods + 118 halal = 348 items
- Fetch total foods count from /api/stats endpoint
- Created TypewriterNumber component with typing/backspace animation
- Shows combined count of foods + halal certified items
- Fallback to 348 if API fails
- Added food.stats tRPC endpoint (public, no auth required)
- Updated Hero to use trpc.food.stats.useQuery()
- Removed fetch call that was hitting protected /api/stats endpoint
Stats now show:
- Foods: total count (230 + 118 = 348+)
- Halal Certified: 118+
- Free API Access
- <50ms Response Time
Added 54 international foods (Japanese, Thai, Vietnamese, Korean, Chinese, Western, Healthy)
Removed + suffix from food count displays
Total: 284 foods + 118 halal = 402 items
@coderabbitai

coderabbitai Bot commented Dec 26, 2025

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds a new stats endpoint to the food router that queries MongoDB collections in parallel and returns aggregated counts; expands seed data with numerous new food entries and branded items across both production seed scripts; updates the Hero component to fetch and display dynamic stats via TRPC; introduces a TypewriterNumber component for animated numeric rendering.

Changes

Cohort / File(s) Change Summary
Backend Stats Endpoint
packages/kal-backend/src/routers/food.ts
New public stats endpoint that queries foods and halal_foods collections in parallel, returning an object with counts for foods, halal items, and total.
Database Seed Data
packages/kal-db/scripts/seed.ts, packages/kal-db/scripts/seed-safe.ts
Significant expansion of seed data across both scripts, adding new food entries spanning categories: Additional Kuih, Regional Dishes, Street Food, Drinks, International (Asian/Western), Healthy & Fitness, Common Ingredients, Desserts, and Branded items (SAJI, A&W, Marigold, Yeo's, 100 Plus). Seed logic and control flow remain unchanged.
Frontend Stats Display
packages/kal-frontend/src/components/landing/Hero.tsx
Updates to fetch stats dynamically via trpc.food.stats.useQuery instead of hardcoded data; reconfigures stats grid from mapped items to explicit blocks for Foods, Halal Certified, Free/API Access, and Response Time; integrates TypewriterNumber for animated display.
Animated Number Component
packages/kal-frontend/src/components/ui/TypewriterNumber.tsx
New React client component that animates typing and backspacing numeric values with optional suffix; maintains internal state for displayText and typing animation loop with configurable styling.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Poem

🐰 Seeds now sprout in digital soil,
Stats dance and twirl—no more toil,
Numbers typewrite with typewriter's spin,
Let the frontend feast on data within!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the two main changes: expanding the food database (with specific regional/international additions) and removing the '+' suffix from display counters.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dev

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/kal-db/scripts/seed.ts (1)

271-530: Remove duplicate "Kuih Seri Muka" and "Mee Kari" entries in both seed.ts and seed-safe.ts.

  1. Duplicate entries confirmed:

    • "Kuih Seri Muka" (line 134 vs line 273): Identical except serving format ("2 pieces" vs "2 pcs")
    • "Mee Kari" (line 61 vs line 287): Exact duplicate with identical nutritional values
    • Both duplicates exist in both files; remove them from the "ADDITIONAL KUIH" and "REGIONAL DISHES" sections respectively to keep only the original entries in earlier sections.
  2. International food nutritional data and 2025 halal certification dates: These require verification against official JAKIM records and nutritional databases beyond the codebase scope.

🧹 Nitpick comments (1)
packages/kal-frontend/src/components/ui/TypewriterNumber.tsx (1)

56-64: Consider UX when value changes mid-animation.

When value or suffix props change (causing fullText to change), the animation restarts immediately. This could be jarring if stats update frequently. Consider either:

  • Completing the current animation cycle before showing the new value
  • Using a transition/fade effect when the value changes
  • Adding a debounce or minimum display time
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 90f096a and 4386e53.

📒 Files selected for processing (5)
  • packages/kal-backend/src/routers/food.ts
  • packages/kal-db/scripts/seed-safe.ts
  • packages/kal-db/scripts/seed.ts
  • packages/kal-frontend/src/components/landing/Hero.tsx
  • packages/kal-frontend/src/components/ui/TypewriterNumber.tsx
🧰 Additional context used
🧬 Code graph analysis (2)
packages/kal-backend/src/routers/food.ts (1)
packages/kal-backend/src/lib/trpc.ts (1)
  • publicProcedure (8-8)
packages/kal-frontend/src/components/landing/Hero.tsx (2)
packages/kal-frontend/src/lib/trpc.ts (1)
  • trpc (5-5)
packages/kal-frontend/src/components/ui/TypewriterNumber.tsx (1)
  • TypewriterNumber (11-66)
🔇 Additional comments (4)
packages/kal-backend/src/routers/food.ts (1)

94-105: LGTM! Clean implementation of the stats endpoint.

The parallel queries using Promise.all efficiently fetch counts from both collections, and the endpoint structure is consistent with the existing router patterns. The public access level is appropriate for displaying stats on the landing page.

packages/kal-frontend/src/components/landing/Hero.tsx (1)

49-96: Verify the responsive grid layout.

The grid changed from 3 columns to 4 columns (grid-cols-2 md:grid-cols-4). On mobile, this displays 2 columns per row (2 rows of 2 items each), which should work well. Just ensure this layout has been tested on actual mobile devices to confirm readability and spacing.

packages/kal-db/scripts/seed-safe.ts (2)

314-374: Verify the extensive international food data additions.

The seed data now includes 54 international foods (Japanese, Thai, Vietnamese, Korean, Chinese, Western) and healthy options. While this expands the database significantly:

  1. Ensure accuracy: Nutritional data for international foods should be verified against reliable sources, especially for standardized items like sushi rolls or ramen where serving sizes vary widely.

  2. Consider data source attribution: For a nutrition API, having verifiable sources for this data adds credibility and helps with future maintenance.

  3. Halal status: These international foods are in the naturalFoods array (no formal halal cert). Ensure this aligns with the API's intended use case and user expectations.


491-529: All five brands are verified as JAKIM halal-certified with active certifications. SAJI, A&W Malaysia, MARIGOLD, YEO'S, and 100 PLUS are confirmed in JAKIM's system, with certificate expiry dates extending into 2026 (e.g., YEO'S expiring Aug 15, 2026; 100 PLUS expiring Feb 15, 2026). The halalCertYear: 2025 values reflect current, legitimate certifications rather than placeholders—no action required.

Comment on lines +272 to +283
// === ADDITIONAL KUIH ===
{ name: "Kuih Seri Muka", calories: 180, protein: 3, carbs: 30, fat: 7, serving: "2 pcs", category: "Desserts" },
{ name: "Kuih Dadar", calories: 140, protein: 2, carbs: 24, fat: 5, serving: "2 pcs", category: "Desserts" },
{ name: "Pulut Inti", calories: 200, protein: 3, carbs: 38, fat: 5, serving: "2 pcs", category: "Desserts" },
{ name: "Pulut Tai Tai", calories: 190, protein: 3, carbs: 35, fat: 5, serving: "2 pcs", category: "Desserts" },
{ name: "Kuih Keria", calories: 150, protein: 1, carbs: 28, fat: 5, serving: "3 pcs", category: "Desserts" },
{ name: "Kuih Kodok", calories: 130, protein: 1, carbs: 24, fat: 4, serving: "3 pcs", category: "Desserts" },
{ name: "Kuih Putu Piring", calories: 160, protein: 2, carbs: 32, fat: 4, serving: "3 pcs", category: "Desserts" },
{ name: "Kuih Kapit", calories: 120, protein: 2, carbs: 20, fat: 4, serving: "5 pcs", category: "Desserts" },
{ name: "Kuih Ros", calories: 100, protein: 1, carbs: 18, fat: 3, serving: "5 pcs", category: "Desserts" },
{ name: "Rempeyek", calories: 180, protein: 6, carbs: 18, fat: 10, serving: "5 pcs", category: "Snacks" },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Remove duplicate food entries from the seed data.

The "ADDITIONAL KUIH" section contains duplicates:

  • Line 273 "Kuih Seri Muka" duplicates Line 134 (same nutritional values)

Additionally:

  • Line 287 "Mee Kari" (in REGIONAL DISHES) duplicates Line 61 (in NOODLE DISHES section)

These duplicates will create multiple database entries with identical names, which could confuse users searching for foods and inflate the stats counts displayed on the frontend.

🔎 Entries to remove

Remove these duplicate lines:

  • Line 273: { name: "Kuih Seri Muka", ... } (already exists at line 134)
  • Line 287: { name: "Mee Kari", ... } (already exists at line 61)

Or differentiate them if they represent genuinely different recipes (e.g., "Kuih Seri Muka (Pandan)" vs "Kuih Seri Muka (Traditional)").

🤖 Prompt for AI Agents
In packages/kal-db/scripts/seed-safe.ts around lines 272-283 (and also check
line 287), there are duplicate seed entries — remove the duplicate `{ name:
"Kuih Seri Muka", ... }` at line 273 (original at line 134) and the duplicate `{
name: "Mee Kari", ... }` at line 287 (original at line 61); alternatively, if
they represent distinct recipes, rename them to unique names (e.g., "Kuih Seri
Muka (Pandan)" or "Mee Kari (Penang)") so each seed item has a unique name
before running the seed script.

import { trpc } from "@/lib/trpc";

export function Hero() {
const { data: stats } = trpc.food.stats.useQuery();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add error handling for the stats query.

The TRPC query has no error handling. If the stats endpoint fails, the component will show loading state indefinitely. Consider destructuring error and isLoading from the query result to provide better UX.

🔎 Recommended error handling
- const { data: stats } = trpc.food.stats.useQuery();
+ const { data: stats, error, isLoading } = trpc.food.stats.useQuery();

Then in the render logic:

  <div className="text-2xl md:text-3xl font-bold text-accent mb-1 h-9 flex items-center justify-center">
-   {stats?.total ? (
+   {error ? (
+     <span className="text-content-secondary">N/A</span>
+   ) : stats?.total ? (
      <TypewriterNumber value={stats.total} />
    ) : (
      <span className="animate-pulse">...</span>
    )}
  </div>

Apply similar logic to the halal stats block.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/kal-frontend/src/components/landing/Hero.tsx around line 9, the TRPC
query result is only destructuring data and lacks error/loading handling which
can leave the UI stuck; update the query call to destructure at least data,
error, and isLoading for both stats and halal stats queries, then in the render
return show a loading indicator or skeleton when isLoading is true, display a
concise error UI/message when error is present (and optionally log the error),
and only render the stats UI when data exists; ensure you avoid rendering
undefined values (use safe defaults) so the component never remains in an
indefinite loading state.

Comment on lines +16 to +54
useEffect(() => {
let currentIndex = 0;
let isDeleting = false;
let timeoutId: ReturnType<typeof setTimeout>;

const animate = () => {
if (!isDeleting) {
// Typing
if (currentIndex <= fullText.length) {
setDisplayText(fullText.slice(0, currentIndex));
currentIndex++;
timeoutId = setTimeout(animate, 80 + Math.random() * 40);
} else {
// Pause at full text, then start deleting
setIsTyping(false);
timeoutId = setTimeout(() => {
isDeleting = true;
setIsTyping(true);
animate();
}, 3000);
}
} else {
// Backspacing
if (currentIndex > 0) {
currentIndex--;
setDisplayText(fullText.slice(0, currentIndex));
timeoutId = setTimeout(animate, 50 + Math.random() * 30);
} else {
// Pause, then start typing again
isDeleting = false;
timeoutId = setTimeout(animate, 500);
}
}
};

animate();

return () => clearTimeout(timeoutId);
}, [fullText]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix animation logic issues and improve cleanup.

Several issues with the animation implementation:

  1. Off-by-one error (Line 24): The condition currentIndex <= fullText.length will iterate one extra time showing an empty string at the end. Should be currentIndex < fullText.length.

  2. Closure variable mutations: The currentIndex and isDeleting variables are mutated inside the animate closure but aren't part of React's state management. This pattern works but is fragile—if fullText changes mid-animation, the effect restarts but old timeouts might still reference stale closure variables.

  3. Incomplete cleanup: Only one timeoutId is cleared on unmount, but if the effect runs multiple times (e.g., when fullText changes rapidly), multiple timeouts could be scheduled and not all will be cleaned up properly.

🔎 Recommended refactor for robust animation
  useEffect(() => {
    let currentIndex = 0;
    let isDeleting = false;
-   let timeoutId: ReturnType<typeof setTimeout>;
+   let timeoutId: ReturnType<typeof setTimeout> | undefined;
+   let isCancelled = false;

    const animate = () => {
+     if (isCancelled) return;
+     
      if (!isDeleting) {
        // Typing
-       if (currentIndex <= fullText.length) {
+       if (currentIndex < fullText.length) {
          setDisplayText(fullText.slice(0, currentIndex));
          currentIndex++;
          timeoutId = setTimeout(animate, 80 + Math.random() * 40);
        } else {
          // Pause at full text, then start deleting
          setIsTyping(false);
          timeoutId = setTimeout(() => {
+           if (isCancelled) return;
            isDeleting = true;
            setIsTyping(true);
            animate();
          }, 3000);
        }
      } else {
        // Backspacing
        if (currentIndex > 0) {
          currentIndex--;
          setDisplayText(fullText.slice(0, currentIndex));
          timeoutId = setTimeout(animate, 50 + Math.random() * 30);
        } else {
          // Pause, then start typing again
          isDeleting = false;
          timeoutId = setTimeout(animate, 500);
        }
      }
    };

    animate();

-   return () => clearTimeout(timeoutId);
+   return () => {
+     isCancelled = true;
+     if (timeoutId !== undefined) {
+       clearTimeout(timeoutId);
+     }
+   };
  }, [fullText]);

This adds:

  • A cancellation flag to prevent queued callbacks from running after cleanup
  • Checks for cancellation at key points
  • Fixes the off-by-one error
  • Safer timeout cleanup

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/kal-frontend/src/components/ui/TypewriterNumber.tsx around lines 16
to 54, the animate effect has an off-by-one bug, fragile closure-mutation of
currentIndex/isDeleting, and incomplete cleanup; fix by changing the typing
boundary to use currentIndex < fullText.length, replace mutable local vars with
refs (useRef) for currentIndex, isDeleting and timeoutId so values persist
safely across re-renders, add a mounted/cancel flag (ref) checked at the start
of animate and before scheduling any setTimeout to prevent stale callbacks,
always clear the active timeout via clearTimeout(timeoutRef.current) before
scheduling a new one, and set the cancel flag and clearTimeout in the effect
cleanup so no queued timeouts run after unmount or fullText changes.

@rekabytes
rekabytes merged commit fbd947f into main Dec 26, 2025
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant