feat: add 3s scroll-pause video auto-play to homepage Signature Menu + update Menu page to 3s

- Duplicated mobile scroll-pause logic (3s timeout) to homepage Signature Menu (using .signature-card + .relative.h-48 structure, with video support added to signatureDishes).
- Updated both /menu and homepage Signature Menu from 2s to 3s pause before playing video on scroll stop.
- Added onMouse handlers for desktop hover consistency on signature cards.
- Maintains posters while scrolling, plays video after 3s pause on item.
- Desktop hover behavior preserved.
- Builds cleanly.
This commit is contained in:
Zeeshan Khan
2026-06-02 15:46:00 +02:00
parent ffaef59049
commit bca3d83bb4
2 changed files with 201 additions and 22 deletions
+2 -2
View File
@@ -68,7 +68,7 @@ export default function MenuPage() {
window.scrollTo({ top: 220, behavior: "smooth" });
};
// Mobile video auto-play on scroll pause (2s)
// Mobile video auto-play on scroll pause (3s)
useEffect(() => {
const updateMobile = () => setIsMobile(window.innerWidth < 768);
updateMobile();
@@ -149,7 +149,7 @@ export default function MenuPage() {
if (focusedId) {
playMobileVideo(focusedId);
}
}, 2000);
}, 3000);
};
window.addEventListener("scroll", onScrollOrTouch, { passive: true });
window.addEventListener("touchmove", onScrollOrTouch, { passive: true });
+199 -20
View File
@@ -25,6 +25,67 @@ export default function ShahiKitchenHomepage() {
const t = getTranslation(language);
const [menuFilter, setMenuFilter] = useState<"All" | "Rice" | "Curry" | "Meat" | "Street" | "Roll" | "Sweet">("All");
const [isMobile, setIsMobile] = useState(false);
// Mobile video helpers for Signature Menu (duplicated logic, 3s pause)
const playMobileVideoSignature = (id: string) => {
document.querySelectorAll<HTMLElement>(".signature-card").forEach((card) => {
if (card.dataset.id !== id) return;
const media = card.querySelector<HTMLElement>(".relative.h-48");
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 pauseAllMobileVideosSignature = () => {
document.querySelectorAll<HTMLElement>(".signature-card").forEach((card) => {
const media = card.querySelector<HTMLElement>(".relative.h-48");
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 findFocusedVideoItemSignature = (): string | null => {
const cards = document.querySelectorAll<HTMLElement>('.signature-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;
};
// Lenis smooth scroll
useEffect(() => {
@@ -38,6 +99,47 @@ export default function ShahiKitchenHomepage() {
return () => lenis.destroy();
}, []);
// Mobile detection for signature menu video logic
useEffect(() => {
const updateMobile = () => setIsMobile(window.innerWidth < 768);
updateMobile();
window.addEventListener("resize", updateMobile);
return () => window.removeEventListener("resize", updateMobile);
}, []);
useEffect(() => {
if (!isMobile) {
pauseAllMobileVideosSignature();
return;
}
let scrollTimeout: ReturnType<typeof setTimeout> | null = null;
const onScrollOrTouch = () => {
pauseAllMobileVideosSignature();
if (scrollTimeout) clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(() => {
const focusedId = findFocusedVideoItemSignature();
if (focusedId) {
playMobileVideoSignature(focusedId);
}
}, 3000);
};
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 = findFocusedVideoItemSignature();
if (firstFocused) {
playMobileVideoSignature(firstFocused);
}
}, 1500);
return () => {
window.removeEventListener("scroll", onScrollOrTouch);
window.removeEventListener("touchmove", onScrollOrTouch);
if (scrollTimeout) clearTimeout(scrollTimeout);
clearTimeout(initialTimeout);
};
}, [isMobile]);
const signatureDishes = [
{
id: "chicken-biryani",
@@ -46,7 +148,8 @@ export default function ShahiKitchenHomepage() {
price: 149,
time: "32 min",
desc: "Fragrant aged basmati layered with spiced chicken, saffron & caramelized onions",
image: "chicken-biryani-poster.jpg"
image: "chicken-biryani-poster.jpg",
video: "chicken-biryani.mp4"
},
{
id: "bong-nihari",
@@ -55,7 +158,8 @@ export default function ShahiKitchenHomepage() {
price: 199,
time: "45 min",
desc: "Slow-cooked beef shank in rich aromatic gravy with ginger, lemon & fresh naan",
image: "bong-nihari-poster.jpg"
image: "bong-nihari-poster.jpg",
video: "bong-nihari.mp4"
},
{
id: "panipuri",
@@ -64,7 +168,8 @@ export default function ShahiKitchenHomepage() {
price: 69,
time: "15 min",
desc: "Crispy hollow puris filled with spiced chickpeas, potatoes & tangy tamarind water",
image: "panipuri-poster.jpg"
image: "panipuri-poster.jpg",
video: "panipuri.mp4"
},
{
id: "lamm-palak",
@@ -73,7 +178,8 @@ export default function ShahiKitchenHomepage() {
price: 179,
time: "30 min",
desc: "Tender lamb cooked with fresh spinach in a flavorful mild gravy",
image: "lamm-palak-poster.jpg"
image: "lamm-palak-poster.jpg",
video: "lamm-palak.mp4"
},
{
id: "chicken-karahi",
@@ -82,7 +188,8 @@ export default function ShahiKitchenHomepage() {
price: 149,
time: "28 min",
desc: "Wok-tossed chicken in a robust tomato, chili & ginger gravy",
image: "chicken-karahi-poster.jpg"
image: "chicken-karahi-poster.jpg",
video: "chicken-karahi.mp4"
},
{
id: "chicken-haleem",
@@ -91,7 +198,8 @@ export default function ShahiKitchenHomepage() {
price: 149,
time: "35 min",
desc: "Slow-cooked shredded chicken with lentils, wheat & aromatic spices",
image: "chicken-haleem-poster.jpg"
image: "chicken-haleem-poster.jpg",
video: "chicken-haleem.mp4"
},
{
id: "tikka-boti-roll",
@@ -100,7 +208,8 @@ export default function ShahiKitchenHomepage() {
price: 99,
time: "20 min",
desc: "Juicy chicken tikka wrapped in soft naan with mint chutney & onions",
image: "tikka-boti-roll-poster.jpg"
image: "tikka-boti-roll-poster.jpg",
video: "tikka-boti-roll.mp4"
},
{
id: "jalebi",
@@ -109,7 +218,8 @@ export default function ShahiKitchenHomepage() {
price: 119,
time: "12 min",
desc: "Crispy golden saffron spirals soaked in fragrant sugar syrup",
image: "jalebi-poster.jpg"
image: "jalebi-poster.jpg",
video: "jalebi.mp4"
},
{
id: "kulfi",
@@ -118,7 +228,8 @@ export default function ShahiKitchenHomepage() {
price: 39,
time: "10 min",
desc: "Traditional frozen milk dessert with cardamom, pistachio & saffron",
image: "kulfi-poster.jpg"
image: "kulfi-poster.jpg",
video: "kulfi.mp4"
},
];
@@ -252,21 +363,89 @@ export default function ShahiKitchenHomepage() {
{filtered.map((dish, index) => (
<motion.div
key={dish.id}
data-id={dish.id}
data-has-video={!!dish.video}
layout
onClick={() => addDish(dish)}
className="signature-card group flex cursor-pointer flex-col overflow-hidden rounded-[2rem] border border-[#EDE6D9] bg-white active:scale-[0.985] active:border-[#c99a2e]/60 transition-transform touch-manipulation"
>
<div className="relative h-48 overflow-hidden bg-[#F2EDE4]">
<img
src={`/images/dishes/${dish.image}`}
alt={dish.name}
className="signature-image absolute inset-0 w-full h-full object-cover transition-transform duration-700"
/>
<div className="absolute inset-0 bg-gradient-to-b from-black/5 via-transparent to-black/25" />
<div className="absolute top-4 right-4 px-4 py-1 rounded-full bg-white/95 text-[#B38B4D] text-sm font-medium tracking-tight shadow-sm border border-[#EDE6D9]">
{dish.price} kr
</div>
<div
className="relative h-48 overflow-hidden bg-[#F2EDE4]"
onMouseEnter={dish.video ? (e) => {
const video = e.currentTarget.querySelector("video");
if (video) video.play().catch(() => {});
} : undefined}
onMouseLeave={dish.video ? (e) => {
const video = e.currentTarget.querySelector("video");
if (video) {
video.pause();
video.currentTime = 0;
}
} : undefined}
>
{dish.video ? (
<>
{/* Static Poster (first frame) */}
<img
src={`/images/dishes/${dish.image}`}
alt={dish.name}
className="signature-image absolute inset-0 w-full h-full object-cover transition-opacity duration-150"
loading="lazy"
onError={(e) => {
const base = (dish.video || dish.image || "").replace(".mp4", "").replace(/-optimized/g, "").replace(/-poster/g, "");
const fallbacks = [
`/images/dishes/${base}-poster.jpg`,
`/images/dishes/${base}.jpg`,
].filter(Boolean);
let i = 0;
const tryNext = () => {
if (i < fallbacks.length) {
(e.target as HTMLImageElement).src = fallbacks[i++];
}
};
tryNext();
}}
/>
{/* Video for mobile scroll-pause and desktop hover */}
<video
muted
loop
playsInline
preload="metadata"
className="absolute inset-0 w-full h-full object-cover opacity-0 group-hover:opacity-100 transition-opacity duration-150"
>
<source
src={`/videos/${dish.video.replace(".mp4", "-optimized.webm")}`}
type="video/webm"
/>
<source
src={`/videos/${dish.video.replace(".mp4", "-optimized.mp4")}`}
type="video/mp4"
/>
<source
src={`/videos/${dish.video.replace(".mp4", ".webm")}`}
type="video/webm"
/>
<source src={`/videos/${dish.video}`} type="video/mp4" />
</video>
<div className="absolute inset-0 bg-gradient-to-b from-black/5 via-transparent to-black/25 group-hover:opacity-0 transition-opacity" />
<div className="absolute top-4 right-4 px-4 py-1 rounded-full bg-white/95 text-[#B38B4D] text-sm font-medium tracking-tight shadow-sm border border-[#EDE6D9]">
{dish.price} kr
</div>
</>
) : (
<>
<img
src={`/images/dishes/${dish.image}`}
alt={dish.name}
className="signature-image absolute inset-0 w-full h-full object-cover transition-transform duration-700"
/>
<div className="absolute inset-0 bg-gradient-to-b from-black/5 via-transparent to-black/25" />
<div className="absolute top-4 right-4 px-4 py-1 rounded-full bg-white/95 text-[#B38B4D] text-sm font-medium tracking-tight shadow-sm border border-[#EDE6D9]">
{dish.price} kr
</div>
</>
)}
</div>
<div className="p-5 flex-1 flex flex-col">