From 4293906e951c92d4a8a1b9022c6e584a80f33b04 Mon Sep 17 00:00:00 2001 From: MWorld Deployer Date: Tue, 29 Sep 2026 00:06:20 +0530 Subject: [PATCH] fix: repair EatIt frontend routing, customer discovery page, and D1 database queries --- backend/package-lock.json | 2 +- backend/package.json | 2 +- backend/src/routes/live-location.ts | 2 +- backend/src/routes/public.ts | 4 +- backend/src/routes/utils.ts | 11 +- backend/src/utils.ts | 11 +- frontend/package-lock.json | 66 ++++-- frontend/src/App.tsx | 41 +++- frontend/src/pages/CustomerApp.tsx | 320 ++++++++++++++++++++++++++++ 9 files changed, 420 insertions(+), 39 deletions(-) create mode 100644 frontend/src/pages/CustomerApp.tsx diff --git a/backend/package-lock.json b/backend/package-lock.json index e2f5fdd..0a0853b 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -9,7 +9,7 @@ "hono": "latest" }, "devDependencies": { - "@cloudflare/workers-types": "^4.0.0", + "@cloudflare/workers-types": "^4.20240404.0", "typescript": "^5.0.0", "wrangler": "^4.129.0" } diff --git a/backend/package.json b/backend/package.json index 02798e3..162ad8d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -10,6 +10,6 @@ "devDependencies": { "wrangler": "^4.129.0", "typescript": "^5.0.0", - "@cloudflare/workers-types": "^4.0.0" + "@cloudflare/workers-types": "^4.20240404.0" } } \ No newline at end of file diff --git a/backend/src/routes/live-location.ts b/backend/src/routes/live-location.ts index 492d861..f300108 100644 --- a/backend/src/routes/live-location.ts +++ b/backend/src/routes/live-location.ts @@ -169,7 +169,7 @@ async function readPosition(c: any) { if (!position && record.agentEmail) { for (const table of ['delivery_agents', 'delivery_riders']) { try { - const row = await c.env.DB.prepare('SELECT current_latitude, current_longitude, updated_at FROM ' + table + ' WHERE user_email = ?').bind(record.agentEmail).first(); + const row = (await c.env.DB.prepare('SELECT current_latitude, current_longitude, updated_at FROM ' + table + ' WHERE user_email = ?').bind(record.agentEmail).first()) as any; const lat = finiteCoordinate(row?.current_latitude, -90, 90); const lng = finiteCoordinate(row?.current_longitude, -180, 180); const updatedAt = Date.parse(String(row?.updated_at || '')); diff --git a/backend/src/routes/public.ts b/backend/src/routes/public.ts index 1a091a6..bffaae1 100644 --- a/backend/src/routes/public.ts +++ b/backend/src/routes/public.ts @@ -14,9 +14,9 @@ function __requireRole(allowedRoles: string[]) { const email = authenticatedUser?.email; if (!email) return c.json({ success: false, msg: 'Unauthorized' }, 401); - const persistedUser = await c.env.DB.prepare('SELECT role FROM users WHERE email = ?') + const persistedUser = (await c.env.DB.prepare('SELECT role FROM users WHERE email = ?') .bind(email || null) - .first<{ role?: string | null }>(); + .first()) as { role?: string | null } | null; const role = String(persistedUser?.role || ''); if (!allowedRoles.includes(role)) { return c.json({ success: false, msg: 'You do not have permission for this action' }, 403); diff --git a/backend/src/routes/utils.ts b/backend/src/routes/utils.ts index f1221f1..4231f9c 100644 --- a/backend/src/routes/utils.ts +++ b/backend/src/routes/utils.ts @@ -117,10 +117,9 @@ export const authMiddleware: MiddlewareHandler = async (c, next) => const newId = crypto.randomUUID(); await c.env.DB.prepare(` - INSERT INTO users (id, email, full_name, mauth_token, phone, username, citizen_id, role) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO users (email, full_name, mauth_token, phone, username, citizen_id, role) + VALUES (?, ?, ?, ?, ?, ?, ?) `).bind( - newId, userData.email, userData.fullName, token, @@ -209,10 +208,10 @@ export const adminAuthMiddleware: MiddlewareHandler = async (c, next const ensureAdminUser = async (email: string, fullName: string) => { try { await c.env.DB.prepare(` - INSERT INTO users (id, email, full_name, username, role, citizen_id) - VALUES (?, ?, ?, ?, 'admin', 'admin-bypass-id') + INSERT INTO users (email, full_name, username, role, citizen_id) + VALUES (?, ?, ?, 'admin', 'admin-bypass-id') ON CONFLICT(email) DO NOTHING - `).bind(crypto.randomUUID(), email, fullName, email.split('@')[0]).run(); + `).bind(email, fullName, email.split('@')[0]).run(); } catch (e) { console.error('[Admin Auth] Error upserting admin user:', e); } diff --git a/backend/src/utils.ts b/backend/src/utils.ts index f1221f1..4231f9c 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils.ts @@ -117,10 +117,9 @@ export const authMiddleware: MiddlewareHandler = async (c, next) => const newId = crypto.randomUUID(); await c.env.DB.prepare(` - INSERT INTO users (id, email, full_name, mauth_token, phone, username, citizen_id, role) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO users (email, full_name, mauth_token, phone, username, citizen_id, role) + VALUES (?, ?, ?, ?, ?, ?, ?) `).bind( - newId, userData.email, userData.fullName, token, @@ -209,10 +208,10 @@ export const adminAuthMiddleware: MiddlewareHandler = async (c, next const ensureAdminUser = async (email: string, fullName: string) => { try { await c.env.DB.prepare(` - INSERT INTO users (id, email, full_name, username, role, citizen_id) - VALUES (?, ?, ?, ?, 'admin', 'admin-bypass-id') + INSERT INTO users (email, full_name, username, role, citizen_id) + VALUES (?, ?, ?, 'admin', 'admin-bypass-id') ON CONFLICT(email) DO NOTHING - `).bind(crypto.randomUUID(), email, fullName, email.split('@')[0]).run(); + `).bind(email, fullName, email.split('@')[0]).run(); } catch (e) { console.error('[Admin Auth] Error upserting admin user:', e); } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index fb92c52..c81f3e6 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1043,7 +1043,7 @@ "version": "20.19.43", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -1053,14 +1053,14 @@ "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.31", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -1071,7 +1071,7 @@ "version": "18.3.7", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^18.0.0" @@ -1128,6 +1128,18 @@ "node": ">= 8" } }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -2223,6 +2235,18 @@ "node": ">=8.6" } }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -2328,12 +2352,12 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -2733,6 +2757,18 @@ "node": ">=8.10.0" } }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/recharts": { "version": "2.15.4", "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", @@ -3070,18 +3106,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", - "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3123,7 +3147,7 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/update-browserslist-db": { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 37ba0ed..f8d7968 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,3 +1,42 @@ +import React from 'react'; +import { Routes, Route, Navigate } from 'react-router-dom'; +import Navbar from './components/Navbar'; +import Landing from './pages/Landing'; +import AuthPage from './pages/Auth'; +import Onboarding from './pages/Onboarding'; +import CustomerApp from './pages/CustomerApp'; +import CustomerOrdersPage from './pages/CustomerOrders'; +import OrderTracking from './pages/OrderTracking'; +import RestaurantWorkspace from './pages/RestaurantWorkspace'; +import DeliveryWorkspace from './pages/DeliveryWorkspace'; +import AdminPage from './pages/Admin'; +import AdminTokenGate from './components/AdminTokenGate'; + export default function App() { - return
; + return ( +
+ +
+ + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + } + /> + } /> + +
+
+ ); } diff --git a/frontend/src/pages/CustomerApp.tsx b/frontend/src/pages/CustomerApp.tsx new file mode 100644 index 0000000..934f7ce --- /dev/null +++ b/frontend/src/pages/CustomerApp.tsx @@ -0,0 +1,320 @@ +import React, { useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { useNavigate } from 'react-router-dom'; +import { api, resolveMediaUrl } from '../api'; +import { useAuth } from '../context/AuthContext'; +import SearchFilterPanel from '../components/features/SearchFilterPanel'; +import StoreDetailModal from '../components/customer/StoreDetailModal'; +import CartDrawer from '../components/customer/CartDrawer'; +import { Clock, MapPin, ShoppingBag, Sparkles, Store, UtensilsCrossed } from 'lucide-react'; + +interface Restaurant { + id: string; + name: string; + description?: string; + address: string; + latitude: number; + longitude: number; + image_url?: string; + status: string; +} + +interface MenuItem { + id: string; + restaurant_id: string; + name: string; + description?: string; + category?: string; + ingredients?: string; + price: number; + image_url?: string; + is_available: number | boolean; +} + +export default function CustomerApp() { + const { user } = useAuth(); + const navigate = useNavigate(); + + const [searchQuery, setSearchQuery] = useState(''); + const [selectedCategory, setSelectedCategory] = useState('All'); + const [selectedSort, setSelectedSort] = useState('featured'); + + const [selectedRestaurantId, setSelectedRestaurantId] = useState(null); + const [isCartOpen, setIsCartOpen] = useState(false); + + // Cart state: items and selected restaurant + const [cartItems, setCartItems] = useState<{ item: MenuItem; quantity: number }[]>([]); + const [activeCartRestaurant, setActiveCartRestaurant] = useState<{ id: string; name: string; address?: string } | null>(null); + + const { data: restaurantsRes, isLoading, isError, refetch } = useQuery({ + queryKey: ['restaurants', searchQuery], + queryFn: async () => { + const res = await api.get('/api/restaurants', { + params: searchQuery ? { search: searchQuery } : {}, + }); + return res.data; + }, + }); + + const restaurants: Restaurant[] = Array.isArray(restaurantsRes?.restaurants) ? restaurantsRes.restaurants : []; + + // Filter and sort restaurants + const filteredRestaurants = restaurants.filter((r) => { + if (selectedCategory !== 'All') { + const catLower = selectedCategory.toLowerCase(); + const nameMatch = r.name.toLowerCase().includes(catLower); + const descMatch = (r.description || '').toLowerCase().includes(catLower); + if (!nameMatch && !descMatch) return false; + } + return true; + }).sort((a, b) => { + if (selectedSort === 'name_asc') return a.name.localeCompare(b.name); + if (selectedSort === 'name_desc') return b.name.localeCompare(a.name); + return 0; // featured + }); + + const handleAddToCart = (item: MenuItem, delta: number, storeInfo?: { id: string; name: string; address?: string }) => { + setCartItems((prev) => { + // If switching restaurants, clear cart first + if (activeCartRestaurant && storeInfo && activeCartRestaurant.id !== storeInfo.id) { + if (!window.confirm(`Your cart contains items from ${activeCartRestaurant.name}. Clear cart and start a new order?`)) { + return prev; + } + setActiveCartRestaurant(storeInfo); + if (delta > 0) return [{ item, quantity: delta }]; + return []; + } + + if (storeInfo && !activeCartRestaurant) { + setActiveCartRestaurant(storeInfo); + } + + const existingIndex = prev.findIndex((ci) => ci.item.id === item.id); + if (existingIndex >= 0) { + const newQty = prev[existingIndex].quantity + delta; + if (newQty <= 0) { + const updated = prev.filter((_, idx) => idx !== existingIndex); + if (updated.length === 0) setActiveCartRestaurant(null); + return updated; + } + const updated = [...prev]; + updated[existingIndex] = { ...updated[existingIndex], quantity: newQty }; + return updated; + } else if (delta > 0) { + return [...prev, { item, quantity: delta }]; + } + return prev; + }); + }; + + const handleUpdateCartQuantity = (itemId: string, delta: number) => { + setCartItems((prev) => { + const index = prev.findIndex((ci) => ci.item.id === itemId); + if (index >= 0) { + const newQty = prev[index].quantity + delta; + if (newQty <= 0) { + const updated = prev.filter((_, idx) => idx !== index); + if (updated.length === 0) setActiveCartRestaurant(null); + return updated; + } + const updated = [...prev]; + updated[index] = { ...updated[index], quantity: newQty }; + return updated; + } + return prev; + }); + }; + + const handleClearCart = () => { + setCartItems([]); + setActiveCartRestaurant(null); + }; + + const totalCartCount = cartItems.reduce((sum, ci) => sum + ci.quantity, 0); + const totalCartPrice = cartItems.reduce((sum, ci) => sum + Number(ci.item.price) * ci.quantity, 0); + + const selectedRestaurantForModal = restaurants.find((r) => r.id === selectedRestaurantId); + + return ( +
+
+ {/* Welcome Header */} +
+
+
+ + Customer Discovery +
+

+ Welcome back, {user?.full_name || 'Diner'}! +

+

+ Explore local kitchens, discover delicious dishes, and place your order for fast delivery. +

+
+ +
+ + {totalCartCount > 0 && ( + + )} +
+
+ + {/* Search & Filter Component */} + + + {/* Restaurant Grid */} +
+
+

+ {selectedCategory === 'All' ? 'Nearby Restaurants' : `${selectedCategory} Kitchens`} +

+ + Showing {filteredRestaurants.length} partner store(s) + +
+ + {isLoading ? ( +
+
+

Discovering open restaurants...

+
+ ) : isError ? ( +
+

Failed to load local restaurants.

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

No restaurants match your search

+

+ Try clearing your search query or choosing another category filter. +

+ +
+ ) : ( +
+ {filteredRestaurants.map((restaurant) => ( +
+
+
+ {restaurant.image_url ? ( + {restaurant.name} + ) : ( +
+ +
+ )} +
+ + {restaurant.status === 'active' ? 'Open Now' : 'Closed'} +
+
+ +
+

+ {restaurant.name} +

+ {restaurant.description && ( +

+ {restaurant.description} +

+ )} +
+ + {restaurant.address} +
+
+
+ +
+ +
+
+ ))} +
+ )} +
+
+ + {/* Store Detail & Menu Modal */} + {selectedRestaurantId && ( + setSelectedRestaurantId(null)} + cartItems={cartItems} + onAddToCart={(item, qty) => { + if (selectedRestaurantForModal) { + handleAddToCart(item, qty, { + id: selectedRestaurantForModal.id, + name: selectedRestaurantForModal.name, + address: selectedRestaurantForModal.address, + }); + } + }} + onOpenCart={() => setIsCartOpen(true)} + /> + )} + + {/* Cart Drawer */} + setIsCartOpen(false)} + items={cartItems} + restaurant={activeCartRestaurant} + onUpdateQuantity={handleUpdateCartQuantity} + onClearCart={handleClearCart} + /> +
+ ); +}