implement cinematic 3D entrance experience and initialize core app layout components
This commit is contained in:
parent
d2ec0e574d
commit
25064ad8f6
Binary file not shown.
|
Before Width: | Height: | Size: 252 KiB After Width: | Height: | Size: 7.2 KiB |
@ -326,13 +326,13 @@ export default function ContactPage() {
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{[
|
||||
{ day: 'Friday', time: '5:00 – 10:00 pm' },
|
||||
{ day: 'Saturday', time: '12:00 – 10:00 pm' },
|
||||
{ day: 'Sunday', time: '12:00 – 9:00 pm' },
|
||||
{ day: 'Monday', time: '4:00 – 10:00 pm' },
|
||||
{ day: 'Tuesday', time: 'Closed', closed: true },
|
||||
{ day: 'Wednesday', time: '4:30 – 10:00 pm' },
|
||||
{ day: 'Thursday', time: '4:00 – 10:00 pm' },
|
||||
{ day: 'Friday', time: '5:00 – 10:00 pm' },
|
||||
{ day: 'Saturday', time: '12:00 – 10:00 pm' },
|
||||
{ day: 'Sunday', time: '12:00 – 9:00 pm' },
|
||||
].map(({ day, time, closed }) => (
|
||||
<div key={day} className="flex items-center justify-between py-2.5 border-b border-[#bc9e65]/10 last:border-0">
|
||||
<span className="font-sans text-sm font-semibold text-[#e8dcb5] tracking-wide">{day}</span>
|
||||
|
||||
@ -118,3 +118,12 @@ body {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Scrollbar hiding utility */
|
||||
.scrollbar-none::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.scrollbar-none {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
@ -86,11 +86,19 @@ function CinematicFlight({ progress }: { progress: number }) {
|
||||
|
||||
const { width, height } = useThree((state) => state.viewport);
|
||||
|
||||
// Compute baseScale to prevent clipping of "MY DOSA PLACE" sign on portrait/mobile viewports
|
||||
const isPortrait = width < height;
|
||||
const baseScale = 1.0; // Keep full height, no empty spaces at top and bottom
|
||||
|
||||
// Compute facade dimensions based on dusk photo aspect ratio
|
||||
const aspect = 1920 / 864;
|
||||
// Use dynamically compressed aspect ratio on mobile to fit the sign width responsive to screen width
|
||||
const aspect = isPortrait ? Math.max(1.7, Math.min(1.95, (width / height) * 3.8)) : 1920 / 864;
|
||||
const planeH = height;
|
||||
const planeW = planeH * aspect;
|
||||
|
||||
// Compute horizontal offset to shift image to left on mobile so text is perfectly centered/visible
|
||||
const offsetX = isPortrait ? -planeW * 0.025 : 0;
|
||||
|
||||
// Door dimensions matching facade coordinate percentages
|
||||
const dw = 0.04 * planeW;
|
||||
const dh = 0.42 * planeH;
|
||||
@ -112,14 +120,18 @@ function CinematicFlight({ progress }: { progress: number }) {
|
||||
// Smooth frame loop camera zoom and door swing animations
|
||||
useFrame(() => {
|
||||
// 1. Storefront zoom and centering translation
|
||||
// Scale goes from 1.0 to 5.5
|
||||
const targetExtScale = 1.0 + progress * 4.5;
|
||||
// Scale goes from baseScale to 6.5
|
||||
const targetExtScale = baseScale + progress * (6.5 - baseScale);
|
||||
if (extGroupRef.current) {
|
||||
extGroupRef.current.scale.setScalar(THREE.MathUtils.lerp(extGroupRef.current.scale.x, targetExtScale, 0.08));
|
||||
// Pan Y upward to center the doors as we scale
|
||||
const currentScale = extGroupRef.current.scale.x;
|
||||
const targetY = -doorY * (currentScale - 1.0);
|
||||
const targetY = -doorY * (currentScale - baseScale);
|
||||
extGroupRef.current.position.y = THREE.MathUtils.lerp(extGroupRef.current.position.y, targetY, 0.08);
|
||||
|
||||
// Interpolate horizontal offset from offsetX to 0 as progress goes from 0 to 1
|
||||
const targetX = offsetX * (1.0 - progress);
|
||||
extGroupRef.current.position.x = THREE.MathUtils.lerp(extGroupRef.current.position.x, targetX, 0.08);
|
||||
}
|
||||
|
||||
// 2. Door opening rotation (swing inward Y-axis)
|
||||
@ -206,7 +218,7 @@ function CinematicFlight({ progress }: { progress: number }) {
|
||||
<directionalLight position={[0, 5, 5]} intensity={1.5} />
|
||||
|
||||
{/* Plane 1: Storefront Facade & 3D Swinging Door Panels (Z = 0.02) */}
|
||||
<group ref={extGroupRef} position={[0, 0, 0.02]}>
|
||||
<group ref={extGroupRef} position={[offsetX, 0, 0.02]} scale={baseScale}>
|
||||
<mesh>
|
||||
<planeGeometry args={[planeW, planeH]} />
|
||||
<shaderMaterial
|
||||
@ -393,7 +405,7 @@ export default function EntranceExperience({ onProgress }: { onProgress?: (progr
|
||||
<div className="w-24 h-[1px] bg-gradient-to-r from-transparent via-[#d8c396] to-transparent mx-auto mb-3" />
|
||||
|
||||
<p className="font-sans text-xs sm:text-sm md:text-base text-stone-800 leading-relaxed tracking-wide font-medium text-center">
|
||||
A royal banquet served on fresh banana leaves—crisp ghee dosas, slow-simmered sambar, and chutneys hand-ground fresh every morning. 100% pure vegetarian.
|
||||
A royal banquet of crisp ghee dosas, slow-simmered sambar, and chutneys hand-ground fresh every morning. 100% pure vegetarian.
|
||||
</p>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
@ -156,21 +156,6 @@ export default function Footer() {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 font-sans text-sm text-stone-300 tracking-wider">
|
||||
{/* Friday */}
|
||||
<div className="flex justify-between items-center select-text">
|
||||
<span className="text-[#bc9e65] font-semibold uppercase tracking-widest">Friday</span>
|
||||
<span>5–10 pm</span>
|
||||
</div>
|
||||
{/* Saturday */}
|
||||
<div className="flex justify-between items-center select-text">
|
||||
<span className="text-[#bc9e65] font-semibold uppercase tracking-widest">Saturday</span>
|
||||
<span>12–10 pm</span>
|
||||
</div>
|
||||
{/* Sunday */}
|
||||
<div className="flex justify-between items-center select-text">
|
||||
<span className="text-[#bc9e65] font-semibold uppercase tracking-widest">Sunday</span>
|
||||
<span>12–9 pm</span>
|
||||
</div>
|
||||
{/* Monday */}
|
||||
<div className="flex justify-between items-center select-text">
|
||||
<span className="text-[#bc9e65] font-semibold uppercase tracking-widest">Monday</span>
|
||||
@ -197,6 +182,21 @@ export default function Footer() {
|
||||
<span className="text-[#bc9e65] font-semibold uppercase tracking-widest">Thursday</span>
|
||||
<span>4–10 pm</span>
|
||||
</div>
|
||||
{/* Friday */}
|
||||
<div className="flex justify-between items-center select-text">
|
||||
<span className="text-[#bc9e65] font-semibold uppercase tracking-widest">Friday</span>
|
||||
<span>5–10 pm</span>
|
||||
</div>
|
||||
{/* Saturday */}
|
||||
<div className="flex justify-between items-center select-text">
|
||||
<span className="text-[#bc9e65] font-semibold uppercase tracking-widest">Saturday</span>
|
||||
<span>12–10 pm</span>
|
||||
</div>
|
||||
{/* Sunday */}
|
||||
<div className="flex justify-between items-center select-text">
|
||||
<span className="text-[#bc9e65] font-semibold uppercase tracking-widest">Sunday</span>
|
||||
<span>12–9 pm</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import Image from "next/image";
|
||||
import { useState } from "react";
|
||||
import AnimatedSection from "@/components/AnimatedSection";
|
||||
@ -135,6 +135,7 @@ const MENU_ITEMS = [
|
||||
|
||||
export default function MenuPageContent() {
|
||||
const [activeTab, setActiveTab] = useState("classics");
|
||||
const [isPopupOpen, setIsPopupOpen] = useState(false);
|
||||
|
||||
// Map tab ids → item tag values
|
||||
const TAB_TAG_MAP: Record<string, string> = {
|
||||
@ -165,24 +166,26 @@ export default function MenuPageContent() {
|
||||
<div className="absolute inset-0 opacity-10 pointer-events-none" style={{ backgroundImage: 'url("/images/home/menu/our-menu-bg.webp")', backgroundSize: 'cover', backgroundPosition: 'center top' }}></div>
|
||||
</div>
|
||||
|
||||
{/* Section Title */}
|
||||
<div className="flex flex-col items-center mb-10">
|
||||
<div className="flex items-center gap-6">
|
||||
<span className="text-[#bc9e65]">❧</span>
|
||||
<h2 className="font-serif text-3xl font-bold text-[#0f3f26] uppercase tracking-widest">MY DOSA SPECIALTIES</h2>
|
||||
<span className="text-[#bc9e65] scale-x-[-1]">❧</span>
|
||||
{/* Section Title */}
|
||||
<div className="flex flex-col items-center mb-10 w-full px-4">
|
||||
<div className="flex items-center gap-4 justify-center flex-wrap">
|
||||
<span className="text-[#bc9e65] hidden sm:inline-block">❧</span>
|
||||
<h2 className="font-serif text-2xl sm:text-3xl font-bold text-[#0f3f26] uppercase tracking-widest text-center leading-tight">MY DOSA SPECIALTIES</h2>
|
||||
<span className="text-[#bc9e65] scale-x-[-1] hidden sm:inline-block">❧</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 mt-3 text-sm font-semibold tracking-wide text-stone-600">
|
||||
<span className="w-8 h-[1px] bg-[#bc9e65]/60"></span>
|
||||
<div className="flex items-center gap-3 mt-3 text-xs sm:text-sm font-semibold tracking-wide text-stone-600 justify-center">
|
||||
<span className="w-6 sm:w-8 h-[1px] bg-[#bc9e65]/60"></span>
|
||||
Crispy <span className="text-[#bc9e65] text-xs">●</span> Flavorful <span className="text-[#bc9e65] text-xs">●</span> Authentic
|
||||
<span className="w-8 h-[1px] bg-[#bc9e65]/60"></span>
|
||||
<span className="w-6 sm:w-8 h-[1px] bg-[#bc9e65]/60"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<AnimatedSection variant="fadeUp" delay={0}>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mb-16 relative z-10">
|
||||
<div className="flex flex-wrap items-center gap-2 justify-center">
|
||||
|
||||
{/* Desktop View (wrapped categories) */}
|
||||
<div className="hidden md:flex flex-wrap items-center gap-2 justify-center">
|
||||
{TABS.map((tab) => {
|
||||
const isActive = activeTab === tab.id;
|
||||
const isAllItems = tab.id === "all";
|
||||
@ -216,7 +219,7 @@ export default function MenuPageContent() {
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`relative flex items-center gap-3 px-2 whitespace-nowrap transition-all duration-300 font-serif font-bold text-[11px] uppercase tracking-wider rounded-[20px]
|
||||
className={`relative flex items-center gap-3 px-2.5 whitespace-nowrap transition-all duration-300 font-serif font-bold text-[11px] uppercase tracking-wider rounded-[20px]
|
||||
${isActive
|
||||
? "bg-[#0a4a2b] text-white border-[3px] border-[#bc9e65] py-2 shadow-[0_4px_15px_rgba(188,158,101,0.3)] z-10"
|
||||
: "bg-[#fdfcf9] text-[#0f3f26] border border-[#eaddc4] py-2.5 hover:bg-white hover:shadow-md"
|
||||
@ -235,6 +238,45 @@ export default function MenuPageContent() {
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Mobile View: Single Category Pill + Distinct 'VIEW ALL' Trigger */}
|
||||
<div className="md:hidden flex justify-center w-full px-4">
|
||||
<div className="flex items-center justify-between w-full max-w-[310px] bg-white border-2 border-[#bc9e65] rounded-full pl-5 pr-2 py-1.5 shadow-lg">
|
||||
{/* Left: Selected Category Details */}
|
||||
<div className="flex items-center gap-2.5">
|
||||
{activeTab === "all" ? (
|
||||
<div className="text-[#0a4a2b]">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="3" y="3" width="7" height="7" rx="1.5" />
|
||||
<rect x="14" y="3" width="7" height="7" rx="1.5" />
|
||||
<rect x="14" y="14" width="7" height="7" rx="1.5" />
|
||||
<rect x="3" y="14" width="7" height="7" rx="1.5" />
|
||||
</svg>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-6 h-6 flex items-center justify-center">
|
||||
<Image src="/images/menu/tab-icon.png" alt="" width={22} height={22} className="object-contain" />
|
||||
</div>
|
||||
)}
|
||||
<span className="font-serif font-bold text-xs tracking-wider text-[#0f3f26] uppercase">
|
||||
{TABS.find((t) => t.id === activeTab)?.label}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Right: "VIEW ALL" Trigger Button */}
|
||||
<button
|
||||
onClick={() => setIsPopupOpen(true)}
|
||||
className="flex items-center gap-1 bg-[#0a4a2b] text-white border border-[#bc9e65] rounded-full px-3.5 py-2 text-[9px] font-serif font-bold tracking-widest hover:bg-[#073820] active:scale-95 transition-all shadow-sm"
|
||||
>
|
||||
<span>VIEW ALL</span>
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="12" y1="5" x2="12" y2="19" />
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</AnimatedSection>
|
||||
|
||||
@ -312,6 +354,89 @@ export default function MenuPageContent() {
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Categories Choice Popup Modal for Mobile View */}
|
||||
<AnimatePresence>
|
||||
{isPopupOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/75 backdrop-blur-md p-4"
|
||||
onClick={() => setIsPopupOpen(false)}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ scale: 0.9, y: 20, opacity: 0 }}
|
||||
animate={{ scale: 1, y: 0, opacity: 1 }}
|
||||
exit={{ scale: 0.9, y: 20, opacity: 0 }}
|
||||
transition={{ type: "spring", duration: 0.45 }}
|
||||
className="relative w-full max-w-sm bg-[#faf7f2] border-2 border-double border-[#bc9e65] rounded-3xl p-6 shadow-2xl overflow-hidden"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Gold decorative corners inside */}
|
||||
<div className="absolute top-2 left-2 right-2 bottom-2 border border-[#bc9e65]/15 pointer-events-none rounded-[22px]" />
|
||||
|
||||
{/* Close Button */}
|
||||
<button
|
||||
onClick={() => setIsPopupOpen(false)}
|
||||
className="absolute top-4 right-4 text-stone-500 hover:text-[#0f3f26] p-2 transition-colors z-30"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Title */}
|
||||
<div className="flex flex-col items-center mb-5 mt-2">
|
||||
<h4 className="font-serif text-base font-bold text-[#0f3f26] uppercase tracking-widest">Select Category</h4>
|
||||
<div className="w-16 h-[1px] bg-[#bc9e65] mt-1.5" />
|
||||
</div>
|
||||
|
||||
{/* Grid of Categories */}
|
||||
<div className="grid grid-cols-2 gap-2.5 max-h-[60vh] overflow-y-auto scrollbar-none pr-1">
|
||||
{TABS.map((tab) => {
|
||||
const isActive = activeTab === tab.id;
|
||||
const isAllItems = tab.id === "all";
|
||||
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => {
|
||||
setActiveTab(tab.id);
|
||||
setIsPopupOpen(false);
|
||||
}}
|
||||
className={`flex items-center gap-2.5 px-3 py-2.5 rounded-xl transition-all duration-300 font-serif font-bold text-[10px] uppercase tracking-wider text-left border
|
||||
${isActive
|
||||
? "bg-[#0a4a2b] text-white border-[#bc9e65] shadow-[0_4px_12px_rgba(10,74,43,0.2)]"
|
||||
: "bg-white text-[#0f3f26] border-[#eaddc4] hover:bg-stone-50"
|
||||
}`}
|
||||
>
|
||||
<div className="shrink-0">
|
||||
{isAllItems ? (
|
||||
<div className={`w-7 h-7 flex items-center justify-center rounded-full ${isActive ? 'text-[#bc9e65]' : 'text-[#0a4a2b]'}`}>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="3" y="3" width="7" height="7" rx="1.5" />
|
||||
<rect x="14" y="3" width="7" height="7" rx="1.5" />
|
||||
<rect x="14" y="14" width="7" height="7" rx="1.5" />
|
||||
<rect x="3" y="14" width="7" height="7" rx="1.5" />
|
||||
</svg>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-7 h-7 flex items-center justify-center rounded-full bg-transparent">
|
||||
<Image src="/images/menu/tab-icon.png" alt="" width={24} height={24} className="object-contain" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<span className="truncate">{tab.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user