Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 42 additions & 27 deletions client/src/components/cart/CartDrawer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,13 @@ const CartDrawer = () => {
return (
<div className="fixed inset-0 z-[70] flex justify-end">
{/* Backdrop */}
<div className="absolute inset-0 bg-black/50 backdrop-blur-sm" onClick={() => setIsCartOpen(false)}></div>
<div
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
onClick={(e) => {
e.stopPropagation();
setIsCartOpen(false);
}}
></div>

{/* Drawer */}
<div className="relative w-full max-w-md bg-surface h-full shadow-2xl flex flex-col border-l border-white/10 animate-in slide-in-from-right duration-300">
Expand All @@ -75,36 +81,45 @@ const CartDrawer = () => {
</Button>
</div>
) : (
cartItems.map((item) => (
<div key={item._id} className="flex gap-4 p-3 bg-background rounded-xl border border-white/5">
<div className="w-20 h-20 rounded-lg overflow-hidden shrink-0">
<img src={item.image} alt={item.name} className="w-full h-full object-cover" />
</div>
<div className="flex-1 flex flex-col justify-between">
<div className="flex justify-between items-start">
<h3 className="font-bold text-text line-clamp-1">{item.name}</h3>
<p className="font-bold text-primary">${(item.price * item.quantity).toFixed(2)}</p>
cartItems.map((item) => {
const imageUrl = typeof item.image === 'string' ? item.image.trim() : '';
const hasImage = Boolean(imageUrl);

return (
<div key={item._id} className="flex gap-4 p-3 bg-background rounded-xl border border-white/5">
<div className="w-20 h-20 rounded-lg overflow-hidden shrink-0 bg-secondary/10 flex items-center justify-center">
{hasImage ? (
<img src={imageUrl} alt={item.name} className="w-full h-full object-cover" />
) : (
<span className="text-xs text-secondary">No image</span>
)}
</div>
<div className="flex justify-between items-center mt-2">
<div className="flex items-center gap-3 bg-surface rounded-lg p-1 border border-white/10">
<button
onClick={() => updateQuantity(item._id, item.quantity - 1, item.modifiers)}
className="p-1 hover:text-primary transition-colors"
>
<Minus className="w-4 h-4" />
</button>
<span className="text-sm font-medium w-4 text-center">{item.quantity}</span>
<button
onClick={() => updateQuantity(item._id, item.quantity + 1, item.modifiers)}
className="p-1 hover:text-primary transition-colors"
>
<Plus className="w-4 h-4" />
</button>
<div className="flex-1 flex flex-col justify-between">
<div className="flex justify-between items-start">
<h3 className="font-bold text-text line-clamp-1">{item.name}</h3>
<p className="font-bold text-primary">${(item.price * item.quantity).toFixed(2)}</p>
</div>
<div className="flex justify-between items-center mt-2">
<div className="flex items-center gap-3 bg-surface rounded-lg p-1 border border-white/10">
<button
onClick={() => updateQuantity(item._id, item.quantity - 1, item.modifiers)}
className="p-1 hover:text-primary transition-colors"
>
<Minus className="w-4 h-4" />
</button>
<span className="text-sm font-medium w-4 text-center">{item.quantity}</span>
<button
onClick={() => updateQuantity(item._id, item.quantity + 1, item.modifiers)}
className="p-1 hover:text-primary transition-colors"
>
<Plus className="w-4 h-4" />
</button>
</div>
</div>
</div>
</div>
</div>
))
);
})
)}
</div>

Expand Down
9 changes: 8 additions & 1 deletion client/src/components/ui/MenuCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,14 @@ const MenuCard = ({ item }) => {
<p className="text-secondary text-sm mb-4 flex-grow">{item.description}</p>
<div className="flex justify-between items-center mt-auto">
<span className="text-lg font-bold text-text">${item.price}</span>
<Button variant="outline" className="text-sm px-4 py-1" onClick={() => addToCart(item)}>
<Button
variant="outline"
className="text-sm px-4 py-1"
onClick={(e) => {
e.stopPropagation();
addToCart(item);
}}
>
Add
</Button>
</div>
Expand Down
53 changes: 42 additions & 11 deletions client/src/context/CartContext.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useRef, useCallback } from 'react';
import { CartContext } from './CartContextValues';

export const CartProvider = ({ children }) => {
Expand Down Expand Up @@ -35,25 +35,56 @@ export const CartProvider = ({ children }) => {
}
}, [tableInfo]);

const addToCart = (item, quantity = 1, modifiers = []) => {
const addGuardRef = useRef(new Map());
const timeoutRef = useRef(null);

const addToCart = useCallback((item, quantity = 1, modifiers = []) => {
if (!item?._id) {
return;
}

const normalizedModifiers = Array.isArray(modifiers) ? modifiers : [];
const guardKey = `${item._id}-${JSON.stringify(normalizedModifiers)}`;

// Check if this exact item+modifiers combo is already being added
const lastAddTime = addGuardRef.current.get(guardKey);
if (lastAddTime && Date.now() - lastAddTime < 1000) {
return;
}

// Record this add attempt
addGuardRef.current.set(guardKey, Date.now());

setCartItems((prevItems) => {
// Check if item with same ID and modifiers exists
// For simplicity, we'll just check ID for now.
// Should ideally check modifiers too (deep equal).
const normalizedImage = typeof item.image === 'string' && item.image.trim() ? item.image.trim() : null;
const safeQuantity = Math.max(1, quantity);
const existingItemIndex = prevItems.findIndex(
(i) => i._id === item._id && JSON.stringify(i.modifiers) === JSON.stringify(modifiers)
(i) => i._id === item._id && JSON.stringify(i.modifiers) === JSON.stringify(normalizedModifiers)
);

if (existingItemIndex > -1) {
const newItems = [...prevItems];
newItems[existingItemIndex].quantity += quantity;
newItems[existingItemIndex].quantity += safeQuantity;
return newItems;
} else {
return [...prevItems, { ...item, quantity, modifiers }];
}

return [
...prevItems,
{ ...item, image: normalizedImage, quantity: safeQuantity, modifiers: normalizedModifiers },
];
});
setIsCartOpen(true);
};

// Open cart drawer
setTimeout(() => setIsCartOpen(true), 0);

// Clear guard after 1 second to allow new adds
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
addGuardRef.current.delete(guardKey);
}, 1000);
}, []);

const removeFromCart = (itemId, modifiers = []) => {
setCartItems((prevItems) =>
Expand Down
71 changes: 24 additions & 47 deletions client/src/features/home/FeaturedItems.jsx
Original file line number Diff line number Diff line change
@@ -1,70 +1,47 @@
import React, { useEffect, useState } from 'react';
import MenuCard from '../../components/ui/MenuCard';
import { Button } from '../../components/ui/button';
import { useOutlet } from '../../context/OutletContextValues';
import api from '../../lib/axios';

const FeaturedItems = () => {
const { selectedOutlet } = useOutlet();
const FeaturedItems = ({ outletId, showAll = false }) => {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(false);

useEffect(() => {
const fetchMenu = async () => {
if (!selectedOutlet?._id) return;
if (!outletId) return;

setLoading(true);
try {
const res = await api.get(`/api/public/menu/${selectedOutlet._id}`);
const res = await api.get(`/api/menu/public/${outletId}`);

if (res.data.success) {
// For featured items, maybe we just take the first 4 or random?
// Or we can filter by a 'featured' tag if we add it later.
// For now, let's just slice the top 4.
setItems(res.data.data.slice(0, 4));
const allItems = res.data.data;

// Filter to show only available items
const availableItems = allItems.filter((item) => item.isAvailable !== false);

setItems(availableItems);
}
} catch (error) {
console.error('Failed to fetch menu:', error);
} finally {
setLoading(false);
console.error('Error fetching menu:', error);
}
};

fetchMenu();
}, [selectedOutlet]);
}, [outletId]);

if (!selectedOutlet) return null;
if (!outletId) return null;

return (
<section className="py-16 bg-background">
<div className="container mx-auto px-4">
<div className="text-center mb-12">
<h2 className="text-3xl md:text-4xl font-bold text-text mb-4">Our Signature Dishes</h2>
<p className="text-secondary max-w-2xl mx-auto">
Discover the most loved dishes from our kitchen at{' '}
<span className="text-primary font-semibold">{selectedOutlet.name}</span>.
</p>
</div>

{loading ? (
<div className="text-center py-10 text-secondary">Loading menu...</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8 mb-12">
{items.map((item) => (
<MenuCard key={item._id} item={item} />
))}
{items.length === 0 && (
<div className="col-span-full text-center text-secondary">No items available at this outlet yet.</div>
)}
</div>
)}
// Display limited items (8) or all items based on showAll prop
const displayItems = showAll ? items : items.slice(0, 8);

<div className="text-center">
<Button variant="primary" className="px-8 py-3 text-lg">
View Full Menu
</Button>
</div>
</div>
</section>
return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{displayItems.map((item) => (
<MenuCard key={item._id} item={item} />
))}
{displayItems.length === 0 && (
<div className="col-span-full text-center text-zinc-400 py-8">No items available at this outlet yet.</div>
)}
</div>
);
};

Expand Down
10 changes: 3 additions & 7 deletions client/src/features/home/HeroSection.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import { Button } from '../../components/ui/button';

const HeroSection = ({ onBookTable }) => {
const HeroSection = () => {
return (
<div className="relative h-screen min-h-[600px] flex items-center justify-center">
{/* Background Image with Overlay */}
Expand All @@ -21,16 +21,12 @@ const HeroSection = ({ onBookTable }) => {
<span className="text-primary">Modern Dining Experience</span>
</h1>
<p className="text-lg md:text-xl text-gray-200 mb-10 max-w-2xl mx-auto">
Experience the rich heritage of Indonesian cuisine. Whether you want a cozy dine-in experience or quick
delivery to your doorstep, we serve happiness.
Experience the rich heritage of Indonesian cuisine with our exquisite menu selection.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<div className="flex justify-center">
<Button variant="primary" className="text-lg px-8 py-3">
Order Now
</Button>
<Button variant="surface" className="text-lg px-8 py-3" onClick={onBookTable}>
Book a Table
</Button>
</div>
</div>
</div>
Expand Down
57 changes: 57 additions & 0 deletions client/src/features/home/TrendingItems.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React, { useEffect, useState } from 'react';
import { useCart } from '../../context/CartContextValues';
import api from '../../lib/axios';
import { Flame } from 'lucide-react';
import MenuCard from '../../components/ui/MenuCard';

const TrendingItems = ({ outletId }) => {
const { addToCart } = useCart();
const [trendingItems, setTrendingItems] = useState([]);

useEffect(() => {
const fetchTrendingItems = async () => {
if (!outletId) return;

try {
const res = await api.get(`/api/menu/public/trending/${outletId}`);
if (res.data.success) {
setTrendingItems(res.data.data || []);
}
} catch (err) {
console.error('Error fetching trending items:', err);
setTrendingItems([]);
}
};

fetchTrendingItems();
}, [outletId]);

// Don't show section if no trending items found
if (trendingItems.length === 0) {
return null;
}

return (
<section className="mb-12">
<div className="flex items-center gap-2 mb-6">
<Flame className="w-6 h-6 text-orange-500" />
<h2 className="text-2xl font-bold text-white">Trending Now</h2>
<span className="text-xs bg-orange-500/20 text-orange-400 px-2 py-1 rounded-full">Popular</span>
</div>

<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{trendingItems.map((item) => (
<MenuCard
key={item._id}
item={item}
onAddToCart={() => {
addToCart(item);
}}
/>
))}
</div>
</section>
);
};

export default TrendingItems;
13 changes: 4 additions & 9 deletions client/src/main.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
import { createRoot } from 'react-dom/client';
import './index.css';
import App from './App.jsx';

createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)
createRoot(document.getElementById('root')).render(<App />);
Loading
Loading