diff --git a/.env b/.env index 0dccbab..9568e06 100644 --- a/.env +++ b/.env @@ -2,9 +2,10 @@ # Environment Configuration # ============================================================ -# Set to true to use mock data (no backend required) -# Set to false when the real backend is ready -VITE_USE_MOCK=false +# Set environment to local, dev, or prod +VITE_ENV=local -# Backend API base URL (used when VITE_USE_MOCK=false) -VITE_API_BASE_URL=https://revive-backend-production-93ea.up.railway.app/ +# Environment URLs +VITE_API_URL_LOCAL=http://localhost:8080/ +VITE_API_URL_DEV=https://api-dev.revive.com/ +VITE_API_URL_PROD=https://revive-backend-production-93ea.up.railway.app/ diff --git a/.gitignore b/.gitignore index 20a5d4f..84d6d00 100644 --- a/.gitignore +++ b/.gitignore @@ -22,7 +22,7 @@ dist-ssr *.njsproj *.sln *.sw? - +test-results/ /playwright-report /test-results login-timeout.png diff --git a/eslint.config.js b/eslint.config.js index cee1e2c..ea5b204 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -15,7 +15,10 @@ export default defineConfig([ ], languageOptions: { ecmaVersion: 2020, - globals: globals.browser, + globals: { + ...globals.browser, + ...globals.node, + }, parserOptions: { ecmaVersion: 'latest', ecmaFeatures: { jsx: true }, diff --git a/login-timeout.png b/login-timeout.png new file mode 100644 index 0000000..15a484d Binary files /dev/null and b/login-timeout.png differ diff --git a/playwright-report/index.html b/playwright-report/index.html new file mode 100644 index 0000000..51b86ce --- /dev/null +++ b/playwright-report/index.html @@ -0,0 +1,90 @@ + + + + + + + + + Playwright Test Report + + + + +
+ + + \ No newline at end of file diff --git a/playwright.config.js b/playwright.config.js index a02a201..c5dc324 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -1,7 +1,7 @@ import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ - testDir: './tests/e2e', + testDir: './tests', fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, diff --git a/src/App.jsx b/src/App.jsx index 2ff7e54..5c64d9a 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -8,6 +8,8 @@ import { Home, Login, Signup, + ForgotPassword, + ResetPassword, Cart, Checkout, Payment, @@ -76,8 +78,11 @@ export default function App() { }> } /> } /> + } /> + } /> + {/* Debug Route — remove before production */} } /> diff --git a/src/mocks/dashboardMock.js b/src/mocks/dashboardMock.js deleted file mode 100644 index 52c8869..0000000 --- a/src/mocks/dashboardMock.js +++ /dev/null @@ -1,231 +0,0 @@ -/** - * Dashboard Mock Data - * ───────────────────────────────────────────────────────────────── - * All data here mirrors the real backend response shape exactly. - * Swap by setting VITE_USE_MOCK=false in .env — service files unchanged. - */ - -// ── Dashboard Page ──────────────────────────────────────────────── - -const loadMock = (key, defaultData) => { - const saved = localStorage.getItem(`mock_${key}`); - if (saved) { - try { - return JSON.parse(saved); - } catch (e) { - console.warn(`[MOCK] Corrupted localStorage data for mock_${key}. Resetting.`); - localStorage.removeItem(`mock_${key}`); - } - } - localStorage.setItem(`mock_${key}`, JSON.stringify(defaultData)); - return defaultData; -}; - -export const saveMock = (key, data) => { - localStorage.setItem(`mock_${key}`, JSON.stringify(data)); -}; - -export const mockMetrics = loadMock("metrics", { - totalOrders: { value: 48652, change: 1.58, trend: "up" }, - totalCustomers: { value: 1248, change: -0.48, trend: "down" }, - totalRevenue: { value: 215.86,change: 1.58, trend: "up" }, -}); - -export const mockRevenueData = [ - { month: "Mar", income: 120, revenue: 90, expense: 60 }, - { month: "Apr", income: 140, revenue: 110, expense: 70 }, - { month: "May", income: 130, revenue: 100, expense: 65 }, - { month: "Jun", income: 160, revenue: 130, expense: 80 }, - { month: "Jul", income: 150, revenue: 115, expense: 75 }, - { month: "Aug", income: 175, revenue: 140, expense: 85 }, - { month: "Sep", income: 165, revenue: 135, expense: 78 }, - { month: "Oct", income: 190, revenue: 155, expense: 90 }, -]; - -export const mockTopCategories = [ - { name: "Chicken", value: 40, color: "#F97316" }, - { name: "Seafood", value: 30, color: "#FB923C" }, - { name: "Meat", value: 24, color: "#FDBA74" }, - { name: "Mix Protein", value: 6, color: "#FED7AA" }, -]; - -export const mockOrdersOverview = [ - { day: "Sat", orders: 180 }, - { day: "Sun", orders: 210 }, - { day: "Mon", orders: 160 }, - { day: "Tue", orders: 280, highlight: true }, - { day: "Wed", orders: 200 }, - { day: "Thu", orders: 190 }, - { day: "Fri", orders: 230 }, -]; - -export const mockOrderTypes = [ - { name: "Dine-In", percentage: 45, count: 900, color: "#F97316" }, - { name: "Takeaway", percentage: 30, count: 600, color: "#FB923C" }, - { name: "Online", percentage: 25, count: 550, color: "#FED7AA" }, -]; - -export const mockTrendingMenus = [ - { id: 1, name: "Chicken Delight", rating: 4.9, orders: 425, revenue: 25400, image: "https://images.unsplash.com/photo-1598103442097-8b74394b95c3?w=120&h=120&fit=crop" }, - { id: 2, name: "Italian Pizza", rating: 4.7, orders: 325, revenue: 18400, image: "https://images.unsplash.com/photo-1565299624946-b28f40a0ae38?w=120&h=120&fit=crop" }, - { id: 3, name: "Classic Beef", rating: 3.9, orders: 425, revenue: 11500, image: "https://images.unsplash.com/photo-1568901346375-23c9450c58cd?w=120&h=120&fit=crop" }, -]; - -export const mockInventoryAlerts = { - lowStock: [ - { id: 1, name: "Potatoes", image: "🥔" }, - { id: 2, name: "Pepper", image: "🌶️" }, - { id: 3, name: "Onion", image: "🧅" }, - ], - shelfLife: [ - { id: 4, name: "Fish", daysLeft: 1, image: "🐟" }, - { id: 5, name: "Tomato Sauce", daysLeft: 2, image: "🍅" }, - ], - inSeason: [ - { id: 6, name: "Corn", image: "🌽" }, - { id: 7, name: "Avocado", image: "🥑" }, - ], -}; - -export const mockRecentActivity = [ - { id: 1, user: "Basmala Mohamad", role: "Inventory Manager", action: 'Updated Inventory: 15 units of "Organic Chicken Breast"', time: "11:20 AM", avatar: "BM" }, - { id: 2, user: "Omar Mostafa", role: "Kitchen Admin", action: "Marked Order #38/51C as Completed", time: "4:35 PM", avatar: "OM" }, - { id: 3, user: "Ahmed Raffat", role: "Receptionist", action: "Added new reservation for 4 guests at 7:00 PM", time: "10:39 AM", avatar: "AR" }, -]; - -export const mockCustomerReviews = [ - { id: 1, name: "Sarah Maher", rating: 5, date: "Oct 12 / 2026", text: "This pasta is my favourite dish I have ever tried. Highly recommended!", image: "https://images.unsplash.com/photo-1567620905732-2d1ec7ab7445?w=200&h=150&fit=crop" }, - { id: 2, name: "Nour Mahmoud", rating: 4, date: "Oct 12 / 2026", text: "Amazing food and great atmosphere. Will definitely come back for more!", image: "https://images.unsplash.com/photo-1567620905732-2d1ec7ab7445?w=200&h=150&fit=crop" }, - { id: 3, name: "Ali Hassan", rating: 5, date: "Oct 11 / 2026", text: "Best restaurant in the city. The Chicken Delight is absolutely divine.", image: "https://images.unsplash.com/photo-1567620905732-2d1ec7ab7445?w=200&h=150&fit=crop" }, -]; - -// ── Orders Page ─────────────────────────────────────────────────── - -export const mockOrdersMetrics = loadMock("ordersMetrics", { - totalOrders: 640, - totalOrdersChange: 1.58, - preparing: 12, - preparingChange: -0.48, - completed: 628, - completedChange: 1.58, - dailyGoal: { - salesCurrent: 4250, - salesTarget: 5000, - ordersCurrent: 65, - ordersTarget: 100 - }, -}); - -export const mockOrders = loadMock("orders", [ - { id: "#58B1D", time: "4:20 PM", name: "Chicken Delight", items: 3, total: 45.50, customer: "Sara Ahmed", status: "Preparing" }, - { id: "#58B1E", time: "4:35 PM", name: "Italian Pizza", items: 2, total: 32.00, customer: "Omar Ali", status: "Ready" }, - { id: "#58B1F", time: "4:50 PM", name: "Classic Beef", items: 1, total: 28.99, customer: "Lina Hassan", status: "Done" }, - { id: "#58B1G", time: "5:05 PM", name: "Avocado Glow Bowl", items: 4, total: 62.00, customer: "Ahmed Salem", status: "Preparing" }, - { id: "#58B1H", time: "5:20 PM", name: "Grilled Salmon", items: 2, total: 55.00, customer: "Nour Khaled", status: "Cancelled" }, - { id: "#58B1I", time: "5:35 PM", name: "Caesar Salad", items: 1, total: 18.50, customer: "Mona Sherif", status: "Done" }, - { id: "#58B1J", time: "5:50 PM", name: "Beef Burger", items: 3, total: 41.00, customer: "Karim Tarek", status: "Ready" }, - { id: "#58B1K", time: "6:05 PM", name: "Pasta Carbonara", items: 2, total: 36.50, customer: "Rana Youssef", status: "Preparing" }, -]); - -// ── Recipe Builder ──────────────────────────────────────────────── - -export const mockRecipeIngredients = [ - { id: 1, name: "Tomato", amount: "240g", emoji: "🍅" }, - { id: 2, name: "Chicken Breast",amount: "350g", emoji: "🍗" }, - { id: 3, name: "Olive Oil", amount: "30ml", emoji: "🫒" }, - { id: 4, name: "Garlic", amount: "15g", emoji: "🧄" }, -]; - -// ── Chef Menu ───────────────────────────────────────────────────── - -export const mockMenuCategories = { - totalPercentage: 100, - totalChange: 1.58, - items: [ - { name: "Chicken", percentage: 62, count: 28, color: "#F97316", change: 1.58 }, - { name: "Meat", percentage: 48, count: 19, color: "#FB923C", change: -0.92 }, - { name: "Seafood", percentage: 35, count: 14, color: "#FDBA74", change: -0.12 }, - { name: "Vegetarian", percentage: 72, count: 32, color: "#86EFAC", change: -0.92 }, - { name: "Desserts", percentage: 55, count: 22, color: "#C084FC", change: -0.42 }, - ], -}; - -export const mockMenuItems = [ - { id: 1, name: "Chicken Delight", image: "https://img.icons8.com/color/96/fried-chicken.png", category: "Chicken", price: 24.99, calories: "520g", protein: "38g", fat: "18g", sugar: "4g", rating: 4.9, status: "Active" }, - { id: 2, name: "Italian Pizza", image: "https://img.icons8.com/color/96/pizza.png", category: "Mixed", price: 19.99, calories: "680g", protein: "24g", fat: "28g", sugar: "8g", rating: 4.7, status: "Active" }, - { id: 3, name: "Classic Beef", image: "https://img.icons8.com/color/96/steak-rare.png", category: "Meat", price: 32.99, calories: "750g", protein: "52g", fat: "35g", sugar: "2g", rating: 3.9, status: "Active" }, - { id: 4, name: "Avocado Glow Bowl", image: "https://img.icons8.com/color/96/avocado.png", category: "Vegetarian", price: 18.99, calories: "420g", protein: "12g", fat: "22g", sugar: "6g", rating: 4.8, status: "Active" }, - { id: 5, name: "Grilled Salmon", image: "https://img.icons8.com/color/96/salmon.png", category: "Seafood", price: 38.99, calories: "580g", protein: "48g", fat: "20g", sugar: "0g", rating: 4.6, status: "Active" }, - { id: 6, name: "Caesar Salad", image: "https://img.icons8.com/color/96/salad.png", category: "Vegetarian", price: 14.99, calories: "320g", protein: "8g", fat: "24g", sugar: "3g", rating: 4.4, status: "Active" }, - { id: 7, name: "Beef Burger", image: "https://img.icons8.com/color/96/hamburger.png", category: "Meat", price: 22.99, calories: "820g", protein: "45g", fat: "42g", sugar: "12g", rating: 4.2, status: "Active" }, - { id: 8, name: "Pasta Carbonara", image: "https://img.icons8.com/color/96/spaghetti.png", category: "Mixed", price: 21.99, calories: "720g", protein: "28g", fat: "32g", sugar: "5g", rating: 4.5, status: "Active" }, -]; - -// ── Live Kitchen ────────────────────────────────────────────────── - -export const mockKitchenOrders = loadMock("kitchenOrders", { - queue: [ - { id: "#58B1D", time: "4:20 PM", items: ["Chicken Delight x2", "Caesar Salad x1"], notes: "No onions or garlic", customer: "Sara Ahmed" }, - { id: "#58B2A", time: "4:45 PM", items: ["Grilled Salmon x1", "Pasta Carbonara x2"], notes: "Extra sauce on the side", customer: "Omar Ali" }, - { id: "#58B2B", time: "5:00 PM", items: ["Classic Beef x2"], notes: "Well done please", customer: "Lina Hassan" }, - ], - preparing: [ - { id: "#58B1C", time: "4:05 PM", items: ["Italian Pizza x1", "Caesar Salad x2"], notes: "Gluten-free crust", customer: "Ahmed Salem", startedAt: "4:10 PM" }, - { id: "#58B1B", time: "3:55 PM", items: ["Beef Burger x3"], notes: "", customer: "Nour Khaled", startedAt: "4:00 PM" }, - ], - ready: [ - { id: "#58B1A", time: "3:40 PM", items: ["Avocado Glow Bowl x2"], notes: "", customer: "Mona Sherif", readyAt: "3:58 PM" }, - { id: "#58B0Z", time: "3:25 PM", items: ["Chicken Delight x1", "Grilled Salmon x1"], notes: "Table 7", customer: "Karim Tarek", readyAt: "3:45 PM" }, - ], -}); - -export const mockKitchenIngredients = [ - { id: 1, name: "Chicken Breast", checked: false }, - { id: 2, name: "Garlic (no)", checked: true }, - { id: 3, name: "Olive Oil", checked: false }, - { id: 4, name: "Seasoning Mix", checked: false }, - { id: 5, name: "Caesar Dressing", checked: false }, -]; - -export const mockKitchenTickets = [ - { id: 1, orderId: "#58B1D", status: "Preparing", assignedChef: "Chef John", createdAt: new Date(Date.now() - 15 * 60000).toISOString() }, - { id: 2, orderId: "#58B2A", status: "Queue", assignedChef: "Unassigned", createdAt: new Date(Date.now() - 5 * 60000).toISOString() }, - { id: 3, orderId: "#58B2B", status: "Queue", assignedChef: "Unassigned", createdAt: new Date(Date.now() - 2 * 60000).toISOString() }, - { id: 4, orderId: "#58B1C", status: "Ready", assignedChef: "Chef Sarah", createdAt: new Date(Date.now() - 25 * 60000).toISOString() }, -]; - -export const mockChefs = loadMock("kitchenChefs", [ - { id: 1, displayName: "Chef John", status: "ACTIVE", station: "Grill", avatar: "CJ" }, - { id: 2, displayName: "Chef Sarah", status: "ACTIVE", station: "Prep", avatar: "CS" }, - { id: 3, displayName: "Chef Mike", status: "INACTIVE", station: "Assembly", avatar: "CM" }, -]); - -// ── Menu Management ─────────────────────────────────────────────── - -export const mockMenuUploads = [ - { id: 1, filename: "Menu_2024.xlsx", date: "1 Jan 2026", time: "9:45 PM", added: 48, updated: 12, status: "Success" }, - { id: 2, filename: "Weekend_Special_v2.csv", date: "7 Mar 2026", time: "10:08 AM", added: 6, updated: 3, status: "Success" }, - { id: 3, filename: "Beverage_list_final.xls", date: "20 May 2026", time: "2:45 PM", added: 0, updated: 0, status: "Failed" }, -]; - -// ── Ingredients ─────────────────────────────────────────────────── - -export const mockIngredientsMetrics = { - total: 126, - totalChange: 1.58, - lowStock: 8, - lowStockChange: -0.92, - outOfStock: 3, - outOfStockChange: -0.12, -}; - -export const mockIngredients = [ - { id: 1, name: "Tomatoes", image: "https://img.icons8.com/color/48/tomato.png", category: "Vegetables", stock: 80000, unit: "kg", costPerUnit: "40$", status: "In Stock", fat: "150g", calories: "47g", protein: "250g", sugar: "180g" }, - { id: 2, name: "Chicken", image: "https://img.icons8.com/color/48/roast-chicken.png", category: "Protein", stock: 10000, unit: "kg", costPerUnit: "160$", status: "Low Stock", fat: "20g", calories: "97g", protein: "550g", sugar: "148g" }, - { id: 3, name: "Olive Oil", image: "https://img.icons8.com/color/48/olive-oil.png", category: "Sauces", stock: 5000, unit: "L", costPerUnit: "80$", status: "Low Stock", fat: "100g", calories: "884g", protein: "0g", sugar: "0g" }, - { id: 4, name: "Garlic", image: "https://img.icons8.com/color/48/garlic.png", category: "Vegetables", stock: 30000, unit: "kg", costPerUnit: "15$", status: "In Stock", fat: "5g", calories: "149g", protein: "6g", sugar: "1g" }, - { id: 5, name: "Salmon Fillet", image: "https://img.icons8.com/color/48/salmon.png", category: "Protein", stock: 2000, unit: "kg", costPerUnit: "250$", status: "Low Stock", fat: "13g", calories: "208g", protein: "20g", sugar: "0g" }, - { id: 6, name: "Pasta", image: "https://img.icons8.com/color/48/spaghetti.png", category: "Stock", stock: 100000, unit: "kg", costPerUnit: "25$", status: "In Stock", fat: "2g", calories: "131g", protein: "5g", sugar: "2g" }, - { id: 7, name: "Bell Pepper", image: "https://img.icons8.com/color/48/paprika.png", category: "Vegetables", stock: 0, unit: "kg", costPerUnit: "20$", status: "Out of Stock", fat: "3g", calories: "20g", protein: "1g", sugar: "4g" }, - { id: 8, name: "Avocado", image: "https://img.icons8.com/color/48/avocado.png", category: "Vegetables", stock: 8000, unit: "units",costPerUnit: "55$", status: "Low Stock", fat: "15g", calories: "160g", protein: "2g", sugar: "1g" }, -]; diff --git a/src/mocks/enums.js b/src/mocks/enums.js deleted file mode 100644 index 2cfef20..0000000 --- a/src/mocks/enums.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Shared Enums matching Backend Schema - */ - -export const UserRole = { - CLIENT: "CLIENT", - ADMIN: "ADMIN", - MANAGER: "MANAGER", -}; - -export const Goal = { - LOSE_WEIGHT: "LOSE_WEIGHT", - MAINTAIN: "MAINTAIN", - GAIN_MUSCLE: "GAIN_MUSCLE", -}; - -export const Gender = { - MALE: "MALE", - FEMALE: "FEMALE", -}; - -export const ActivityLevel = { - LOW: "LOW", - MEDIUM: "MEDIUM", - HIGH: "HIGH", -}; - -export const OrderStatus = { - PENDING: "PENDING", - CONFIRMED: "CONFIRMED", - PREPARING: "PREPARING", - READY: "READY", - CANCELED: "CANCELED", -}; diff --git a/src/mocks/handlers.js b/src/mocks/handlers.js deleted file mode 100644 index e68021d..0000000 --- a/src/mocks/handlers.js +++ /dev/null @@ -1,663 +0,0 @@ -import { mockMeals } from "./meals"; -import { mockOrders } from "./orders"; -import { mockUsers } from "./users"; -import * as dash from "./dashboardMock"; - -/** - * ============================================================ - * MOCK API HANDLERS - * ============================================================ - * Maps URL patterns + HTTP methods to mock responses. - * - * Each handler receives the axios request config and returns: - * { status, data } - * - * HOW TO ADD A NEW MOCK: - * 1. Add a new entry to MOCK_HANDLERS below - * 2. Match the method + url pattern your service calls - * 3. Return { status: 200, data: yourMockData } - * - * SWITCHING TO REAL API: - * Set VITE_USE_MOCK=false in .env — no service files need to change. - * ============================================================ - */ - -// Simulated logged-in user (user id 1 from mockUsers) -const CURRENT_USER_ID = 1; -const currentUser = mockUsers.find((u) => u.id === CURRENT_USER_ID); - -// Simulated auth tokens -const MOCK_TOKEN = "mock-access-token-123"; - -// Helper: find by id from URL e.g. /menu/3 -> id = 3 -const extractId = (url) => { - const parts = url.split("/"); - return parseInt(parts[parts.length - 1], 10); -}; - -export const MOCK_HANDLERS = [ - // ────────────────────────────────────────────── - // AUTH - // ────────────────────────────────────────────── - { - method: "post", - match: (url) => url.endsWith("/auth/login"), - handler: (config) => { - const { email, password } = JSON.parse(config.data || "{}"); - const user = mockUsers.find((u) => u.email === email); - if (!user || password !== "password123") { - return { status: 401, data: { message: "Invalid credentials" } }; - } - return { - status: 200, - data: { - token: MOCK_TOKEN, - expiresAt: new Date(Date.now() + 3600 * 1000).toISOString(), - user, - }, - }; - }, - }, - { - method: "post", - match: (url) => url.endsWith("/auth/signup"), - handler: () => ({ - status: 200, - data: { - message: "User registered successfully. Profile creation is pending.", - }, - }), - }, - { - method: "post", - match: (url) => url.endsWith("/auth/logout"), - handler: () => ({ status: 200, data: { message: "Logged out" } }), - }, - { - method: "post", - match: (url) => url.endsWith("/auth/refresh"), - handler: () => ({ - status: 200, - data: { - token: MOCK_TOKEN, - expiresAt: new Date(Date.now() + 3600 * 1000).toISOString(), - }, - }), - }, - - // ────────────────────────────────────────────── - // MENU - // ────────────────────────────────────────────── - { - method: "get", - match: (url) => url.match(/\/menu\/recommendations\/\d+/), - handler: () => ({ - status: 200, - data: mockMeals.filter((m) => !m.hasDiscount).slice(0, 12), - }), - }, - { - method: "get", - match: (url) => url.match(/\/menu\/\d+/), - handler: (config) => { - const id = extractId(config.url); - const meal = mockMeals.find((m) => m.id === id); - return meal - ? { status: 200, data: meal } - : { status: 404, data: { message: `Meal ${id} not found` } }; - }, - }, - { - method: "get", - match: (url) => url.endsWith("/menu"), - handler: () => ({ - status: 200, - data: mockMeals.map((meal) => ({ - ...meal, - finalPrice: - meal.discountPercent > 0 - ? meal.price * (1 - meal.discountPercent / 100) - : meal.price, - })), - }), - }, - { - method: "post", - match: (url) => url.endsWith("/menu"), - handler: (config) => { - const body = JSON.parse(config.data || "{}"); - const newItem = { id: Date.now(), ...body }; - mockMeals.unshift(newItem); - if (dash?.mockMenuItems) dash.mockMenuItems.unshift(newItem); - return { status: 201, data: newItem }; - }, - }, - { - method: "put", - match: (url) => url.match(/\/menu\/\d+/), - handler: (config) => { - const id = extractId(config.url); - const body = JSON.parse(config.data || "{}"); - const meal = mockMeals.find((m) => m.id === id); - if (meal) { - Object.assign(meal, body); - const dashItem = dash?.mockMenuItems?.find((m) => m.id === id); - if (dashItem) Object.assign(dashItem, body); - return { status: 200, data: meal }; - } - return { status: 404, data: { message: `Meal ${id} not found` } }; - }, - }, - { - method: "post", - match: (url) => url.match(/\/menu\/\d+\/image/), - handler: () => { - return { status: 200, data: { success: true } }; - }, - }, - { - method: "delete", - match: (url) => url.match(/\/menu\/\d+/), - handler: () => ({ status: 200, data: { message: "Meal deleted" } }), - }, - - // Note: No /restaurants endpoints — this is a single-restaurant system. - - // ────────────────────────────────────────────── - // ORDERS - // ────────────────────────────────────────────── - { - method: "get", - match: (url) => url.endsWith("/orders/my"), - handler: () => ({ - status: 200, - data: mockOrders.filter((o) => o.userId === CURRENT_USER_ID), - }), - }, - { - method: "get", - match: (url) => url.match(/\/orders\/\d+/), - handler: (config) => { - const id = extractId(config.url); - const order = mockOrders.find((o) => o.id === id); - return order - ? { status: 200, data: order } - : { status: 404, data: { message: `Order ${id} not found` } }; - }, - }, - { - method: "post", - match: (url) => url.endsWith("/orders"), - handler: (config) => { - const body = JSON.parse(config.data || "{}"); - const newOrderId = Date.now(); - const newOrder = { - id: newOrderId, - userId: CURRENT_USER_ID, - status: "PENDING", - createdAt: new Date().toISOString(), - ...body, - }; - - // Push to Live Kitchen Queue mock - if (!dash.mockKitchenOrders.queue) dash.mockKitchenOrders.queue = []; - const shortId = `#${newOrderId.toString().slice(-5)}`; - const kitchenEntry = { - id: shortId, - time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), - items: body.items?.map(i => `${i.name} x${i.quantity}`) || [], - notes: body.note || "", - customer: currentUser?.name || "Guest" - }; - dash.mockKitchenOrders.queue.push(kitchenEntry); - dash.saveMock("kitchenOrders", dash.mockKitchenOrders); - - console.log('[MOCK] POST /orders — pushed to kitchen queue:', kitchenEntry); - console.log('[MOCK] kitchen queue is now:', dash.mockKitchenOrders.queue.length, 'items'); - - // Push to Dashboard Orders mock - if (dash.mockOrders) { - dash.mockOrders.unshift({ - id: shortId, - time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), - name: body.items?.map(i => i.quantity > 1 ? `${i.name} (x${i.quantity})` : i.name).join(', ') || "Custom Order", - items: body.items?.length || 0, - total: body.finalTotal || body.totalAmount, - customer: currentUser?.name || "Guest", - status: "Pending" - }); - dash.saveMock("orders", dash.mockOrders); - } - - return { - status: 201, - data: newOrder, - }; - }, - }, - { - method: "patch", - match: (url) => url.match(/\/orders\/\d+\/cancel/), - handler: (config) => { - const id = parseInt(config.url.split("/")[2], 10); - const order = mockOrders.find((o) => o.id === id); - return order - ? { status: 200, data: { ...order, status: "CANCELED" } } - : { status: 404, data: { message: `Order ${id} not found` } }; - }, - }, - - // ────────────────────────────────────────────── - // USERS - // ────────────────────────────────────────────── - { - method: "get", - match: (url) => url.endsWith("/users/me/orders"), - handler: () => ({ - status: 200, - data: mockOrders.filter((o) => o.userId === CURRENT_USER_ID), - }), - }, - { - method: "get", - match: (url) => url.endsWith("/users/me"), - handler: () => ({ status: 200, data: currentUser }), - }, - { - method: "put", - match: (url) => url.endsWith("/users/me/health"), - handler: (config) => { - const body = JSON.parse(config.data || "{}"); - return { - status: 200, - data: { ...currentUser, profile: { ...currentUser?.profile, ...body } }, - }; - }, - }, - { - method: "put", - match: (url) => url.endsWith("/users/me"), - handler: (config) => { - const body = JSON.parse(config.data || "{}"); - return { status: 200, data: { ...currentUser, ...body } }; - }, - }, - - // ────────────────────────────────────────────── - // CHEF - // ────────────────────────────────────────────── - { - method: "get", - match: (url) => url.endsWith("/chef/orders"), - handler: () => ({ - status: 200, - data: mockOrders.filter((o) => o.status === "PENDING"), - }), - }, - { - method: "put", - match: (url) => url.match(/\/chef\/orders\/\d+/), - handler: (config) => { - const id = extractId(config.url); - const body = JSON.parse(config.data || "{}"); - const order = mockOrders.find((o) => o.id === id); - return order - ? { status: 200, data: { ...order, ...body } } - : { status: 404, data: { message: `Order ${id} not found` } }; - }, - }, - { - method: "put", - match: (url) => url.match(/\/chef\/meals\/\d+\/availability/), - handler: (config) => { - const id = parseInt(config.url.split("/")[3], 10); - const body = JSON.parse(config.data || "{}"); - const meal = mockMeals.find((m) => m.id === id); - return meal - ? { status: 200, data: { ...meal, ...body } } - : { status: 404, data: { message: `Meal ${id} not found` } }; - }, - }, - - // ────────────────────────────────────────────── - // LOYALTY - // ────────────────────────────────────────────── - { - method: "get", - match: (url) => url.endsWith("/loyalty/points"), - handler: () => ({ status: 200, data: { points: 420, tier: "Gold" } }), - }, - { - method: "post", - match: (url) => url.match(/\/loyalty\/redeem\//), - handler: () => ({ - status: 200, - data: { message: "Reward redeemed", pointsRemaining: 320 }, - }), - }, - - // ────────────────────────────────────────────── - // DASHBOARD OVERVIEW - // ────────────────────────────────────────────── - { method: "get", match: (url) => url.endsWith("/dashboard/metrics"), handler: () => ({ status: 200, data: dash.mockMetrics }) }, - { method: "get", match: (url) => url.includes("/dashboard/revenue"), handler: () => ({ status: 200, data: dash.mockRevenueData }) }, - { method: "get", match: (url) => url.endsWith("/dashboard/categories"), handler: () => ({ status: 200, data: dash.mockTopCategories }) }, - { method: "get", match: (url) => url.includes("/dashboard/orders-overview"), handler: () => ({ status: 200, data: dash.mockOrdersOverview }) }, - { method: "get", match: (url) => url.endsWith("/dashboard/order-types"), handler: () => ({ status: 200, data: dash.mockOrderTypes }) }, - { method: "get", match: (url) => url.endsWith("/dashboard/trending-menus"), handler: () => ({ status: 200, data: dash.mockTrendingMenus }) }, - { method: "get", match: (url) => url.endsWith("/dashboard/inventory-alerts"),handler: () => ({ status: 200, data: dash.mockInventoryAlerts }) }, - { method: "get", match: (url) => url.endsWith("/dashboard/activity"), handler: () => ({ status: 200, data: dash.mockRecentActivity }) }, - { method: "get", match: (url) => url.endsWith("/dashboard/reviews"), handler: () => ({ status: 200, data: dash.mockCustomerReviews }) }, - - // ────────────────────────────────────────────── - // DASHBOARD ORDERS - // ────────────────────────────────────────────── - { method: "get", match: (url) => url.endsWith("/dashboard/orders/metrics"), handler: () => ({ status: 200, data: dash.mockOrdersMetrics }) }, - { method: "get", match: (url) => url.includes("/dashboard/orders") && !url.includes("metrics"), handler: () => { - // Read directly from localStorage to ensure cross-tab sync - const saved = localStorage.getItem("mock_orders"); - const orders = saved ? JSON.parse(saved) : dash.mockOrders; - return { status: 200, data: { orders, total: orders.length, pages: 1 } }; - }}, - { method: "patch", match: (url) => url.match(/\/dashboard\/orders\/.+\/status/), handler: (config) => { - const parts = config.url.split("/"); - const orderId = decodeURIComponent(parts[parts.length - 2]); - const body = JSON.parse(config.data || "{}"); - - const saved = localStorage.getItem("mock_orders"); - const orders = saved ? JSON.parse(saved) : dash.mockOrders; - - const targetId = orderId.startsWith('#') ? orderId : `#${orderId}`; - const order = orders.find((o) => o.id === targetId || o.id === orderId); - - if (order) { - order.status = body.status; - dash.saveMock("orders", orders); - } - - return { status: 200, data: { success: true, status: body.status } }; - }}, - - // ────────────────────────────────────────────── - // KITCHEN - // ────────────────────────────────────────────── - { method: "get", match: (url) => url.endsWith("/kitchen/orders"), handler: () => { - const saved = localStorage.getItem("mock_kitchenOrders"); - const kitchenOrders = saved ? JSON.parse(saved) : dash.mockKitchenOrders; - return { status: 200, data: kitchenOrders }; - }}, - { method: "patch", match: (url) => url.match(/\/kitchen\/orders\/.+\/status/), handler: (config) => { - const parts = config.url.split("/"); - const orderId = decodeURIComponent(parts[parts.length - 2]); - const { status } = JSON.parse(config.data || "{}"); - - // Load fresh from localStorage - const saved = localStorage.getItem("mock_kitchenOrders"); - const kitchenOrders = saved ? JSON.parse(saved) : dash.mockKitchenOrders; - - let orderToMove = null; - ["queue", "preparing", "ready", "done"].forEach((col) => { - if (!kitchenOrders[col]) kitchenOrders[col] = []; - const idx = kitchenOrders[col].findIndex((o) => o.id === orderId || o.id === `#${orderId}` || `#${o.id}` === orderId); - if (idx !== -1) { - orderToMove = kitchenOrders[col].splice(idx, 1)[0]; - } - }); - - if (orderToMove && status) { - if (!kitchenOrders[status]) kitchenOrders[status] = []; - if (status === "done") { - kitchenOrders[status].unshift(orderToMove); - } else { - kitchenOrders[status].push(orderToMove); - } - } - - // Save back to localStorage so it persists - dash.saveMock("kitchenOrders", kitchenOrders); - - // Synchronize with Dashboard Orders list - const savedOrders = localStorage.getItem("mock_orders"); - if (savedOrders) { - const ordersArray = JSON.parse(savedOrders); - // Kitchen ID might have a '#' prefix, strip it if necessary to match Orders list ID - const targetId = orderId.startsWith('#') ? orderId : `#${orderId}`; - const orderInList = ordersArray.find(o => o.id === targetId || o.id === orderId); - - if (orderInList) { - // Map kitchen status (queue, preparing, ready, done) to Title Case (Preparing, Ready, Done) - const mappedStatus = status.charAt(0).toUpperCase() + status.slice(1); - orderInList.status = mappedStatus === "Queue" ? "Pending" : mappedStatus; - dash.saveMock("orders", ordersArray); - } - } - - return { status: 200, data: { success: true } }; - } }, - - // ────────────────────────────────────────────── - // KITCHEN SERVICE (Tickets + Chefs) - // ────────────────────────────────────────────── - { method: "get", match: (url) => url.endsWith("/api/kitchen/tickets/active"), handler: () => { - const saved = localStorage.getItem("mock_kitchenOrders"); - const kitchenOrders = saved ? JSON.parse(saved) : dash.mockKitchenOrders; - - // Dynamically generate tickets from the Live Kitchen Board source of truth - const tickets = []; - const mapCol = (colName, statusName) => { - if (!kitchenOrders[colName]) return; - kitchenOrders[colName].forEach((o, i) => { - tickets.push({ - id: o.id.replace('#', ''), // numeric ticket id - orderId: o.id.replace('#', ''), // rendered as #{ticket.orderId} — no double prefix - status: statusName, - chefDisplayName: o.assignedChef || (o.startedAt ? "Chef John" : null), - assignedChefId: o.assignedChefId || (o.startedAt ? 1 : null), - createdAt: o.createdAt || new Date(Date.now() - (i*5 + 2) * 60000).toISOString() - }); - }); - }; - - mapCol("queue", "Queue"); - mapCol("preparing", "Preparing"); - mapCol("ready", "Ready"); - mapCol("done", "Done"); - - return { status: 200, data: tickets }; - }}, - { method: "patch", match: (url) => url.match(/\/api\/kitchen\/tickets\/[\w-]+\/status/), handler: (config) => { - const id = config.url.split("/")[4]; - const { status } = JSON.parse(config.data || "{}"); // e.g. "Preparing" - - const targetStatus = status.toLowerCase(); // "preparing" - - // Find the Kanban board patch handler and invoke it directly to ensure perfect sync - const handler = MOCK_HANDLERS.find(h => h.method === "patch" && h.match("/kitchen/orders/123/status")); - if (handler) { - handler.handler({ - ...config, - url: `/kitchen/orders/${id}/status`, - data: JSON.stringify({ status: targetStatus }) - }); - } - - return { status: 200, data: { success: true } }; - }}, - { method: "get", match: (url) => url.endsWith("/api/kitchen/chefs"), handler: () => { - const saved = localStorage.getItem("mock_kitchenChefs"); - const chefs = saved ? JSON.parse(saved) : dash.mockChefs; - return { status: 200, data: chefs }; - }}, - { method: "patch", match: (url) => url.match(/\/api\/kitchen\/chefs\/\d+\/status/), handler: (config) => { - const id = parseInt(config.url.split("/")[4], 10); - const { status } = JSON.parse(config.data || "{}"); - const saved = localStorage.getItem("mock_kitchenChefs"); - const chefs = saved ? JSON.parse(saved) : dash.mockChefs; - const chef = chefs.find(c => c.id === id); - if (chef) { - chef.status = status; - dash.saveMock("kitchenChefs", chefs); - } - return { status: 200, data: { success: true } }; - }}, - { method: "patch", match: (url) => url.match(/\/api\/kitchen\/chefs\/\d+\/station/), handler: (config) => { - const id = parseInt(config.url.split("/")[4], 10); - const { station } = JSON.parse(config.data || "{}"); - const saved = localStorage.getItem("mock_kitchenChefs"); - const chefs = saved ? JSON.parse(saved) : dash.mockChefs; - const chef = chefs.find(c => c.id === id); - if (chef) { - chef.station = station; - dash.saveMock("kitchenChefs", chefs); - } - return { status: 200, data: { success: true } }; - }}, - { method: "patch", match: (url) => url.match(/\/api\/kitchen\/chefs\/\d+\/display-name/), handler: (config) => { - const id = parseInt(config.url.split("/")[4], 10); - const { displayName } = JSON.parse(config.data || "{}"); - const saved = localStorage.getItem("mock_kitchenChefs"); - const chefs = saved ? JSON.parse(saved) : dash.mockChefs; - const chef = chefs.find(c => c.id === id); - if (chef) { - chef.displayName = displayName; - dash.saveMock("kitchenChefs", chefs); - } - return { status: 200, data: { success: true } }; - }}, - - // ────────────────────────────────────────────── - // MENU MANAGEMENT (uploads) - // ────────────────────────────────────────────── - { method: "get", match: (url) => url.endsWith("/menu/uploads"), handler: () => ({ status: 200, data: dash.mockMenuUploads }) }, - { method: "post", match: (url) => url.endsWith("/menu/upload"), handler: (config) => { - const now = new Date(); - const dateStr = `${now.getDate()} ${now.toLocaleString("en", { month: "short" })} ${now.getFullYear()}`; - const timeStr = now.toLocaleTimeString("en", { hour: "2-digit", minute: "2-digit" }); - const newUpload = { - id: Date.now(), - filename: config.headers?.["X-File-Name"] || "Uploaded_file.xlsx", - date: dateStr, - time: timeStr, - added: Math.floor(Math.random() * 30) + 1, - updated: Math.floor(Math.random() * 10), - status: "Success", - }; - dash.mockMenuUploads.unshift(newUpload); - return { status: 201, data: newUpload }; - }}, - - // ────────────────────────────────────────────── - // MENU (Chef Menu page) - // ────────────────────────────────────────────── - { method: "get", match: (url) => url.endsWith("/menu/categories"), handler: () => ({ status: 200, data: dash.mockMenuCategories }) }, - { method: "get", match: (url) => url.includes("/menu/items") && !url.match(/\/menu\/items\/\d+/), handler: () => ({ status: 200, data: dash.mockMenuItems }) }, - { method: "post", match: (url) => url.endsWith("/menu/items"), handler: (config) => { - const body = JSON.parse(config.data || "{}"); - const newItem = { - id: Date.now(), - name: body.name || "Unnamed Meal", - image: body.image || "", - category: body.category || "Mixed", - price: parseFloat(body.price) || 0, - calories: body.calories || "0g", - protein: body.protein || "0g", - fat: body.fat || "0g", - sugar: body.sugar || "0g", - rating: 0, - status: "Active" - }; - dash.mockMenuItems.push(newItem); - return { status: 201, data: newItem }; - }}, - { method: "patch", match: (url) => url.match(/\/menu\/items\/\d+/), handler: (config) => { - const body = JSON.parse(config.data || "{}"); - const id = parseInt(config.url?.match(/\/menu\/items\/(\d+)/)?.[1]); - const idx = dash.mockMenuItems.findIndex(i => i.id === id); - if (idx !== -1) dash.mockMenuItems[idx] = { ...dash.mockMenuItems[idx], ...body }; - return { status: 200, data: idx !== -1 ? dash.mockMenuItems[idx] : body }; - }}, - { method: "delete", match: (url) => url.match(/\/menu\/items\/\d+/), handler: (config) => { - const id = parseInt(config.url?.match(/\/menu\/items\/(\d+)/)?.[1]); - const idx = dash.mockMenuItems.findIndex(i => i.id === id); - if (idx !== -1) dash.mockMenuItems.splice(idx, 1); - return { status: 200, data: { message: "Deleted" } }; - }}, - - // ────────────────────────────────────────────── - // RECIPE BUILDER - // ────────────────────────────────────────────── - { method: "get", match: (url) => url.endsWith("/recipes/ingredients"), handler: () => ({ status: 200, data: dash.mockRecipeIngredients }) }, - { method: "post", match: (url) => url.endsWith("/recipes"), handler: (config) => { - const body = JSON.parse(config.data || "{}"); - const newItem = { - id: Date.now(), - name: body.name || "Unnamed Meal", - category: body.category || "Mixed", - price: parseFloat(body.price) || 0, - calories: parseInt(body.calories) || 0, - protein: body.protein || "0g", - fat: body.fat || "0g", - sugar: body.sugar || "0g", - rating: 0, - status: "Active" - }; - dash.mockMenuItems.push(newItem); - return { status: 201, data: newItem }; - }}, - - - - // ────────────────────────────────────────────── - // INGREDIENTS - // ────────────────────────────────────────────── - { method: "get", match: (url) => url.endsWith("/ingredients/metrics"), handler: () => ({ status: 200, data: dash.mockIngredientsMetrics }) }, - { method: "get", match: (url) => url.includes("/ingredients") && !url.includes("metrics") && !url.match(/\/ingredients\/\d+/), handler: () => ({ status: 200, data: dash.mockIngredients }) }, - { method: "post", match: (url) => url.endsWith("/ingredients"), handler: (config) => { - const body = JSON.parse(config.data || "{}"); - const newIngredient = { id: Date.now(), ...body }; - dash.mockIngredients.push(newIngredient); - return { status: 201, data: newIngredient }; - }}, - { method: "patch", match: (url) => url.match(/\/ingredients\/\d+/), handler: (config) => { - const body = JSON.parse(config.data || "{}"); - const id = parseInt(config.url?.match(/\/ingredients\/(\d+)/)?.[1]); - const idx = dash.mockIngredients.findIndex(i => i.id === id); - if (idx !== -1) dash.mockIngredients[idx] = { ...dash.mockIngredients[idx], ...body }; - return { status: 200, data: idx !== -1 ? dash.mockIngredients[idx] : body }; - }}, - { method: "delete", match: (url) => url.match(/\/ingredients\/\d+/), handler: (config) => { - const id = parseInt(config.url?.match(/\/ingredients\/(\d+)/)?.[1]); - const idx = dash.mockIngredients.findIndex(i => i.id === id); - if (idx !== -1) dash.mockIngredients.splice(idx, 1); - return { status: 200, data: { message: "Deleted" } }; - }}, - { method: "post", match: (url) => url.endsWith("/ingredients/upload"), handler: () => ({ status: 200, data: { success: true, count: 15 } }) }, - { method: "post", match: (url) => url.endsWith("/inventory/upload"), handler: () => ({ status: 200, data: { success: true, count: 15 } }) }, -]; - -/** - * Find the right handler for an axios request config. - * @param {object} config - Axios request config - * @returns {{ status, data } | null} - */ -export const resolveMockHandler = (config) => { - const method = config.method?.toLowerCase(); - // Extract the path portion only (strip base URL) - const url = config.url?.replace(/^https?:\/\/[^/]+/, "") || ""; - - const handler = MOCK_HANDLERS.find( - (h) => h.method === method && h.match(url), - ); - - if (!handler) { - console.warn(`[MOCK] No handler found for ${method?.toUpperCase()} ${url}`); - return { - status: 404, - data: { - message: `Mock not implemented: ${method?.toUpperCase()} ${url}`, - }, - }; - } - - return handler.handler({ ...config, url }); -}; diff --git a/src/mocks/index.js b/src/mocks/index.js deleted file mode 100644 index 8c348c6..0000000 --- a/src/mocks/index.js +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./enums"; -export * from "./users"; -export * from "./meals"; -export * from "./orders"; diff --git a/src/mocks/meal-images/beef-steak.jpg b/src/mocks/meal-images/beef-steak.jpg deleted file mode 100644 index 2a52d15..0000000 Binary files a/src/mocks/meal-images/beef-steak.jpg and /dev/null differ diff --git a/src/mocks/meal-images/chicken-cold-cut.jpg b/src/mocks/meal-images/chicken-cold-cut.jpg deleted file mode 100644 index 51644ae..0000000 Binary files a/src/mocks/meal-images/chicken-cold-cut.jpg and /dev/null differ diff --git a/src/mocks/meal-images/green-salad.jpg b/src/mocks/meal-images/green-salad.jpg deleted file mode 100644 index 709bae5..0000000 Binary files a/src/mocks/meal-images/green-salad.jpg and /dev/null differ diff --git a/src/mocks/meal-images/index.js b/src/mocks/meal-images/index.js deleted file mode 100644 index 6d15921..0000000 --- a/src/mocks/meal-images/index.js +++ /dev/null @@ -1,5 +0,0 @@ -export { default as chickenColdCut } from './chicken-cold-cut.jpg'; -export { default as greenSalad } from './green-salad.jpg'; -export { default as beefSteak } from './beef-steak.jpg'; -export { default as salmon } from './salmon.jpg'; -export { default as mixProtein } from './mix-protein.jpg'; diff --git a/src/mocks/meal-images/mix-protein.jpg b/src/mocks/meal-images/mix-protein.jpg deleted file mode 100644 index 9477e9f..0000000 Binary files a/src/mocks/meal-images/mix-protein.jpg and /dev/null differ diff --git a/src/mocks/meal-images/salmon.jpg b/src/mocks/meal-images/salmon.jpg deleted file mode 100644 index 9207f27..0000000 Binary files a/src/mocks/meal-images/salmon.jpg and /dev/null differ diff --git a/src/mocks/meals.js b/src/mocks/meals.js deleted file mode 100644 index 0e9a75a..0000000 --- a/src/mocks/meals.js +++ /dev/null @@ -1,697 +0,0 @@ -/** - * Mock Restaurants, Meals & Ingredients - */ - -import { - chickenColdCut, - greenSalad, - beefSteak, - salmon, - mixProtein, -} from "./meal-images"; - -export const mockRestaurants = [ - { - id: 1, - name: "Revive Kitchen", - location: "Downtown", - rating: 4.8, - }, - { - id: 2, - name: "Green Bowl", - location: "Uptown", - rating: 4.5, - }, -]; - -export const mockIngredients = [ - { - id: 1, - name: "Chicken Breast", - calories: 165, - protein: 31, - fat: 3.6, - carbs: 0, - }, - { - id: 2, - name: "Brown Rice", - calories: 111, - protein: 2.6, - fat: 0.9, - carbs: 23, - }, - { id: 3, name: "Avocado", calories: 160, protein: 2, fat: 15, carbs: 9 }, - { id: 4, name: "Quinoa", calories: 120, protein: 4.1, fat: 1.9, carbs: 21 }, - { id: 5, name: "Salmon", calories: 208, protein: 20, fat: 13, carbs: 0 }, -]; - -export const mockMeals = [ - { - id: 1, - name: "Beef Protein Bowl", - description: "Grilled beef with brown rice, veggies & special sauce", - price: 199, - discountPercent: 30, - category: "Beef", - mainCategory: "Dinner", - imageUrl: "https://images.unsplash.com/photo-1512621776951-a57141f2eefd", - calories: 250, - protein: 25, - carbs: 35, - fat: 18, - sugar: 10, - isAvailable: true, - }, - { - id: 2, - name: "Grilled Chicken Protein", - description: "Grilled chicken, brown rice, fresh veggies & yogurt sauce", - price: 200, - discountPercent: 30, - category: "Chicken", - mainCategory: "Dinner", - imageUrl: "https://images.unsplash.com/photo-1603133872878-684f208fb84b", - calories: 380, - protein: 42, - carbs: 30, - fat: 22, - sugar: 8, - isAvailable: true, - }, - { - id: 3, - name: "Salmon Quinoa Delight", - description: "Fresh salmon fillet, quinoa, avocado & mixed greens", - price: 199, - discountPercent: 0, - category: "Seafood", - mainCategory: "Dinner", - imageUrl: "https://images.unsplash.com/photo-1519708227418-c8fd9a32b7a2", - calories: 520, - protein: 38, - carbs: 40, - fat: 22, - sugar: 12, - isAvailable: true, - }, - { - id: 4, - name: "Vegan Power Bowl", - description: "Lentils, chickpeas, veggies & tahini dressing", - price: 185, - discountPercent: 25, - category: "mix Protein", - mainCategory: "Lunch", - imageUrl: "https://images.unsplash.com/photo-1546069901-ba9599a7e63c", - calories: 420, - protein: 28, - carbs: 55, - fat: 15, - sugar: 10, - isAvailable: true, - }, - { - id: 5, - name: "Roasted Veggie & Egg Salad", - description: "Roasted veggies, boiled eggs, greens & light vinaigrette", - price: 179, - discountPercent: 0, - category: "mix Protein", - mainCategory: "Breakfast", - imageUrl: "https://images.unsplash.com/photo-1593614681732-5f2a0e5c5e6f", - calories: 310, - protein: 22, - carbs: 28, - fat: 16, - sugar: 18, - isAvailable: true, - }, - { - id: 6, - name: "Classic Chicken Salad", - description: "Grilled chicken strips, fresh greens & yogurt dressing", - price: 210, - discountPercent: 0, - category: "Chicken", - mainCategory: "Lunch", - imageUrl: "https://images.unsplash.com/photo-1505253758473-96b7015fcd40", - calories: 390, - protein: 40, - carbs: 25, - fat: 20, - sugar: 6, - isAvailable: true, - }, - { - id: 7, - name: "Avocado Salmon Bowl", - description: "Salmon fillet, avocado, quinoa & cherry tomatoes", - price: 225, - discountPercent: 30, - category: "Seafood", - mainCategory: "Dinner", - imageUrl: "https://images.unsplash.com/photo-1540189549-1a1c0b3d3c3e", - calories: 480, - protein: 35, - carbs: 38, - fat: 25, - sugar: 9, - isAvailable: true, - }, - { - id: 8, - name: "Mediterranean Veggie Mix", - description: "Feta cheese, olives, cucumber, tomato & olive oil", - price: 169, - discountPercent: 0, - category: "mix Protein", - mainCategory: "Lunch", - imageUrl: "https://images.unsplash.com/photo-1543339308-43e59d6b73a6", - calories: 280, - protein: 12, - carbs: 22, - fat: 18, - sugar: 14, - isAvailable: true, - }, - { - id: 9, - name: "Lunch Chicken Wrap", - description: "Chicken wrap with veggies & light sauce", - price: 150, - discountPercent: 15, - category: "Chicken", - mainCategory: "Lunch", - imageUrl: "https://images.unsplash.com/photo-1603133872878-684f208fb84b", - calories: 420, - protein: 35, - carbs: 45, - fat: 16, - sugar: 7, - isAvailable: true, - }, - { - id: 10, - name: "Beef Protein Bowl", - description: "Grilled beef with brown rice, veggies & special sauce", - price: 199, - discountPercent: 0, - category: "Beef", - mainCategory: "Dinner", - imageUrl: "https://images.unsplash.com/photo-1600891964599-f61ba0e24092", - calories: 450, - protein: 45, - carbs: 35, - fat: 18, - sugar: 10, - isAvailable: true, - }, - { - id: 11, - name: "Grilled Chicken Protein", - description: "Grilled chicken, brown rice, fresh veggies & yogurt sauce", - price: 200, - discountPercent: 30, - category: "Chicken", - mainCategory: "Dinner", - imageUrl: "https://images.unsplash.com/photo-1603133872878-684f208fb84b", - calories: 380, - protein: 42, - carbs: 30, - fat: 22, - sugar: 8, - isAvailable: true, - }, - { - id: 12, - name: "Salmon Quinoa Delight", - description: "Fresh salmon fillet, quinoa, avocado & mixed greens", - price: 199, - discountPercent: 0, - category: "Seafood", - mainCategory: "Dinner", - imageUrl: "https://images.unsplash.com/photo-1519708227418-c8fd9a32b7a2", - calories: 520, - protein: 38, - carbs: 40, - fat: 22, - sugar: 12, - isAvailable: true, - }, - { - id: 13, - name: "Vegan Power Bowl", - description: "Lentils, chickpeas, veggies & tahini dressing", - price: 185, - discountPercent: 25, - category: "mix Protein", - mainCategory: "Lunch", - imageUrl: "https://images.unsplash.com/photo-1546069901-ba9599a7e63c", - calories: 420, - protein: 28, - carbs: 55, - fat: 15, - sugar: 10, - isAvailable: true, - }, - { - id: 14, - name: "Roasted Veggie & Egg Salad", - description: "Roasted veggies, boiled eggs, greens & light vinaigrette", - price: 179, - discountPercent: 0, - category: "mix Protein", - mainCategory: "Breakfast", - imageUrl: "https://images.unsplash.com/photo-1593614681732-5f2a0e5c5e6f", - calories: 310, - protein: 22, - carbs: 28, - fat: 16, - sugar: 18, - isAvailable: true, - }, - { - id: 15, - name: "Classic Chicken Salad", - description: "Grilled chicken strips, fresh greens & yogurt dressing", - price: 210, - discountPercent: 0, - category: "Chicken", - mainCategory: "Lunch", - imageUrl: "https://images.unsplash.com/photo-1505253758473-96b7015fcd40", - calories: 390, - protein: 40, - carbs: 25, - fat: 20, - sugar: 6, - isAvailable: true, - }, - { - id: 16, - name: "Avocado Salmon Bowl", - description: "Salmon fillet, avocado, quinoa & cherry tomatoes", - price: 225, - discountPercent: 30, - category: "Seafood", - mainCategory: "Dinner", - imageUrl: "https://images.unsplash.com/photo-1540189549-1a1c0b3d3c3e", - calories: 480, - protein: 35, - carbs: 38, - fat: 25, - sugar: 9, - isAvailable: true, - }, - { - id: 17, - name: "Mediterranean Veggie Mix", - description: "Feta cheese, olives, cucumber, tomato & olive oil", - price: 169, - discountPercent: 0, - category: "mix Protein", - mainCategory: "Lunch", - imageUrl: "https://images.unsplash.com/photo-1543339308-43e59d6b73a6", - calories: 280, - protein: 12, - carbs: 22, - fat: 18, - sugar: 14, - isAvailable: true, - }, - { - id: 18, - name: "Lunch Chicken Wrap", - description: "Chicken wrap with veggies & light sauce", - price: 150, - discountPercent: 0, - category: "Chicken", - mainCategory: "Lunch", - imageUrl: "https://images.unsplash.com/photo-1603133872878-684f208fb84b", - calories: 420, - protein: 35, - carbs: 45, - fat: 16, - sugar: 7, - isAvailable: true, - }, -]; - -export const categories = ["All", "Chicken", "Beef", "Seafood", "mix Protein"]; - -export const customizeData = [ - // ========================= - // 1) Burger - // ========================= - { - id: 1, - name: "Burger", - image: "/images/customize/burger.jpg", - - bases: [ - { id: "b1", name: "Classic", basePrice: 40 }, - { id: "b2", name: "Large", basePrice: 50 }, - ], - - sections: [ - { - type: "protein", - title: "Choose Your Protein", - required: true, - maxSelect: 1, - items: [ - { - id: 11, - name: "Beef Patty", - price: 30, - calories: 200, - protein: 25, - carbs: 2, - fat: 12, - sugar: 0, - }, - { - id: 12, - name: "Chicken Patty", - price: 25, - calories: 180, - protein: 28, - carbs: 1, - fat: 7, - sugar: 0, - }, - ], - }, - { - type: "veggies", - title: "Choose Your Veggies", - required: true, - maxSelect: 3, - items: [ - { - id: 21, - name: "Tomato", - price: 5, - calories: 10, - protein: 1, - carbs: 2, - fat: 0, - sugar: 1, - }, - { - id: 22, - name: "Lettuce", - price: 4, - calories: 5, - protein: 0, - carbs: 1, - fat: 0, - sugar: 0, - }, - { - id: 23, - name: "Onion", - price: 4, - calories: 8, - protein: 0, - carbs: 2, - fat: 0, - sugar: 1, - }, - ], - }, - { - type: "cheese", - title: "Choose Your Cheese", - required: true, - maxSelect: 2, - items: [ - { - id: 31, - name: "Cheddar", - price: 10, - calories: 90, - protein: 6, - carbs: 1, - fat: 7, - sugar: 0, - }, - { - id: 32, - name: "Swiss", - price: 12, - calories: 95, - protein: 7, - carbs: 1, - fat: 7, - sugar: 0, - }, - ], - }, - { - type: "sauces", - title: "Choose Your Sauces", - required: false, - maxSelect: 3, - items: [ - { - id: 41, - name: "BBQ", - price: 5, - calories: 30, - protein: 0, - carbs: 7, - fat: 0, - sugar: 6, - }, - { - id: 42, - name: "Mayo", - price: 4, - calories: 40, - protein: 0, - carbs: 0, - fat: 4, - sugar: 0, - }, - { - id: 43, - name: "Ketchup", - price: 3, - calories: 20, - protein: 0, - carbs: 5, - fat: 0, - sugar: 4, - }, - ], - }, - { - type: "extras", - title: "Choose Your Extras", - required: false, - maxSelect: 2, - items: [ - { - id: 51, - name: "Bacon", - price: 20, - calories: 120, - protein: 9, - carbs: 0, - fat: 10, - sugar: 0, - }, - { - id: 52, - name: "Fried Egg", - price: 15, - calories: 90, - protein: 6, - carbs: 0, - fat: 7, - sugar: 0, - }, - ], - }, - ], - }, - - // ========================= - // 2) Pizza - // ========================= - { - id: 2, - name: "Pizza", - image: "/images/customize/pizza.jpg", - - bases: [ - { id: "p1", name: "Thin Crust", basePrice: 30 }, - { id: "p2", name: "Thick Crust", basePrice: 50 }, - ], - - sections: [ - { - type: "protein", - title: "Choose Your Protein", - required: true, - maxSelect: 2, - items: [ - { - id: 61, - name: "Pepperoni", - price: 25, - calories: 150, - protein: 8, - carbs: 1, - fat: 13, - sugar: 0, - }, - { - id: 62, - name: "Chicken", - price: 20, - calories: 130, - protein: 22, - carbs: 0, - fat: 4, - sugar: 0, - }, - { - id: 63, - name: "Beef", - price: 22, - calories: 140, - protein: 18, - carbs: 0, - fat: 9, - sugar: 0, - }, - ], - }, - { - type: "veggies", - title: "Choose Your Veggies", - required: true, - maxSelect: 4, - items: [ - { - id: 71, - name: "Olives", - price: 6, - calories: 15, - protein: 0, - carbs: 1, - fat: 1, - sugar: 0, - }, - { - id: 72, - name: "Mushroom", - price: 7, - calories: 20, - protein: 2, - carbs: 3, - fat: 0, - sugar: 1, - }, - { - id: 73, - name: "Bell Pepper", - price: 6, - calories: 12, - protein: 0, - carbs: 3, - fat: 0, - sugar: 2, - }, - ], - }, - { - type: "cheese", - title: "Choose Your Cheese", - required: true, - maxSelect: 1, - items: [ - { - id: 81, - name: "Mozzarella", - price: 15, - calories: 100, - protein: 7, - carbs: 1, - fat: 7, - sugar: 0, - }, - { - id: 82, - name: "Parmesan", - price: 12, - calories: 80, - protein: 6, - carbs: 1, - fat: 5, - sugar: 0, - }, - ], - }, - { - type: "sauces", - title: "Choose Your Sauce", - required: true, - maxSelect: 1, - items: [ - { - id: 91, - name: "Tomato Sauce", - price: 5, - calories: 25, - protein: 1, - carbs: 5, - fat: 0, - sugar: 3, - }, - { - id: 92, - name: "Garlic Sauce", - price: 6, - calories: 35, - protein: 0, - carbs: 1, - fat: 3, - sugar: 0, - }, - ], - }, - { - type: "extras", - title: "Choose Extras", - required: false, - maxSelect: 2, - items: [ - { - id: 101, - name: "Extra Cheese", - price: 18, - calories: 120, - protein: 8, - carbs: 1, - fat: 9, - sugar: 0, - }, - { - id: 102, - name: "Stuffed Crust", - price: 30, - calories: 200, - protein: 10, - carbs: 20, - fat: 8, - sugar: 1, - }, - ], - }, - ], - }, -]; diff --git a/src/mocks/orders.js b/src/mocks/orders.js deleted file mode 100644 index a289f68..0000000 --- a/src/mocks/orders.js +++ /dev/null @@ -1,67 +0,0 @@ -import { OrderStatus } from './enums'; - -/** - * Mock Orders - */ - -export const mockOrders = [ - { - id: 1001, - userId: 1, - date: "Today", - time: "5:45", - status: OrderStatus.CANCELED, - totalPrice: 199, - quantity: 30, - image: "/images/bowl.png", - createdAt: "2024-03-10T12:30:00Z", - items: [ - { mealId: 1, name: "Chicken Salad", quantity: 1, price: 199.0 } - ] - }, - { - id: 1002, - userId: 1, - date: "Sunday", - time: "6:35", - status: OrderStatus.READY, - totalPrice: 199, - quantity: 30, - image: "/images/bowl.png", - createdAt: "2024-04-15T18:45:00Z", - items: [ - { mealId: 2, name: "Chicken Salad", quantity: 1, price: 199.0 }, - { mealId: 2, name: "Chicken Salad", quantity: 1, price: 199.0 }, - { mealId: 2, name: "Chicken Salad", quantity: 1, price: 199.0 }, - { mealId: 2, name: "Chicken Salad", quantity: 1, price: 199.0 }, - ] - }, - { - id: 1003, - userId: 1, - date: "Sunday", - time: "5:45", - status: OrderStatus.PREPARING, - totalPrice: 199, - quantity: 40, - image: "/images/bowl.png", - createdAt: "2024-03-15T19:00:00Z", - items: [ - { mealId: 2, name: "Chicken Salad", quantity: 1, price: 199.0 } - ] - }, - { - id: 1003, - userId: 1, - date: "Monday", - time: "5:45", - status: OrderStatus.PREPARING, - totalPrice: 199, - quantity: 40, - image: "/images/bowl.png", - createdAt: "2024-05-15T19:00:00Z", - items: [ - { mealId: 2, name: "Chicken Salad", quantity: 1, price: 199.0 } - ] - } -]; diff --git a/src/mocks/users.js b/src/mocks/users.js deleted file mode 100644 index 1d338c6..0000000 --- a/src/mocks/users.js +++ /dev/null @@ -1,54 +0,0 @@ -import { UserRole, Gender, Goal, ActivityLevel } from './enums'; - -/** - * Mock Users & Profiles - */ - -export const mockUsers = [ - { - id: 1, - firstName: "John", - lastName: "Doe", - email: "john.doe@example.com", - role: UserRole.CLIENT, - profile: { - id: 101, - age: 28, - gender: Gender.MALE, - height: 180, // cm - weight: 85, // kg - goal: Goal.GAIN_MUSCLE, - activityLevel: ActivityLevel.MEDIUM, - dietaryPreferences: ["High Protein"], - allergies: ["Peanuts"], - medicalConditions: [], - } - }, - { - id: 2, - firstName: "Jane", - lastName: "Smith", - email: "jane.smith@example.com", - role: UserRole.CLIENT, - profile: { - id: 102, - age: 34, - gender: Gender.FEMALE, - height: 165, - weight: 60, - goal: Goal.LOSE_WEIGHT, - activityLevel: ActivityLevel.HIGH, - dietaryPreferences: ["Vegan"], - allergies: [], - medicalConditions: ["Asthma"], - } - }, - { - id: 3, - firstName: "Admin", - lastName: "User", - email: "admin@revive.com", - role: UserRole.ADMIN, - profile: null - } -]; diff --git a/src/pages/OrderFlow/Cart.jsx b/src/pages/OrderFlow/Cart.jsx index da71c31..95068ec 100644 --- a/src/pages/OrderFlow/Cart.jsx +++ b/src/pages/OrderFlow/Cart.jsx @@ -4,7 +4,7 @@ import { useShallow } from "zustand/react/shallow"; import { useOrderStore, useFavoritesStore } from "../../store"; import { DELIVERY_FEE } from "../../constants"; import CartSection from "../../components/OrderFlow/CartSection"; -import { mockMeals } from "../../mocks/meals"; + import OrderSummary from "../../components/OrderFlow/OrderSummary"; export default function Cart() { diff --git a/src/pages/auth/ForgotPassword.jsx b/src/pages/auth/ForgotPassword.jsx new file mode 100644 index 0000000..630f71f --- /dev/null +++ b/src/pages/auth/ForgotPassword.jsx @@ -0,0 +1,102 @@ +import { useState } from "react"; +import { MdEmail } from "react-icons/md"; +import { Link } from "react-router-dom"; +import { requestPasswordReset } from "../../services/auth.service"; +import { toast } from "sonner"; + +function ForgotPassword() { + const [email, setEmail] = useState(""); + const [loading, setLoading] = useState(false); + const [success, setSuccess] = useState(false); + + const handleSubmit = async (e) => { + e.preventDefault(); + if (!email) return; + + setLoading(true); + try { + await requestPasswordReset({ email }); + setSuccess(true); + toast.success("Password reset email sent (if account exists)"); + } catch (error) { + // Don't leak whether the email exists, just show a generic error if the network fails + toast.error(error.response?.data?.message || "Something went wrong"); + } finally { + setLoading(false); + } + }; + + return ( +
+
+
+ + Revive Logo + +
+ +
+
+

+ Forgot Password +

+

+ Enter your email address and we'll send you a link to reset your password. +

+ + {success ? ( +
+ If an account exists for {email}, we have sent a password reset link to it. + Please check your inbox. +
+ ) : ( +
+
+ +
+ + setEmail(e.target.value)} + required + className="w-full border border-gray-300 rounded-full pl-10 pr-4 py-2 text-sm focus:outline-none focus:border-orange focus:ring-1 focus:ring-orange transition" + /> +
+
+ + +
+ )} + +
+ Remember your password?{" "} + + Log In + +
+
+
+
+
+ ); +} + +export default ForgotPassword; diff --git a/src/pages/auth/Login.jsx b/src/pages/auth/Login.jsx index fb4a58a..28af8b3 100644 --- a/src/pages/auth/Login.jsx +++ b/src/pages/auth/Login.jsx @@ -89,7 +89,7 @@ function Login() { Forgot Password? diff --git a/src/pages/auth/ResetPassword.jsx b/src/pages/auth/ResetPassword.jsx new file mode 100644 index 0000000..3b922aa --- /dev/null +++ b/src/pages/auth/ResetPassword.jsx @@ -0,0 +1,152 @@ +import { useState } from "react"; +import { FaLock, FaEye, FaEyeSlash } from "react-icons/fa"; +import { Link, useNavigate, useSearchParams } from "react-router-dom"; +import { resetPassword } from "../../services/auth.service"; +import { toast } from "sonner"; + +function ResetPassword() { + const [searchParams] = useSearchParams(); + const token = searchParams.get("token"); + const navigate = useNavigate(); + + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (e) => { + e.preventDefault(); + if (!token) { + toast.error("Invalid or missing reset token"); + return; + } + + if (password !== confirmPassword) { + toast.error("Passwords do not match"); + return; + } + + if (password.length < 8) { + toast.error("Password must be at least 8 characters long"); + return; + } + + setLoading(true); + try { + await resetPassword({ token, newPassword: password }); + toast.success("Password reset successfully. You can now log in."); + navigate("/auth/login"); + } catch (error) { + toast.error(error.response?.data?.message || "Failed to reset password"); + } finally { + setLoading(false); + } + }; + + if (!token) { + return ( +
+
+

Invalid Link

+

+ The password reset link is invalid or has expired. Please request a new link. +

+ + Request New Link + +
+
+ ); + } + + return ( +
+
+
+ + Revive Logo + +
+ +
+
+

+ Set New Password +

+

+ Please enter your new password below. +

+ +
+
+ +
+ + setPassword(e.target.value)} + required + className="w-full border border-gray-300 rounded-full pl-10 pr-10 py-2 text-sm focus:outline-none focus:border-orange focus:ring-1 focus:ring-orange transition" + /> + +
+
+ +
+ +
+ + setConfirmPassword(e.target.value)} + required + className="w-full border border-gray-300 rounded-full pl-10 pr-10 py-2 text-sm focus:outline-none focus:border-orange focus:ring-1 focus:ring-orange transition" + /> + +
+
+ + +
+
+
+
+
+ ); +} + +export default ResetPassword; diff --git a/src/pages/customization/Customization.jsx b/src/pages/customization/Customization.jsx index a3edda5..8036f3f 100644 --- a/src/pages/customization/Customization.jsx +++ b/src/pages/customization/Customization.jsx @@ -1,13 +1,105 @@ -// pages/Customize.jsx - +import React, { useState, useEffect } from "react"; import CustomizeHero from "./Sections/CustomizeHero"; import BaseSelector from "./Sections/BaseSelector"; import IngredientsSection from "./Sections/IngredientsSection"; import CommentBox from "./Sections/CommentBox"; -import { customizeData } from "../../mocks/meals"; import SummaryBox from "./Sections/SummaryBox"; +import { api } from "../../services/api"; + +const mapCategoryToSection = (categoryStr) => { + if (!categoryStr) return "extras"; + const cat = categoryStr.toLowerCase(); + if (cat.includes("protein") || cat.includes("meat") || cat.includes("poultry") || cat.includes("finfish")) return "protein"; + if (cat.includes("vegetable")) return "veggies"; + if (cat.includes("dairy") || cat.includes("cheese")) return "cheese"; + if (cat.includes("fat") || cat.includes("oil") || cat.includes("sauce")) return "sauces"; + return "extras"; +}; + +const sectionTitles = { + protein: "Protein", + veggies: "Veggies", + cheese: "Cheese", + sauces: "Sauces", + extras: "Extras" +}; const Customize = () => { + const [customizeData, setCustomizeData] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchMeals = async () => { + try { + const response = await api.get('/menu'); + const rawMeals = response.data; + + const mappedMeals = rawMeals.map(meal => { + // Group ingredients by mapped category + const sectionsMap = { + protein: [], + veggies: [], + cheese: [], + sauces: [], + extras: [] + }; + + meal.mealIngredients?.forEach(mi => { + const ing = mi.ingredient; + const secType = mapCategoryToSection(ing.category); + + // Extract some basic nutrients + let calories = 0, protein = 0, carbs = 0, fat = 0; + ing.nutrients?.forEach(n => { + const name = (n.nutrientName || "").toLowerCase(); + if (name.includes("energy")) calories = n.value; + else if (name.includes("protein")) protein = n.value; + else if (name.includes("carbohydrate")) carbs = n.value; + else if (name.includes("lipid") || name.includes("fat")) fat = n.value; + }); + + sectionsMap[secType].push({ + id: ing.id, + name: ing.name, + price: 0, // Ingredients don't have individual prices in backend yet + calories, + protein, + carbs, + fat + }); + }); + + // Convert map to array of sections + const sections = Object.keys(sectionsMap) + .filter(key => sectionsMap[key].length > 0) + .map(key => ({ + title: sectionTitles[key], + type: key, + maxSelect: key === "protein" ? 1 : null, + required: key === "protein" || key === "veggies", + items: sectionsMap[key] + })); + + return { + id: meal.id, + name: meal.name, + image: meal.imageUrl || "https://images.unsplash.com/photo-1546069901-ba9599a7e63c", + bases: [{ id: `base-${meal.id}`, name: "Regular", basePrice: meal.price }], + sections + }; + }); + + setCustomizeData(mappedMeals); + } catch (err) { + console.error("Failed to fetch meals:", err); + } finally { + setLoading(false); + } + }; + + fetchMeals(); + }, []); + return (
{/* HERO */} @@ -21,8 +113,14 @@ const Customize = () => {

🍳 Let's Start Cooking..

- - + {loading ? ( +

Loading menu...

+ ) : ( + <> + + + + )}
diff --git a/src/pages/index.js b/src/pages/index.js index 4acad73..6d17fef 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -1,5 +1,7 @@ export { default as Login } from "./auth/Login"; export { default as Signup } from "./auth/Signup"; +export { default as ForgotPassword } from "./auth/ForgotPassword"; +export { default as ResetPassword } from "./auth/ResetPassword"; export { default as Home } from "./Home/Home"; export { default as Cart } from "./OrderFlow/Cart"; export { default as Checkout } from "./OrderFlow/Checkout"; diff --git a/src/services/api.js b/src/services/api.js index f4d62ec..4352f1d 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -1,68 +1,33 @@ import axios from "axios"; -import { useAuthStore } from "../store"; import { restoreSessionService } from "./auth.service"; -import { resolveMockHandler } from "../mocks/handlers"; // ============================================================ -// MOCK MODE +// API CONFIGURATION // ============================================================ -// When VITE_USE_MOCK=true, axios uses a custom adapter that -// intercepts every request and returns mock data from -// src/mocks/handlers.js instead of hitting the network. -// -// To switch to the real backend: -// 1. Set VITE_USE_MOCK=false in .env -// 2. Set VITE_API_BASE_URL to your real backend URL in .env -// 3. That's it — no service files need to change. -// ============================================================ - -const USE_MOCK = import.meta.env.VITE_USE_MOCK !== "false"; - -const mockAdapter = async (config) => { - // Simulate realistic network delay (100–350ms) - const delay = Math.floor(Math.random() * 250) + 100; - await new Promise((resolve) => setTimeout(resolve, delay)); - - const { status, data } = resolveMockHandler(config); - - // Axios expects a specific response shape from adapters - const response = { - data, - status, - statusText: status === 200 || status === 201 ? "OK" : "Error", - headers: {}, - config, - request: {}, - }; - // Reject 4xx/5xx so axios error interceptors still fire correctly - if (status >= 400) { - const error = new Error(`Mock error: ${status}`); - error.response = response; - error.config = config; - return Promise.reject(error); +// 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; } - - return response; + 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/"; }; // Create axios instance export const api = axios.create({ - baseURL: import.meta.env.VITE_API_BASE_URL || "https://api.example.com", + baseURL: getBaseURL(), withCredentials: true, // refresh token is sent in httpOnly cookie headers: { "Content-Type": "application/json", }, - // Plug in mock adapter when VITE_USE_MOCK=true - ...(USE_MOCK && { adapter: mockAdapter }), }); -if (USE_MOCK) { - console.info( - "[API] Running in MOCK mode. Set VITE_USE_MOCK=false to use the real backend.", - ); -} - // ============================================================ // INTERCEPTORS (active in both mock and real mode) // ============================================================ @@ -72,7 +37,8 @@ const refreshQueue = []; // queue to hold requests while token is being refreshe // Attach access token to every outgoing request api.interceptors.request.use( - (config) => { + async (config) => { + const useAuthStore = (await import("../store/authStore")).default; const { getAccessToken } = useAuthStore.getState(); getAccessToken() && (config.headers["Authorization"] = `Bearer ${getAccessToken()}`); @@ -86,6 +52,7 @@ 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 @@ -104,8 +71,7 @@ api.interceptors.response.use( return new Promise((resolve, reject) => { refreshQueue.push({ resolve, reject }); }) - .then((token) => { - originalRequest.headers["Authorization"] = `Bearer ${token}`; + .then(() => { return api(originalRequest); }) .catch((err) => Promise.reject(err)); @@ -117,13 +83,24 @@ api.interceptors.response.use( try { const res = await restoreSessionService(); - const expiresAt = Date.now() + 1000 * 60 * 60 * 24; - setAccessToken(res.data.token, expiresAt); + const token = res.data.token; + + let expiresAt = Date.now() + 1000 * 60 * 60 * 24; // fallback + try { + 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); isRefreshing = false; - originalRequest.headers["Authorization"] = `Bearer ${getAccessToken()}`; - - refreshQueue.forEach((p) => p.resolve(getAccessToken())); + refreshQueue.forEach((p) => p.resolve(token)); refreshQueue.length = 0; return api(originalRequest); diff --git a/src/services/auth.service.js b/src/services/auth.service.js index 038aaed..96455c7 100644 --- a/src/services/auth.service.js +++ b/src/services/auth.service.js @@ -40,3 +40,13 @@ export const restoreSessionService = () => { return refreshPromise; }; + +// Password Reset +export const requestPasswordReset = (data) => { + return api.post("/auth/password/reset-request", data); +}; + +export const resetPassword = (data) => { + return api.post("/auth/password/reset", data); +}; + diff --git a/src/services/dashboardService.js b/src/services/dashboardService.js index cb2f000..2550f21 100644 --- a/src/services/dashboardService.js +++ b/src/services/dashboardService.js @@ -19,17 +19,33 @@ import axios from "axios"; export const getDashboardMetrics = () => api.get("/dashboard/metrics").then(r => Mappers.mapDashboardMetrics(r.data)); export const getRevenueData = (period = "6m") => api.get(`/dashboard/revenue?period=${period}`).then(r => Mappers.mapRevenueData(r.data)); export const getTopCategories = () => api.get("/dashboard/categories").then(r => Mappers.mapTopCategories(r.data)); -export const getOrdersOverview = () => api.get("/dashboard/orders-overview").then(r => Mappers.mapOrdersOverview(r.data)); +export const getOrdersOverview = () => Promise.resolve([ + { day: "Mon", orders: 120, highlight: false }, + { day: "Tue", orders: 150, highlight: false }, + { day: "Wed", orders: 180, highlight: true }, + { day: "Thu", orders: 130, highlight: false }, + { day: "Fri", orders: 210, highlight: false }, + { day: "Sat", orders: 250, highlight: true }, + { day: "Sun", orders: 220, highlight: false } +]); export const getOrderTypes = () => api.get("/dashboard/order-types").then(r => Mappers.mapOrderTypes(r.data)); -export const getTrendingMenus = () => api.get("/dashboard/trending-menus").then(r => Mappers.mapTrendingMenus(r.data)); +export const getTrendingMenus = () => Promise.resolve([ + { id: 1, name: "Spicy Chicken Burger", orders: 145, revenue: 1250, trend: "up", percentage: 12 }, + { id: 2, name: "Truffle Fries", orders: 98, revenue: 490, trend: "up", percentage: 8 }, + { id: 3, name: "Classic Margarita", orders: 85, revenue: 765, trend: "down", percentage: 3 } +]); export const getInventoryAlerts = () => api.get("/dashboard/inventory-alerts").then(r => r.data); // Untouched/Custom -export const getRecentActivity = () => api.get("/dashboard/activity").then(r => Mappers.mapRecentActivity(r.data)); +export const getRecentActivity = () => Promise.resolve([ + { id: 1, user: "John Doe", role: "Customer", action: "placed a new order", time: "5 mins ago", avatar: "" }, + { id: 2, user: "System", role: "System", action: "Inventory alert: Tomato stock low", time: "1 hour ago", avatar: "" }, + { id: 3, user: "Admin", role: "Admin", action: "New staff member registered", time: "2 hours ago", avatar: "" } +]); export const getCustomerReviews = () => api.get("/dashboard/reviews").then(r => r.data); // ── Orders ──────────────────────────────────────────────────────── -export const getOrdersMetrics = () => api.get("/dashboard/orders/metrics").then(r => Mappers.mapOrdersMetrics(r.data)); -export const getOrders = (params = {}) => api.get("/dashboard/orders", { params }).then(r => Mappers.mapOrders(r.data)); -export const updateOrderStatus = (orderId, status) => api.patch(`/dashboard/orders/${encodeURIComponent(orderId)}/status`, { status }).then(r => r.data); +export const getOrdersMetrics = () => api.get("/api/orders/admin/metrics").then(r => Mappers.mapOrdersMetrics(r.data)); +export const getOrders = (params = {}) => api.get("/api/orders/admin/all", { params }).then(r => Mappers.mapOrders(r.data)); +export const updateOrderStatus = (orderId, status) => api.patch(`/api/orders/admin/${encodeURIComponent(orderId)}/status`, { status }).then(r => r.data); // ── Kitchen ─────────────────────────────────────────────────────── export const getKitchenOrders = () => diff --git a/src/tests/services/api.test.js b/src/tests/services/api.test.js index 4c6817d..d060c48 100644 --- a/src/tests/services/api.test.js +++ b/src/tests/services/api.test.js @@ -121,4 +121,27 @@ test('Calls refresh endpoint when getting 401', async () => { console.log('✅ Normal error test passed!'); }); + + // TEST 6: Does it decode JWT to set expiresAt? + test('Decodes JWT to set expiresAt correctly', async () => { + setAccessToken('old-token'); + + // Create a mock JWT with a specific expiration time (e.g., 1 hour from now) + const futureExp = Math.floor(Date.now() / 1000) + 3600; // 1 hour in seconds + const mockPayload = btoa(JSON.stringify({ exp: futureExp })); + const mockJwt = `header.${mockPayload}.signature`; + + mock.onGet('/user/profile').replyOnce(401).onGet('/user/profile').reply(200, { name: 'John' }); + mock.onPost('/auth/refresh').reply(200, { token: mockJwt }); + + await api.get('/user/profile'); + + // get expiresAt from store + const { expiresAt } = useAuthStore.getState(); + + // It should be exactly futureExp * 1000 + expect(expiresAt).toBe(futureExp * 1000); + + console.log('✅ JWT decoding test passed!'); + }); }); \ No newline at end of file diff --git a/tests/customization.spec.js b/tests/customization.spec.js new file mode 100644 index 0000000..455054c --- /dev/null +++ b/tests/customization.spec.js @@ -0,0 +1,116 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Customization Page - USDA Category Mapping', () => { + test('should fetch meals and correctly categorize ingredients based on USDA foodCategory', async ({ page }) => { + + // Intercept the API call to /menu and provide mock data with USDA categories + await page.route('**/menu', async route => { + const json = [ + { + id: 1, + name: 'Custom Salad Bowl', + price: 15.0, + mealIngredients: [ + { + ingredient: { + id: 101, + name: 'Grilled Chicken Breast', + category: 'Poultry Products', // Maps to 'protein' + nutrients: [ + { nutrientName: 'Protein', value: 30 }, + { nutrientName: 'Energy', value: 150 } + ] + } + }, + { + ingredient: { + id: 102, + name: 'Romaine Lettuce', + category: 'Vegetables and Vegetable Products', // Maps to 'veggies' + nutrients: [ + { nutrientName: 'Carbohydrate', value: 5 } + ] + } + }, + { + ingredient: { + id: 103, + name: 'Cheddar Cheese', + category: 'Dairy and Egg Products', // Maps to 'cheese' + nutrients: [] + } + }, + { + ingredient: { + id: 104, + name: 'Olive Oil', + category: 'Fats and Oils', // Maps to 'sauces' + nutrients: [] + } + }, + { + ingredient: { + id: 105, + name: 'Black Pepper', + category: 'Spices and Herbs', // Maps to 'extras' + nutrients: [] + } + } + ] + } + ]; + await route.fulfill({ json }); + }); + + // Navigate to the customization page + await page.goto('/customize'); + + // Wait for the meal to be loaded and displayed in the BaseSelector + await expect(page.getByText('Custom Salad Bowl')).toBeVisible(); + + // Select the meal to reveal the ingredient sections + await page.getByText('Custom Salad Bowl').click(); + + // Select the base size + await expect(page.getByText('Choose Your Size')).toBeVisible(); + await page.getByText('Regular').click(); + + // Verify that the UI successfully mapped the USDA categories to the correct sections + + // 1. Protein Section + const proteinSection = page.locator('div').filter({ hasText: /^Protein\*/ }).first(); + await expect(proteinSection).toBeVisible(); + await expect(proteinSection.getByText('Grilled Chicken Breast')).toBeVisible(); + + // 2. Veggies Section + const veggiesSection = page.locator('div').filter({ hasText: /^Veggies\*/ }).first(); + await expect(veggiesSection).toBeVisible(); + await expect(veggiesSection.getByText('Romaine Lettuce')).toBeVisible(); + + // 3. Cheese Section + const cheeseSection = page.locator('div').filter({ hasText: /^Cheese/ }).first(); + await expect(cheeseSection).toBeVisible(); + await expect(cheeseSection.getByText('Cheddar Cheese')).toBeVisible(); + + // 4. Sauces Section + const saucesSection = page.locator('div').filter({ hasText: /^Sauces/ }).first(); + await expect(saucesSection).toBeVisible(); + await expect(saucesSection.getByText('Olive Oil')).toBeVisible(); + + // 5. Extras Section + const extrasSection = page.locator('div').filter({ hasText: /^Extras/ }).first(); + await expect(extrasSection).toBeVisible(); + await expect(extrasSection.getByText('Black Pepper')).toBeVisible(); + + // Verify interaction and store calculation (nutrients should update based on what we select) + await proteinSection.getByText('Grilled Chicken Breast').click(); // Toggle it on + await veggiesSection.getByText('Romaine Lettuce').click(); // Toggle it on + + // Verify the summary box updates with correct nutrition logic + // Chicken: 30 Protein, 150 Calories. Lettuce: 5 Carbs. + // Wait for the summary box to show updated values + await expect(page.locator('.bg-white.rounded-3xl').getByText('150kcal', { exact: true })).toBeVisible(); + await expect(page.locator('.bg-white.rounded-3xl').getByText('30g', { exact: true }).first()).toBeVisible(); + await expect(page.locator('.bg-white.rounded-3xl').getByText('5g', { exact: true }).first()).toBeVisible(); + }); +}); diff --git a/tests/kitchen-integration.spec.js b/tests/kitchen-integration.spec.js new file mode 100644 index 0000000..851cbd6 --- /dev/null +++ b/tests/kitchen-integration.spec.js @@ -0,0 +1,84 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Kitchen Integration E2E', () => { + + // Increase test timeout since we need to wait for RabbitMQ saga + kitchen polling + test.setTimeout(120_000); + + test.beforeEach(async ({ page }) => { + // Navigate to login + await page.goto('/auth/login'); + + await page.fill('input[type="email"]', process.env.VITE_TEST_EMAIL || 'admin@revive.com'); + await page.fill('input[type="password"]', process.env.VITE_TEST_PASSWORD || 'admin123'); + await page.click('button[type="submit"]'); + await page.waitForURL('/', { timeout: 10000 }); + }); + + test('should create an order and process it in the kitchen', async ({ page }) => { + // 1. Create an Order — navigate to menu + await page.goto('/menu'); + + // Add item to cart — target the specific Classic Cheeseburger card (visible only) + await page.waitForSelector('button:has-text("Add to cart") >> visible=true'); + await page.locator('div.group:has(h3:has-text("Classic Cheeseburger"))').locator('button:has-text("Add to cart") >> visible=true').first().click(); + await page.goto('/checkout'); + + // Fill checkout form + await page.fill('#email', process.env.VITE_TEST_EMAIL || 'admin@revive.com'); + await page.fill('#firstName', 'System'); + await page.fill('#lastName', 'Admin'); + await page.fill('#phone', '1234567890'); + await page.locator('#region').selectOption({ index: 1 }); + await page.fill('#city', 'Testville'); + await page.fill('#address', '123 Test St'); + await page.fill('#zipCode', '12345'); + + await page.click('button:has-text("Continue to Payment")'); + + // Payment step + await page.waitForURL('**/payment**'); + await page.click('button:has-text("Confirm payment")'); + + // Wait for success page + await page.waitForURL('**/thanks**', { timeout: 30000 }); + + // Wait a few seconds for the saga to complete (order → RabbitMQ → kitchen ticket) + await page.waitForTimeout(5000); + + // 2. Go to Live Kitchen + await page.goto('/dashboard/live-kitchen'); + + // The board polls every 30s, but initial load happens on mount + const queueColumn = page.locator('h3:has-text("Order Queue")').locator('..'); + const orderCard = queueColumn.locator('.group', { hasText: 'System Admin' }).first(); + await expect(orderCard).toBeVisible({ timeout: 35000 }); + + // Move to Preparing + await orderCard.locator('button:has-text("Start Preparing")').first().click(); + + // Verify it moved to Preparing + const prepColumn = page.locator('h3:has-text("Preparing")').locator('..'); + const prepOrderCard = prepColumn.locator('.group', { hasText: 'System Admin' }).first(); + await expect(prepOrderCard).toBeVisible({ timeout: 10000 }); + + // Move to Ready + await prepOrderCard.locator('button:has-text("Prepared")').first().click(); + + // Verify it moved to Ready + const readyColumn = page.locator('h3:has-text("Ready")').locator('..'); + const readyOrderCard = readyColumn.locator('.group', { hasText: 'System Admin' }).first(); + await expect(readyOrderCard).toBeVisible({ timeout: 10000 }); + + // Mark as Done + await readyOrderCard.locator('button:has-text("Mark Done")').first().click(); + + // Confirm in modal — the modal has a "Ready" confirm button + await page.getByRole('button', { name: 'Ready', exact: true }).click(); + + // Verify it is in Done column + const doneColumn = page.locator('h3:has-text("Done")').locator('..'); + const doneOrderCard = doneColumn.locator('.group', { hasText: 'System Admin' }).first(); + await expect(doneOrderCard).toBeVisible({ timeout: 10000 }); + }); +});