feat: mobile menu video auto-play on scroll pause (2s)

- On mobile (<768px): posters shown while scrolling the menu.
- After user stops scrolling and lingers on a video item for ~2s: the animation (video) plays in place of the poster (replaces img opacity, plays video, hides gradient).
- On scroll/touchmove resume: immediately pause videos and show posters.
- Initial load on mobile: auto-plays the first focused video item after settle.
- Added isMobile state + resize listener, scroll/touch listeners with 2s debounce timeout.
- Helper functions: playMobileVideo(id), pauseAllMobileVideos(), findFocusedVideoItem() (visibility + center scoring).
- Data attributes data-id and data-has-video added to .menu-card for reliable targeting.
- Desktop hover logic untouched.
- Only affects items with .video in menu-data (e.g. Lahore Chana now works on mobile too).
- Cleanup on unmount/resize to desktop.
- Builds cleanly.
This commit is contained in:
Zeeshan Khan
2026-06-02 15:24:47 +02:00
parent f21c5fd83c
commit ffaef59049
2 changed files with 103 additions and 1 deletions
+103
View File
@@ -20,6 +20,7 @@ export default function MenuPage() {
const [activeCategory, setActiveCategory] = useState("All"); const [activeCategory, setActiveCategory] = useState("All");
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [showVegetarianOnly, setShowVegetarianOnly] = useState(false); const [showVegetarianOnly, setShowVegetarianOnly] = useState(false);
const [isMobile, setIsMobile] = useState(false);
const { addToCart } = useCart(); const { addToCart } = useCart();
const { language } = useLanguage(); const { language } = useLanguage();
@@ -67,6 +68,106 @@ export default function MenuPage() {
window.scrollTo({ top: 220, behavior: "smooth" }); window.scrollTo({ top: 220, behavior: "smooth" });
}; };
// Mobile video auto-play on scroll pause (2s)
useEffect(() => {
const updateMobile = () => setIsMobile(window.innerWidth < 768);
updateMobile();
window.addEventListener("resize", updateMobile);
return () => window.removeEventListener("resize", updateMobile);
}, []);
const playMobileVideo = (id: string) => {
document.querySelectorAll<HTMLElement>(".menu-card").forEach((card) => {
if (card.dataset.id !== id) return;
const media = card.querySelector<HTMLElement>(".relative.h-52");
if (!media) return;
const img = media.querySelector<HTMLImageElement>("img");
const video = media.querySelector<HTMLVideoElement>("video");
const gradient = media.querySelector<HTMLElement>(".absolute.inset-0.bg-gradient-to-b");
if (img) img.style.opacity = "0";
if (video) {
video.style.opacity = "1";
video.play().catch(() => {});
}
if (gradient) gradient.style.opacity = "0";
});
};
const pauseAllMobileVideos = () => {
document.querySelectorAll<HTMLElement>(".menu-card").forEach((card) => {
const media = card.querySelector<HTMLElement>(".relative.h-52");
if (!media) return;
const img = media.querySelector<HTMLImageElement>("img");
const video = media.querySelector<HTMLVideoElement>("video");
const gradient = media.querySelector<HTMLElement>(".absolute.inset-0.bg-gradient-to-b");
if (img) img.style.opacity = "";
if (video) {
video.style.opacity = "0";
video.pause();
video.currentTime = 0;
}
if (gradient) gradient.style.opacity = "";
});
};
const findFocusedVideoItem = (): string | null => {
const cards = document.querySelectorAll<HTMLElement>('.menu-card[data-has-video="true"]');
let bestId: string | null = null;
let bestScore = 0;
const vh = window.innerHeight;
const viewportCenter = vh / 2;
cards.forEach((card) => {
const rect = card.getBoundingClientRect();
if (rect.bottom <= 0 || rect.top >= vh) return;
const visibleTop = Math.max(rect.top, 0);
const visibleBottom = Math.min(rect.bottom, vh);
const visibleHeight = visibleBottom - visibleTop;
const intersectionRatio = visibleHeight / rect.height;
const cardCenter = rect.top + rect.height / 2;
const distFromCenter = Math.abs(cardCenter - viewportCenter);
const centerScore = Math.max(0, 1 - distFromCenter / (vh / 2));
const score = intersectionRatio * 0.6 + centerScore * 0.4;
if (score > bestScore && score > 0.15) {
bestScore = score;
bestId = card.dataset.id || null;
}
});
return bestId;
};
useEffect(() => {
if (!isMobile) {
pauseAllMobileVideos();
return;
}
let scrollTimeout: ReturnType<typeof setTimeout> | null = null;
const onScrollOrTouch = () => {
pauseAllMobileVideos();
if (scrollTimeout) clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(() => {
const focusedId = findFocusedVideoItem();
if (focusedId) {
playMobileVideo(focusedId);
}
}, 2000);
};
window.addEventListener("scroll", onScrollOrTouch, { passive: true });
window.addEventListener("touchmove", onScrollOrTouch, { passive: true });
// Initial play after load on mobile (if not scrolling)
const initialTimeout = setTimeout(() => {
const firstFocused = findFocusedVideoItem();
if (firstFocused) {
playMobileVideo(firstFocused);
}
}, 1500);
return () => {
window.removeEventListener("scroll", onScrollOrTouch);
window.removeEventListener("touchmove", onScrollOrTouch);
if (scrollTimeout) clearTimeout(scrollTimeout);
clearTimeout(initialTimeout);
};
}, [isMobile]);
return ( return (
<div className="min-h-screen bg-[#F8F5F0] text-[#2C2A26]"> <div className="min-h-screen bg-[#F8F5F0] text-[#2C2A26]">
<Navbar /> <Navbar />
@@ -184,6 +285,8 @@ export default function MenuPage() {
{category.items.map((item) => ( {category.items.map((item) => (
<div <div
key={item.id} key={item.id}
data-id={item.id}
data-has-video={!!item.video}
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 cursor-pointer touch-manipulation" 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 cursor-pointer touch-manipulation"
onClick={() => addToCart({ id: item.id, name: item.name, price: item.price, image: item.image })} onClick={() => addToCart({ id: item.id, name: item.name, price: item.price, image: item.image })}
> >
File diff suppressed because one or more lines are too long