Conversation
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
📝 WalkthroughWalkthroughAdds 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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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.
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.
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
valueorsuffixprops change (causingfullTextto 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
📒 Files selected for processing (5)
packages/kal-backend/src/routers/food.tspackages/kal-db/scripts/seed-safe.tspackages/kal-db/scripts/seed.tspackages/kal-frontend/src/components/landing/Hero.tsxpackages/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.allefficiently 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:
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.
Consider data source attribution: For a nutrition API, having verifiable sources for this data adds credibility and helps with future maintenance.
Halal status: These international foods are in the
naturalFoodsarray (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). ThehalalCertYear: 2025values reflect current, legitimate certifications rather than placeholders—no action required.
| // === 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" }, | ||
|
|
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
🛠️ 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.
| 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]); |
There was a problem hiding this comment.
Fix animation logic issues and improve cleanup.
Several issues with the animation implementation:
-
Off-by-one error (Line 24): The condition
currentIndex <= fullText.lengthwill iterate one extra time showing an empty string at the end. Should becurrentIndex < fullText.length. -
Closure variable mutations: The
currentIndexandisDeletingvariables are mutated inside theanimateclosure but aren't part of React's state management. This pattern works but is fragile—iffullTextchanges mid-animation, the effect restarts but old timeouts might still reference stale closure variables. -
Incomplete cleanup: Only one
timeoutIdis cleared on unmount, but if the effect runs multiple times (e.g., whenfullTextchanges 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.
Foods (230 items):
Halal Brands (118 items, 5 new brands for 2025):
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
✅ Checklist
dev(notmain)pnpm lint:fixpnpm typecheck📸 Screenshots (if applicable)
Add screenshots to help explain your changes.
🧪 How to Test
Steps to test this PR:
📝 Additional Notes
Any additional information reviewers should know.
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.