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
46 changes: 37 additions & 9 deletions client/src/components/cart/CartDrawer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import api from '../../lib/axios';
import toast from 'react-hot-toast';

const CartDrawer = () => {
const { cartItems, cartTotal, isCartOpen, setIsCartOpen, updateQuantity, clearCart, tableInfo, clearTableInfo } =
useCart();
const { cartItems, cartTotal, isCartOpen, setIsCartOpen, updateQuantity, clearCart, tableInfo } = useCart();
const { selectedOutlet } = useOutlet();

const handleCheckout = async () => {
if (!selectedOutlet) {
const outletId = selectedOutlet?._id || selectedOutlet || tableInfo?.outletId;
if (!outletId) {
toast.error('Please select an outlet first.');
return;
}
Expand All @@ -25,21 +25,30 @@ const CartDrawer = () => {
modifiers: item.modifiers,
}));

// Get existing session token if customer already placed an order
const existingToken = tableInfo?.tableId ? localStorage.getItem(`customerToken_${tableInfo.tableId}`) : null;

const payload = {
outletId: selectedOutlet._id,
outletId,
items: orderItems,
totalAmount: cartTotal,
...(tableInfo && { tableId: tableInfo.tableId }), // Include tableId if customer selected a table
...(tableInfo && { tableId: tableInfo.tableId }),
...(existingToken && { customerToken: existingToken }),
};

// API call to create order
const res = await api.post('/api/public/orders', payload);

if (res.data.success) {
// Always store server-provided token
if (res.data.customerToken && tableInfo?.tableId) {
localStorage.setItem(`customerToken_${tableInfo.tableId}`, res.data.customerToken);
}
toast.success('Order Placed Successfully!');
clearCart();
clearTableInfo();
setIsCartOpen(false);
// Trigger order refresh if there's a callback
if (window.refreshOrders) window.refreshOrders();
}
} catch (error) {
console.error('Checkout failed', error);
Expand Down Expand Up @@ -95,9 +104,28 @@ const CartDrawer = () => {
)}
</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>
<div className="flex justify-between items-start gap-2">
<div className="flex-1 min-w-0">
<h3 className="font-bold text-text line-clamp-1">{item.name}</h3>
{Array.isArray(item.modifiers) && item.modifiers.length > 0 && (
<div className="mt-1 text-xs text-secondary space-y-1">
{item.modifiers.map((mod, idx) => (
<div key={`${mod.name}-${mod.option}-${idx}`} className="flex items-center gap-2">
<span className="text-text font-semibold">{mod.name}:</span>
<span>{mod.option || 'No selection'}</span>
{mod.priceAdjustment ? (
<span className="text-[11px] text-zinc-400">
{mod.priceAdjustment > 0 ? `+${mod.priceAdjustment}` : mod.priceAdjustment}
</span>
) : null}
</div>
))}
</div>
)}
</div>
<p className="font-bold text-primary whitespace-nowrap">
${(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">
Expand Down
157 changes: 145 additions & 12 deletions client/src/components/ui/MenuCard.jsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,51 @@
import React from 'react';
import React, { useMemo, useState } from 'react';
import { Button } from './button';
import { useCart } from '../../context/CartContextValues';

const MenuCard = ({ item }) => {
const { addToCart } = useCart();
const hasVariants = Array.isArray(item?.variants) && item.variants.length > 0;
const [showVariants, setShowVariants] = useState(false);

// Calculate default selections based on variants (only when item changes)
const defaultSelections = useMemo(() => {
if (!hasVariants) return {};
const defaults = {};
item.variants.forEach((variant) => {
defaults[variant.name] = [];
});
return defaults;
}, [item, hasVariants]);

// Initialize with function to use memoized defaultSelections
const [selections, setSelections] = useState(() => defaultSelections);

const selectedModifiers = useMemo(() => {
return Object.entries(selections).flatMap(([name, opts]) => {
if (!Array.isArray(opts) || opts.length === 0) return [{ name, option: null, priceAdjustment: 0 }];
return opts.map((option) => ({
name,
option: option?.label,
priceAdjustment: Number(option?.priceAdjustment) || 0,
}));
});
}, [selections]);

const finalPrice = useMemo(() => {
const base = Number(item.price) || 0;
const adjustments = selectedModifiers.reduce((acc, m) => acc + (m.priceAdjustment || 0), 0);
return base + adjustments;
}, [item.price, selectedModifiers]);

const handleConfirmAdd = () => {
const enrichedItem = { ...item, price: finalPrice };
addToCart(enrichedItem, 1, selectedModifiers);
setShowVariants(false);
};

return (
<div className="bg-surface rounded-xl shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300 flex flex-col h-full border border-secondary/10">
<div className="h-48 overflow-hidden bg-secondary/10">
<div className="h-44 sm:h-48 overflow-hidden bg-secondary/10">
{item.image && (
<img
src={item.image}
Expand All @@ -16,28 +54,123 @@ const MenuCard = ({ item }) => {
/>
)}
</div>
<div className="p-5 flex flex-col flex-grow">
<div className="flex justify-between items-start mb-2">
<h3 className="text-xl font-bold text-text">{item.name}</h3>
<span className="bg-primary/10 text-primary px-2 py-1 rounded-full text-xs font-semibold">
<div className="p-4 sm:p-5 flex flex-col flex-grow gap-3">
<div className="flex justify-between items-start gap-3">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span
className={`w-3 h-3 rounded-full border ${item.isVeg ? 'bg-green-500 border-green-500' : 'bg-red-500 border-red-500'}`}
aria-label={item.isVeg ? 'Vegetarian' : 'Non-Vegetarian'}
></span>
<h3 className="text-lg sm:text-xl font-bold text-text leading-tight line-clamp-2">{item.name}</h3>
</div>
<p className="text-secondary text-sm line-clamp-2">{item.description}</p>
{Array.isArray(item.tags) && item.tags.length > 0 && (
<div className="flex flex-wrap gap-2 mt-2">
{item.tags.slice(0, 3).map((tag) => (
<span
key={tag}
className="bg-zinc-800 text-xs text-zinc-200 px-2 py-1 rounded-full border border-white/10"
>
{tag}
</span>
))}
{item.tags.length > 3 && <span className="text-xs text-zinc-400">+{item.tags.length - 3} more</span>}
</div>
)}
</div>
<span className="bg-primary/10 text-primary px-2 py-1 rounded-full text-[11px] sm:text-xs font-semibold whitespace-nowrap">
{item.category}
</span>
</div>
<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>
<div className="flex justify-between items-center mt-auto pt-1">
<span className="text-base sm:text-lg font-bold text-text">${finalPrice.toFixed(2)}</span>
<Button
variant="outline"
className="text-sm px-4 py-1"
className="text-sm px-4 py-2 min-h-[44px]"
onClick={(e) => {
e.stopPropagation();
addToCart(item);
if (hasVariants) {
setShowVariants(true);
} else {
addToCart(item);
}
}}
>
Add
{hasVariants ? 'Choose' : 'Add'}
</Button>
</div>
</div>

{showVariants && hasVariants && (
<div
className="fixed inset-0 z-[80] bg-black/60 backdrop-blur-sm flex items-end sm:items-center justify-center p-3"
onClick={() => setShowVariants(false)}
>
<div
className="w-full max-w-md bg-surface rounded-2xl shadow-2xl border border-white/10 overflow-hidden"
onClick={(e) => e.stopPropagation()}
>
<div className="p-4 border-b border-white/10 flex items-center justify-between">
<div>
<p className="text-xs text-secondary">Customize</p>
<h3 className="text-lg font-bold text-text">{item.name}</h3>
</div>
<button className="text-secondary hover:text-text" onClick={() => setShowVariants(false)}>
</button>
</div>

<div className="p-4 space-y-4 max-h-[60vh] overflow-y-auto">
{item.variants.map((variant) => (
<div key={variant.name} className="space-y-2">
<p className="text-sm font-semibold text-text">{variant.name}</p>
<div className="flex flex-wrap gap-2">
{variant.options.map((option) => {
const selectedList = selections[variant.name] || [];
const isSelected = selectedList.some((o) => o.label === option.label);
return (
<button
key={option.label}
className={`px-3 py-2 rounded-lg border text-sm transition-colors ${
isSelected ? 'border-amber-500 bg-amber-500/10 text-amber-400' : 'border-white/10 text-text'
}`}
onClick={() =>
setSelections((prev) => {
const current = prev[variant.name] || [];
const exists = current.some((o) => o.label === option.label);
const next = exists
? current.filter((o) => o.label !== option.label)
: [...current, option];
return { ...prev, [variant.name]: next };
})
}
>
<span className="font-medium">{option.label}</span>
{option.priceAdjustment ? (
<span className="ml-2 text-xs text-secondary">
{option.priceAdjustment > 0 ? `+${option.priceAdjustment}` : option.priceAdjustment}
</span>
) : null}
</button>
);
})}
</div>
</div>
))}
</div>

<div className="p-4 border-t border-white/10 flex items-center justify-between">
<div className="text-sm text-secondary">
<p className="text-text font-semibold">Total: ${finalPrice.toFixed(2)}</p>
</div>
<Button className="px-5" onClick={handleConfirmAdd}>
Add to Cart
</Button>
</div>
</div>
</div>
)}
</div>
);
};
Expand Down
102 changes: 91 additions & 11 deletions client/src/features/home/FeaturedItems.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import MenuCard from '../../components/ui/MenuCard';
import api from '../../lib/axios';

const FeaturedItems = ({ outletId, showAll = false }) => {
const [items, setItems] = useState([]);
const [searchTerm, setSearchTerm] = useState('');
const [category, setCategory] = useState('all');
const [diet, setDiet] = useState('all'); // 'all' | 'veg' | 'non-veg'
const [tag, setTag] = useState('all');

useEffect(() => {
const fetchMenu = async () => {
Expand All @@ -28,19 +32,95 @@ const FeaturedItems = ({ outletId, showAll = false }) => {
fetchMenu();
}, [outletId]);

if (!outletId) return null;
const filteredItems = useMemo(() => {
if (!outletId) return [];
const term = searchTerm.trim().toLowerCase();
return items.filter((item) => {
const matchesCategory = category === 'all' || item.category === category;
const matchesDiet = diet === 'all' || (diet === 'veg' ? item.isVeg === true : item.isVeg === false);
const matchesTag = tag === 'all' || (Array.isArray(item.tags) && item.tags.includes(tag));
const matchesSearch =
!term || item.name.toLowerCase().includes(term) || (item.description || '').toLowerCase().includes(term);
return matchesCategory && matchesDiet && matchesTag && matchesSearch;
});
}, [items, searchTerm, category, diet, tag, outletId]);

const categories = useMemo(() => {
const unique = Array.from(new Set(items.map((item) => item.category).filter(Boolean)));
return ['all', ...unique];
}, [items]);

const tags = useMemo(() => {
const unique = new Set();
items.forEach((item) => {
if (Array.isArray(item.tags)) {
item.tags.forEach((t) => t && unique.add(t));
}
});
return ['all', ...Array.from(unique)];
}, [items]);

// Display limited items (8) or all items based on showAll prop
const displayItems = showAll ? items : items.slice(0, 8);
const displayItems = showAll ? filteredItems : filteredItems.slice(0, 8);

if (!outletId) return null;

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 className="space-y-4">
<div className="flex flex-col gap-3 bg-zinc-900/60 border border-white/5 rounded-xl p-3">
<div className="flex flex-col sm:flex-row gap-3 sm:items-center sm:justify-between">
<input
type="search"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search dishes..."
className="w-full sm:w-1/2 bg-zinc-800 text-white text-sm rounded-lg px-3 py-2 border border-white/10 focus:border-amber-500 focus:outline-none"
/>
<div className="flex gap-3 w-full sm:w-auto">
<select
value={category}
onChange={(e) => setCategory(e.target.value)}
className="w-1/2 sm:w-44 bg-zinc-800 text-white text-sm rounded-lg px-3 py-2 border border-white/10 focus:border-amber-500 focus:outline-none"
>
{categories.map((cat) => (
<option key={cat} value={cat}>
{cat === 'all' ? 'All Categories' : cat}
</option>
))}
</select>
<select
value={diet}
onChange={(e) => setDiet(e.target.value)}
className="w-1/2 sm:w-36 bg-zinc-800 text-white text-sm rounded-lg px-3 py-2 border border-white/10 focus:border-amber-500 focus:outline-none"
>
<option value="all">Veg & Non-Veg</option>
<option value="veg">Veg</option>
<option value="non-veg">Non-Veg</option>
</select>
</div>
</div>
<div className="flex gap-3">
<select
value={tag}
onChange={(e) => setTag(e.target.value)}
className="w-full sm:w-60 bg-zinc-800 text-white text-sm rounded-lg px-3 py-2 border border-white/10 focus:border-amber-500 focus:outline-none"
>
{tags.map((t) => (
<option key={t} value={t}>
{t === 'all' ? 'All Tags' : t}
</option>
))}
</select>
</div>
</div>

<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 match your filters yet.</div>
)}
</div>
</div>
);
};
Expand Down
Loading
Loading