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:
@@ -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
Reference in New Issue
Block a user