Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/pages/Home/Home.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Hero from "./Sections/Hero";
import PopularMenus from "./Sections/PopularMenus";
import SpecialOffer from "./Sections/SpecialOffer";
import SuggestedMeals from "./Sections/SuggestedMeals";
import SuggestedMealsTeaser from "./Sections/SuggestedMealsTeaser";
import Testimonials from "./Sections/Testimonials";
import SmartMealBanners from "./Sections/SmartMealBanners";

Expand All @@ -28,7 +29,8 @@ export default function Home() {
{/* Popular Meals — always visible */}
<PopularMenus />
{/* Suggested Meals — only shown to authenticated users */}
{isAuthenticated && <SuggestedMeals />}
{isAuthenticated ? <SuggestedMeals /> : <SuggestedMealsTeaser />}

<SpecialOffer />
<AboutUs />
<FAQSection />
Expand Down
140 changes: 26 additions & 114 deletions src/pages/Home/Sections/SuggestedMeals.jsx
Original file line number Diff line number Diff line change
@@ -1,134 +1,46 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useEffect } from "react";
import { useAuthStore } from "../../../store";
import { getSuggestedMeals } from "../../../services/recommendation.service";
import PopularMenuCard from "../../../components/ui/PopularMenuCard";
import useRecommendationStore from "../../../store/recommendationStore";
import RegularFoodCard from "../../../components/ui/RegularFoodCard";
import LoadingSpinner from "../../../components/ui/LoadingSpinner";
import ScrollArrows from "../../../components/ui/ScrollArrows";
const SCROLL_AMOUNT = 340; // px per button click

/**
* SuggestedMeals
* ---------------
* Shown only when the user is authenticated (gated in Home.jsx).
* Fetches personalised meal recommendations from the service layer.
*
* Mock mode: getSuggestedMeals returns local mock data — no backend needed.
* API integration: when ready, just swap recommendation.service.js — no
* changes needed here.
*/
const SuggestedMeals = () => {
const SuggestedMeals = ({ selectedCategory = "All" }) => {
const { user } = useAuthStore();

const [meals, setMeals] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

const scrollRef = useRef(null);
const [canScrollLeft, setCanScrollLeft] = useState(false);
const [canScrollRight, setCanScrollRight] = useState(true);
const { recommendations, isLoading, error, fetchRecommendations } =
useRecommendationStore();

useEffect(() => {
// Do not fetch until we have a real user ID — the API endpoint requires it
if (!user?.id) return;

let cancelled = false;

const fetchSuggestions = async () => {
setLoading(true);
setError(null);
try {
const response = await getSuggestedMeals(user.id);
if (!cancelled) {
setMeals(response.data);
}
} catch (err) {
if (!cancelled) {
setError(err.message || "Failed to load suggestions");
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
};

fetchSuggestions();

// Cleanup: prevent state updates after unmount
return () => {
cancelled = true;
};
}, [user?.id]);

// Track scroll position so buttons hide/show at the edges
const updateScrollState = useCallback(() => {
const el = scrollRef.current;
if (!el) return;
setCanScrollLeft(el.scrollLeft > 4);
setCanScrollRight(el.scrollLeft + el.clientWidth < el.scrollWidth - 4);
}, []);

useEffect(() => {
const el = scrollRef.current;
if (!el) return;
// Run once on mount to set initial state
updateScrollState();
el.addEventListener("scroll", updateScrollState, { passive: true });
return () => el.removeEventListener("scroll", updateScrollState);
}, [updateScrollState, meals]);

const scrollLeft = useCallback(() => {
scrollRef.current?.scrollBy({ left: -SCROLL_AMOUNT, behavior: "smooth" });
}, []);

const scrollRight = useCallback(() => {
scrollRef.current?.scrollBy({ left: SCROLL_AMOUNT, behavior: "smooth" });
}, []);

// thoose three lines hide the suggested meals when no meal is suggested
//if you want to test the suggested meals comment it it
if (loading) return <LoadingSpinner />;
if (user?.role && recommendations.length === 0) {
fetchRecommendations();
}
}, [user?.role]);

const filteredMeals = recommendations.filter((meal) => {
if (meal.isActive === false) return false;
return (
selectedCategory === "All" ||
meal.category?.toLowerCase() === selectedCategory?.toLowerCase()
);
});

if (isLoading) return <LoadingSpinner />;
if (error) return <p className="text-red-500 text-center">{error}</p>;
if (meals.length === 0) return null;
//***********************************************************************/
if (filteredMeals.length === 0) return null;

return (
<section className="py-8 md:py-12">
<div className="container mx-auto px-4">
{/* Header row with title */}
<div className="flex items-center gap-3 mb-6">
<h2 className="text-2xl md:text-3xl font-bold text-gray-800">
Suggested For You
</h2>
<span className="text-3xl">✨</span>
</div>

{/* Horizontally scrollable card row */}
<div className="relative">
<ScrollArrows
onScrollLeft={scrollLeft}
onScrollRight={scrollRight}
canScrollLeft={canScrollLeft}
canScrollRight={canScrollRight}
/>

<div
ref={scrollRef}
className="flex gap-5 md:gap-6 overflow-x-auto pb-4 scrollbar-hide snap-x snap-mandatory"
>
{meals.map((meal) => (
<PopularMenuCard
key={meal.id}
name={meal.name}
imageUrl={meal.imageUrl}
price={meal.price}
/>
))}

{/* Spacer so short lists don't look broken on wide screens */}
{meals.length < 4 && (
<div className="shrink-0 w-64 sm:w-72 md:w-80" />
)}
</div>
<div className="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-2 md:gap-8">
{filteredMeals.map((meal) => (
<RegularFoodCard key={meal.id} meal={meal} />
))}
</div>
</div>
</section>
Expand Down
109 changes: 109 additions & 0 deletions src/pages/Home/Sections/SuggestedMealsTeaser.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { Link } from "react-router-dom";
import RegularFoodCard from "../../../components/ui/RegularFoodCard";

const PLACEHOLDER_MEALS = [
{
id: 1,
name: "Mystery Meal",
price: 12.99,
hasDiscount: false,
discountPercentage: 0,
nutrients: [],
imageUrl: null,
},
{
id: 2,
name: "Mystery Meal",
price: 9.99,
hasDiscount: false,
discountPercentage: 0,
nutrients: [],
imageUrl: null,
},
{
id: 3,
name: "Mystery Meal",
price: 15.99,
hasDiscount: false,
discountPercentage: 0,
nutrients: [],
imageUrl: null,
},
{
id: 4,
name: "Mystery Meal",
price: 11.99,
hasDiscount: false,
discountPercentage: 0,
nutrients: [],
imageUrl: null,
},
];

const SuggestedMealsTeaser = () => {
return (
<section className="py-8 md:py-12">
<div className="container mx-auto px-4">
<div className="flex items-center gap-3 mb-6">
<h2 className="text-2xl md:text-3xl font-bold text-gray-800">
Suggested For You
</h2>
<span className="text-3xl">✨</span>
</div>

<div className="relative">
{/* Cards with light blur */}
<div className="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-2 md:gap-8 blur-[2px] pointer-events-none select-none opacity-60">
{PLACEHOLDER_MEALS.map((meal) => (
<RegularFoodCard key={meal.id} meal={meal} />
))}
</div>
Comment on lines +56 to +62

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Decorative blurred cards remain keyboard/screen-reader focusable.

pointer-events-none prevents mouse clicks but not keyboard tab focus or screen-reader traversal. Keyboard users can still tab into the placeholder cards' heart/add-to-cart buttons, which do nothing meaningful and aren't visually indicated as focused behind the blur/overlay.

♿ Suggested fix
-          <div className="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-2 md:gap-8 blur-[2px] pointer-events-none select-none opacity-60">
+          <div
+            aria-hidden="true"
+            className="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-2 md:gap-8 blur-[2px] pointer-events-none select-none opacity-60"
+          >
             {PLACEHOLDER_MEALS.map((meal) => (
               <RegularFoodCard key={meal.id} meal={meal} />
             ))}
           </div>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/Home/Sections/SuggestedMealsTeaser.jsx` around lines 56 - 60, The
placeholder meal cards in SuggestedMealsTeaser remain reachable by keyboard and
assistive tech even though they are only decorative. Update the
PLACEHOLDER_MEALS rendering in SuggestedMealsTeaser so the RegularFoodCard
content is removed from the accessibility/focus order, e.g. by passing the right
props or wrapping it to hide interactive descendants from tabbing and screen
readers, while keeping the blurred preview visual only.


{/* Overlay */}
<div className="absolute inset-0 flex flex-col items-center justify-center bg-white/40 rounded-2xl">
{/* Lock Icon */}
<div className="w-16 h-16 rounded-full bg-gray-100 flex items-center justify-center mb-4 shadow-sm">
<svg
className="w-8 h-8 text-green-600"
fill="none"
stroke="currentColor"
strokeWidth={2}
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
/>
</svg>
</div>

<h3 className="text-xl md:text-2xl font-bold text-gray-800 mb-2 text-center">
Meals tailored just for you!
</h3>
<p className="text-gray-500 text-sm md:text-base mb-6 text-center px-4">
Sign in to get AI-powered meal recommendations based on your
health goals
</p>
<div className="flex gap-3">
<Link
to="/auth/login"
className="bg-orange-500 hover:bg-orange-600 text-white px-6 py-2.5 rounded-full text-sm font-semibold transition"
>
Sign In
</Link>
<Link
to="/auth/signup"
className="border border-orange-500 text-orange-500 hover:bg-orange-50 px-6 py-2.5 rounded-full text-sm font-semibold transition"
>
Create Account
</Link>
</div>
</div>
</div>
</div>
</section>
);
};

export default SuggestedMealsTeaser;
Loading