'use client';
import { FC, useState, useEffect, useMemo, useCallback, useRef } from 'react';
import menuDataRaw from './menu.json';
import ShoppingCart from '../components/ShoppingCart';
import { createClient } from '@/utils/supabase/client';
import Confetti from 'react-confetti';
interface Article {
article_id: number;
article_name: string;
actual_price?: number | null;
}
interface Category {
cat_id: number;
parent_id: number | null;
cat_name: string;
articles?: Article[];
}
interface CartItem extends Article {
quantity: number;
}
const menuData: Category[] = menuDataRaw as Category[];
const BarMenu: FC = () => {
const [activeCategory, setActiveCategory] = useState<number | null>(null);
const categoryRefs = useRef<{[key: number]: HTMLDivElement | null}>({});
const observerRef = useRef<IntersectionObserver | null>(null);
const [showOrderDialog, setShowOrderDialog] = useState(false);
const [orderCode, setOrderCode] = useState<string>('');
const [tableName, setTableName] = useState<string | null>(null);
const [tableId, setTableId] = useState<string | null>(null);
const [showBillDialog, setShowBillDialog] = useState(false);
const [paymentMethod, setPaymentMethod] = useState<'cash' | 'card'>('cash');
const [billRequestStatus, setBillRequestStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
const [billRequestError, setBillRequestError] = useState<string | null>(null);
const [submittedOrder, setSubmittedOrder] = useState<{
cart: CartItem[];
totalPrice: number;
orderNote: string;
} | null>(null);
const [showConfetti, setShowConfetti] = useState(false);
const [confettiPosition, setConfettiPosition] = useState({ x: 0, y: 0 });
// Handle table parameter from URL
useEffect(() => {
const searchParams = new URLSearchParams(window.location.search);
const tableIdFromUrl = searchParams.get('table');
if (tableIdFromUrl) {
setTableId(tableIdFromUrl);
// Fetch table information from the database
const fetchTableInfo = async () => {
try {
const supabase = createClient();
const { data, error } = await supabase
.from('tables')
.select('label')
.eq('id', parseInt(tableIdFromUrl, 10))
.single();
if (error) throw error;
// Store table info in localStorage with expiration
const tableInfo = {
id: tableIdFromUrl,
name: data?.label || `Маса ${tableIdFromUrl}`, // Use table label or fallback to "Маса {id}"
expiresAt: new Date().getTime() + (24 * 60 * 60 * 1000) // 24 hours from now
};
localStorage.setItem('tableInfo', JSON.stringify(tableInfo));
setTableName(tableInfo.name);
} catch (error) {
console.error('Error fetching table info:', error);
// Fallback to default naming
const tableInfo = {
id: tableIdFromUrl,
name: `Маса ${tableIdFromUrl}`,
expiresAt: new Date().getTime() + (24 * 60 * 60 * 1000)
};
localStorage.setItem('tableInfo', JSON.stringify(tableInfo));
setTableName(tableInfo.name);
}
};
fetchTableInfo();
}
}, []);
// Check if there's table information in localStorage and if it's still valid
useEffect(() => {
const tableInfoString = localStorage.getItem('tableInfo');
if (tableInfoString) {
try {
const tableInfo = JSON.parse(tableInfoString);
const now = new Date().getTime();
// Check if the table info has expired
if (tableInfo.expiresAt > now) {
setTableName(tableInfo.name);
setTableId(tableInfo.id);
} else {
// Clear expired table info
localStorage.removeItem('tableInfo');
}
} catch (error) {
console.error('Error parsing table info:', error);
localStorage.removeItem('tableInfo');
}
}
}, []);
// Set first top-level category as active by default
useEffect(() => {
const firstCategory = menuData.find(c => c.parent_id === null);
if (firstCategory) {
setActiveCategory(firstCategory.cat_id);
}
}, []);
// Get top-level categories for navigation
const topLevelCategories = useMemo(() =>
menuData.filter(c => c.parent_id === null),
[]);
// Scroll to category when clicking on navigation
const scrollToCategory = (categoryId: number) => {
setActiveCategory(categoryId);
const element = categoryRefs.current[categoryId];
if (element) {
// Scroll with offset to account for sticky header
const headerHeight = 60; // Approximate height of the navigation header
const elementPosition = element.getBoundingClientRect().top + window.scrollY;
window.scrollTo({
top: elementPosition - headerHeight,
behavior: 'smooth'
});
}
};
// Add an item to the cart - implemented as a hook to pass to ShoppingCart
const addToCart = useCallback((item: Article, event?: React.MouseEvent<HTMLButtonElement>) => {
// Get the position of the clicked button or use default position (center of screen)
if (event) {
const buttonRect = event.currentTarget.getBoundingClientRect();
setConfettiPosition({
x: buttonRect.left + buttonRect.width / 2,
y: buttonRect.top + buttonRect.height / 2
});
} else {
// Default to center bottom of screen if no event
setConfettiPosition({
x: window.innerWidth / 2,
y: window.innerHeight - 100
});
}
const storedCart = localStorage.getItem('cart');
let currentCart: CartItem[] = storedCart ? JSON.parse(storedCart) : [];
// Check if the item is already in the cart
const existingItemIndex = currentCart.findIndex(cartItem => cartItem.article_id === item.article_id);
if (existingItemIndex > -1) {
// Increase quantity if item exists
currentCart[existingItemIndex].quantity += 1;
} else {
// Add new item with quantity 1
currentCart.push({
...item,
quantity: 1
});
}
// Update localStorage
localStorage.setItem('cart', JSON.stringify(currentCart));
// Dispatch custom event for components in the same window
window.dispatchEvent(new CustomEvent('cartUpdated', {
detail: { cart: currentCart }
}));
// Show confetti
setShowConfetti(true);
setTimeout(() => setShowConfetti(false), 2000);
console.log('Item added to cart:', item);
}, []);
// Handle order submission
const handleOrderSubmit = async (orderData: { cart: CartItem[], orderNote: string, totalPrice: number }) => {
console.log('Order submitted:', orderData);
try {
// Send order to API endpoint with table information if available
const response = await fetch('/api/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...orderData,
tableName: tableName
})
});
if (response.ok) {
const data = await response.json();
// Store the order code and submitted order
setOrderCode(data.orderCode);
setSubmittedOrder(orderData);
// Show the dialog
setShowOrderDialog(true);
// Clear the cart in localStorage
localStorage.setItem('cart', JSON.stringify([]));
// Dispatch custom event to update cart in all components
window.dispatchEvent(new CustomEvent('cartUpdated', {
detail: { cart: [] }
}));
} else {
const data = await response.json();
alert(`Грешка: ${data.message || 'Възникна проблем при изпращане на поръчката.'}`);
}
} catch (error) {
console.error('Error submitting order:', error);
alert('Възникна грешка при изпращането на поръчката. Моля, опитайте отново.');
}
};
// Handle bill request submission
const handleBillRequest = async () => {
try {
// Set loading state
setBillRequestStatus('loading');
const response = await fetch('/api/bill', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tableName: tableName,
tableId: tableId,
paymentMethod: paymentMethod
})
});
if (response.ok) {
// Set success state
setBillRequestStatus('success');
setBillRequestError(null);
// Close the dialog after a delay to show success message
setTimeout(() => {
setShowBillDialog(false);
setBillRequestStatus('idle');
}, 3000);
} else {
const data = await response.json();
// Set error state with message
setBillRequestStatus('error');
setBillRequestError(data.message || 'Възникна проблем при изпращане на искането за сметка.');
}
} catch (error) {
console.error('Error requesting bill:', error);
// Set error state with generic message
setBillRequestStatus('error');
setBillRequestError('Възникна грешка при изпращането на искането за сметка. Моля, опитайте отново.');
}
};
// Render categories and items
const renderCategoriesAndItems = useCallback(
(parentId: number | null, depth: number = 0): JSX.Element[] => {
const categories = menuData.filter((c) => c.parent_id === parentId);
return categories
.map((category) => {
const items = category.articles || [];
const subElements = renderCategoriesAndItems(category.cat_id, depth + 1);
if (items.length === 0 && subElements.length === 0) return null;
return (
<div
key={category.cat_id}
ref={(el) => {
if (depth === 0) {
categoryRefs.current[category.cat_id] = el;
}
}}
className={`py-4 ${
depth === 0 ? 'border-b mb-6' : 'border-l pl-4 ml-2'
}`}
id={`category-${category.cat_id}`}
>
<h2
className={`font-bold ${
depth === 0 ? 'text-xl mb-4' : 'text-lg my-2'
}`}
>
{category.cat_name}
</h2>
{items.length > 0 && (
<ul className="mb-2 divide-y">
{items.map((item) => (
<li
key={item.article_id}
className="flex justify-between py-2 items-center hover:bg-orange-100 rounded-md px-2"
>
<span className="font-medium">{item.article_name}</span>
<div className="flex items-center gap-2">
{item.actual_price !== undefined && item.actual_price !== null && (
<span className="font-medium">
{item.actual_price.toFixed(2)} лв
</span>
)}
{tableName && (
<button
onClick={(e) => addToCart(item, e)}
className="btn btn-sm btn-ghost border-2 border-orange-500 text-sm px-3 py-1 rounded-full transition-colors"
aria-label="Добави в поръчката"
>
+
</button>
)}
</div>
</li>
))}
</ul>
)}
{subElements}
</div>
);
})
.filter(Boolean) as JSX.Element[];
},
[addToCart, tableName]
);
const menuContent = useMemo(() => renderCategoriesAndItems(null), [renderCategoriesAndItems]);
// Set up intersection observer to detect which category is currently visible
useEffect(() => {
const options = {
root: null,
rootMargin: "-80px 0px 0px 0px", // Adjust based on header height
threshold: 0.1
};
const handleIntersect = (entries: IntersectionObserverEntry[]) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const categoryId = Number(entry.target.id.replace('category-', ''));
setActiveCategory(categoryId);
}
});
};
observerRef.current = new IntersectionObserver(handleIntersect, options);
// Observe all category elements
Object.values(categoryRefs.current).forEach(ref => {
if (ref) observerRef.current?.observe(ref);
});
return () => {
if (observerRef.current) {
observerRef.current.disconnect();
}
};
}, [menuContent]); // Add menuContent as dependency so it runs after categories are rendered
return (
<div className="max-w-4xl mx-auto pb-6 relative">
{/* Table info and Bill Request Button */}
{tableName && (
<div className="flex justify-between items-center px-4 py-2 md:px-6 lg:px-8 bg-white shadow-sm rounded-lg mb-2">
<div className="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-2 text-amber-700" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
<span className="font-medium text-amber-800">Маса: {tableName}</span>
</div>
<button
onClick={() => setShowBillDialog(true)}
className="btn btn-sm btn-primary text-white px-4 py-1 rounded-lg transition-colors flex items-center gap-1"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 9V7a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2m2 4h10a2 2 0 002-2v-6a2 2 0 00-2-2H9a2 2 0 00-2 2v6a2 2 0 002 2z" />
</svg>
Поискай сметка
</button>
</div>
)}
{/* Categories navigation */}
<div className="sticky top-20 sm:top-10 bg-white z-30 shadow-md w-full rounded-lg">
<div className="max-w-4xl mx-auto px-4 md:px-6 lg:px-8">
<div className="overflow-x-auto py-4 no-scrollbar">
<div className="flex gap-2 items-center">
{topLevelCategories.map((category) => (
<button
key={category.cat_id}
onClick={() => scrollToCategory(category.cat_id)}
className={`whitespace-nowrap px-4 py-2 rounded-full text-sm font-medium transition-colors ${
activeCategory === category.cat_id
? 'border-orange-500 border'
: 'bg-gray-100 text-gray-800 hover:bg-gray-200'
}`}
>
{category.cat_name}
</button>
))}
</div>
</div>
</div>
</div>
<div className="px-4 md:px-6 lg:px-8 pt-4">
{menuContent}
</div>
{tableName && (
<ShoppingCart
addToCart={addToCart}
onOrderSubmit={handleOrderSubmit}
/>
)}
{/* Bill Request Dialog */}
{showBillDialog && (
<div className="fixed inset-0 bg-opacity-50 z-50 flex items-center justify-center">
<div className="modal modal-open">
<div className="modal-box relative bg-white max-w-md p-6 rounded-lg">
{billRequestStatus === 'idle' || billRequestStatus === 'loading' ? (
<>
<h3 className="text-xl font-bold text-center mb-4">Поискай сметка</h3>
{tableName && (
<div className="bg-blue-50 p-3 rounded-lg mb-4 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-blue-500 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
<div>
<h4 className="font-semibold text-blue-800">Маса</h4>
<p className="text-sm text-blue-700">{tableName}</p>
</div>
</div>
)}
<div className="mb-4">
<h4 className="font-semibold mb-2">Изберете начин на плащане:</h4>
<div className="flex gap-2 justify-center">
<button
onClick={() => setPaymentMethod('cash')}
className={`flex-1 py-3 px-4 rounded-lg flex flex-col items-center justify-center border-2 transition-colors ${
paymentMethod === 'cash'
? 'bg-green-50 border-green-500 text-green-700'
: 'bg-white border-gray-200 text-gray-700 hover:bg-gray-50'
}`}
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8 mb-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 9V7a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2m2 4h10a2 2 0 002-2v-6a2 2 0 00-2-2H9a2 2 0 00-2 2v6a2 2 0 002 2z" />
</svg>
<span className="font-medium">В брой</span>
</button>
<button
onClick={() => setPaymentMethod('card')}
className={`flex-1 py-3 px-4 rounded-lg flex flex-col items-center justify-center border-2 transition-colors ${
paymentMethod === 'card'
? 'bg-blue-50 border-blue-500 text-blue-700'
: 'bg-white border-gray-200 text-gray-700 hover:bg-gray-50'
}`}
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8 mb-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
</svg>
<span className="font-medium">Карта</span>
</button>
</div>
</div>
<div className="flex justify-between gap-2">
<button
onClick={() => setShowBillDialog(false)}
className="flex-1 bg-gray-200 hover:bg-gray-300 text-gray-800 py-2 px-4 rounded"
>
Отказ
</button>
<button
onClick={handleBillRequest}
disabled={billRequestStatus === 'loading'}
className={`flex-1 ${billRequestStatus === 'loading' ? 'bg-gray-400' : 'bg-green-600 hover:bg-green-700'} text-white py-2 px-4 rounded flex justify-center items-center`}
>
{billRequestStatus === 'loading' ? (
<>
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Изпращане...
</>
) : (
'Изпрати'
)}
</button>
</div>
</>
) : billRequestStatus === 'success' ? (
<div className="text-center">
<div className="flex justify-center mb-4">
<div className="rounded-full bg-green-100 p-3">
<svg xmlns="http://www.w3.org/2000/svg" className="h-12 w-12 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
</div>
<h3 className="text-xl font-bold text-center mb-4">Сметката е поискана успешно!</h3>
<p className="text-gray-600 mb-6">Вашата сметка ще бъде донесена скоро.</p>
<div className="text-center">
<p className="text-sm text-gray-500">Този прозорец ще се затвори автоматично</p>
</div>
</div>
) : (
<div className="text-center">
<div className="flex justify-center mb-4">
<div className="rounded-full bg-red-100 p-3">
<svg xmlns="http://www.w3.org/2000/svg" className="h-12 w-12 text-red-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
</div>
<h3 className="text-xl font-bold text-center mb-4">Възникна грешка</h3>
<p className="text-gray-600 mb-6">{billRequestError}</p>
<button
onClick={() => {
setBillRequestStatus('idle');
setBillRequestError(null);
}}
className="bg-blue-600 hover:bg-blue-700 text-white py-2 px-6 rounded"
>
Опитайте отново
</button>
</div>
)}
</div>
</div>
</div>
)}
{/* Order Confirmation Dialog */}
{showOrderDialog && submittedOrder && (
<div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center">
<div className="modal modal-open">
<div className="modal-box relative bg-white max-w-md">
<h3 className="text-xl font-bold text-center mb-4">Поръчката е успешна!</h3>
<div className="bg-amber-100 p-4 rounded-lg mb-4 text-center">
<p className="text-sm text-amber-800">Вашият код за поръчката</p>
<p className="text-4xl font-bold tracking-wider text-amber-900">{orderCode}</p>
<p className="text-xs text-amber-700 mt-2">Запазете кода за справка</p>
</div>
{tableName && (
<div className="bg-blue-50 p-3 rounded-lg mb-4 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-blue-500 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
<div>
<h4 className="font-semibold text-blue-800">Доставка на масата</h4>
<p className="text-sm text-blue-700">{tableName}</p>
</div>
</div>
)}
<div className="max-h-60 overflow-y-auto mb-4">
<h4 className="font-semibold mb-2">Поръчани продукти:</h4>
<ul className="divide-y">
{submittedOrder.cart.map((item) => (
<li key={item.article_id} className="py-2 flex justify-between">
<span>
{item.quantity}x {item.article_name}
</span>
{item.actual_price && (
<span className="font-medium">
{(item.actual_price * item.quantity).toFixed(2)} лв
</span>
)}
</li>
))}
</ul>
</div>
{submittedOrder.orderNote && (
<div className="mb-4 bg-gray-50 p-3 rounded-lg">
<h4 className="font-semibold mb-1">Бележка:</h4>
<p className="text-sm text-gray-700">{submittedOrder.orderNote}</p>
</div>
)}
<div className="flex justify-between font-bold border-t pt-3">
<span>Общо:</span>
<span>{submittedOrder.totalPrice.toFixed(2)} лв</span>
</div>
<div className="mt-6">
<button
onClick={() => setShowOrderDialog(false)}
className="w-full py-2 bg-orange-500 text-white rounded-lg hover:bg-orange-600 transition-colors"
>
Затвори
</button>
</div>
</div>
</div>
</div>
)}
{/* Confetti Animation for adding items */}
{showConfetti && (
<div className="fixed inset-0 pointer-events-none">
<Confetti
width={window.innerWidth}
height={window.innerHeight}
colors={['#f59e0b', '#d97706', '#fdba74', '#fbbf24', '#f97316']}
recycle={false}
numberOfPieces={100}
gravity={0.3}
confettiSource={{
x: confettiPosition.x,
y: confettiPosition.y,
w: 0,
h: 0
}}
initialVelocityX={10}
initialVelocityY={10}
tweenDuration={100}
/>
</div>
)}
</div>
);
};
export default BarMenu;