Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# ============================================================

# Set environment to local, dev, or prod
VITE_ENV=local
VITE_ENV=prod

# Environment URLs
VITE_API_URL_LOCAL=http://localhost:8080/
Expand Down
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
142 changes: 27 additions & 115 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 LoadingSpinner from "../../../components/ui/LoadingSpinner";
import ScrollArrows from "../../../components/ui/ScrollArrows";
const SCROLL_AMOUNT = 340; // px per button click
import useRecommendationStore from "../../../store/recommendationStore";
import RegularFoodCard from "../../../components/UI/RegularFoodCard";
import LoadingSpinner from "../../../components/UI/LoadingSpinner";

/**
* 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 =
selectedCategory === "All"
? recommendations
: recommendations.filter(
(meal) =>
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>

{/* 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