diff --git a/.env b/.env index 9568e06..ee096e3 100644 --- a/.env +++ b/.env @@ -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/ diff --git a/src/pages/Home/Home.jsx b/src/pages/Home/Home.jsx index 9f06e22..47ec79a 100644 --- a/src/pages/Home/Home.jsx +++ b/src/pages/Home/Home.jsx @@ -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"; @@ -28,7 +29,8 @@ export default function Home() { {/* Popular Meals — always visible */} {/* Suggested Meals — only shown to authenticated users */} - {isAuthenticated && } + {isAuthenticated ? : } + diff --git a/src/pages/Home/Sections/SuggestedMeals.jsx b/src/pages/Home/Sections/SuggestedMeals.jsx index 712133f..01f3506 100644 --- a/src/pages/Home/Sections/SuggestedMeals.jsx +++ b/src/pages/Home/Sections/SuggestedMeals.jsx @@ -1,100 +1,35 @@ -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 ; + 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 ; if (error) return

{error}

; - if (meals.length === 0) return null; - //***********************************************************************/ + if (filteredMeals.length === 0) return null; + return (
- {/* Header row with title */}

Suggested For You @@ -102,33 +37,10 @@ const SuggestedMeals = () => {

- {/* Horizontally scrollable card row */} -
- - -
- {meals.map((meal) => ( - - ))} - - {/* Spacer so short lists don't look broken on wide screens */} - {meals.length < 4 && ( -
- )} -
+
+ {filteredMeals.map((meal) => ( + + ))}
diff --git a/src/pages/Home/Sections/SuggestedMealsTeaser.jsx b/src/pages/Home/Sections/SuggestedMealsTeaser.jsx new file mode 100644 index 0000000..49c1740 --- /dev/null +++ b/src/pages/Home/Sections/SuggestedMealsTeaser.jsx @@ -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 ( +
+
+
+

+ Suggested For You +

+ +
+ +
+ {/* Cards with light blur */} +
+ {PLACEHOLDER_MEALS.map((meal) => ( + + ))} +
+ + {/* Overlay */} +
+ {/* Lock Icon */} +
+ + + +
+ +

+ Meals tailored just for you! +

+

+ Sign in to get AI-powered meal recommendations based on your + health goals +

+
+ + Sign In + + + Create Account + +
+
+
+
+
+ ); +}; + +export default SuggestedMealsTeaser; diff --git a/src/pages/Menu/Menu.jsx b/src/pages/Menu/Menu.jsx index ef0e38b..a72443b 100644 --- a/src/pages/Menu/Menu.jsx +++ b/src/pages/Menu/Menu.jsx @@ -1,75 +1,60 @@ -import { - useMenuStore, - useAuthStore, - useRecommendationStore, -} from "../../store"; +import { useMenuStore, useAuthStore } from "../../store"; import { useMenuItems } from "../../hooks/dashboard/useMenuItems"; -import { useMemo, useEffect } from "react"; +import { useMemo } from "react"; import MenuFilter from "./Sections/MenuFilter"; import OffersSection from "./Sections/OffersSection"; -import SuggestedMealsSection from "./Sections/SuggestedMealsSection"; import RegularFood from "../Home/Sections/RegularFood"; +import SuggestedMeals from "../Home/Sections/SuggestedMeals"; +import SuggestedMealsTeaser from "../Home/Sections/SuggestedMealsTeaser"; export default function Menu() { - const { user } = useAuthStore(); - const { - recommendations, - fetchRecommendations, - } = useRecommendationStore(); - const { data: meals = [], isLoading: mealsLoading, error: mealsErrorObj } = useMenuItems({}); - const mealsError = mealsErrorObj ? (mealsErrorObj.message || "Failed to load meals") : null; - const { selectedCategory } = useMenuStore(); - - const isGuest = !user; - - useEffect(() => { - // If user is logged in, fetch personalized recommendations - if (!isGuest) { - fetchRecommendations(user?.id); - } - }, [user, isGuest, fetchRecommendations]); - - - const filteredMeals = useMemo(() => { - return meals.filter((item) => { - return selectedCategory === "All" || item.category === selectedCategory; - }); - }, [selectedCategory, meals]); - - const filteredRecommendations = useMemo(() => { - return (recommendations || []).filter((item) => { - return selectedCategory === "All" || item.category === selectedCategory; - }); - }, [selectedCategory, recommendations]); - - return ( -
-
- - - - {/* Suggested Meals Section (Displayed for logged-in users when recommendations exist) */} - {!isGuest && ( - - )} - - {/* Regular Food Section (Always displayed for everyone) */} - {mealsLoading && meals.length === 0 ? ( -
-

Loading menu...

-
- ) : mealsError && meals.length === 0 ? ( -
-

{mealsError}

-
- ) : filteredMeals.length === 0 ? ( -
-

No meals available in this category

-
- ) : ( - - )} -
-
- ); + const { isAuthenticated } = useAuthStore(); + const { + data: meals = [], + isLoading: mealsLoading, + error: mealsErrorObj, + } = useMenuItems({}); + const mealsError = mealsErrorObj + ? mealsErrorObj.message || "Failed to load meals" + : null; + const { selectedCategory } = useMenuStore(); + + const filteredMeals = useMemo(() => { + return meals.filter((item) => { + return selectedCategory === "All" || item.category === selectedCategory; + }); + }, [selectedCategory, meals]); + + return ( +
+
+ + + + {isAuthenticated ? ( + + ) : ( + + )} + + {mealsLoading && meals.length === 0 ? ( +
+

Loading menu...

+
+ ) : mealsError && meals.length === 0 ? ( +
+

{mealsError}

+
+ ) : filteredMeals.length === 0 ? ( +
+

+ No meals available in this category +

+
+ ) : ( + + )} +
+
+ ); } diff --git a/src/pages/Profile/Profile.jsx b/src/pages/Profile/Profile.jsx index f192e12..efe6dc2 100644 --- a/src/pages/Profile/Profile.jsx +++ b/src/pages/Profile/Profile.jsx @@ -7,36 +7,44 @@ import { toast } from "sonner"; export default function Profile() { const user = useProfileStore((s) => s.user); + const fetchProfile = useProfileStore((s) => s.fetchProfile); const updateHealth = useProfileStore((s) => s.updateHealth); const [editing, setEditing] = useState(false); const [saving, setSaving] = useState(false); - const joinDate = user?.createdAt ? new Date(user.createdAt).toLocaleDateString() : "-"; - const profile = user?.profile || {}; + useEffect(() => { + fetchProfile(); + }, []); return (
- setEditing(true)} /> + setEditing(true)} + />
{!editing ? (
- +
) : ( setEditing(false)} onSave={async (form) => { setSaving(true); try { const updated = await updateHealth(form); - if (!updated) throw new Error("Failed to update user profile. Please try again."); + if (!updated) + throw new Error("Failed to update profile. Please try again."); toast.success("Profile updated successfully."); setEditing(false); } catch (err) { - toast.error(err?.message || "Failed to update user profile. Please try again."); + toast.error( + err?.message || "Failed to update profile. Please try again.", + ); } finally { setSaving(false); } diff --git a/src/pages/Profile/Profile.test.jsx b/src/pages/Profile/Profile.test.jsx new file mode 100644 index 0000000..3ce2280 --- /dev/null +++ b/src/pages/Profile/Profile.test.jsx @@ -0,0 +1,25 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import Profile from "./Profile"; + +const mockFetchProfile = vi.fn(); +const mockUpdateHealth = vi.fn(); + +vi.mock("../../store", () => ({ + useProfileStore: (selector) => + selector({ + user: { createdAt: "2024-01-15T00:00:00.000Z" }, + fetchProfile: mockFetchProfile, + updateHealth: mockUpdateHealth, + }), +})); + +describe("Profile", () => { + it("renders the join date from the profile data", () => { + render(); + + expect(screen.getByText(/Join date/i)).toBeTruthy(); + expect(screen.getByText(/2024-01-15/i)).toBeTruthy(); + }); +}); diff --git a/src/services/api.js b/src/services/api.js index ecf4145..db56e1c 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -1,47 +1,43 @@ import axios from "axios"; +import { useAuthStore } from "../store"; import { restoreSessionService } from "./auth.service"; // ============================================================ -// API CONFIGURATION +// API BASE URL // ============================================================ +const ENV = import.meta.env.VITE_ENV || "prod"; -// Determine baseURL based on VITE_ENV (local, dev, prod) -const getBaseURL = () => { - const env = import.meta.env.VITE_ENV || "local"; - if (env === "prod") { - if (!import.meta.env.VITE_API_URL_PROD) throw new Error("VITE_API_URL_PROD is not defined"); - return import.meta.env.VITE_API_URL_PROD; - } - if (env === "dev") { - if (!import.meta.env.VITE_API_URL_DEV) throw new Error("VITE_API_URL_DEV is not defined"); - return import.meta.env.VITE_API_URL_DEV; - } - return import.meta.env.VITE_API_URL_LOCAL || "http://localhost:8080/"; +const BASE_URLS = { + local: import.meta.env.VITE_API_URL_LOCAL, + dev: import.meta.env.VITE_API_URL_DEV, + prod: import.meta.env.VITE_API_URL_PROD, }; +const BASE_URL = BASE_URLS[ENV] || import.meta.env.VITE_API_URL_PROD; + // Create axios instance export const api = axios.create({ - baseURL: getBaseURL(), - withCredentials: true, // refresh token is sent in httpOnly cookie + baseURL: BASE_URL, + withCredentials: true, headers: { "Content-Type": "application/json", }, }); // ============================================================ -// INTERCEPTORS (active in both mock and real mode) +// INTERCEPTORS // ============================================================ -let isRefreshing = false; // flag to indicate if token refresh is in progress -const refreshQueue = []; // queue to hold requests while token is being refreshed +let isRefreshing = false; +const refreshQueue = []; // Attach access token to every outgoing request api.interceptors.request.use( - async (config) => { - const useAuthStore = (await import("../store/authStore")).default; + (config) => { const { getAccessToken } = useAuthStore.getState(); - getAccessToken() && - (config.headers["Authorization"] = `Bearer ${getAccessToken()}`); + if (getAccessToken()) { + config.headers["Authorization"] = `Bearer ${getAccessToken()}`; + } return config; }, (error) => Promise.reject(error), @@ -52,10 +48,9 @@ api.interceptors.response.use( (response) => response, async (error) => { const originalRequest = error.config; - const useAuthStore = (await import("../store/authStore")).default; const { getAccessToken, setAccessToken, logout } = useAuthStore.getState(); - // If refresh endpoint fails, refresh token expired — user must re-login + // If refresh endpoint fails → session expired → logout if (originalRequest.url?.includes("/auth/refresh")) { isRefreshing = false; refreshQueue.forEach((p) => p.reject(new Error("Session expired"))); @@ -64,45 +59,31 @@ api.interceptors.response.use( return Promise.reject(error); } - // If any endpoint returns 401 (unauthorized), attempt to refresh the access token - if (error.response?.status === 401 && !originalRequest._retry && !originalRequest.url?.includes('/auth/login')) { - // If already refreshing, queue this request and wait + // If 401 → attempt token refresh + if (error.response?.status === 401 && !originalRequest._retry) { if (isRefreshing) { return new Promise((resolve, reject) => { refreshQueue.push({ resolve, reject }); }) - .then(() => { + .then((token) => { + originalRequest.headers["Authorization"] = `Bearer ${token}`; return api(originalRequest); }) .catch((err) => Promise.reject(err)); } - // Start token refresh process isRefreshing = true; originalRequest._retry = true; try { const res = await restoreSessionService(); - const token = res.data.token; - - let expiresAt = Date.now() + 1000 * 60 * 60 * 24; // fallback - try { - if (token && typeof token === 'string' && token.split('.').length === 3) { - const base64Url = token.split('.')[1]; - const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); - const payload = JSON.parse(atob(base64)); - if (payload.exp) { - expiresAt = payload.exp * 1000; - } - } - } catch (e) { - console.warn("Failed to decode JWT payload", e); - } - - setAccessToken(token, expiresAt); + const expiresAt = Date.now() + 1000 * 60 * 60 * 24; + setAccessToken(res.data.token, expiresAt); isRefreshing = false; - refreshQueue.forEach((p) => p.resolve(token)); + originalRequest.headers["Authorization"] = `Bearer ${getAccessToken()}`; + + refreshQueue.forEach((p) => p.resolve(getAccessToken())); refreshQueue.length = 0; return api(originalRequest); diff --git a/src/services/recommendation.service.js b/src/services/recommendation.service.js index 398dc94..f04927c 100644 --- a/src/services/recommendation.service.js +++ b/src/services/recommendation.service.js @@ -1,11 +1,67 @@ +import axios from "axios"; import { api } from "./api"; +import { getMenu } from "./menu.service"; +import { useAuthStore } from "../store"; + +const AI_API_URL = + "https://youssef-ashraf-healthy-meal-ai-api.hf.space/recommend"; /** - * Fetch AI-suggested meals for the authenticated user from the backend recommendation engine. + * Fetch AI meal recommendations for the authenticated user. + * 1. Fetches client profile + all meals in parallel + * 2. Sends both to the AI recommendation engine + * 3. Enriches the recommendations with imageUrl & nutrients from the original meals * - * @param {string|number} userId - The authenticated user's ID - * @returns {Promise} + * @param {string} role - The authenticated user's role (e.g. "CLIENT") + * @returns {{ data: Array }} - Enriched recommended meals */ -export const getSuggestedMeals = (userId) => { - return api.get(`/api/menu/recommendations/${userId}`); +export const getSuggestedMeals = async () => { + const [profileRes, mealsRes] = await Promise.allSettled([ + api.get(`/api/clients/profile/${useAuthStore.getState().user?.id}`), + getMenu(), + ]); + + if (profileRes.status !== "fulfilled") { + throw new Error("Failed to fetch user profile"); + } + if (mealsRes.status !== "fulfilled") { + throw new Error("Failed to fetch meals"); + } + + const user = profileRes.value.data; + const meals = mealsRes.value.data; + + const aiResponse = await axios.post( + AI_API_URL, + { + user, + meals, + top_n: 5, + }, + { timeout: 10000 }, + ); + + const aiPayload = aiResponse?.data; + const recommendations = Array.isArray(aiPayload?.recommendations) + ? aiPayload.recommendations + : []; + + // Enrich recommendations with original meal data (imageUrl, nutrients, discount) + const enrichedMeals = recommendations.map((rec) => { + const originalMeal = meals.find((m) => m.id === rec.meal_id); + return { + id: rec.meal_id, + name: rec.name, + price: rec.price, + category: rec.category, + imageUrl: originalMeal?.imageUrl ?? null, + nutrients: originalMeal?.nutrients ?? [], + hasDiscount: originalMeal?.hasDiscount ?? false, + discountPercentage: originalMeal?.discountPercentage ?? 0, + score: rec.score_percentage, + reasons: rec.reasons, + }; + }); + + return { data: enrichedMeals }; }; diff --git a/src/services/user.service.js b/src/services/user.service.js index a1385b6..b613261 100644 --- a/src/services/user.service.js +++ b/src/services/user.service.js @@ -1,21 +1,33 @@ -import { api } from './api'; +import { api } from "./api"; -// Get current user profile +// Get current logged-in client's profile (all profiles - admin) export const getProfile = () => { - return api.get('/users/me'); + return api.get("/api/clients/profile"); }; -// Update profile -export const updateProfile = (data) => { - return api.put('/users/me', data); +// Get specific client profile by ID +export const getProfileById = (id) => { + return api.get(`/api/clients/profile/${id}`); }; -// Update health profile -export const updateHealthProfile = (data) => { - return api.put('/users/me/health', data); +// Update client profile by ID +export const updateProfile = (id, data) => { + return api.put(`/api/clients/profile/${id}`, data); }; -// Get order history -export const getOrderHistory = () => { - return api.get('/users/me/orders'); +// Delete client profile by ID +export const deleteProfile = (id) => { + return api.delete(`/api/clients/profile/${id}`); +}; + +// Upload profile picture +export const uploadProfilePicture = (id, file) => { + const formData = new FormData(); + formData.append("file", file); + return api.patch(`/api/clients/profile/${id}/picture`, formData); +}; + +// Delete profile picture +export const deleteProfilePicture = (id) => { + return api.delete(`/api/clients/profile/${id}/picture`); }; diff --git a/src/store/__tests__/profileStore.test.js b/src/store/__tests__/profileStore.test.js new file mode 100644 index 0000000..16af4ac --- /dev/null +++ b/src/store/__tests__/profileStore.test.js @@ -0,0 +1,32 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import useProfileStore from "../profileStore"; +import useAuthStore from "../authStore"; + +vi.mock("../../services/user.service", () => ({ + getProfileById: vi.fn(), + updateProfile: vi.fn(), +})); + +const { getProfileById } = await import("../../services/user.service"); + +describe("useProfileStore", () => { + beforeEach(() => { + useProfileStore.setState({ user: null, loading: false, error: null }, true); + useAuthStore.setState( + { user: { id: 1 }, token: null, expiresAt: null, isAuthenticated: true }, + true, + ); + vi.clearAllMocks(); + }); + + it("does not reuse a cached profile from a different auth user", async () => { + useProfileStore.setState({ user: { id: 99, firstName: "Old" } }, true); + + getProfileById.mockResolvedValue({ data: { id: 1, firstName: "New" } }); + + const result = await useProfileStore.getState().fetchProfile(); + + expect(result).toEqual({ id: 1, firstName: "New" }); + expect(getProfileById).toHaveBeenCalledWith(1); + }); +}); diff --git a/src/store/profileStore.js b/src/store/profileStore.js index 0f55685..998e473 100644 --- a/src/store/profileStore.js +++ b/src/store/profileStore.js @@ -1,12 +1,7 @@ import { create } from "zustand"; import { persist } from "zustand/middleware"; -import { getProfile, updateProfile, updateHealthProfile } from "../services/user.service"; - -/** - * Profile Store - * - Holds user profile/meta and health sub-object - * - Contains actions that call services so components stay dumb - */ +import { getProfileById, updateProfile } from "../services/user.service"; +import useAuthStore from "../store/authStore"; const useProfileStore = create( persist( (set, get) => ({ @@ -15,9 +10,22 @@ const useProfileStore = create( error: null, fetchProfile: async () => { + const activeUserId = useAuthStore.getState().user?.id; + const cachedUser = get().user; + + if (cachedUser && cachedUser.id === activeUserId) { + return cachedUser; + } + + if (cachedUser && cachedUser.id != null && activeUserId != null) { + set({ user: null, loading: false, error: null }); + } + set({ loading: true, error: null }); try { - const res = await getProfile(); + const id = useAuthStore.getState().user?.id; + if (!id) throw new Error("User ID not found"); + const res = await getProfileById(id); const user = res?.data || null; if (!user) { set({ error: "Profile not found", loading: false }); @@ -34,7 +42,9 @@ const useProfileStore = create( updateUser: async (data) => { set({ loading: true, error: null }); try { - const res = await updateProfile(data); + const id = useAuthStore.getState().user?.id; + if (!id) throw new Error("User ID not found"); + const res = await updateProfile(id, data); const user = res?.data || null; if (user) set({ user, loading: false, error: null }); else set({ loading: false }); @@ -45,65 +55,9 @@ const useProfileStore = create( } }, + // updateHealth بقى نفس updateUser لأن مفيش endpoint منفصل للـ health updateHealth: async (data) => { - set({ loading: true, error: null }); - try { - const res = await updateHealthProfile(data); - const user = res?.data || null; - if (user) set({ user, loading: false, error: null }); - return user; - } catch (error) { - set({ error: error.message, loading: false }); - return null; - } finally { - set({ loading: false }); - } - }, - - addAllergy: (allergy) => { - if (!allergy || typeof allergy !== "string") { - set({ error: "Allergy cannot be empty" }); - return; - } - - const normalized = allergy.trim().toLowerCase(); - const user = get().user; - const current = user?.profile?.allergies || []; - if (current.includes(normalized)) { - set({ error: "Allergy already added" }); - return; - } - - const updatedUser = { - ...user, - profile: { - ...user?.profile, - allergies: [...current, normalized], - }, - }; - - set({ user: updatedUser, error: null }); - }, - - removeAllergy: (allergy) => { - if (!allergy) return; - const normalized = allergy.trim().toLowerCase(); - const user = get().user; - const current = user?.profile?.allergies || []; - if (!current.includes(normalized)) { - set({ error: "Allergy not found" }); - return; - } - - const updatedUser = { - ...user, - profile: { - ...user?.profile, - allergies: current.filter((a) => a !== normalized), - }, - }; - - set({ user: updatedUser, error: null }); + return get().updateUser(data); }, clearError: () => set({ error: null }), @@ -111,8 +65,8 @@ const useProfileStore = create( { name: "revive-profile-store", partialize: (state) => ({ user: state.user }), - } - ) + }, + ), ); export default useProfileStore; diff --git a/src/store/recommendationStore.js b/src/store/recommendationStore.js index 6632779..b0d544d 100644 --- a/src/store/recommendationStore.js +++ b/src/store/recommendationStore.js @@ -31,18 +31,21 @@ const useRecommendationStore = create( * * @param {string|number|Object} [param] - userId or context object */ - fetchRecommendations: async (param) => { - const userId = - typeof param === "number" || typeof param === "string" - ? param - : param?.userId || param?.id || useAuthStore.getState().user?.id || "guest"; + fetchRecommendations: async () => { + const role = useAuthStore.getState().user?.role; + + if (!role || role !== "CLIENT") { + set({ recommendations: [], isLoading: false, error: null }); + return; + } set({ isLoading: true, error: null }); try { - const response = await getSuggestedMeals(userId); - const data = response.data; - const validRecommendations = Array.isArray(data) ? data : []; + const response = await getSuggestedMeals(role); + const validRecommendations = Array.isArray(response.data) + ? response.data + : []; set({ recommendations: validRecommendations, @@ -75,5 +78,3 @@ const useRecommendationStore = create( ); export default useRecommendationStore; - -