134 lines
4.3 KiB
TypeScript
134 lines
4.3 KiB
TypeScript
import { useEffect, useState } from "preact/hooks";
|
|
import { getMenuItemsService } from "../../services/locations";
|
|
|
|
export interface MenuItemRow {
|
|
id: number;
|
|
location_id: number;
|
|
name: string;
|
|
price: number;
|
|
category: string;
|
|
description: string;
|
|
is_available: boolean;
|
|
submitted_by: number;
|
|
avg_score: number | null;
|
|
}
|
|
|
|
const CATEGORY_LABELS: Record<string, string> = {
|
|
appetizer: 'Appetizer',
|
|
main_course: 'Main Course',
|
|
dessert: 'Dessert',
|
|
beverages: 'Beverages',
|
|
snack: 'Snack',
|
|
};
|
|
|
|
function formatPrice(price: number): string {
|
|
return 'Rp' + price.toLocaleString('id-ID');
|
|
}
|
|
|
|
function getRatingColor(rating: number): string {
|
|
if (rating >= 70) return '#3ba55d';
|
|
if (rating >= 40) return '#faa61a';
|
|
return '#ed4245';
|
|
}
|
|
|
|
interface Props {
|
|
locationId: number;
|
|
locationName: string;
|
|
onClose: () => void;
|
|
}
|
|
|
|
export default function MenuPopup({ locationId, locationName, onClose }: Props) {
|
|
const [items, setItems] = useState<MenuItemRow[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [activeCategory, setActiveCategory] = useState('all');
|
|
const [search, setSearch] = useState('');
|
|
|
|
useEffect(() => {
|
|
getMenuItemsService(locationId).then(res => {
|
|
if (res.data) setItems(res.data);
|
|
setLoading(false);
|
|
});
|
|
}, [locationId]);
|
|
|
|
const categories = ['all', ...Array.from(new Set(items.map(i => i.category).filter(Boolean)))];
|
|
|
|
const filtered = items.filter(item => {
|
|
const matchCat = activeCategory === 'all' || item.category === activeCategory;
|
|
const matchSearch = item.name.toLowerCase().includes(search.toLowerCase());
|
|
return matchCat && matchSearch;
|
|
});
|
|
|
|
// split into two columns
|
|
const mid = Math.ceil(filtered.length / 2);
|
|
const col1 = filtered.slice(0, mid);
|
|
const col2 = filtered.slice(mid);
|
|
|
|
return (
|
|
<div className="menu-popup-overlay" onClick={onClose}>
|
|
<div className="menu-popup-modal" onClick={e => e.stopPropagation()}>
|
|
|
|
<div className="menu-popup-header">
|
|
<div>
|
|
<h2 className="menu-popup-title">{locationName} Menu</h2>
|
|
</div>
|
|
<div className="menu-popup-header-actions">
|
|
<input
|
|
className="menu-popup-search"
|
|
type="text"
|
|
placeholder="Search dishes..."
|
|
value={search}
|
|
onInput={e => setSearch((e.target as HTMLInputElement).value)}
|
|
/>
|
|
<button className="menu-popup-close" onClick={onClose}>✕</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="menu-popup-tabs">
|
|
{categories.map(cat => (
|
|
<button
|
|
key={cat}
|
|
className={`menu-popup-tab ${activeCategory === cat ? 'menu-popup-tab--active' : ''}`}
|
|
onClick={() => setActiveCategory(cat)}
|
|
>
|
|
{cat === 'all' ? 'All' : (CATEGORY_LABELS[cat] ?? cat)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
<div className="menu-popup-body">
|
|
{loading ? (
|
|
<p style={{ padding: '20px', opacity: 0.5 }}>Loading...</p>
|
|
) : filtered.length === 0 ? (
|
|
<p style={{ padding: '20px', opacity: 0.5 }}>No items found.</p>
|
|
) : (
|
|
<div className="menu-popup-grid">
|
|
{[col1, col2].map((col, ci) => (
|
|
<div key={ci} className="menu-popup-col">
|
|
<div className="menu-popup-col-header">
|
|
<span style={{ flex: 1 }}></span>
|
|
<span className="menu-popup-col-rating-label">Rating</span>
|
|
</div>
|
|
{col.map(item => (
|
|
<div key={item.id} className="menu-popup-item">
|
|
<span className="menu-popup-item-name">{item.name}</span>
|
|
<span className="menu-popup-item-price">{formatPrice(item.price)}</span>
|
|
{item.avg_score != null ? (
|
|
<span className="menu-popup-item-rating" style={{ color: getRatingColor(item.avg_score) }}>
|
|
{Math.round(item.avg_score)}
|
|
</span>
|
|
) : (
|
|
<span className="menu-popup-item-rating" style={{ opacity: 0.3 }}>—</span>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|