fix: repair EatIt frontend routing, customer discovery page, and D1 database queries

This commit is contained in:
MWorld Deployer 2026-09-29 00:06:20 +05:30
parent e08ccfaeda
commit 4293906e95
9 changed files with 420 additions and 39 deletions

View File

@ -9,7 +9,7 @@
"hono": "latest" "hono": "latest"
}, },
"devDependencies": { "devDependencies": {
"@cloudflare/workers-types": "^4.0.0", "@cloudflare/workers-types": "^4.20240404.0",
"typescript": "^5.0.0", "typescript": "^5.0.0",
"wrangler": "^4.129.0" "wrangler": "^4.129.0"
} }

View File

@ -10,6 +10,6 @@
"devDependencies": { "devDependencies": {
"wrangler": "^4.129.0", "wrangler": "^4.129.0",
"typescript": "^5.0.0", "typescript": "^5.0.0",
"@cloudflare/workers-types": "^4.0.0" "@cloudflare/workers-types": "^4.20240404.0"
} }
} }

View File

@ -169,7 +169,7 @@ async function readPosition(c: any) {
if (!position && record.agentEmail) { if (!position && record.agentEmail) {
for (const table of ['delivery_agents', 'delivery_riders']) { for (const table of ['delivery_agents', 'delivery_riders']) {
try { try {
const row = await c.env.DB.prepare('SELECT current_latitude, current_longitude, updated_at FROM ' + table + ' WHERE user_email = ?').bind(record.agentEmail).first<any>(); 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 lat = finiteCoordinate(row?.current_latitude, -90, 90);
const lng = finiteCoordinate(row?.current_longitude, -180, 180); const lng = finiteCoordinate(row?.current_longitude, -180, 180);
const updatedAt = Date.parse(String(row?.updated_at || '')); const updatedAt = Date.parse(String(row?.updated_at || ''));

View File

@ -14,9 +14,9 @@ function __requireRole(allowedRoles: string[]) {
const email = authenticatedUser?.email; const email = authenticatedUser?.email;
if (!email) return c.json({ success: false, msg: 'Unauthorized' }, 401); 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) .bind(email || null)
.first<{ role?: string | null }>(); .first()) as { role?: string | null } | null;
const role = String(persistedUser?.role || ''); const role = String(persistedUser?.role || '');
if (!allowedRoles.includes(role)) { if (!allowedRoles.includes(role)) {
return c.json({ success: false, msg: 'You do not have permission for this action' }, 403); return c.json({ success: false, msg: 'You do not have permission for this action' }, 403);

View File

@ -117,10 +117,9 @@ export const authMiddleware: MiddlewareHandler<AppContext> = async (c, next) =>
const newId = crypto.randomUUID(); const newId = crypto.randomUUID();
await c.env.DB.prepare(` await c.env.DB.prepare(`
INSERT INTO users (id, email, full_name, mauth_token, phone, username, citizen_id, role) INSERT INTO users (email, full_name, mauth_token, phone, username, citizen_id, role)
VALUES (?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?)
`).bind( `).bind(
newId,
userData.email, userData.email,
userData.fullName, userData.fullName,
token, token,
@ -209,10 +208,10 @@ export const adminAuthMiddleware: MiddlewareHandler<AppContext> = async (c, next
const ensureAdminUser = async (email: string, fullName: string) => { const ensureAdminUser = async (email: string, fullName: string) => {
try { try {
await c.env.DB.prepare(` await c.env.DB.prepare(`
INSERT INTO users (id, email, full_name, username, role, citizen_id) INSERT INTO users (email, full_name, username, role, citizen_id)
VALUES (?, ?, ?, ?, 'admin', 'admin-bypass-id') VALUES (?, ?, ?, 'admin', 'admin-bypass-id')
ON CONFLICT(email) DO NOTHING ON CONFLICT(email) DO NOTHING
`).bind(crypto.randomUUID(), email, fullName, email.split('@')[0]).run(); `).bind(email, fullName, email.split('@')[0]).run();
} catch (e) { } catch (e) {
console.error('[Admin Auth] Error upserting admin user:', e); console.error('[Admin Auth] Error upserting admin user:', e);
} }

View File

@ -117,10 +117,9 @@ export const authMiddleware: MiddlewareHandler<AppContext> = async (c, next) =>
const newId = crypto.randomUUID(); const newId = crypto.randomUUID();
await c.env.DB.prepare(` await c.env.DB.prepare(`
INSERT INTO users (id, email, full_name, mauth_token, phone, username, citizen_id, role) INSERT INTO users (email, full_name, mauth_token, phone, username, citizen_id, role)
VALUES (?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?)
`).bind( `).bind(
newId,
userData.email, userData.email,
userData.fullName, userData.fullName,
token, token,
@ -209,10 +208,10 @@ export const adminAuthMiddleware: MiddlewareHandler<AppContext> = async (c, next
const ensureAdminUser = async (email: string, fullName: string) => { const ensureAdminUser = async (email: string, fullName: string) => {
try { try {
await c.env.DB.prepare(` await c.env.DB.prepare(`
INSERT INTO users (id, email, full_name, username, role, citizen_id) INSERT INTO users (email, full_name, username, role, citizen_id)
VALUES (?, ?, ?, ?, 'admin', 'admin-bypass-id') VALUES (?, ?, ?, 'admin', 'admin-bypass-id')
ON CONFLICT(email) DO NOTHING ON CONFLICT(email) DO NOTHING
`).bind(crypto.randomUUID(), email, fullName, email.split('@')[0]).run(); `).bind(email, fullName, email.split('@')[0]).run();
} catch (e) { } catch (e) {
console.error('[Admin Auth] Error upserting admin user:', e); console.error('[Admin Auth] Error upserting admin user:', e);
} }

View File

@ -1043,7 +1043,7 @@
"version": "20.19.43", "version": "20.19.43",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz",
"integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==",
"dev": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
@ -1053,14 +1053,14 @@
"version": "15.7.15", "version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
"dev": true, "devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "18.3.31", "version": "18.3.31",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz",
"integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==",
"dev": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/prop-types": "*", "@types/prop-types": "*",
@ -1071,7 +1071,7 @@
"version": "18.3.7", "version": "18.3.7",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
"dev": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"@types/react": "^18.0.0" "@types/react": "^18.0.0"
@ -1128,6 +1128,18 @@
"node": ">= 8" "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": { "node_modules/arg": {
"version": "5.0.2", "version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
@ -2223,6 +2235,18 @@
"node": ">=8.6" "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": { "node_modules/mime-db": {
"version": "1.52.0", "version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@ -2328,12 +2352,12 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/picomatch": { "node_modules/picomatch": {
"version": "2.3.2", "version": "4.0.7",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8.6" "node": ">=12"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
@ -2733,6 +2757,18 @@
"node": ">=8.10.0" "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": { "node_modules/recharts": {
"version": "2.15.4", "version": "2.15.4",
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz",
@ -3070,18 +3106,6 @@
"url": "https://github.com/sponsors/SuperchupuDev" "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": { "node_modules/to-regex-range": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@ -3123,7 +3147,7 @@
"version": "6.21.0", "version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true, "devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/update-browserslist-db": { "node_modules/update-browserslist-db": {

View File

@ -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() { export default function App() {
return <main className="min-h-screen" />; return (
<div className="min-h-screen bg-[#F8F7F4] flex flex-col font-sans text-[#0F172A]">
<Navbar />
<main className="flex-1">
<Routes>
<Route path="/" element={<Landing />} />
<Route path="/auth" element={<AuthPage />} />
<Route path="/app" element={<CustomerApp />} />
<Route path="/app/onboarding" element={<Onboarding />} />
<Route path="/app/orders" element={<CustomerOrdersPage />} />
<Route path="/app/orders/:orderId" element={<OrderTracking />} />
<Route path="/app/restaurant" element={<RestaurantWorkspace />} />
<Route path="/app/delivery" element={<DeliveryWorkspace />} />
<Route
path="/admin"
element={
<AdminTokenGate>
<AdminPage />
</AdminTokenGate>
}
/>
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</main>
</div>
);
} }

View File

@ -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<string | null>(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 (
<div className="min-h-[calc(100vh-4rem)] bg-[#F8F7F4] py-8 px-4 sm:px-6 lg:px-8">
<div className="max-w-7xl mx-auto space-y-8">
{/* Welcome Header */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 bg-white p-6 rounded-2xl border border-[#E2E1DA] shadow-sm">
<div className="space-y-1">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-orange-100 text-[#B93812] text-xs font-semibold uppercase tracking-wider">
<Sparkles className="w-3.5 h-3.5" />
<span>Customer Discovery</span>
</div>
<h1 className="text-2xl sm:text-3xl font-bold text-[#0F172A] font-['Outfit',sans-serif]">
Welcome back, {user?.full_name || 'Diner'}!
</h1>
<p className="text-xs sm:text-sm text-slate-600 font-['Plus_Jakarta_Sans',sans-serif]">
Explore local kitchens, discover delicious dishes, and place your order for fast delivery.
</p>
</div>
<div className="flex items-center gap-3 self-start md:self-auto">
<button
onClick={() => navigate('/app/orders')}
className="inline-flex items-center gap-2 px-4 py-2.5 bg-[#F0EFEA] hover:bg-[#E2E1DA] text-[#0F172A] text-xs font-semibold rounded-xl transition-colors border border-[#E2E1DA]"
>
<ShoppingBag className="w-4 h-4 text-[#B93812]" />
<span>Track Orders</span>
</button>
{totalCartCount > 0 && (
<button
onClick={() => setIsCartOpen(true)}
className="inline-flex items-center gap-2 px-5 py-2.5 bg-[#B93812] hover:bg-[#962d0e] text-white text-xs font-bold rounded-xl transition-colors shadow-sm"
>
<ShoppingBag className="w-4 h-4" />
<span>Basket ({totalCartCount})</span>
<span className="bg-white/20 px-2 py-0.5 rounded-md font-mono">${totalCartPrice.toFixed(2)}</span>
</button>
)}
</div>
</div>
{/* Search & Filter Component */}
<SearchFilterPanel
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
selectedCategory={selectedCategory}
onCategoryChange={setSelectedCategory}
selectedSort={selectedSort}
onSortChange={setSelectedSort}
totalResults={filteredRestaurants.length}
isLoading={isLoading}
/>
{/* Restaurant Grid */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-xl font-bold text-[#0F172A] font-['Outfit',sans-serif]">
{selectedCategory === 'All' ? 'Nearby Restaurants' : `${selectedCategory} Kitchens`}
</h2>
<span className="text-xs text-slate-500 font-medium">
Showing {filteredRestaurants.length} partner store(s)
</span>
</div>
{isLoading ? (
<div className="p-16 text-center bg-white rounded-2xl border border-[#E2E1DA]">
<div className="w-8 h-8 border-4 border-[#B93812] border-t-transparent rounded-full animate-spin mx-auto mb-3" />
<p className="text-sm font-medium text-slate-600">Discovering open restaurants...</p>
</div>
) : isError ? (
<div className="p-12 text-center bg-red-50 rounded-2xl border border-red-200 text-red-700">
<p className="font-semibold text-sm">Failed to load local restaurants.</p>
<button
onClick={() => refetch()}
className="mt-3 px-4 py-2 bg-[#B93812] text-white text-xs font-semibold rounded-lg hover:bg-[#962d0e]"
>
Retry
</button>
</div>
) : filteredRestaurants.length === 0 ? (
<div className="p-16 text-center bg-white rounded-2xl border border-[#E2E1DA] space-y-3">
<Store className="w-12 h-12 text-slate-300 mx-auto" />
<h3 className="text-base font-bold text-[#0F172A]">No restaurants match your search</h3>
<p className="text-xs text-slate-500 max-w-md mx-auto">
Try clearing your search query or choosing another category filter.
</p>
<button
onClick={() => {
setSearchQuery('');
setSelectedCategory('All');
}}
className="px-4 py-2 bg-[#0F172A] text-white text-xs font-semibold rounded-xl hover:bg-slate-800"
>
Reset Search
</button>
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredRestaurants.map((restaurant) => (
<div
key={restaurant.id}
className="group bg-white rounded-2xl border border-[#E2E1DA] overflow-hidden shadow-xs hover:shadow-md transition-all duration-200 flex flex-col justify-between"
>
<div>
<div className="relative h-44 bg-slate-100 overflow-hidden">
{restaurant.image_url ? (
<img
src={resolveMediaUrl(restaurant.image_url)}
alt={restaurant.name}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
/>
) : (
<div className="w-full h-full flex items-center justify-center bg-slate-200 text-slate-400">
<UtensilsCrossed className="w-12 h-12 stroke-1" />
</div>
)}
<div className="absolute top-3 left-3 bg-white/95 backdrop-blur-xs px-2.5 py-1 rounded-full text-[11px] font-bold text-[#0F172A] shadow-xs flex items-center gap-1.5">
<span className={`w-2 h-2 rounded-full ${restaurant.status === 'active' ? 'bg-emerald-500' : 'bg-red-500'}`} />
<span>{restaurant.status === 'active' ? 'Open Now' : 'Closed'}</span>
</div>
</div>
<div className="p-5 space-y-2">
<h3 className="font-bold text-[#0F172A] text-lg font-['Outfit',sans-serif] group-hover:text-[#B93812] transition-colors">
{restaurant.name}
</h3>
{restaurant.description && (
<p className="text-xs text-slate-600 line-clamp-2 leading-relaxed">
{restaurant.description}
</p>
)}
<div className="flex items-center gap-1 text-xs text-slate-500 pt-1">
<MapPin className="w-3.5 h-3.5 text-[#B93812] shrink-0" />
<span className="truncate">{restaurant.address}</span>
</div>
</div>
</div>
<div className="p-5 pt-0">
<button
onClick={() => setSelectedRestaurantId(restaurant.id)}
className="w-full py-2.5 px-4 bg-[#0F172A] hover:bg-[#B93812] text-white text-xs font-semibold rounded-xl transition-colors flex items-center justify-center gap-2 cursor-pointer"
>
<ShoppingBag className="w-3.5 h-3.5" />
<span>View Menu & Order</span>
</button>
</div>
</div>
))}
</div>
)}
</div>
</div>
{/* Store Detail & Menu Modal */}
{selectedRestaurantId && (
<StoreDetailModal
restaurantId={selectedRestaurantId}
onClose={() => 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 */}
<CartDrawer
isOpen={isCartOpen}
onClose={() => setIsCartOpen(false)}
items={cartItems}
restaurant={activeCartRestaurant}
onUpdateQuantity={handleUpdateCartQuantity}
onClearCart={handleClearCart}
/>
</div>
);
}