Replace entire repo content with code from /root/shahikitchen-google/
This commit is contained in:
+149
-33
@@ -4,8 +4,8 @@
|
||||
* MENU PAGE — Premium Sidebar Navigation
|
||||
*/
|
||||
|
||||
import { useEffect, useRef, useState, useMemo } from "react";
|
||||
import { menuCategories } from "@/lib/menu-data";
|
||||
import { useCallback, useEffect, useRef, useState, useMemo } from "react";
|
||||
import { useMenu } from "@/presentation/providers/menu-provider";
|
||||
import { buildCartLineFromMenuItem } from "@/application/cart/cart-line-builder";
|
||||
import type { MenuItem } from "@/domain/menu/entities";
|
||||
import { gsap } from "gsap";
|
||||
@@ -20,6 +20,7 @@ import { useLanguage } from "@/lib/language-context";
|
||||
import { getTranslation } from "@/lib/translations";
|
||||
import {
|
||||
getCategoryName,
|
||||
getMenuItemInclusionNote,
|
||||
getMenuItemDescription,
|
||||
getMenuItemName,
|
||||
} from "@/application/i18n/menu-localization";
|
||||
@@ -41,12 +42,24 @@ export default function MenuPage() {
|
||||
const [showVegetarianOnly, setShowVegetarianOnly] = useState(false);
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
const [detailItem, setDetailItem] = useState<MenuItem | null>(null);
|
||||
const [categoryScrollHints, setCategoryScrollHints] = useState({
|
||||
top: false,
|
||||
bottom: false,
|
||||
left: false,
|
||||
right: false,
|
||||
});
|
||||
const categoryListRef = useRef<HTMLDivElement>(null);
|
||||
const prevCategoryCountRef = useRef(0);
|
||||
|
||||
const { addToCart } = useCart();
|
||||
const { categories: menuCategories, refreshMenu } = useMenu();
|
||||
const { language } = useLanguage();
|
||||
const t = getTranslation(language);
|
||||
|
||||
useEffect(() => {
|
||||
void refreshMenu();
|
||||
}, [refreshMenu]);
|
||||
|
||||
// Sidebar categories
|
||||
const sidebarCategories = [
|
||||
{ id: "All", name: t.menu.allDishes || "All Dishes" },
|
||||
@@ -88,12 +101,22 @@ export default function MenuPage() {
|
||||
|
||||
return { ...category, items };
|
||||
})
|
||||
.filter((category) => category.items.length > 0);
|
||||
}, [searchQuery, showVegetarianOnly, activeCategory, language, t]);
|
||||
.filter((category) => {
|
||||
if (activeCategory !== 'All' && category.id !== activeCategory) return false;
|
||||
if (searchQuery.trim() || showVegetarianOnly) {
|
||||
return category.items.length > 0;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}, [searchQuery, showVegetarianOnly, activeCategory, language, t, menuCategories]);
|
||||
|
||||
const handleCategorySelect = (id: string) => {
|
||||
setActiveCategory(id);
|
||||
window.scrollTo({ top: 220, behavior: "smooth" });
|
||||
const headerHeight = Number.parseInt(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--header-height'),
|
||||
10,
|
||||
) || 116;
|
||||
window.scrollTo({ top: headerHeight + 80, behavior: 'smooth' });
|
||||
};
|
||||
|
||||
const getItemDescription = (item: MenuItem) =>
|
||||
@@ -149,6 +172,40 @@ export default function MenuPage() {
|
||||
return () => window.removeEventListener("resize", updateMobile);
|
||||
}, []);
|
||||
|
||||
const updateCategoryScrollHints = useCallback(() => {
|
||||
const container = categoryListRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const threshold = 6;
|
||||
const { scrollTop, scrollLeft, scrollHeight, scrollWidth, clientHeight, clientWidth } =
|
||||
container;
|
||||
|
||||
setCategoryScrollHints({
|
||||
top: scrollTop > threshold,
|
||||
bottom: scrollTop + clientHeight < scrollHeight - threshold,
|
||||
left: scrollLeft > threshold,
|
||||
right: scrollLeft + clientWidth < scrollWidth - threshold,
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const container = categoryListRef.current;
|
||||
if (!container) return;
|
||||
|
||||
updateCategoryScrollHints();
|
||||
|
||||
const onScroll = () => updateCategoryScrollHints();
|
||||
container.addEventListener("scroll", onScroll, { passive: true });
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => updateCategoryScrollHints());
|
||||
resizeObserver.observe(container);
|
||||
|
||||
return () => {
|
||||
container.removeEventListener("scroll", onScroll);
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [updateCategoryScrollHints, sidebarCategories.length]);
|
||||
|
||||
useEffect(() => {
|
||||
const container = categoryListRef.current;
|
||||
if (!container) return;
|
||||
@@ -166,6 +223,32 @@ export default function MenuPage() {
|
||||
});
|
||||
}, [activeCategory]);
|
||||
|
||||
useEffect(() => {
|
||||
if (menuCategories.length <= prevCategoryCountRef.current) {
|
||||
prevCategoryCountRef.current = menuCategories.length;
|
||||
return;
|
||||
}
|
||||
|
||||
const newestCategory = menuCategories[menuCategories.length - 1];
|
||||
prevCategoryCountRef.current = menuCategories.length;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const container = categoryListRef.current;
|
||||
const newestButton = container?.querySelector<HTMLElement>(
|
||||
`[data-category-id="${newestCategory.id}"]`,
|
||||
);
|
||||
if (!newestButton) return;
|
||||
|
||||
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
newestButton.scrollIntoView({
|
||||
behavior: prefersReducedMotion ? "auto" : "smooth",
|
||||
block: "nearest",
|
||||
inline: "center",
|
||||
});
|
||||
updateCategoryScrollHints();
|
||||
});
|
||||
}, [menuCategories, updateCategoryScrollHints]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8F5F0] text-[#2C2A26]">
|
||||
<Navbar />
|
||||
@@ -182,17 +265,17 @@ export default function MenuPage() {
|
||||
|
||||
{/* BEAUTIFUL SIDEBAR */}
|
||||
<div className="lg:w-72 flex-shrink-0">
|
||||
<div className="sticky top-[116px]">
|
||||
<div className="mb-6">
|
||||
<div className="sticky top-[var(--header-height)] z-10 flex flex-col lg:max-h-[calc(100dvh-var(--header-height)-1.5rem)]">
|
||||
<div className="mb-4 shrink-0">
|
||||
<div className="text-xs tracking-[3px] text-[#c99a2e] mb-2">EXPLORE OUR</div>
|
||||
<h3 className="font-serif text-2xl sm:text-3xl tracking-tight">Signature Categories</h3>
|
||||
</div>
|
||||
|
||||
{/* Smooth scroll: horizontal on mobile, vertical on desktop when list exceeds viewport */}
|
||||
<div className="relative">
|
||||
<div className="relative min-h-0 lg:flex-1">
|
||||
<div
|
||||
ref={categoryListRef}
|
||||
className="menu-category-scroll flex gap-2 overflow-x-auto overflow-y-hidden scroll-smooth overscroll-x-contain overscroll-y-contain pb-3 pr-1 snap-x snap-mandatory scroll-px-1 [-webkit-overflow-scrolling:touch] lg:flex-col lg:overflow-x-hidden lg:overflow-y-auto lg:max-h-[calc(100dvh-13.5rem)] lg:snap-none lg:scroll-px-0 lg:pb-2 lg:pr-2"
|
||||
className="menu-category-scroll flex gap-2 overflow-x-auto overflow-y-hidden scroll-smooth overscroll-x-contain overscroll-y-contain pb-3 pe-4 ps-1 snap-x snap-mandatory scroll-px-1 [-webkit-overflow-scrolling:touch] lg:h-full lg:min-h-0 lg:flex-col lg:overflow-x-hidden lg:overflow-y-auto lg:snap-none lg:scroll-px-0 lg:pb-4 lg:pe-2 lg:ps-0"
|
||||
>
|
||||
{sidebarCategories.map((cat) => {
|
||||
const isActive = activeCategory === cat.id;
|
||||
@@ -223,29 +306,59 @@ export default function MenuPage() {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-y-0 right-0 w-6 bg-gradient-to-l from-[#F8F5F0] to-transparent lg:hidden"
|
||||
/>
|
||||
{categoryScrollHints.left && (
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-y-0 left-0 z-10 w-8 bg-gradient-to-r from-[#F8F5F0] via-[#F8F5F0]/80 to-transparent lg:hidden"
|
||||
/>
|
||||
)}
|
||||
{categoryScrollHints.right && (
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-y-0 right-0 z-10 w-8 bg-gradient-to-l from-[#F8F5F0] via-[#F8F5F0]/80 to-transparent lg:hidden"
|
||||
/>
|
||||
)}
|
||||
{categoryScrollHints.top && (
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-x-0 top-0 z-10 hidden h-7 bg-gradient-to-b from-[#F8F5F0] via-[#F8F5F0]/80 to-transparent lg:block"
|
||||
/>
|
||||
)}
|
||||
{categoryScrollHints.bottom && (
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-x-0 bottom-0 z-10 hidden h-10 bg-gradient-to-t from-[#F8F5F0] via-[#F8F5F0]/90 to-transparent lg:block"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{categoryScrollHints.bottom && (
|
||||
<p className="mt-2 hidden text-center text-[10px] font-medium tracking-wide text-[#8A8478] lg:block">
|
||||
Scroll for more categories
|
||||
</p>
|
||||
)}
|
||||
{categoryScrollHints.right && (
|
||||
<p className="mt-2 text-center text-[10px] font-medium tracking-wide text-[#8A8478] lg:hidden">
|
||||
Swipe for more categories
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* MAIN CONTENT */}
|
||||
<div className="flex-1 min-w-0">
|
||||
{/* Search + Vegetarian Filter */}
|
||||
<div className="mb-8 flex flex-col md:flex-row gap-4 items-center">
|
||||
<div className="mb-8 flex flex-col gap-3 sm:flex-row sm:items-center sm:gap-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t.menu.searchPlaceholder}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full md:w-80 rounded-2xl border border-[#e5e1d7] bg-white px-5 py-3 text-sm placeholder:text-[#8A8478] focus:border-[#c99a2e] focus:ring-2 focus:ring-[#c99a2e]/20 transition-all"
|
||||
className="w-full sm:max-w-80 rounded-2xl border border-[#e5e1d7] bg-white px-5 py-3 text-base sm:text-sm placeholder:text-[#8A8478] focus:border-[#c99a2e] focus:ring-2 focus:ring-[#c99a2e]/20 transition-all"
|
||||
/>
|
||||
|
||||
<button
|
||||
onClick={() => setShowVegetarianOnly(!showVegetarianOnly)}
|
||||
className={`px-6 py-3 rounded-2xl text-sm font-medium border transition-all active:scale-[0.985] ${
|
||||
className={`w-full sm:w-auto px-6 py-3.5 rounded-2xl text-sm font-medium border transition-all active:scale-[0.985] touch-manipulation ${
|
||||
showVegetarianOnly
|
||||
? "bg-[#0f5a4a] text-white border-[#0f5a4a]"
|
||||
: "border-[#e5e1d7] hover:border-[#c99a2e] text-[#101724] bg-white"
|
||||
@@ -286,28 +399,26 @@ export default function MenuPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{category.items.length === 0 ? (
|
||||
<p className="rounded-2xl border border-dashed border-[#EDE6D9] bg-white/60 px-6 py-10 text-center text-sm text-[#8A8478]">
|
||||
Dishes coming soon to this category.
|
||||
</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{category.items.map((item) => {
|
||||
const usesDetailModal = hasDishDetailModal(item.id, category.id);
|
||||
const inclusionNote = getMenuItemInclusionNote(language, category.id);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={item.id}
|
||||
data-id={item.id}
|
||||
className={`menu-card group bg-white border border-[#EDE6D9] rounded-2xl overflow-hidden flex flex-col hover:border-[#c99a2e]/40 active:border-[#c99a2e] active:scale-[0.985] transition-all duration-150 touch-manipulation ${
|
||||
usesDetailModal ? "" : "cursor-pointer"
|
||||
}`}
|
||||
onClick={
|
||||
usesDetailModal
|
||||
? undefined
|
||||
: () => addToCart(buildCartLineFromMenuItem(item))
|
||||
}
|
||||
className="menu-card group bg-white border border-[#EDE6D9] rounded-2xl overflow-hidden flex flex-col hover:border-[#c99a2e]/40 transition-all duration-150 touch-manipulation"
|
||||
whileHover={!isMobile ? {
|
||||
y: -4,
|
||||
boxShadow: "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)",
|
||||
transition: { type: "spring", stiffness: 300, damping: 20 }
|
||||
} : {}}
|
||||
whileTap={{ scale: 0.985 }}
|
||||
>
|
||||
{/* Media - Always poster for performance. Advanced desktop hover effect (scale + lift + glow) + subtle sound. Mobile uses simple scale. */}
|
||||
<div
|
||||
@@ -358,10 +469,17 @@ export default function MenuPage() {
|
||||
</div>
|
||||
|
||||
<div className="p-6 flex flex-col flex-1">
|
||||
<div className="flex justify-between items-start gap-3 mb-4 min-w-0">
|
||||
<h3 className="min-w-0 flex-1 text-lg sm:text-[22px] leading-tight tracking-[-0.4px] text-[#2C2A26] break-words">
|
||||
{getItemName(item)}
|
||||
</h3>
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:justify-between sm:items-start mb-4 min-w-0">
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="text-lg sm:text-[22px] leading-tight tracking-[-0.4px] text-[#2C2A26] break-words">
|
||||
{getItemName(item)}
|
||||
</h3>
|
||||
{inclusionNote && (
|
||||
<p className="mt-1 text-xs font-medium text-[#8f6b22]">
|
||||
{inclusionNote}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="shrink-0">{renderMenuPrice(item)}</div>
|
||||
</div>
|
||||
|
||||
@@ -392,10 +510,7 @@ export default function MenuPage() {
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
addToCart(buildCartLineFromMenuItem(item));
|
||||
}}
|
||||
onClick={() => addToCart(buildCartLineFromMenuItem(item))}
|
||||
className="w-full py-3.5 text-sm tracking-[0.6px] border border-[#B38B4D] text-[#B38B4D] rounded-full hover:bg-[#B38B4D] hover:text-white active:bg-[#8C6B3A] active:text-white active:scale-[0.985] font-medium transition-all touch-manipulation"
|
||||
>
|
||||
{t.wishlistDrawer.addToCart}
|
||||
@@ -406,6 +521,7 @@ export default function MenuPage() {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user