first commit

This commit is contained in:
Alaguraj0361 2026-07-14 14:31:41 +05:30
parent 88f293b284
commit 7b2bbf0a31
45 changed files with 4128 additions and 94 deletions

View File

@ -1,7 +1,16 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
images: {
remotePatterns: [
{
protocol: "https",
hostname: "images.unsplash.com",
port: "",
pathname: "/**",
},
],
},
};
export default nextConfig;

756
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,15 +9,24 @@
"lint": "eslint"
},
"dependencies": {
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.6.1",
"framer-motion": "^12.42.2",
"gsap": "^3.15.0",
"lenis": "^1.3.25",
"lucide-react": "^0.470.0",
"motion": "^12.42.2",
"next": "16.2.10",
"react": "19.2.4",
"react-dom": "19.2.4"
"react-dom": "19.2.4",
"three": "^0.185.1"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/three": "^0.185.1",
"eslint": "^9",
"eslint-config-next": "16.2.10",
"tailwindcss": "^4",

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 906 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 992 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 827 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 833 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 966 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 888 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 329 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 888 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 639 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1000 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1000 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 809 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 798 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 642 KiB

View File

@ -1,26 +1,117 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--color-maroon: #188754;
--color-maroon-dark: #0c5030;
--color-maroon-light: #2ea670;
--color-gold: #d8c396;
--color-gold-light: #e4d7bc;
--color-gold-dark: #bc9e65;
--color-wood: #1e1412;
--color-stone: #3A3B3C;
--color-brass: #d8c396;
--color-orange: #d8c396;
--color-darkbg: #050c08; /* Dark green-cocoa tone background */
--color-darkbg-light: #0d1e14;
--font-serif: var(--font-cinzel);
--font-sans: var(--font-montserrat);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
:root {
--background: #FAF7F2; /* Premium warm ivory background */
--foreground: #1c2d24; /* Dark forest green-black text */
}
body {
background: var(--background);
background-color: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
font-family: var(--font-sans), sans-serif;
overflow-x: hidden;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: var(--background);
}
::-webkit-scrollbar-thumb {
background: var(--color-maroon);
border-radius: 4px;
border: 2px solid var(--background);
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-gold);
}
/* Custom transitions and glow effects */
.text-glow {
text-shadow: 0 0 10px rgba(24, 135, 84, 0.15), 0 0 20px rgba(24, 135, 84, 0.05);
}
.gold-border {
border-image: linear-gradient(to right, #bc9e65, #d8c396, #e4d7bc, #d8c396, #bc9e65) 1;
}
.brass-glow {
box-shadow: 0 0 15px rgba(216, 195, 150, 0.2), inset 0 0 15px rgba(216, 195, 150, 0.1);
}
.diya-flame {
filter: drop-shadow(0 0 12px rgba(230, 126, 34, 0.8)) drop-shadow(0 0 25px rgba(216, 195, 150, 0.6));
animation: flicker 2s infinite alternate;
}
@keyframes flicker {
0% { transform: scale(1) rotate(-1deg); opacity: 0.95; }
20% { transform: scale(1.05) rotate(1deg); opacity: 1; }
40% { transform: scale(0.95) rotate(-2deg); opacity: 0.9; }
60% { transform: scale(1.1) rotate(2deg); opacity: 1; }
80% { transform: scale(1) rotate(-1deg); opacity: 0.95; }
100% { transform: scale(1.05) rotate(0deg); opacity: 1; }
}
.custom-cursor {
width: 24px;
height: 24px;
border: 2.5px solid #188754; /* Green accent cursor */
border-radius: 50%;
position: fixed;
transform: translate(-50%, -50%);
pointer-events: none;
z-index: 9999;
}
.custom-cursor-dot {
width: 6px;
height: 6px;
background-color: #188754;
border-radius: 50%;
position: fixed;
transform: translate(-50%, -50%);
pointer-events: none;
z-index: 9999;
}
/* Hide default cursor on desktop */
@media (min-width: 1024px) {
body, a, button, select, input, textarea {
cursor: none;
}
}
/* Glassmorphism - Premium light layout */
.glass {
background: rgba(253, 251, 247, 0.85);
backdrop-filter: blur(16px);
border: 1px solid rgba(24, 135, 84, 0.18);
}
/* Hide custom cursor on touchscreens */
@media (pointer: coarse) {
.custom-cursor-container {
display: none !important;
}
}

View File

@ -1,20 +1,29 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { Cinzel, Montserrat } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
const cinzel = Cinzel({
variable: "--font-cinzel",
subsets: ["latin"],
weight: ["400", "500", "600", "700", "800", "900"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
const montserrat = Montserrat({
variable: "--font-montserrat",
subsets: ["latin"],
weight: ["300", "400", "500", "600", "700", "800"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "My Dosa Place - Pure Vegetarian South Indian Heritage Dining",
description: "Experience the authentic culinary legacy of South India at My Dosa Place in Waterloo, ON. A premium dining experience featuring ghee-roasted dosas, sambar, and home-ground chutneys.",
keywords: ["My Dosa Place", "Waterloo South Indian Restaurant", "Pure Vegetarian Indian", "Waterloo Dosa", "Traditional Indian Feast", "Waterloo Dining"],
authors: [{ name: "My Dosa Place" }],
openGraph: {
title: "My Dosa Place - Pure Vegetarian South Indian Heritage Dining",
description: "Experience the authentic culinary legacy of South India at My Dosa Place in Waterloo, ON.",
images: ["/images/chettinad_exterior.png"],
}
};
export default function RootLayout({
@ -25,9 +34,11 @@ export default function RootLayout({
return (
<html
lang="en"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
className={`${cinzel.variable} ${montserrat.variable} h-full antialiased`}
>
<body className="min-h-full flex flex-col">{children}</body>
<body className="min-h-full bg-darkbg text-foreground">
{children}
</body>
</html>
);
}

View File

@ -1,65 +1,74 @@
import Image from "next/image";
"use client";
import { useEffect, useState } from "react";
import SmoothScroll from "@/components/SmoothScroll";
import EntranceExperience from "@/components/EntranceExperience";
import RestaurantStory from "@/components/RestaurantStory";
import SignatureFoods from "@/components/SignatureFoods";
import ChefShowcase from "@/components/ChefShowcase";
import InteriorGallery from "@/components/InteriorGallery";
import Testimonials from "@/components/Testimonials";
import EventsAccordion from "@/components/EventsAccordion";
import MenuPreview from "@/components/MenuPreview";
import ReservationForm from "@/components/ReservationForm";
import RouteMap from "@/components/RouteMap";
import Footer from "@/components/Footer";
import CustomCursor from "@/components/CustomCursor";
import SoundManager from "@/components/SoundManager";
export default function Home() {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
return (
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the page.tsx file.
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
</p>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={16}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
</div>
</main>
</div>
<main className="w-full relative bg-darkbg text-foreground overflow-hidden select-none">
{/* Isolated wrapper for client-only widgets to prevent hydration insertion conflicts */}
<div id="client-only-widgets">
{mounted && <SoundManager />}
{mounted && <CustomCursor />}
</div>
{/* Lenis Scroll syncing with GSAP triggers */}
<SmoothScroll>
{/* Sections 1 & 2: Landing Exterior Zoom & Door opening into Welcome reception */}
<EntranceExperience />
{/* Section 3: Heritage Story timeline */}
<RestaurantStory />
{/* Section 4: Signature Foods featuring interactive 3D WebGL plates */}
<SignatureFoods />
{/* Section 5: Chef Sundaram showcase with canvas heat waves */}
<ChefShowcase />
{/* Section 6: GSAP Horizontal space gallery */}
<InteriorGallery />
{/* Section 7: 3D-tilting Testimonials diaries */}
<Testimonials />
{/* Section 8: Expanding Special Events Accordion */}
<EventsAccordion />
{/* Section 9: Luxury Menu Categories preview */}
<MenuPreview />
{/* Section 10: Reservation Form with canvas sparks */}
<ReservationForm />
{/* Section 11: Cartographic Route SVG Map with animated Chariot */}
<RouteMap />
{/* Section 12: Footer with hanging Toran, contact logs and floating diyas */}
<Footer />
</SmoothScroll>
</main>
);
}

View File

@ -0,0 +1,239 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import Image from "next/image";
import { Sparkles, Flame, ShieldAlert } from "lucide-react";
export default function ChefShowcase() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [activeSpice, setActiveSpice] = useState(0);
// Spices database
const spices = [
{
name: "Tellicherry Black Pepper",
role: "The Emperor of Heat",
desc: "Sourced from the high ranges of Malabar, these late-harvest vine berries provide a complex, fruity heat rather than a sharp sting, forming the backbone of our chettinad gravies.",
},
{
name: "Green Cardamom",
role: "The Floral Note",
desc: "Harvested from the slopes of the Western Ghats. Cardamom pods are split by hand at dawn to capture the volatile essential oils that impart sweet floral notes to our Payasam.",
},
{
name: "Star Anise",
role: "The Licorice Sweetness",
desc: "Imported since ancient times through maritime trade. It releases a warming, woody-licorice fragrance when roasted dry on iron pans for our heritage rice dishes.",
},
];
// Sparks and Embers Canvas Animation
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
let animId: number;
let width = (canvas.width = canvas.parentElement?.clientWidth || 500);
let height = (canvas.height = canvas.parentElement?.clientHeight || 600);
const handleResize = () => {
if (!canvas.parentElement) return;
width = canvas.width = canvas.parentElement.clientWidth;
height = canvas.height = canvas.parentElement.clientHeight;
};
window.addEventListener("resize", handleResize);
class Ember {
x: number;
y: number;
vx: number;
vy: number;
size: number;
life: number;
maxLife: number;
color: string;
constructor() {
// start near the bottom-center of the image (simulating cooking pan)
this.x = width * 0.5 + (Math.random() - 0.5) * 150;
this.y = height * 0.75 + (Math.random() - 0.5) * 50;
this.vx = (Math.random() - 0.5) * 1.5;
this.vy = -Math.random() * 2.5 - 1.0;
this.size = Math.random() * 2.5 + 1.0;
this.maxLife = Math.random() * 80 + 40;
this.life = 0;
this.color = Math.random() > 0.4 ? "243, 229, 171" : "230, 126, 34"; // Gold or Orange sparks
}
update() {
this.x += this.vx;
this.y += this.vy;
this.life++;
// Add draft wind effect
this.vx += (Math.random() - 0.5) * 0.1;
}
draw(c: CanvasRenderingContext2D) {
const opacity = 1 - this.life / this.maxLife;
c.save();
c.beginPath();
c.fillStyle = `rgba(${this.color}, ${opacity})`;
c.shadowBlur = 8;
c.shadowColor = `rgb(${this.color})`;
c.arc(this.x, this.y, this.size, 0, Math.PI * 2);
c.fill();
c.restore();
}
}
const embers: Ember[] = [];
const maxEmbers = 40;
const render = () => {
ctx.clearRect(0, 0, width, height);
// Spawn new embers
if (embers.length < maxEmbers && Math.random() > 0.4) {
embers.push(new Ember());
}
// Update and draw embers
for (let i = embers.length - 1; i >= 0; i--) {
const e = embers[i];
e.update();
e.draw(ctx);
if (e.life >= e.maxLife || e.y < 0) {
embers.splice(i, 1);
}
}
animId = requestAnimationFrame(render);
};
render();
return () => {
window.removeEventListener("resize", handleResize);
cancelAnimationFrame(animId);
};
}, []);
return (
<section
ref={containerRef}
className="relative min-h-screen py-24 px-6 md:px-12 flex flex-col justify-center bg-darkbg border-b border-gold/10"
>
<div className="container max-w-6xl mx-auto relative z-10 grid grid-cols-1 lg:grid-cols-12 gap-16 items-center">
{/* Left Side: Chef Info & Spice Selection */}
<div className="lg:col-span-6 flex flex-col justify-center">
<span className="font-sans text-[10px] tracking-[0.4em] uppercase text-gold font-semibold flex items-center gap-2 mb-3">
<Flame size={12} className="text-orange animate-pulse" /> T H E M A S T E R A L C H E M I S T
</span>
<h2 className="font-serif text-4xl md:text-6xl font-bold tracking-wide text-[#1c2d24] uppercase mb-6 leading-tight">
CHEF SUNDARAM <br />
<span className="bg-gradient-to-r from-maroon-dark via-maroon to-maroon-light bg-clip-text text-transparent">
IYER
</span>
</h2>
<p className="font-sans text-sm md:text-base text-stone-700 leading-relaxed tracking-wide mb-8 max-w-xl">
With three decades of culinary devotion, Chef Sundaram has traversed the villages of Tamil Nadu,
uncovering regional temple recipes and masterfully blending heritage masalas. He believes that true
gastronomy is a spiritual act, uniting soil, tradition, and warm wood fire.
</p>
{/* Interactive Spice Guide Wrapper */}
<div className="border border-gold/30 p-6 md:p-8 rounded-lg bg-white/80 backdrop-blur-md relative overflow-hidden shadow-xl">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_bottom_right,_var(--tw-gradient-stops))] from-maroon/5 via-transparent to-transparent pointer-events-none" />
<h3 className="font-serif text-maroon text-lg tracking-widest uppercase mb-6 border-b border-gold/15 pb-3 flex items-center gap-2 font-bold">
<Sparkles size={16} /> Heritage Spice Altar
</h3>
{/* Spice Tabs Selector */}
<div className="flex gap-3 mb-6 overflow-x-auto pb-2 scrollbar-thin">
{spices.map((spice, idx) => (
<button
key={idx}
onClick={() => setActiveSpice(idx)}
className={`clickable font-serif text-xs tracking-wider uppercase px-4 py-2 border rounded-full whitespace-nowrap transition-all duration-300 ${
activeSpice === idx
? "bg-maroon text-white border-maroon shadow-[0_0_15px_rgba(24,135,84,0.2)]"
: "bg-transparent text-stone-500 border-stone-200 hover:border-maroon/40 hover:text-maroon"
}`}
>
{spice.name.split(" ")[0]}
</button>
))}
</div>
{/* Content pane with slide/fade animation */}
<div className="min-h-[140px] flex flex-col justify-between">
<AnimatePresence mode="wait">
<motion.div
key={activeSpice}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.3 }}
>
<span className="font-sans text-[10px] tracking-[0.2em] uppercase text-orange font-bold block mb-1">
{spices[activeSpice].role}
</span>
<h4 className="font-serif text-stone-900 text-lg tracking-wide uppercase mb-3 font-bold">
{spices[activeSpice].name}
</h4>
<p className="font-sans text-xs md:text-sm text-stone-700 leading-relaxed tracking-wider">
{spices[activeSpice].desc}
</p>
</motion.div>
</AnimatePresence>
</div>
</div>
</div>
{/* Right Side: Chef Cooking Portrait with embers canvas */}
<div className="lg:col-span-6 flex justify-center items-center relative">
{/* Framed Image Container */}
<div className="relative w-full max-w-[450px] aspect-[4/5] border-8 border-wood bg-darkbg p-2 shadow-[0_30px_60px_rgba(0,0,0,0.8)] overflow-hidden group">
{/* Brass Inner Frame Border */}
<div className="absolute inset-1 border-2 border-double border-gold-dark/40 z-10 pointer-events-none" />
{/* Action Portrait */}
<div className="w-full h-full relative overflow-hidden">
<Image
src="/images/chef_cooking.png"
alt="Chef Sundaram Cooking with fire"
fill
priority
sizes="(max-width: 768px) 100vw, 40vw"
className="object-cover group-hover:scale-105 transition-transform duration-[2s] brightness-[0.85] contrast-[1.05]"
/>
</div>
{/* Sparks/Embers Canvas Overlay */}
<canvas
ref={canvasRef}
className="absolute inset-0 z-20 pointer-events-none mix-blend-screen"
/>
{/* Subtle heat wave distortion visual overlay */}
<div className="absolute inset-x-0 bottom-0 h-1/3 bg-gradient-to-t from-orange/15 to-transparent z-15 pointer-events-none animate-pulse" />
</div>
{/* Decorative Corner Filigrees outside */}
<div className="absolute -top-4 -left-4 w-12 h-12 border-t border-l border-gold/40 pointer-events-none hidden md:block" />
<div className="absolute -bottom-4 -right-4 w-12 h-12 border-b border-r border-gold/40 pointer-events-none hidden md:block" />
</div>
</div>
</section>
);
}

View File

@ -0,0 +1,86 @@
"use client";
import { useEffect, useState } from "react";
import { motion, useMotionValue, useSpring } from "framer-motion";
export default function CustomCursor() {
const [isHovered, setIsHovered] = useState(false);
// Motion values for smooth cursor tracking
const cursorX = useMotionValue(-100);
const cursorY = useMotionValue(-100);
// Spring physics for trailing effect on the outer circle
const springConfig = { damping: 30, stiffness: 220, mass: 0.6 };
const cursorXSpring = useSpring(cursorX, springConfig);
const cursorYSpring = useSpring(cursorY, springConfig);
useEffect(() => {
const moveCursor = (e: MouseEvent) => {
cursorX.set(e.clientX);
cursorY.set(e.clientY);
};
const handleMouseOver = (e: MouseEvent) => {
const target = e.target as HTMLElement;
if (
target.tagName === "A" ||
target.tagName === "BUTTON" ||
target.closest("a") ||
target.closest("button") ||
target.classList.contains("clickable") ||
target.closest(".clickable")
) {
setIsHovered(true);
} else {
setIsHovered(false);
}
};
window.addEventListener("mousemove", moveCursor);
window.addEventListener("mouseover", handleMouseOver);
return () => {
window.removeEventListener("mousemove", moveCursor);
window.removeEventListener("mouseover", handleMouseOver);
};
}, [cursorX, cursorY]);
return (
<div className="custom-cursor-container pointer-events-none fixed inset-0 z-[9999] hidden lg:block">
{/* Outer Spring Ring */}
<motion.div
className="custom-cursor pointer-events-none fixed left-0 top-0"
style={{
x: cursorXSpring,
y: cursorYSpring,
translateX: "-50%",
translateY: "-50%",
}}
animate={{
width: isHovered ? 64 : 24,
height: isHovered ? 64 : 24,
backgroundColor: isHovered ? "rgba(212, 175, 55, 0.15)" : "rgba(212, 175, 55, 0)",
borderColor: isHovered ? "#F3E5AB" : "#D4AF37",
boxShadow: isHovered ? "0 0 20px rgba(225, 177, 44, 0.4)" : "none",
}}
transition={{ type: "tween", ease: "backOut", duration: 0.2 }}
/>
{/* Inner Pinpoint Dot */}
<motion.div
className="custom-cursor-dot pointer-events-none fixed left-0 top-0"
style={{
x: cursorX,
y: cursorY,
translateX: "-50%",
translateY: "-50%",
}}
animate={{
scale: isHovered ? 0 : 1,
backgroundColor: isHovered ? "#F3E5AB" : "#D4AF37",
}}
transition={{ duration: 0.1 }}
/>
</div>
);
}

View File

@ -0,0 +1,418 @@
"use client";
import { useEffect, useRef, useState, Suspense, useMemo } from "react";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { motion, AnimatePresence } from "framer-motion";
import { Canvas, useFrame, useThree } from "@react-three/fiber";
import { useTexture } from "@react-three/drei";
import * as THREE from "three";
gsap.registerPlugin(ScrollTrigger);
// Custom shader source for storefront door clipping
const vertexShader = `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const facadeFragmentShader = `
uniform sampler2D uTexture;
uniform float uOpacity;
varying vec2 vUv;
void main() {
vec4 col = texture2D(uTexture, vUv);
// Discard double doors in center of storefront
if (vUv.x >= 0.468 && vUv.x <= 0.532 && vUv.y >= 0.15 && vUv.y <= 0.50) {
discard;
}
gl_FragColor = vec4(col.rgb, col.a * uOpacity);
}
`;
const leftDoorFragmentShader = `
uniform sampler2D uTexture;
uniform float uOpacity;
varying vec2 vUv;
void main() {
// Crop texture to left door leaf boundaries
vec2 uv = vec2(0.468 + vUv.x * 0.032, 0.15 + vUv.y * 0.35);
vec4 col = texture2D(uTexture, uv);
gl_FragColor = vec4(col.rgb, col.a * uOpacity);
}
`;
const rightDoorFragmentShader = `
uniform sampler2D uTexture;
uniform float uOpacity;
varying vec2 vUv;
void main() {
// Crop texture to right door leaf boundaries
vec2 uv = vec2(0.50 + vUv.x * 0.032, 0.15 + vUv.y * 0.35);
vec4 col = texture2D(uTexture, uv);
gl_FragColor = vec4(col.rgb, col.a * uOpacity);
}
`;
// Helper notifier to hide CSS loader once textures resolve
function LoaderNotifier({ onLoad }: { onLoad: () => void }) {
useEffect(() => {
onLoad();
}, [onLoad]);
return null;
}
// 3D Cinematic flight renderer
function CinematicFlight({ progress }: { progress: number }) {
const exteriorTex = useTexture("/images/my_dosa_place_exterior_webgl.jpg");
const interiorTex = useTexture("/images/my_dosa_place_interior_webgl.jpg");
const foodTex = useTexture("/images/my_dosa_place_food_webgl.jpg");
const leftDoorRef = useRef<THREE.Group>(null);
const rightDoorRef = useRef<THREE.Group>(null);
// Set texture filtering to high quality linear
useEffect(() => {
[exteriorTex, interiorTex, foodTex].forEach((tex) => {
tex.minFilter = THREE.LinearFilter;
tex.magFilter = THREE.LinearFilter;
tex.generateMipmaps = false;
});
}, [exteriorTex, interiorTex, foodTex]);
const { width, height } = useThree((state) => state.viewport);
// Compute facade dimensions based on dusk photo aspect ratio
const aspect = 1920 / 864;
const planeH = height;
const planeW = planeH * aspect;
// Door dimensions matching facade coordinate percentages
const dw = 0.032 * planeW;
const dh = 0.35 * planeH;
const doorY = -0.175 * planeH;
const leftHingeX = -0.032 * planeW;
const rightHingeX = 0.032 * planeW;
const extGroupRef = useRef<THREE.Group>(null);
const intMeshRef = useRef<THREE.Mesh>(null);
const foodMeshRef = useRef<THREE.Mesh>(null);
// Refs for materials to update opacities in the high-performance frame loop
const facadeMatRef = useRef<THREE.ShaderMaterial>(null);
const leftDoorMatRef = useRef<THREE.ShaderMaterial>(null);
const rightDoorMatRef = useRef<THREE.ShaderMaterial>(null);
const intMatRef = useRef<THREE.MeshBasicMaterial>(null);
const foodMatRef = useRef<THREE.MeshBasicMaterial>(null);
// 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;
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);
extGroupRef.current.position.y = THREE.MathUtils.lerp(extGroupRef.current.position.y, targetY, 0.08);
}
// 2. Door opening rotation (swing inward Y-axis)
let doorProgress = 0;
if (progress >= 0.08 && progress <= 0.25) {
doorProgress = (progress - 0.08) / 0.17;
} else if (progress > 0.25) {
doorProgress = 1;
}
const angle = (Math.PI / 1.8) * doorProgress;
if (leftDoorRef.current) {
leftDoorRef.current.rotation.y = THREE.MathUtils.lerp(leftDoorRef.current.rotation.y, angle, 0.08);
}
if (rightDoorRef.current) {
rightDoorRef.current.rotation.y = THREE.MathUtils.lerp(rightDoorRef.current.rotation.y, -angle, 0.08);
}
// 3. Interior Dining Hall zoom
// Scale goes from 1.0 to 2.6
let intProgress = 0;
if (progress > 0.08) {
intProgress = Math.min(1, (progress - 0.08) / 0.62);
}
const targetIntScale = 1.0 + intProgress * 1.6;
if (intMeshRef.current) {
intMeshRef.current.scale.setScalar(THREE.MathUtils.lerp(intMeshRef.current.scale.x, targetIntScale, 0.08));
}
// 4. Food Platter landing zoom
// Scale goes from 0.8 to 1.15
let foodProgress = 0;
if (progress > 0.58) {
foodProgress = Math.min(1, (progress - 0.58) / 0.27);
}
const targetFoodScale = 0.8 + foodProgress * 0.35;
if (foodMeshRef.current) {
foodMeshRef.current.scale.setScalar(THREE.MathUtils.lerp(foodMeshRef.current.scale.x, targetFoodScale, 0.08));
}
// 5. Reactive Opacity Updates inside useFrame to prevent unmounting stutters
const extOpacity = progress < 0.25 ? 1 : Math.max(0, 1 - (progress - 0.25) / 0.15);
let intOpacity = 0;
if (progress >= 0.08 && progress <= 0.25) {
intOpacity = (progress - 0.08) / 0.17;
} else if (progress > 0.25 && progress < 0.60) {
intOpacity = 1;
} else if (progress >= 0.60 && progress <= 0.78) {
intOpacity = Math.max(0, 1 - (progress - 0.60) / 0.18);
} else if (progress > 0.78) {
intOpacity = 0;
}
const foodOpacity = progress < 0.58 ? 0 : Math.min(1, (progress - 0.58) / 0.20);
// Apply opacities to materials
if (facadeMatRef.current) facadeMatRef.current.uniforms.uOpacity.value = extOpacity;
if (leftDoorMatRef.current) leftDoorMatRef.current.uniforms.uOpacity.value = extOpacity;
if (rightDoorMatRef.current) rightDoorMatRef.current.uniforms.uOpacity.value = extOpacity;
if (intMatRef.current) intMatRef.current.opacity = intOpacity;
if (foodMatRef.current) foodMatRef.current.opacity = foodOpacity;
});
// Memoize initial shader uniforms
const facadeUniforms = useMemo(() => ({
uTexture: { value: exteriorTex },
uOpacity: { value: 1 }
}), [exteriorTex]);
const leftDoorUniforms = useMemo(() => ({
uTexture: { value: exteriorTex },
uOpacity: { value: 1 }
}), [exteriorTex]);
const rightDoorUniforms = useMemo(() => ({
uTexture: { value: exteriorTex },
uOpacity: { value: 1 }
}), [exteriorTex]);
return (
<>
<ambientLight intensity={1.5} />
<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]}>
<mesh>
<planeGeometry args={[planeW, planeH]} />
<shaderMaterial
ref={facadeMatRef}
vertexShader={vertexShader}
fragmentShader={facadeFragmentShader}
uniforms={facadeUniforms}
transparent
depthWrite={false}
/>
</mesh>
{/* Left Door Hinge Pivot Group */}
<group ref={leftDoorRef} position={[leftHingeX, doorY, 0.002]}>
<mesh position={[dw / 2, 0, 0]}>
<planeGeometry args={[dw, dh]} />
<shaderMaterial
ref={leftDoorMatRef}
vertexShader={vertexShader}
fragmentShader={leftDoorFragmentShader}
uniforms={leftDoorUniforms}
transparent
depthWrite={false}
/>
</mesh>
</group>
{/* Right Door Hinge Pivot Group */}
<group ref={rightDoorRef} position={[rightHingeX, doorY, 0.002]}>
<mesh position={[-dw / 2, 0, 0]}>
<planeGeometry args={[dw, dh]} />
<shaderMaterial
ref={rightDoorMatRef}
vertexShader={vertexShader}
fragmentShader={rightDoorFragmentShader}
uniforms={rightDoorUniforms}
transparent
depthWrite={false}
/>
</mesh>
</group>
</group>
{/* Plane 2: Pillars Dining Hall (Z = 0.01) */}
<mesh ref={intMeshRef} position={[0, 0, 0.01]}>
<planeGeometry args={[planeW * 1.1, planeH * 1.1]} />
<meshBasicMaterial
ref={intMatRef}
map={interiorTex}
transparent
opacity={0}
depthWrite={false}
/>
</mesh>
{/* Plane 3: South Indian Platter Table (Z = 0.0) */}
<mesh ref={foodMeshRef} position={[0, 0, 0.0]}>
<planeGeometry args={[planeW * 1.1, planeH * 1.1]} />
<meshBasicMaterial
ref={foodMatRef}
map={foodTex}
transparent
opacity={0}
depthWrite={false}
/>
</mesh>
</>
);
}
export default function EntranceExperience() {
const containerRef = useRef<HTMLDivElement>(null);
const scrollIndicatorRef = useRef<HTMLDivElement>(null);
const [scrollProgress, setScrollProgress] = useState(0);
const [loaded, setLoaded] = useState(false);
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setIsMounted(true);
}, []);
// Sync GSAP pinning scrub triggers
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const ctx = gsap.context(() => {
const scrollIndicator = scrollIndicatorRef.current;
const tl = gsap.timeline({
scrollTrigger: {
trigger: container,
start: "top top",
end: "+=450%",
scrub: 1.2,
pin: true,
onUpdate: (self) => {
setScrollProgress(self.progress);
},
},
});
// Initial fade-out of the scroll indicator
if (scrollIndicator) {
tl.to(scrollIndicator, {
opacity: 0,
y: -25,
duration: 1,
});
}
}, container);
return () => ctx.revert();
}, []);
return (
<div
ref={containerRef}
className="relative w-full h-screen overflow-hidden bg-[#050c08] select-none"
>
{/* Traditional loading spinner overlay */}
{!loaded && (
<div className="absolute inset-0 flex flex-col justify-center items-center z-50 bg-[#050c08]">
<div className="w-12 h-12 rounded-full border-2 border-gold/30 border-t-gold animate-spin mb-4" />
<span className="font-serif text-gold text-xs tracking-[0.3em] uppercase animate-pulse">
Entering My Dosa Place...
</span>
</div>
)}
{/* Three.js R3F WebGL Scene */}
{isMounted && (
<div className="absolute inset-0 w-full h-full z-0">
<Canvas>
<Suspense fallback={null}>
<CinematicFlight progress={scrollProgress} />
<LoaderNotifier onLoad={() => setLoaded(true)} />
</Suspense>
</Canvas>
</div>
)}
{/* Screen edge lighting overlays */}
<div className="absolute inset-0 bg-[radial-gradient(circle,_transparent_40%,_rgba(0,0,0,0.5)_95%)] pointer-events-none z-10" />
{/* ----------------- OVERLAY HERO ELEMENT (OUTSIDE STATE) ----------------- */}
<div className="absolute inset-0 flex flex-col justify-end items-center pb-20 px-6 z-30 pointer-events-none">
{/* Scroll Indicator */}
<div
ref={scrollIndicatorRef}
className="flex flex-col items-center gap-3 animate-bounce"
>
<div className="w-6 h-10 rounded-full border border-gold/45 flex justify-center p-1">
<div className="w-1.5 h-2.5 bg-gold rounded-full animate-[scroll-dot_1.5s_infinite_linear]" />
</div>
</div>
</div>
{/* ----------------- FOOD PLATTER STORYTELLING CARD (REVEALS AT END) ----------------- */}
<AnimatePresence>
{scrollProgress > 0.75 && (
<motion.div
initial={{ opacity: 0, x: -50, scale: 0.95 }}
animate={{ opacity: 1, x: 0, scale: 1 }}
exit={{ opacity: 0, x: -50, scale: 0.95 }}
transition={{ duration: 0.8, ease: "easeOut" }}
className="absolute top-28 left-8 z-30 w-[340px] sm:w-[400px] max-w-[calc(100vw-32px)] max-sm:top-auto max-sm:bottom-16 max-sm:left-4 max-sm:right-4 max-sm:mx-auto bg-[#fcfaf2]/95 border-2 border-double border-gold/45 rounded-b-lg p-6 sm:p-8 pt-10 sm:pt-12 shadow-2xl pointer-events-none"
style={{
borderTopLeftRadius: "180px 150px",
borderTopRightRadius: "180px 150px",
}}
>
{/* Inner traditional gold frame matching the arch dome */}
<div
className="absolute top-2 left-2 right-2 bottom-2 border border-gold/15 pointer-events-none rounded-b-md"
style={{
borderTopLeftRadius: "170px 140px",
borderTopRightRadius: "170px 140px",
}}
/>
<span className="font-sans text-[#188754] text-[10px] sm:text-[11px] tracking-[0.4em] uppercase font-bold block mb-2.5 text-center mt-2">
P U R E V E G E T A R I A N F E A S T
</span>
<h3 className="font-serif text-[#188754] text-xl sm:text-2xl tracking-[0.15em] uppercase font-bold mb-3 text-center">
A Royal Banquet
</h3>
<div className="w-24 h-[1px] bg-gradient-to-r from-transparent via-[#d8c396] to-transparent mx-auto mb-4" />
<p className="font-sans text-xs sm:text-[13px] text-stone-800 leading-relaxed tracking-wider font-medium text-justify">
Your table is set. Experience a royal banquet served on fresh banana leaves, where crispy ghee-roasted dosas, steaming lentil sambar, and hand-ground chutneys await you.
</p>
</motion.div>
)}
</AnimatePresence>
{/* Styled inline keyframes animations */}
<style jsx>{`
@keyframes scroll-dot {
0% { transform: translateY(0); opacity: 1; }
50% { transform: translateY(6px); opacity: 0.3; }
100% { transform: translateY(0); opacity: 1; }
}
`}</style>
</div>
);
}

View File

@ -0,0 +1,180 @@
"use client";
import { useState } from "react";
import { motion } from "framer-motion";
import { Calendar, Sparkles, Gift, Users, Award, ShieldAlert } from "lucide-react";
export default function EventsAccordion() {
const [hoveredIdx, setHoveredIdx] = useState<number | null>(null);
const events = [
{
title: "Kalyana Feasts",
tamilTitle: "கல்யாண விருந்து",
subtitle: "WEDDING BANQUETS",
desc: "A grand imperial feast served to celebrate auspicious marital unions. Traditional multi-course menus served on fresh organic banana leaves with royal silver chalices, accommodating up to five hundred guests in heritage luxury.",
icon: Sparkles,
bgGradient: "from-[#fdfcf9] via-[#e4d7bc]/40 to-[#FAF7F2]",
accentColor: "#188754",
},
{
title: "Ayush Dinners",
tamilTitle: "ஆயுள் விருந்து",
subtitle: "ANNIVERSARIES & BIRTHDAYS",
desc: "Honor milestones and life chapters. Our ancestral dining rooms provide an intimate, warm-lit atmosphere with traditional music ensembles and customized family menus curated by Chef Sundaram.",
icon: Gift,
bgGradient: "from-[#fdfcf9] via-[#e4d7bc]/20 to-[#FAF7F2]",
accentColor: "#bc9e65",
},
{
title: "Sabha Assemblies",
tamilTitle: "சபை கூட்டங்கள்",
subtitle: "CORPORATE COLLABORATIONS",
desc: "Host partners and executives in private boardrooms crafted from vintage rosewood. Pair high-level meetings with gourmet curated South Indian starters, specialized filter coffee breaks, and impeccable service.",
icon: Award,
bgGradient: "from-[#fdfcf9] via-[#188754]/8 to-[#FAF7F2]",
accentColor: "#188754",
},
{
title: "Kudumba Dining",
tamilTitle: "குடும்ப விருந்து",
subtitle: "GRAND FAMILY REUNIONS",
desc: "Bring generations together under one carved timber ceiling. We recreate the authentic community dining experience of South Indian heritage mansions, complete with traditional brass servers and folklore music.",
icon: Users,
bgGradient: "from-[#fdfcf9] via-[#bc9e65]/15 to-[#FAF7F2]",
accentColor: "#bc9e65",
},
];
return (
<section className="relative min-h-screen py-24 px-6 md:px-12 flex flex-col justify-center overflow-hidden bg-darkbg-light border-b border-gold/10">
{/* Background Decorative */}
<div className="absolute top-[5%] left-[50%] -translate-x-1/2 w-[500px] h-[500px] rounded-full bg-maroon/5 blur-[150px] pointer-events-none" />
<div className="container max-w-6xl mx-auto relative z-10 flex flex-col h-full">
{/* Title */}
<div className="mb-16 flex flex-col items-center text-center">
<span className="font-sans text-[10px] tracking-[0.4em] uppercase text-gold font-semibold flex items-center gap-2 mb-3">
<Calendar size={12} /> C E L E B R A T I O N S
</span>
<h2 className="font-serif text-4xl md:text-6xl font-bold tracking-wide text-foreground uppercase text-glow">
ROYAL ASSEMBLIES & EVENTS
</h2>
<div className="w-20 h-[1.5px] bg-maroon mt-4" />
</div>
{/* Horizontal Accordion Track */}
<div className="flex flex-col lg:flex-row gap-4 h-[600px] lg:h-[450px] w-full items-stretch">
{events.map((event, idx) => {
const isHovered = hoveredIdx === idx;
const isAnyHovered = hoveredIdx !== null;
const Icon = event.icon;
// Compute dynamic flex basis
// Default: equal sizes. On hover: active scales up, rest shrink
let flexClass = "lg:flex-[1]";
if (isAnyHovered) {
flexClass = isHovered ? "lg:flex-[3.5] bg-opacity-100" : "lg:flex-[0.8] opacity-60";
}
return (
<div
key={idx}
onMouseEnter={() => setHoveredIdx(idx)}
onMouseLeave={() => setHoveredIdx(null)}
className={`clickable flex-1 flex flex-col justify-between p-6 md:p-8 rounded-xl border border-gold/10 transition-all duration-700 ease-out relative overflow-hidden group ${flexClass} bg-gradient-to-b ${event.bgGradient}`}
>
{/* Vintage Border overlay on hover */}
<div className={`absolute inset-3 border border-gold/10 rounded-lg pointer-events-none transition-opacity duration-500 ${isHovered ? "opacity-100" : "opacity-0"}`} />
<div className={`absolute inset-[18px] border border-dashed border-gold/5 rounded-lg pointer-events-none transition-opacity duration-500 ${isHovered ? "opacity-100" : "opacity-0"}`} />
{/* Left Line Accent (Gold) */}
<div
className="absolute left-0 top-0 bottom-0 w-[4px] transition-all duration-500"
style={{ backgroundColor: event.accentColor, opacity: isHovered ? 1 : 0.2 }}
/>
{/* Top Section: Icon & Header */}
<div className="flex justify-between items-start z-10">
<div className="flex items-center gap-3">
<div
className="w-10 h-10 rounded-full border border-gold/30 flex items-center justify-center transition-all duration-500"
style={{
borderColor: isHovered ? event.accentColor : "rgba(216,195,150,0.4)",
backgroundColor: isHovered ? "rgba(24,135,84,0.06)" : "transparent",
}}
>
<Icon
size={18}
style={{ color: isHovered ? event.accentColor : "#bc9e65" }}
className={isHovered ? "animate-pulse" : ""}
/>
</div>
{/* Collapsed view Title details */}
<div className="lg:hidden group-hover:block transition-all duration-300">
<span className="font-sans text-[8px] tracking-[0.2em] uppercase text-gold-dark font-bold block">
{event.subtitle}
</span>
<h3 className="font-serif text-lg font-bold text-stone-900 uppercase tracking-wider">
{event.title}
</h3>
</div>
</div>
{/* Desktop Collapsed view rotated Title */}
<div className={`hidden lg:block absolute left-1/2 -translate-x-1/2 top-28 origin-center -rotate-90 whitespace-nowrap transition-opacity duration-500 pointer-events-none ${
isHovered ? "opacity-0" : "opacity-100"
}`}>
<h3 className="font-serif text-sm font-bold tracking-[0.4em] uppercase text-gold-dark/80 font-bold">
{event.title}
</h3>
</div>
</div>
{/* Expanded content view (Desktop only hides when collapsed, shows when hovered) */}
<div className={`flex flex-col gap-4 mt-8 z-10 transition-all duration-500 ${
isHovered ? "lg:opacity-100 lg:translate-y-0" : "lg:opacity-0 lg:translate-y-4 lg:pointer-events-none"
}`}>
{/* Category meta */}
<div>
<span className="font-sans text-[9px] tracking-[0.3em] text-gold-dark font-bold block mb-1">
{event.subtitle}
</span>
<h3 className="font-serif text-2xl md:text-3xl font-bold text-stone-900 uppercase tracking-wider text-glow mb-1">
{event.title}
</h3>
<p className="font-serif text-xs text-gold-dark/70 font-semibold italic tracking-wider">
{event.tamilTitle}
</p>
</div>
{/* Description */}
<p className="font-sans text-xs md:text-sm text-stone-700 leading-relaxed tracking-wider max-w-md">
{event.desc}
</p>
{/* Inquire CTA Button */}
<button className="clickable w-fit mt-2 font-serif text-[11px] tracking-[0.2em] uppercase text-maroon hover:text-white border border-maroon/30 hover:border-maroon hover:bg-maroon px-4 py-2 bg-transparent transition-all duration-300 relative group/btn overflow-hidden">
<span className="absolute inset-0 bg-maroon translate-x-[-100%] group-hover/btn:translate-x-0 transition-transform duration-300 z-0" />
<span className="relative z-10">Request Brochure</span>
</button>
</div>
{/* Bottom Number Indicator */}
<div className="text-right z-10 mt-6 lg:mt-0">
<span className="font-serif text-lg font-bold text-maroon/35 tracking-widest">
0{idx + 1}
</span>
</div>
</div>
);
})}
</div>
</div>
</section>
);
}

View File

@ -0,0 +1,317 @@
"use client";
import { useRef, useEffect, useState } from "react";
import { Canvas, useFrame, useLoader } from "@react-three/fiber";
import { Center, OrbitControls, useTexture } from "@react-three/drei";
import * as THREE from "three";
// Individual Food Plate 3D Component
function FoodPlate({ activeIdx, isHovered }: { activeIdx: number; isHovered: boolean }) {
const plateRef = useRef<THREE.Group>(null);
// Load food textures dynamically
const textures = [
useTexture("/images/masala_dosa_luxury.png"),
useTexture("/images/south_indian_thali_luxury.png"),
];
// Store target rotation/position for smooth spring interpolation
const targetRotationY = useRef(0);
const targetPositionY = useRef(0);
const targetScale = useRef(1);
useFrame((state) => {
if (!plateRef.current) return;
// 1. Continuous slow rotation
targetRotationY.current += 0.005;
// 2. Mouse interactive tilt effect
const mouseX = state.pointer.x * 0.4;
const mouseY = state.pointer.y * 0.4;
plateRef.current.rotation.x = THREE.MathUtils.lerp(plateRef.current.rotation.x, 0.65 - mouseY, 0.1);
plateRef.current.rotation.y = THREE.MathUtils.lerp(plateRef.current.rotation.y, targetRotationY.current + mouseX, 0.1);
// 3. Hover lift effect & slow float oscillation
const hoverOffset = isHovered ? 0.3 : 0;
const floatOffset = Math.sin(state.clock.getElapsedTime() * 1.5) * 0.08;
targetPositionY.current = hoverOffset + floatOffset;
plateRef.current.position.y = THREE.MathUtils.lerp(plateRef.current.position.y, targetPositionY.current, 0.08);
// Scale spring transition
targetScale.current = isHovered ? 1.08 : 1.0;
plateRef.current.scale.setScalar(
THREE.MathUtils.lerp(plateRef.current.scale.x, targetScale.current, 0.1)
);
});
return (
<group ref={plateRef} rotation={[0.65, 0, 0]} position={[0, -0.2, 0]}>
{/* 1. Base Brass Plate */}
<mesh castShadow receiveShadow>
<cylinderGeometry args={[2.5, 2.3, 0.18, 64]} />
<meshPhysicalMaterial
color="#D4AF37"
metalness={0.9}
roughness={0.18}
clearcoat={1.0}
clearcoatRoughness={0.1}
reflectivity={1.0}
/>
</mesh>
{/* Brass Rim/lip details */}
<mesh position={[0, 0.1, 0]}>
<torusGeometry args={[2.45, 0.08, 16, 100]} />
<meshStandardMaterial color="#AA7C11" metalness={0.95} roughness={0.15} />
</mesh>
{/* 2. Banana Leaf Liner */}
<mesh position={[0, 0.1, 0]}>
<cylinderGeometry args={[2.35, 2.35, 0.02, 64]} />
<meshStandardMaterial
color="#1e5128"
roughness={0.8}
bumpScale={0.05}
/>
</mesh>
{/* 3. Photorealistic Food Mesh with texture projection */}
<mesh position={[0, 0.12, 0]}>
<cylinderGeometry args={[2.25, 2.25, 0.01, 64]} />
<meshStandardMaterial
map={textures[activeIdx]}
roughness={0.4}
metalness={0.0}
/>
</mesh>
</group>
);
}
// Spices / Ingredients flying around the plate
function FloatingIngredients({ activeIdx }: { activeIdx: number }) {
const count = 18;
const meshRef = useRef<THREE.Group>(null);
const particles = useRef<Array<{
pos: THREE.Vector3;
speed: THREE.Vector3;
rot: THREE.Vector3;
rotSpeed: THREE.Vector3;
scale: number;
color: string;
}>>([]);
useEffect(() => {
// Generate random ingredients positions and velocities
const temp: typeof particles.current = [];
for (let i = 0; i < count; i++) {
const isCurryLeaf = i % 3 === 0;
const isRedChili = i % 3 === 1;
temp.push({
pos: new THREE.Vector3(
(Math.random() - 0.5) * 8,
(Math.random() - 0.5) * 6 + 2,
(Math.random() - 0.5) * 4
),
speed: new THREE.Vector3(
(Math.random() - 0.5) * 0.02,
-Math.random() * 0.015 - 0.005,
(Math.random() - 0.5) * 0.01
),
rot: new THREE.Vector3(Math.random() * Math.PI, Math.random() * Math.PI, 0),
rotSpeed: new THREE.Vector3(Math.random() * 0.02, Math.random() * 0.02, 0),
scale: isCurryLeaf ? 0.25 : isRedChili ? 0.12 : 0.05,
color: isCurryLeaf ? "#2E7D32" : isRedChili ? "#D84315" : "#212121", // Green, Red, Black (mustard seeds)
});
}
particles.current = temp;
}, [activeIdx]);
useFrame(() => {
if (!meshRef.current) return;
// Animate flying items
const children = meshRef.current.children;
particles.current.forEach((p, idx) => {
const child = children[idx] as THREE.Mesh;
if (!child) return;
// Update position
p.pos.add(p.speed);
p.rot.add(p.rotSpeed);
// Magnet effect: pull slightly towards center plate [0, 0, 0]
const toCenter = new THREE.Vector3(0, 0, 0).sub(p.pos);
p.speed.add(toCenter.multiplyScalar(0.0002));
// Speed limits
p.speed.clampLength(0.005, 0.04);
// Boundaries wrap-around
if (p.pos.y < -4) {
p.pos.y = 4;
p.pos.x = (Math.random() - 0.5) * 6;
}
if (p.pos.x < -6) p.pos.x = 6;
if (p.pos.x > 6) p.pos.x = -6;
child.position.copy(p.pos);
child.rotation.set(p.rot.x, p.rot.y, p.rot.z);
});
});
return (
<group ref={meshRef}>
{Array.from({ length: count }).map((_, idx) => {
const isCurryLeaf = idx % 3 === 0;
const isRedChili = idx % 3 === 1;
if (isCurryLeaf) {
// Curved plane for Curry Leaf
return (
<mesh key={idx}>
<boxGeometry args={[0.6, 0.3, 0.02]} />
<meshStandardMaterial color="#1B5E20" roughness={0.7} />
</mesh>
);
} else if (isRedChili) {
// Cylinder for chili pod
return (
<mesh key={idx}>
<cylinderGeometry args={[0.08, 0.08, 0.4, 8]} />
<meshStandardMaterial color="#C62828" roughness={0.5} />
</mesh>
);
} else {
// Sphere for Mustard Seed
return (
<mesh key={idx}>
<sphereGeometry args={[0.08, 8, 8]} />
<meshStandardMaterial color="#1a0f05" roughness={0.9} metalness={0.1} />
</mesh>
);
}
})}
</group>
);
}
// Steam Particles rising from plate
function SteamEffect() {
const count = 15;
const groupRef = useRef<THREE.Group>(null);
const steamParticles = useRef<Array<{
pos: THREE.Vector3;
speedY: number;
speedX: number;
opacity: number;
scale: number;
}>>([]);
useEffect(() => {
const temp: typeof steamParticles.current = [];
for (let i = 0; i < count; i++) {
temp.push({
pos: new THREE.Vector3(
(Math.random() - 0.5) * 1.5,
Math.random() * 2 + 0.2,
(Math.random() - 0.5) * 1.5
),
speedY: Math.random() * 0.015 + 0.008,
speedX: (Math.random() - 0.5) * 0.006,
opacity: Math.random() * 0.3 + 0.1,
scale: Math.random() * 0.3 + 0.2,
});
}
steamParticles.current = temp;
}, []);
useFrame(() => {
if (!groupRef.current) return;
const children = groupRef.current.children;
steamParticles.current.forEach((p, idx) => {
const child = children[idx] as THREE.Mesh;
if (!child) return;
p.pos.y += p.speedY;
p.pos.x += p.speedX;
p.opacity -= 0.0018; // Fade out as it rises
// Reset when faded or high
if (p.pos.y > 3.0 || p.opacity <= 0) {
p.pos.y = Math.random() * 0.2 + 0.15;
p.pos.x = (Math.random() - 0.5) * 1.2;
p.opacity = Math.random() * 0.3 + 0.1;
}
child.position.copy(p.pos);
child.scale.setScalar(p.scale * (1 + p.pos.y * 0.5)); // expand as it rises
const mat = child.material as THREE.MeshBasicMaterial;
if (mat) {
mat.opacity = p.opacity;
}
});
});
return (
<group ref={groupRef}>
{Array.from({ length: count }).map((_, idx) => (
<mesh key={idx}>
<sphereGeometry args={[0.3, 16, 16]} />
<meshBasicMaterial
color="#EFEBE9"
transparent
opacity={0.1}
blending={THREE.AdditiveBlending}
depthWrite={false}
/>
</mesh>
))}
</group>
);
}
export default function FoodCanvas({ activeIdx, isHovered }: { activeIdx: number; isHovered: boolean }) {
return (
<div className="w-full h-full relative">
<Canvas
shadows
camera={{ position: [0, 2.2, 5.0], fov: 45 }}
gl={{ antialias: true, alpha: true }}
>
<ambientLight intensity={1.5} color="#F5F0E6" />
{/* Spotlight shining on plate for royal gold reflections */}
<spotLight
position={[5, 10, 5]}
angle={0.3}
penumbra={1}
intensity={50}
castShadow
shadow-bias={-0.0001}
color="#D4AF37"
/>
<directionalLight position={[-5, 5, -5]} intensity={1.5} color="#AA7C11" />
<pointLight position={[0, -2, 2]} intensity={2.0} color="#6C1E27" />
<Center>
<FoodPlate activeIdx={activeIdx} isHovered={isHovered} />
<SteamEffect />
<FloatingIngredients activeIdx={activeIdx} />
</Center>
<OrbitControls
enableZoom={false}
enablePan={false}
maxPolarAngle={Math.PI / 2 - 0.05} // prevent going below table
minPolarAngle={Math.PI / 6}
/>
</Canvas>
</div>
);
}

159
src/components/Footer.tsx Normal file
View File

@ -0,0 +1,159 @@
"use client";
import { useState } from "react";
import { Instagram, Facebook, Mail, Phone, MapPin, Send, Compass } from "lucide-react";
export default function Footer() {
const [email, setEmail] = useState("");
const [subscribed, setSubscribed] = useState(false);
const handleSubscribe = (e: React.FormEvent) => {
e.preventDefault();
if (!email) return;
setSubscribed(true);
setEmail("");
};
return (
<footer className="relative bg-darkbg border-t-2 border-gold-dark/30 pt-16 pb-8 px-6 md:px-12 overflow-hidden">
{/* Traditional Toran Hanging Garland at the very top of footer */}
<div className="absolute top-0 inset-x-0 h-8 bg-repeat-x bg-[length:80px_auto] bg-[url('data:image/svg+xml;utf8,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%2280%22 height=%2224%22 viewBox=%220 0 80 24%22><path d=%22M0,0 Q20,20 40,0 Q60,20 80,0 M40,0 L40,15 M80,0 L80,15 M0,0 L0,15%22 fill=%22none%22 stroke=%22%23AA7C11%22 stroke-width=%221.5%22/><circle cx=%2240%22 cy=%2215%22 r=%223.5%22 fill=%22%23D4AF37%22/><circle cx=%220%22 cy=%2215%22 r=%223.5%22 fill=%22%23D4AF37%22/><circle cx=%2280%22 cy=%2215%22 r=%223.5%22 fill=%22%23D4AF37%22/></svg>')] bg-top pointer-events-none opacity-40" />
{/* Floating Diyas (Lamps) floating gently */}
<div className="absolute inset-0 pointer-events-none overflow-hidden">
{/* Diya 1 */}
<div className="absolute bottom-16 left-[8%] w-10 h-6 bg-brass rounded-b-full border-t border-gold flex justify-center items-start shadow-lg opacity-35 animate-[float-diya_8s_infinite_ease-in-out_alternate]">
<div className="w-2.5 h-5 bg-gradient-to-t from-orange to-gold-light rounded-full absolute -top-4 left-1/2 -translate-x-1/2 diya-flame" />
</div>
{/* Diya 2 */}
<div className="absolute bottom-24 right-[12%] w-8 h-5 bg-brass rounded-b-full border-t border-gold flex justify-center items-start shadow-lg opacity-30 animate-[float-diya_10s_infinite_ease-in-out_alternate-reverse]">
<div className="w-2 h-4 bg-gradient-to-t from-orange to-gold-light rounded-full absolute -top-3 left-1/2 -translate-x-1/2 diya-flame" />
</div>
</div>
<div className="container max-w-6xl mx-auto relative z-10 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-12 gap-12 mb-12">
{/* Col 1: Brand & Bio */}
<div className="lg:col-span-4 flex flex-col gap-5">
<div className="flex flex-col">
<span className="font-serif text-gold text-base tracking-[0.3em] uppercase text-glow">MY DOSA</span>
<span className="font-serif text-gold-light text-xl tracking-[0.5em] uppercase text-glow">PLACE</span>
<div className="w-20 h-[1px] bg-gold/30 mt-2" />
</div>
<p className="font-sans text-xs text-stone-400 leading-relaxed tracking-wider max-w-sm">
Waterloo's premier destination for authentic, pure vegetarian South Indian flavors. Crispy ghee-roasted dosas, steaming lentil sambar, and home-ground chutneys crafted with devotion.
</p>
{/* Social Icons */}
<div className="flex gap-4 items-center mt-2">
<a
href="https://www.instagram.com"
target="_blank"
rel="noopener noreferrer"
className="clickable w-8 h-8 rounded-full border border-gold/20 flex items-center justify-center text-gold hover:text-gold-light hover:border-gold shadow-[0_0_8px_rgba(212,175,55,0.05)] transition-all duration-300"
>
<Instagram size={14} />
</a>
<a
href="https://www.facebook.com"
target="_blank"
rel="noopener noreferrer"
className="clickable w-8 h-8 rounded-full border border-gold/20 flex items-center justify-center text-gold hover:text-gold-light hover:border-gold shadow-[0_0_8px_rgba(212,175,55,0.05)] transition-all duration-300"
>
<Facebook size={14} />
</a>
</div>
</div>
{/* Col 2: Opening Hours */}
<div className="lg:col-span-3 flex flex-col gap-4">
<h4 className="font-serif text-gold text-xs tracking-widest uppercase border-b border-gold/10 pb-2">
Feast Sessions
</h4>
<ul className="flex flex-col gap-3 font-sans text-xs text-stone-300 tracking-wider">
<li className="flex flex-col">
<span className="font-bold text-white uppercase text-[10px] tracking-widest text-glow">Wednesday Monday</span>
<span className="text-stone-400 mt-0.5">04:30 PM 10:00 PM</span>
</li>
<li className="flex flex-col mt-2">
<span className="font-bold text-white uppercase text-[10px] tracking-widest text-glow">Tuesday</span>
<span className="text-stone-400 mt-0.5">Closed</span>
</li>
</ul>
</div>
{/* Col 3: Contact & Address */}
<div className="lg:col-span-3 flex flex-col gap-4">
<h4 className="font-serif text-gold text-xs tracking-widest uppercase border-b border-gold/10 pb-2">
The Dining Court
</h4>
<ul className="flex flex-col gap-3 font-sans text-xs text-stone-300 tracking-wider">
<li className="flex items-start gap-2.5">
<MapPin size={14} className="text-gold mt-0.5 flex-shrink-0" />
<span>922 Erb St. W Unit C3#3, Waterloo, ON N2T 0C9, Canada</span>
</li>
<li className="flex items-center gap-2.5">
<Phone size={14} className="text-gold flex-shrink-0" />
<span>+1 519-888-7459</span>
</li>
<li className="flex items-center gap-2.5">
<Mail size={14} className="text-gold flex-shrink-0" />
<span>info@mydosaplace.com</span>
</li>
</ul>
</div>
{/* Col 4: Newsletter */}
<div className="lg:col-span-2 flex flex-col gap-4">
<h4 className="font-serif text-gold text-xs tracking-widest uppercase border-b border-gold/10 pb-2">
Invite Dispatch
</h4>
<p className="font-sans text-[11px] text-stone-400 leading-relaxed tracking-wide">
Subscribe to receive exclusive banquet menus and seasonal festival calendars.
</p>
{!subscribed ? (
<form onSubmit={handleSubscribe} className="flex gap-2">
<input
type="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Digital address"
className="font-sans text-[10px] text-white bg-darkbg-light border border-gold/25 focus:border-gold px-3 py-2 outline-none rounded flex-1 transition-colors duration-300"
/>
<button
type="submit"
className="clickable w-8 h-8 rounded bg-gold border border-gold text-darkbg flex items-center justify-center hover:bg-gold-light hover:border-gold-light transition-all duration-300"
aria-label="Subscribe"
>
<Send size={12} />
</button>
</form>
) : (
<span className="font-serif text-[10px] text-gold tracking-widest uppercase animate-pulse block">
Seal Dispatched!
</span>
)}
</div>
</div>
{/* Footer Bottom copyright & credits */}
<div className="container max-w-6xl mx-auto pt-8 border-t border-gold/10 flex flex-col md:flex-row justify-between items-center gap-4 text-stone-500 font-sans text-[9px] tracking-[0.2em] uppercase">
<span>© 2026 My Dosa Place. All rights reserved.</span>
<span>Pure Veg South Indian Heritage.</span>
</div>
<style jsx>{`
@keyframes float-diya {
0% { transform: translateY(0) rotate(0deg); }
100% { transform: translateY(-20px) rotate(2deg); }
}
`}</style>
</footer>
);
}

View File

@ -0,0 +1,178 @@
"use client";
import { useEffect, useRef } from "react";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import Image from "next/image";
import { Compass, Sparkles } from "lucide-react";
gsap.registerPlugin(ScrollTrigger);
export default function InteriorGallery() {
const containerRef = useRef<HTMLDivElement>(null);
const trackRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const container = containerRef.current;
const track = trackRef.current;
if (!container || !track) return;
const ctx = gsap.context(() => {
// Calculate translation distance
const trackWidth = track.scrollWidth;
const scrollDistance = trackWidth - window.innerWidth;
// Horizontal Scroll Trigger
const scrollTween = gsap.to(track, {
x: -scrollDistance,
ease: "none",
scrollTrigger: {
trigger: container,
pin: true,
start: "top top",
end: `+=${scrollDistance}`, // scroll height equals translation distance
scrub: 1,
invalidateOnRefresh: true,
},
});
// Image Zoom Parallax during slide
const images = track.querySelectorAll(".gallery-img");
images.forEach((img) => {
gsap.fromTo(
img,
{ scale: 1.0 },
{
scale: 1.15,
ease: "none",
scrollTrigger: {
trigger: img,
containerAnimation: scrollTween, // sync with parent horizontal tween
start: "left right",
end: "right left",
scrub: true,
},
}
);
});
}, container);
return () => ctx.revert();
}, []);
const slides = [
{
title: "The Heritage Tour",
subtitle: "C I N E M A T I C S P A C E S",
desc: "Swipe or scroll to explore the visual spatial transitions of My Dosa Place, bringing heritage pillars, brass lanterns, and set feast tables to life.",
isTitle: true,
},
{
title: "The Teakwood Columns",
subtitle: "T H E T E A K C O U R T",
desc: "Supported by eighty grand hand-carved pillars sourced from Chettinad, framing a courtyard that captures cooling cross-breezes.",
img: "/images/chettinad_interior.png",
isTitle: false,
},
{
title: "The Royal Banquet Court",
subtitle: "I M P E R I A L D I N I N G",
desc: "Our ceremonial feast hall features heavy teak tables, soft yellow lanterns, and floral motifs (Kolams) hand-drawn with rice flour daily.",
img: "/images/royal_dining_experience.png",
isTitle: false,
},
{
title: "The Sanctuary Lobby",
subtitle: "T H E W E L C O M E A N T E R O O M",
desc: "Paved with rough-cut temple stone tiles, featuring bubbling brass vessels (urli) filled with fresh jasmine buds and marigolds.",
img: "/images/south_indian_welcome.png",
isTitle: false,
},
];
return (
<div
ref={containerRef}
className="relative w-full h-screen overflow-hidden bg-darkbg border-b border-gold/10"
>
{/* Scroll Track */}
<div
ref={trackRef}
className="absolute top-0 left-0 h-full flex flex-row items-center z-10"
style={{ width: `${slides.length * 100}vw` }}
>
{slides.map((slide, idx) => (
<div
key={idx}
className="w-screen h-screen flex-shrink-0 flex items-center justify-center relative p-8 md:p-16"
>
{slide.isTitle ? (
// Slide 1: Welcome / Title Slide
<div className="max-w-xl text-center flex flex-col items-center z-10">
<span className="font-sans text-[10px] tracking-[0.4em] uppercase text-gold font-semibold flex items-center gap-2 mb-4">
<Compass size={14} className="animate-spin-slow" /> H E R I T A G E W A L K W A Y
</span>
<h2 className="font-serif text-4xl md:text-6xl font-bold tracking-wider text-white uppercase leading-tight">
THE SPATIAL <br />
<span className="bg-gradient-to-r from-gold-light via-gold to-gold-dark bg-clip-text text-transparent">
SANCTUARY
</span>
</h2>
<div className="w-20 h-[1px] bg-gold/30 my-6" />
<p className="font-sans text-sm text-stone-300 leading-relaxed tracking-wider mb-8">
{slide.desc}
</p>
<div className="flex items-center gap-3 animate-pulse">
<span className="font-sans text-[10px] tracking-[0.2em] uppercase text-gold">Scroll Down to Slide Right</span>
<div className="w-10 h-[1px] bg-gold" />
</div>
</div>
) : (
// Other Slides: Immersive Image with Glassmorphism overlay
<div className="relative w-full h-[85%] md:h-[80%] rounded-xl overflow-hidden border border-gold/10 group shadow-[0_30px_70px_rgba(0,0,0,0.8)]">
{/* Image layer */}
<div className="absolute inset-0 w-full h-full">
<Image
src={slide.img || ""}
alt={slide.title}
fill
sizes="90vw"
className="gallery-img object-cover brightness-[0.7] contrast-[1.05] transition-all duration-[1s]"
/>
<div className="absolute inset-0 bg-gradient-to-t from-darkbg via-transparent to-transparent opacity-80" />
</div>
{/* Corner ornaments */}
<div className="absolute inset-4 border border-gold/10 rounded-lg pointer-events-none z-20" />
<div className="absolute inset-[22px] border border-dashed border-gold/5 rounded-lg pointer-events-none z-20" />
{/* Glassmorphic card overlay */}
<div className="absolute bottom-10 left-6 md:left-12 max-w-md p-6 md:p-8 rounded-lg glass border border-gold/25 shadow-2xl z-30 transition-transform duration-500 hover:-translate-y-2">
<span className="font-sans text-[9px] tracking-[0.3em] text-gold font-bold block mb-2">
{slide.subtitle}
</span>
<h3 className="font-serif text-2xl font-bold text-white tracking-wide uppercase mb-3 text-glow">
{slide.title}
</h3>
<p className="font-sans text-xs md:text-sm text-stone-200 leading-relaxed tracking-wide">
{slide.desc}
</p>
</div>
</div>
)}
</div>
))}
</div>
<style jsx global>{`
@keyframes spin-slow {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.animate-spin-slow {
animation: spin-slow 15s linear infinite;
}
`}</style>
</div>
);
}

View File

@ -0,0 +1,257 @@
"use client";
import { motion } from "framer-motion";
import Image from "next/image";
export default function MenuPreview() {
const leftColumnItems = [
{
name: "ONION RAVA DOSA",
desc: "Crispy semolina crepe with onions, herbs & spices.",
tag: "RAVA SPL",
tagType: "green"
},
{
name: "IDLI SAMBAR (2 PCS)",
desc: "Steamed fluffy rice & lentil cakes with hot sambar.",
tag: "TIFFIN SPL",
tagType: "gold"
},
{
name: "MYSORE MASALA DOSA",
desc: "Rice crepe spread with spicy chutney & potato masala.",
tag: "CLASSIC",
tagType: "orange"
},
{
name: "RAVA MASALA DOSA",
desc: "Crispy semolina crepe filled with spiced potato masala.",
tag: "RAVA SPL",
tagType: "green"
},
{
name: "PURI BHAJI (3 PCS)",
desc: "Golden fluffy deep-fried puris with flavorful potato bhaji.",
tag: "TIFFIN SPL",
tagType: "gold"
}
];
const rightColumnItems = [
{
name: "MASALA DOSA",
desc: "Crispy thin rice & lentil crepe stuffed with potato masala.",
tag: "CLASSIC",
tagType: "orange"
},
{
name: "ONION RAVA MASALA DOSA",
desc: "Semolina crepe with onions and spiced potato filling.",
tag: "RAVA SPL",
tagType: "green"
},
{
name: "MEDU VADA (2 PCS)",
desc: "Deep fried crispy lentil donuts served with sambar & chutney.",
tag: "TIFFIN SPL",
tagType: "gold"
},
{
name: "PAPER ROAST DOSA",
desc: "Extra large, paper-thin, crispy dosa served hot.",
tag: "CLASSIC",
tagType: "orange"
},
{
name: "RAVA PLAIN DOSA",
desc: "Crispy semolina crepe served with chutney & sambar.",
tag: "RAVA SPL",
tagType: "green"
}
];
const containerVariants = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: {
staggerChildren: 0.12
}
}
} as const;
const itemVariants = {
hidden: { opacity: 0, y: 15 },
show: { opacity: 1, y: 0, transition: { duration: 0.5, ease: "easeOut" } }
} as const;
return (
<section className="relative min-h-screen py-24 px-6 md:px-12 flex flex-col justify-center overflow-hidden bg-[#f6f3ea] border-b border-[#d8c396]/20">
{/* Background Gopuram sketch (Left) */}
<motion.div
initial={{ opacity: 0, x: -60, scale: 0.95 }}
whileInView={{ opacity: 0.15, x: 0, scale: 1 }}
viewport={{ once: true }}
transition={{ duration: 1.8, ease: "easeOut" }}
className="absolute top-8 left-4 w-[160px] h-[200px] md:w-[280px] md:h-[320px] pointer-events-none mix-blend-multiply"
>
<Image
src="/images/menu_gopuram_sketch.png"
alt="Gopuram Sketch"
fill
priority
sizes="(max-width: 768px) 30vw, 20vw"
className="object-contain filter grayscale"
/>
</motion.div>
{/* Background Palace sketch (Right) */}
<motion.div
initial={{ opacity: 0, x: 60, scale: 0.95 }}
whileInView={{ opacity: 0.15, x: 0, scale: 1 }}
viewport={{ once: true }}
transition={{ duration: 1.8, ease: "easeOut" }}
className="absolute top-8 right-4 w-[160px] h-[200px] md:w-[280px] md:h-[320px] pointer-events-none mix-blend-multiply"
>
<Image
src="/images/menu_palace_sketch.png"
alt="Palace Sketch"
fill
priority
sizes="(max-width: 768px) 30vw, 20vw"
className="object-contain filter grayscale"
/>
</motion.div>
<div className="container max-w-6xl mx-auto relative z-10">
{/* Header Elements */}
<div className="mb-20 flex flex-col items-center text-center">
{/* Top traditional gold design element */}
<div className="text-gold-dark text-xl mb-1 animate-pulse"></div>
<div className="w-16 h-[1px] bg-[#d8c396] mb-3" />
<h2 className="font-serif text-4xl md:text-5xl font-black tracking-wider text-[#188754] uppercase text-glow">
Our Menu
</h2>
{/* Double line with diamond ornament */}
<div className="flex items-center gap-3 w-44 justify-center mt-3">
<div className="h-[1px] flex-1 bg-gradient-to-r from-transparent to-[#d8c396]" />
<span className="text-[#d8c396] text-[8px] animate-pulse"></span>
<div className="h-[1px] flex-1 bg-gradient-to-l from-transparent to-[#d8c396]" />
</div>
<p className="font-sans text-[11px] sm:text-xs text-stone-600 max-w-lg mt-6 leading-relaxed tracking-wider italic font-medium">
Explore a wide variety of authentic South Indian vegetarian dishes,<br className="hidden sm:inline" />
crafted with traditional recipes and the finest ingredients.
</p>
</div>
{/* 2-Column Menu Layout wrapper */}
<div className="relative grid grid-cols-1 md:grid-cols-2 gap-x-16 gap-y-2 px-2 md:px-6">
{/* Vertical Dotted Separator in the center (Desktop only) */}
<div className="hidden md:block absolute left-1/2 top-4 bottom-4 w-[1px] border-l border-dashed border-[#d8c396]/60 -translate-x-1/2" />
{/* Left Column Items */}
<motion.div
variants={containerVariants}
initial="hidden"
whileInView="show"
viewport={{ once: true, margin: "-100px" }}
className="flex flex-col gap-4"
>
{leftColumnItems.map((item, index) => (
<motion.div
key={index}
variants={itemVariants}
whileHover={{ x: 6 }}
transition={{ duration: 0.3 }}
className="group flex justify-between items-start py-4 border-b border-[#d8c396]/15 hover:border-[#188754]/30 transition-all duration-300 cursor-pointer"
>
<div className="flex-1 pr-4">
<div className="flex items-center">
{/* Double Dot Arrow Indicator */}
<span className="text-[#188754] font-serif font-bold text-xs tracking-tighter mr-2 transition-transform duration-300 group-hover:translate-x-1.5 inline-block">
..&gt;
</span>
<h3 className="font-serif text-[14px] sm:text-[15px] font-black text-[#1e1412] tracking-wider group-hover:text-[#188754] transition-colors duration-300">
{item.name}
</h3>
</div>
<p className="font-sans text-[11px] sm:text-[12px] text-stone-600 leading-relaxed mt-1.5 pl-6 font-medium">
{item.desc}
</p>
</div>
{/* Status Badges on the right */}
<div className="flex-shrink-0 pt-0.5">
<span className={`text-[9px] tracking-wider px-2.5 py-0.5 font-serif font-black uppercase rounded border transition-all duration-500 shadow-sm ${
item.tagType === "green"
? "bg-[#188754]/8 text-[#188754] border-[#188754]/25 group-hover:bg-[#188754]/12 group-hover:shadow-[#188754]/5"
: item.tagType === "gold"
? "bg-[#d8c396]/10 text-[#bc9e65] border-[#d8c396]/35 group-hover:bg-[#d8c396]/15 group-hover:shadow-[#d8c396]/5"
: "bg-[#e07a5f]/8 text-[#e07a5f] border-[#e07a5f]/25 group-hover:bg-[#e07a5f]/12 group-hover:shadow-[#e07a5f]/5"
}`}>
{item.tag}
</span>
</div>
</motion.div>
))}
</motion.div>
{/* Right Column Items */}
<motion.div
variants={containerVariants}
initial="hidden"
whileInView="show"
viewport={{ once: true, margin: "-100px" }}
className="flex flex-col gap-4"
>
{rightColumnItems.map((item, index) => (
<motion.div
key={index}
variants={itemVariants}
whileHover={{ x: 6 }}
transition={{ duration: 0.3 }}
className="group flex justify-between items-start py-4 border-b border-[#d8c396]/15 hover:border-[#188754]/30 transition-all duration-300 cursor-pointer"
>
<div className="flex-1 pr-4">
<div className="flex items-center">
{/* Double Dot Arrow Indicator */}
<span className="text-[#188754] font-serif font-bold text-xs tracking-tighter mr-2 transition-transform duration-300 group-hover:translate-x-1.5 inline-block">
..&gt;
</span>
<h3 className="font-serif text-[14px] sm:text-[15px] font-black text-[#1e1412] tracking-wider group-hover:text-[#188754] transition-colors duration-300">
{item.name}
</h3>
</div>
<p className="font-sans text-[11px] sm:text-[12px] text-stone-600 leading-relaxed mt-1.5 pl-6 font-medium">
{item.desc}
</p>
</div>
{/* Status Badges on the right */}
<div className="flex-shrink-0 pt-0.5">
<span className={`text-[9px] tracking-wider px-2.5 py-0.5 font-serif font-black uppercase rounded border transition-all duration-500 shadow-sm ${
item.tagType === "green"
? "bg-[#188754]/8 text-[#188754] border-[#188754]/25 group-hover:bg-[#188754]/12 group-hover:shadow-[#188754]/5"
: item.tagType === "gold"
? "bg-[#d8c396]/10 text-[#bc9e65] border-[#d8c396]/35 group-hover:bg-[#d8c396]/15 group-hover:shadow-[#d8c396]/5"
: "bg-[#e07a5f]/8 text-[#e07a5f] border-[#e07a5f]/25 group-hover:bg-[#e07a5f]/12 group-hover:shadow-[#e07a5f]/5"
}`}>
{item.tag}
</span>
</div>
</motion.div>
))}
</motion.div>
</div>
</div>
</section>
);
}

View File

@ -0,0 +1,289 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { CalendarDays, Clock, Users, ArrowRight, CheckCircle, Sparkles } from "lucide-react";
export default function ReservationForm() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [formData, setFormData] = useState({
name: "",
email: "",
guests: "2",
date: "",
time: "19:00",
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
// Sparks/Gold embers canvas particle system
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
let animId: number;
let width = (canvas.width = canvas.parentElement?.clientWidth || 800);
let height = (canvas.height = canvas.parentElement?.clientHeight || 600);
const handleResize = () => {
if (!canvas.parentElement) return;
width = canvas.width = canvas.parentElement.clientWidth;
height = canvas.height = canvas.parentElement.clientHeight;
};
window.addEventListener("resize", handleResize);
class Spark {
x: number;
y: number;
vx: number;
vy: number;
radius: number;
alpha: number;
decay: number;
constructor() {
this.x = Math.random() * width;
this.y = height + 10;
this.vx = (Math.random() - 0.5) * 0.5;
this.vy = -Math.random() * 1.2 - 0.4;
this.radius = Math.random() * 2.0 + 0.5;
this.alpha = Math.random() * 0.6 + 0.2;
this.decay = Math.random() * 0.003 + 0.001;
}
update() {
this.x += this.vx;
this.y += this.vy;
this.alpha -= this.decay;
this.vx += (Math.random() - 0.5) * 0.05;
}
draw(c: CanvasRenderingContext2D) {
c.save();
c.beginPath();
c.fillStyle = `rgba(212, 175, 55, ${this.alpha})`;
c.shadowBlur = 6;
c.shadowColor = "#D4AF37";
c.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
c.fill();
c.restore();
}
}
const sparks: Spark[] = [];
const maxSparks = 35;
const render = () => {
ctx.clearRect(0, 0, width, height);
if (sparks.length < maxSparks && Math.random() > 0.6) {
sparks.push(new Spark());
}
for (let i = sparks.length - 1; i >= 0; i--) {
const s = sparks[i];
s.update();
s.draw(ctx);
if (s.alpha <= 0 || s.y < 0) {
sparks.splice(i, 1);
}
}
animId = requestAnimationFrame(render);
};
render();
return () => {
window.removeEventListener("resize", handleResize);
cancelAnimationFrame(animId);
};
}, []);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
// Simulate luxury API booking delay
setTimeout(() => {
setIsSubmitting(false);
setIsSuccess(true);
}, 2000);
};
return (
<section className="relative min-h-screen py-24 px-6 md:px-12 flex flex-col justify-center items-center overflow-hidden bg-darkbg-light border-b border-gold/10">
{/* Background Spark Canvas */}
<canvas
ref={canvasRef}
className="absolute inset-0 z-0 pointer-events-none mix-blend-screen opacity-70"
/>
<div className="container max-w-4xl mx-auto relative z-10">
{/* Title */}
<div className="mb-12 flex flex-col items-center text-center">
<span className="font-sans text-[10px] tracking-[0.4em] uppercase text-gold font-semibold flex items-center gap-2 mb-3">
<Sparkles size={12} className="animate-pulse" /> S A N C T U A R Y T A B L E
</span>
<h2 className="font-serif text-4xl md:text-6xl font-bold tracking-wide text-foreground uppercase text-glow">
RESERVE A COURT
</h2>
<div className="w-20 h-[1.5px] bg-maroon mt-4" />
</div>
{/* Reservation Card wrapper */}
<div className="relative w-full max-w-2xl mx-auto rounded-xl p-8 md:p-12 glass shadow-[0_30px_70px_rgba(0,0,0,0.8)] border border-gold/20 overflow-hidden">
{/* Inner ornaments */}
<div className="absolute inset-3 border border-gold/10 rounded-lg pointer-events-none z-0" />
<div className="absolute inset-[20px] border border-dashed border-gold/5 rounded-lg pointer-events-none z-0" />
{/* Form states */}
{!isSuccess ? (
<form onSubmit={handleSubmit} className="relative z-10 flex flex-col gap-6">
{/* Form Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Name */}
<div className="flex flex-col relative group">
<label className="font-serif text-[10px] tracking-widest text-gold-dark font-bold uppercase mb-2">
Patron Name
</label>
<input
type="text"
required
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="font-sans text-xs md:text-sm text-stone-900 bg-white border border-gold/30 focus:border-maroon px-4 py-3 outline-none transition-all duration-300 rounded"
placeholder="Enter your full name"
/>
</div>
{/* Email */}
<div className="flex flex-col relative">
<label className="font-serif text-[10px] tracking-widest text-gold-dark font-bold uppercase mb-2">
Digital Address (Email)
</label>
<input
type="email"
required
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
className="font-sans text-xs md:text-sm text-stone-900 bg-white border border-gold/30 focus:border-maroon px-4 py-3 outline-none transition-all duration-300 rounded"
placeholder="name@domain.com"
/>
</div>
{/* Guest Count */}
<div className="flex flex-col relative">
<label className="font-serif text-[10px] tracking-widest text-gold-dark font-bold uppercase mb-2 flex items-center gap-1.5">
<Users size={12} className="text-maroon" /> Assembly Size (Guests)
</label>
<select
value={formData.guests}
onChange={(e) => setFormData({ ...formData, guests: e.target.value })}
className="font-sans text-xs md:text-sm text-stone-900 bg-white border border-gold/30 focus:border-maroon px-4 py-3 outline-none transition-all duration-300 rounded cursor-pointer appearance-none"
>
<option value="1">1 Patron (Solo Dining)</option>
<option value="2">2 Patrons (Duet)</option>
<option value="4">4 Patrons (Quartet)</option>
<option value="6">6 Patrons (Family Court)</option>
<option value="8">8+ Patrons (Royal Sabha)</option>
</select>
</div>
{/* Date */}
<div className="flex flex-col relative">
<label className="font-serif text-[10px] tracking-widest text-gold-dark font-bold uppercase mb-2 flex items-center gap-1.5">
<CalendarDays size={12} className="text-maroon" /> Feast Date
</label>
<input
type="date"
required
value={formData.date}
onChange={(e) => setFormData({ ...formData, date: e.target.value })}
className="font-sans text-xs md:text-sm text-stone-900 bg-white border border-gold/30 focus:border-maroon px-4 py-3 outline-none transition-all duration-300 rounded cursor-pointer"
/>
</div>
{/* Time */}
<div className="flex flex-col relative md:col-span-2">
<label className="font-serif text-[10px] tracking-widest text-gold-dark font-bold uppercase mb-2 flex items-center gap-1.5">
<Clock size={12} className="text-maroon" /> Dining Session
</label>
<select
value={formData.time}
onChange={(e) => setFormData({ ...formData, time: e.target.value })}
className="font-sans text-xs md:text-sm text-stone-900 bg-white border border-gold/30 focus:border-maroon px-4 py-3 outline-none transition-all duration-300 rounded cursor-pointer appearance-none"
>
<option value="16:30">Early Dinner Session - 04:30 PM</option>
<option value="18:00">Sunset Feast Session - 06:00 PM</option>
<option value="19:30">Royal Banquet Session - 07:30 PM</option>
<option value="21:00">Late Dining Session - 09:00 PM</option>
</select>
</div>
</div>
{/* Submit CTA */}
<button
type="submit"
disabled={isSubmitting}
className="clickable w-full mt-6 bg-maroon border border-gold/60 text-white hover:text-white hover:border-maroon-dark py-4 px-6 rounded font-serif text-sm tracking-[0.2em] uppercase transition-all duration-500 flex items-center justify-center gap-3 shadow-[0_10px_20px_rgba(24,135,84,0.12)] relative group overflow-hidden"
>
<span className="absolute inset-0 bg-gold translate-y-full group-hover:translate-y-0 transition-transform duration-500 ease-out z-0" />
<span className="relative z-10 flex items-center gap-3 group-hover:text-darkbg transition-colors duration-500">
{isSubmitting ? (
<>
<div className="w-4 h-4 rounded-full border-2 border-darkbg border-t-transparent animate-spin" />
Auditing Court Availability...
</>
) : (
<>
Confirm Royal Seating <ArrowRight size={16} />
</>
)}
</span>
</button>
</form>
) : (
// Success Feedback State
<div className="relative z-10 py-12 flex flex-col items-center text-center gap-6">
<CheckCircle size={64} className="text-gold drop-shadow-[0_0_10px_rgba(212,175,55,0.6)] animate-[bounce_1.5s_infinite_alternate]" />
<div>
<h3 className="font-serif text-2xl font-bold text-stone-900 tracking-widest uppercase mb-2">
COURT SECURED
</h3>
<span className="font-sans text-[10px] text-maroon font-bold tracking-[0.3em] uppercase block mb-6">
N A L V A R A V U P A T R O N
</span>
<p className="font-sans text-xs md:text-sm text-stone-700 leading-relaxed tracking-wider max-w-md mx-auto">
Namaste, <strong className="text-maroon-dark font-extrabold">{formData.name}</strong>. Your royal table for{" "}
<strong className="text-maroon-dark font-extrabold">{formData.guests} guests</strong> on{" "}
<strong className="text-maroon-dark font-extrabold">{formData.date}</strong> at{" "}
<strong className="text-maroon-dark font-extrabold">{formData.time}</strong> has been provisionally registered. A digital invite seal has been dispatched to{" "}
<strong className="text-maroon-dark font-extrabold">{formData.email}</strong>. We wait to welcome you.
</p>
</div>
<button
onClick={() => setIsSuccess(false)}
className="clickable mt-6 font-serif text-[10px] tracking-[0.2em] uppercase text-maroon hover:text-white border border-maroon/30 hover:bg-maroon px-6 py-2.5 bg-transparent transition-all duration-300 rounded"
>
Book Another Table
</button>
</div>
)}
</div>
</div>
</section>
);
}

View File

@ -0,0 +1,256 @@
"use client";
import { useEffect, useRef } from "react";
import { motion, useScroll, useTransform } from "framer-motion";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import Image from "next/image";
gsap.registerPlugin(ScrollTrigger);
export default function RestaurantStory() {
const sectionRef = useRef<HTMLDivElement>(null);
const parchmentRef = useRef<HTMLDivElement>(null);
const titleRef = useRef<HTMLHeadingElement>(null);
// Framer Motion scroll tracking for simple parallax of background and frames
const { scrollYProgress } = useScroll({
target: sectionRef,
offset: ["start end", "end start"],
});
const yBackground = useTransform(scrollYProgress, [0, 1], ["-10%", "10%"]);
const yImage1 = useTransform(scrollYProgress, [0, 1], ["0%", "-15%"]);
const yImage2 = useTransform(scrollYProgress, [0, 1], ["10%", "-5%"]);
useEffect(() => {
const parchment = parchmentRef.current;
if (!parchment) return;
const ctx = gsap.context(() => {
// Vintage paper reveal: expands vertically like a scroll unrolling
gsap.fromTo(
parchment,
{
clipPath: "polygon(0 48%, 100% 48%, 100% 52%, 0 52%)",
scale: 0.95,
},
{
clipPath: "polygon(0 0, 100% 0, 100% 100%, 0 100%)",
scale: 1,
duration: 2,
ease: "power3.inOut",
scrollTrigger: {
trigger: parchment,
start: "top 80%",
end: "top 30%",
scrub: true,
},
}
);
// Text ink reveal: lines fade and spread slightly on scroll
const textLines = parchment.querySelectorAll(".ink-reveal");
textLines.forEach((line) => {
gsap.fromTo(
line,
{ opacity: 0.1, filter: "blur(4px)", y: 15 },
{
opacity: 1,
filter: "blur(0px)",
y: 0,
duration: 1.2,
scrollTrigger: {
trigger: line,
start: "top 85%",
end: "top 60%",
scrub: true,
},
}
);
});
}, parchment);
return () => ctx.revert();
}, []);
const timelineEvents = [
{
year: "1928",
title: "The Palace Foundation",
text: "Our journey began in a grand Chettinad palace. Built with Burmese teak and Italian marble, the mansion served as a sanctuary of hospitality, welcoming royal emissaries and regional chieftains with legendary feasts.",
},
{
year: "1972",
title: "Preserving Ancient Spices",
text: "The royal recipes were transcribed from palm leaves. Master spice-blenders preserved the art of hand-pounding cardamoms, cinnamon, and red sun-dried chilies in stone mortars, ensuring the flavors remained pristine.",
},
{
year: "2026",
title: "The Modern Legacy Sanctuary",
text: "Today, My Dosa Place brings this heritage into the modern era. We unite century-old cooking methodologies with contemporary culinary presentation, inviting you to dine like royalty in Waterloo.",
},
];
return (
<section
ref={sectionRef}
className="relative min-h-screen py-24 px-6 md:px-12 flex flex-col justify-center items-center overflow-hidden bg-[#f6f3ea]"
>
{/* Traditional Temple Borders */}
<div className="absolute top-0 left-0 w-full h-[6px] bg-gradient-to-r from-gold-dark via-gold-light to-gold-dark opacity-25" />
<div className="container max-w-6xl relative z-10 grid grid-cols-1 lg:grid-cols-12 gap-12 items-center">
{/* Left Side: Vintage Parchment Scroll */}
<div className="lg:col-span-7 w-full">
<div
ref={parchmentRef}
className="relative w-full py-16 px-8 md:px-12 rounded-lg shadow-xl border border-gold/30 overflow-hidden"
style={{
background: "linear-gradient(135deg, #fcf9f2 0%, #f6eeda 100%)",
boxShadow: "inset 0 0 35px rgba(24, 135, 84, 0.05), 0 20px 40px -10px rgba(28, 45, 36, 0.08)",
}}
>
{/* Distressed Paper Edges and Watermark */}
<div className="absolute inset-0 bg-[radial-gradient(#cda570_1px,transparent_1px)] [background-size:24px_24px] opacity-10 pointer-events-none" />
<div className="absolute inset-4 border border-maroon/10 pointer-events-none" />
<div className="absolute inset-[18px] border-2 border-double border-maroon/20 pointer-events-none" />
{/* Brass Corner Accents */}
<div className="absolute top-4 left-4 w-4 h-4 border-t border-l border-maroon/40" />
<div className="absolute top-4 right-4 w-4 h-4 border-t border-r border-maroon/40" />
<div className="absolute bottom-4 left-4 w-4 h-4 border-b border-l border-maroon/40" />
<div className="absolute bottom-4 right-4 w-4 h-4 border-b border-r border-maroon/40" />
{/* Scroll Header */}
<div className="text-center mb-12 flex flex-col items-center">
<span className="font-sans text-[10px] tracking-[0.4em] uppercase text-maroon-light font-bold mb-2">
O U R C H R O N I C L E S
</span>
<h2
ref={titleRef}
className="font-serif text-3xl md:text-5xl font-bold tracking-wide text-maroon uppercase"
>
OUR DOSA CHRONICLES
</h2>
<div className="w-20 h-[1.5px] bg-maroon/30 mt-3" />
</div>
{/* Timeline List */}
<div className="flex flex-col gap-10 relative pl-4 md:pl-8 border-l border-maroon/15">
{timelineEvents.map((event, idx) => (
<div key={idx} className="relative group ink-reveal">
{/* Timeline Node Ring */}
<div className="absolute -left-[21px] md:-left-[37px] top-1.5 w-[13px] h-[13px] rounded-full bg-[#eae0d0] border-[3px] border-maroon group-hover:scale-125 transition-transform duration-300 shadow-sm" />
{/* Event Meta */}
<div className="flex flex-col md:flex-row md:items-baseline gap-1 md:gap-4 mb-2">
<span className="font-serif text-xl md:text-2xl font-black text-maroon-light tracking-wider">
{event.year}
</span>
<h3 className="font-serif text-base md:text-lg font-bold text-wood uppercase tracking-wider">
{event.title}
</h3>
</div>
{/* Event Text */}
<p className="font-sans text-sm text-stone-800 leading-relaxed max-w-lg">
{event.text}
</p>
</div>
))}
</div>
{/* Bottom Seal Decoration */}
<div className="flex justify-center mt-12">
<div className="w-10 h-10 rounded-full border border-maroon/30 flex items-center justify-center font-serif text-[10px] text-maroon font-bold tracking-wider relative">
DR
<div className="absolute inset-1 border border-dashed border-maroon/20 rounded-full" />
</div>
</div>
</div>
</div>
{/* Right Side: Vintage Framed Pictures with Parallax */}
<div className="lg:col-span-5 flex flex-col gap-8 relative items-center justify-center">
{/* Framed Image 1: Mansion Detail */}
<motion.div
className="w-72 h-96 relative border-8 border-wood bg-darkbg rounded shadow-[0_15px_35px_rgba(0,0,0,0.6)] p-2 z-10"
style={{ y: yImage1 }}
>
{/* Brass Inner Frame Border */}
<div className="absolute inset-1 border border-gold-dark/40" />
<div className="w-full h-full relative overflow-hidden">
<Image
src="/images/my_dosa_place_interior_royal.jpg"
alt="Vintage Teak Columns"
fill
sizes="(max-width: 768px) 100vw, 30vw"
className="object-cover brightness-100 hover:scale-105 transition-transform duration-700"
/>
</div>
{/* Title Tag */}
<div className="absolute -bottom-4 left-1/2 -translate-x-1/2 bg-wood border border-gold/40 px-3 py-1 text-[10px] tracking-widest text-[#188754] uppercase font-serif font-bold">
Circa 1928
</div>
</motion.div>
{/* Framed Image 2: Gopuram Temple Arch styled layout */}
<div className="relative self-end lg:-mr-12 -mt-16 z-20 flex flex-col items-center">
<motion.div
className="w-56 h-72 relative overflow-hidden shadow-2xl"
style={{
y: yImage2,
clipPath: "url(#gopuram-arch)",
WebkitClipPath: "url(#gopuram-arch)"
}}
>
<div className="w-full h-full relative">
<Image
src="/images/my_dosa_place_food_royal.png"
alt="Traditional Serving"
fill
sizes="(max-width: 768px) 100vw, 25vw"
className="object-cover brightness-100 hover:scale-105 transition-transform duration-700"
/>
</div>
</motion.div>
{/* Title Tag (Placed outside the clip-path container so it isn't cropped) */}
<div className="absolute -bottom-4 bg-wood border border-gold/40 px-3 py-1 text-[9px] tracking-widest text-[#188754] uppercase font-serif z-30 font-bold shadow-md">
Royal Dining
</div>
</div>
{/* Background Ambient Glow */}
<div className="absolute w-80 h-80 rounded-full bg-maroon/20 blur-[100px] -z-10 pointer-events-none" />
</div>
{/* SVG ClipPath Definition for the Gopuram Temple Arch frame */}
<svg width="0" height="0" className="absolute pointer-events-none">
<defs>
<clipPath id="gopuram-arch" clipPathUnits="objectBoundingBox">
<path d="M 0.5,0
C 0.65,0 0.72,0.06 0.72,0.15
C 0.72,0.18 0.74,0.19 0.76,0.2
C 0.85,0.22 0.88,0.28 0.88,0.35
C 0.88,0.38 0.9,0.39 0.92,0.4
C 0.98,0.42 1,0.48 1,0.55
L 1,1
L 0,1
C 0,0.48 0.02,0.42 0.08,0.4
C 0.1,0.39 0.12,0.38 0.12,0.35
C 0.12,0.28 0.15,0.22 0.24,0.2
C 0.26,0.19 0.28,0.18 0.28,0.15
C 0.28,0.06 0.35,0 0.5,0 Z" />
</clipPath>
</defs>
</svg>
</div>
</section>
);
}

238
src/components/RouteMap.tsx Normal file
View File

@ -0,0 +1,238 @@
"use client";
import { useState } from "react";
import { motion } from "framer-motion";
import { MapPin, Navigation, Compass, Sparkles } from "lucide-react";
export default function RouteMap() {
const [activePin, setActivePin] = useState<string | null>(null);
const landmarks = [
{
id: "airport",
name: "Waterloo Region Airport (YKF)",
shortName: "YKF Airport",
distance: "25 km (28 mins)",
x: 100,
y: 350,
desc: "Direct regional flight connections, linked via Expressway route 85.",
},
{
id: "university",
name: "University of Waterloo",
shortName: "UWaterloo",
distance: "3 km (5 mins)",
x: 280,
y: 220,
desc: "Academic hub located just east of Erb Street West.",
},
{
id: "market",
name: "St. Jacobs Farmers' Market",
shortName: "St. Jacobs",
distance: "8 km (10 mins)",
x: 480,
y: 320,
desc: "Historic local farmers' market offering local produce.",
},
{
id: "restaurant",
name: "My Dosa Place (Waterloo)",
shortName: "My Dosa Place",
distance: "0 km (Destination)",
x: 680,
y: 180,
desc: "Welcome! Waterloo's destination for authentic pure vegetarian South Indian flavors.",
isDestination: true,
},
];
return (
<section className="relative min-h-screen py-24 px-6 md:px-12 flex flex-col justify-center overflow-hidden bg-darkbg border-b border-gold/10">
{/* Background grids */}
<div className="absolute inset-0 bg-[linear-gradient(to_right,#8080800a_1px,transparent_1px),linear-gradient(to_bottom,#8080800a_1px,transparent_1px)] bg-[size:40px_40px] pointer-events-none" />
<div className="container max-w-6xl mx-auto relative z-10">
{/* Title */}
<div className="mb-16 flex flex-col items-center text-center">
<span className="font-sans text-[10px] tracking-[0.4em] uppercase text-gold font-semibold flex items-center gap-2 mb-3">
<Compass size={12} className="animate-spin-slow" /> P I L G R I M A G E
</span>
<h2 className="font-serif text-4xl md:text-6xl font-bold tracking-wide text-foreground uppercase text-glow">
HERITAGE LOCATION MAP
</h2>
<div className="w-20 h-[1.5px] bg-maroon mt-4" />
</div>
{/* Map Canvas Wrapper */}
<div className="relative w-full max-w-4xl mx-auto h-[450px] bg-white/70 rounded-xl border border-gold/30 p-6 shadow-xl overflow-hidden">
{/* Decorative Corner Ornaments */}
<div className="absolute top-3 left-3 w-4 h-4 border-t border-l border-gold/30" />
<div className="absolute top-3 right-3 w-4 h-4 border-t border-r border-gold/30" />
<div className="absolute bottom-3 left-3 w-4 h-4 border-b border-l border-gold/30" />
<div className="absolute bottom-3 right-3 w-4 h-4 border-b border-r border-gold/30" />
{/* SVG Map Canvas */}
<svg
className="w-full h-full min-w-[700px] select-none"
viewBox="0 0 800 450"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
{/* Grid Coordinates watermark */}
<text x="30" y="40" fill="#188754" opacity="0.35" fontSize="10" fontFamily="monospace">MAP: WATERLOO-2026</text>
{/* Stylized River (Vaigai River representation) */}
<path
d="M -50 400 C 150 350 250 180 400 150 C 550 120 650 280 850 250"
stroke="#188754"
strokeWidth="24"
strokeLinecap="round"
opacity="0.08"
/>
<path
d="M -50 400 C 150 350 250 180 400 150 C 550 120 650 280 850 250"
stroke="#188754"
strokeWidth="2"
strokeDasharray="8 8"
opacity="0.3"
/>
{/* Stylized Local Highways */}
<path
d="M 50 20 L 750 430"
stroke="rgba(212, 175, 55, 0.05)"
strokeWidth="1.5"
/>
<path
d="M 50 430 L 750 20"
stroke="rgba(212, 175, 55, 0.05)"
strokeWidth="1.5"
/>
{/* The Main Route Path (Airport -> Temple -> Station -> Restaurant) */}
<path
id="route-path"
d="M 100 350 C 200 300 220 250 280 220 C 350 185 410 380 480 320 C 560 250 600 200 680 180"
stroke="#D4AF37"
strokeWidth="3.5"
strokeDasharray="6 6"
strokeLinecap="round"
opacity="0.65"
className="animate-[dash_20s_linear_infinite]"
/>
{/* Animated Golden Chariot/Car moving along route-path */}
<g>
<circle r="6" fill="#E67E22" className="animate-ping" style={{ animationDuration: "2s" }} />
<path d="M-6 -4 L6 0 L-6 4 Z" fill="#D4AF37" stroke="#0B0504" strokeWidth="1">
<animateMotion
dur="15s"
repeatCount="indefinite"
rotate="auto"
>
<mpath href="#route-path" />
</animateMotion>
</path>
</g>
{/* Render Landmarks Pins */}
{landmarks.map((pin) => {
const isActive = activePin === pin.id;
return (
<g
key={pin.id}
className="cursor-pointer"
onMouseEnter={() => setActivePin(pin.id)}
onMouseLeave={() => setActivePin(null)}
>
{/* Outer breathing ring */}
<circle
cx={pin.x}
cy={pin.y}
r={pin.isDestination ? "18" : "12"}
fill={pin.isDestination ? "rgba(74, 14, 23, 0.4)" : "rgba(212, 175, 55, 0.1)"}
stroke={pin.isDestination ? "#D4AF37" : "rgba(212,175,55,0.4)"}
strokeWidth={pin.isDestination ? "2" : "1"}
className={isActive ? "scale-110 transition-transform duration-300" : "animate-pulse"}
style={{ animationDuration: pin.isDestination ? "1.5s" : "3s" }}
/>
{/* Inner Pin center */}
<circle
cx={pin.x}
cy={pin.y}
r={pin.isDestination ? "6" : "4"}
fill={pin.isDestination ? "#E67E22" : "#D4AF37"}
/>
{/* Landmark Label */}
<text
x={pin.x}
y={pin.y - 25}
textAnchor="middle"
fill={isActive ? "#F3E5AB" : "#D4AF37"}
fontSize={pin.isDestination ? "12" : "10"}
fontWeight={pin.isDestination ? "bold" : "normal"}
fontFamily="var(--font-cinzel), serif"
className="tracking-wider uppercase text-glow"
>
{pin.shortName}
</text>
</g>
);
})}
</svg>
{/* Dynamic interactive information panel overlay */}
<div className="absolute bottom-6 right-6 left-6 md:left-auto md:w-80 p-5 rounded-lg glass border border-gold/20 shadow-2xl z-20 transition-all duration-300">
<h4 className="font-serif text-gold text-xs tracking-[0.2em] uppercase mb-2 flex items-center gap-1.5">
<Navigation size={12} className="animate-pulse" /> Access Route Details
</h4>
{activePin ? (
// Active pin details
(() => {
const pin = landmarks.find((l) => l.id === activePin);
return (
<div>
<h5 className="font-serif text-stone-900 text-sm font-bold tracking-wide uppercase mb-1">
{pin?.name}
</h5>
<span className="font-sans text-[10px] text-gold-dark tracking-widest uppercase block mb-2 font-bold">
Distance: {pin?.distance}
</span>
<p className="font-sans text-[11px] text-stone-700 leading-relaxed tracking-wider">
{pin?.desc}
</p>
</div>
);
})()
) : (
// Default guide text
<div>
<p className="font-sans text-[11px] text-stone-600 leading-relaxed tracking-wide">
Hover over the glowing nodes on the cartographic map to audit pick-up pathways and transit distances.
</p>
</div>
)}
</div>
</div>
</div>
<style jsx>{`
@keyframes dash {
to {
stroke-dashoffset: -120;
}
}
`}</style>
</section>
);
}

View File

@ -0,0 +1,170 @@
"use client";
import { useState } from "react";
import dynamic from "next/dynamic";
import { motion, AnimatePresence } from "framer-motion";
import { Sparkles, ArrowRight, Heart } from "lucide-react";
// Dynamically import FoodCanvas to prevent SSR errors (Three.js relies on document/window)
const FoodCanvas = dynamic(() => import("./FoodCanvas"), {
ssr: false,
loading: () => (
<div className="w-full h-full flex flex-col justify-center items-center gap-4 bg-darkbg-light/30 rounded-full border border-gold/10">
<div className="w-16 h-16 rounded-full border-t-2 border-r-2 border-gold animate-spin" />
<span className="font-sans text-[10px] tracking-[0.2em] uppercase text-gold/60">Loading 3D Culinary Assets...</span>
</div>
),
});
export default function SignatureFoods() {
const [activeIdx, setActiveIdx] = useState(0);
const [isHovered, setIsHovered] = useState(false);
const [liked, setLiked] = useState<Record<number, boolean>>({});
const foods = [
{
name: "Rajamannar Ghee Dosa",
tamilName: "நெய் மசாலா தோசை",
price: "₹550",
description:
"A crispy golden crepe spun from stone-ground organic red rice batter, smeared with spiced garlic-chili podi, filled with an aromatic mash of heritage forest potatoes, and roasted with hand-churned A2 cow ghee.",
highlight: "A2 Ghee Roast • Slow Fermented Batter",
ingredients: ["Red Rice", "A2 Ghee", "Garlic Podi", "Spiced Potato Mash"],
},
{
name: "Chola Samrat Thali",
tamilName: "சோழ சாம்ராட் தட்டு",
price: "₹1,450",
description:
"A grand imperial culinary tribute featuring twenty distinct heirloom delicacies. Served in heavy brass katoris on a fresh banana leaf, incorporating hand-turned aviyal, steaming ponni rice, robust pepper rasam, and elaneer payasam.",
highlight: "Heritage Recipe • Imperial Dining Feast",
ingredients: ["Chettinad Sambar", "Mappillai Samba Rice", "Pepper Rasam", "Elaneer Payasam"],
},
];
const toggleLike = (idx: number, e: React.MouseEvent) => {
e.stopPropagation();
setLiked(prev => ({ ...prev, [idx]: !prev[idx] }));
};
return (
<section className="relative min-h-screen py-24 px-6 md:px-12 flex flex-col justify-center overflow-hidden bg-darkbg-light border-b border-gold/10">
{/* Background Decorative Element */}
<div className="absolute top-[10%] left-[5%] w-72 h-72 rounded-full bg-gold/5 blur-[120px] pointer-events-none" />
<div className="absolute bottom-[10%] right-[5%] w-72 h-72 rounded-full bg-maroon/10 blur-[120px] pointer-events-none" />
<div className="container max-w-6xl mx-auto relative z-10">
{/* Section Title */}
<div className="mb-16 flex flex-col items-center lg:items-start text-center lg:text-left">
<span className="font-sans text-[10px] tracking-[0.4em] uppercase text-gold font-semibold flex items-center gap-2 mb-3">
<Sparkles size={12} className="animate-pulse" /> S I G N A T U R E M A S T E R P I E C E S
</span>
<h2 className="font-serif text-4xl md:text-6xl font-bold tracking-wide text-foreground uppercase leading-tight">
THE ROYAL TASTES
</h2>
<div className="w-24 h-[1px] bg-gradient-to-r from-gold via-gold-dark to-transparent mt-4" />
</div>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 items-center">
{/* Left Side: Interactive Foods Card List */}
<div className="lg:col-span-6 flex flex-col gap-6 order-2 lg:order-1">
{foods.map((food, idx) => {
const isActive = activeIdx === idx;
return (
<div
key={idx}
onClick={() => setActiveIdx(idx)}
onMouseEnter={() => {
setActiveIdx(idx);
setIsHovered(true);
}}
onMouseLeave={() => setIsHovered(false)}
className={`clickable p-6 md:p-8 rounded-lg transition-all duration-500 border relative group overflow-hidden ${
isActive
? "bg-white border-gold/60 shadow-[0_15px_30px_rgba(24,135,84,0.08)]"
: "bg-white/40 border-stone-200 hover:border-gold/40 hover:bg-white"
}`}
>
{/* Decorative glowing gradient border for active */}
{isActive && (
<div className="absolute inset-x-0 bottom-0 h-[2px] bg-gradient-to-r from-transparent via-gold to-transparent" />
)}
{/* Top line with category, price and like */}
<div className="flex justify-between items-start gap-4 mb-4">
<div>
<span className="font-sans text-[9px] tracking-[0.3em] uppercase text-gold-dark font-bold block mb-1">
{food.highlight}
</span>
<h3 className="font-serif text-xl md:text-2xl font-bold text-stone-900 tracking-wide uppercase transition-colors group-hover:text-maroon">
{food.name}
</h3>
<p className="font-serif text-xs text-gold-dark/75 italic font-medium tracking-wide">
{food.tamilName}
</p>
</div>
<div className="flex items-center gap-3">
<span className="font-serif text-lg md:text-xl font-bold text-maroon">
{food.price}
</span>
<button
onClick={(e) => toggleLike(idx, e)}
className="w-8 h-8 rounded-full border border-gold/30 flex items-center justify-center text-maroon hover:bg-maroon/5 hover:border-maroon/40 transition-all duration-300 relative z-10"
>
<Heart size={14} fill={liked[idx] ? "#188754" : "none"} className={liked[idx] ? "scale-110" : ""} />
</button>
</div>
</div>
{/* Body description */}
<p className="font-sans text-xs md:text-sm text-stone-700 leading-relaxed tracking-wide mb-6">
{food.description}
</p>
{/* Ingredients Tags */}
<div className="flex flex-wrap gap-2">
{food.ingredients.map((ing, i) => (
<span
key={i}
className="font-sans text-[9px] tracking-wider text-maroon/80 bg-maroon/5 border border-maroon/10 px-2 py-0.5 rounded-full"
>
{ing}
</span>
))}
</div>
{/* Hover visual arrow reveal */}
<div className="absolute right-4 bottom-4 opacity-0 group-hover:opacity-100 translate-x-4 group-hover:translate-x-0 transition-all duration-300">
<ArrowRight size={16} className="text-gold" />
</div>
</div>
);
})}
</div>
{/* Right Side: Interactive 3D WebGL Canvas Plate */}
<div
className="lg:col-span-6 h-[400px] md:h-[500px] w-full flex items-center justify-center order-1 lg:order-2"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<div className="w-[300px] h-[300px] md:w-[450px] md:h-[450px] relative rounded-full shadow-[0_30px_70px_rgba(0,0,0,0.7)] flex items-center justify-center bg-[radial-gradient(circle,_var(--tw-gradient-stops))] from-maroon/20 via-transparent to-transparent">
{/* Outer decorative circular rings */}
<div className="absolute inset-0 rounded-full border border-gold/10 pointer-events-none scale-105 animate-[spin_40s_linear_infinite]" />
<div className="absolute inset-6 rounded-full border border-gold/5 pointer-events-none scale-[1.03] animate-[spin_60s_linear_infinite_reverse]" />
{/* R3F WebGL Canvas Container */}
<FoodCanvas activeIdx={activeIdx} isHovered={isHovered} />
</div>
</div>
</div>
</div>
</section>
);
}

View File

@ -0,0 +1,48 @@
"use client";
import { useEffect } from "react";
import Lenis from "lenis";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
// Register ScrollTrigger plugin
gsap.registerPlugin(ScrollTrigger);
export default function SmoothScroll({ children }: { children: React.ReactNode }) {
useEffect(() => {
// Initialize Lenis
const lenis = new Lenis({
duration: 1.5,
easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
orientation: "vertical",
gestureOrientation: "vertical",
smoothWheel: true,
wheelMultiplier: 1,
touchMultiplier: 1.5,
});
// Update ScrollTrigger on scroll
const handleScroll = () => {
ScrollTrigger.update();
};
lenis.on("scroll", handleScroll);
// Connect Lenis frame updates to GSAP ticker
const updateTicker = (time: number) => {
lenis.raf(time * 1000);
};
gsap.ticker.add(updateTicker);
gsap.ticker.lagSmoothing(0);
// Clean up
return () => {
lenis.off("scroll", handleScroll);
lenis.destroy();
gsap.ticker.remove(updateTicker);
};
}, []);
return <>{children}</>;
}

View File

@ -0,0 +1,180 @@
"use client";
import { useEffect, useState, useRef } from "react";
import { Volume2, VolumeX } from "lucide-react";
export default function SoundManager() {
const [isPlaying, setIsPlaying] = useState(false);
const audioCtxRef = useRef<AudioContext | null>(null);
// Refs for oscillators and gain nodes to control the drone
const droneNodesRef = useRef<{
oscillators: OscillatorNode[];
gains: GainNode[];
lfo: OscillatorNode | null;
}>({ oscillators: [], gains: [], lfo: null });
// Web Audio Tanpura Drone Synthesizer
const startDrone = () => {
if (!audioCtxRef.current) {
audioCtxRef.current = new (window.AudioContext || (window as any).webkitAudioContext)();
}
const ctx = audioCtxRef.current;
if (ctx.state === "suspended") {
ctx.resume();
}
const masterGain = ctx.createGain();
masterGain.gain.setValueAtTime(0.08, ctx.currentTime); // Soft, unobtrusive volume
masterGain.connect(ctx.destination);
// Warm Lowpass filter to simulate wood resonance
const filter = ctx.createBiquadFilter();
filter.type = "lowpass";
filter.frequency.setValueAtTime(350, ctx.currentTime);
filter.connect(masterGain);
const freqs = [65.41, 98.0, 130.81, 196.0]; // C2, G2, C3, G3 - traditional Indian scale tuning
const oscillators: OscillatorNode[] = [];
const gains: GainNode[] = [];
// Create harmonically rich drone oscillators
freqs.forEach((freq, idx) => {
const osc = ctx.createOscillator();
// Mix sawtooth and triangle for rich but warm string timbre
osc.type = idx % 2 === 0 ? "triangle" : "sine";
osc.frequency.setValueAtTime(freq, ctx.currentTime);
const gain = ctx.createGain();
// Stagger volumes
gain.gain.setValueAtTime(idx === 0 ? 0.4 : 0.25, ctx.currentTime);
osc.connect(gain);
gain.connect(filter);
osc.start();
oscillators.push(osc);
gains.push(gain);
});
// Create LFO (Low Frequency Oscillator) to model the rhythmic strumming (swells) of a tanpura
const lfo = ctx.createOscillator();
lfo.frequency.setValueAtTime(0.12, ctx.currentTime); // Very slow swell (every 8 seconds)
const lfoGain = ctx.createGain();
lfoGain.gain.setValueAtTime(0.15, ctx.currentTime);
lfo.connect(lfoGain);
// Connect LFO to modulate filter cutoff frequency to create organic breathing texture
lfoGain.connect(filter.frequency);
lfo.start();
droneNodesRef.current = { oscillators, gains, lfo };
};
const stopDrone = () => {
const { oscillators, lfo } = droneNodesRef.current;
oscillators.forEach((osc) => {
try { osc.stop(); } catch (e) {}
});
if (lfo) {
try { lfo.stop(); } catch (e) {}
}
droneNodesRef.current = { oscillators: [], gains: [], lfo: null };
};
// Synthesize a traditional temple bell chime
const playBellChime = () => {
if (!audioCtxRef.current || audioCtxRef.current.state === "suspended") return;
const ctx = audioCtxRef.current;
const now = ctx.currentTime;
// Bell tones consist of a fundamental frequency and inharmonic overtones
const frequencies = [600, 770, 930, 1200, 1540];
const bellGain = ctx.createGain();
bellGain.gain.setValueAtTime(0, now);
// Sudden attack, long delay
bellGain.gain.linearRampToValueAtTime(0.04, now + 0.05);
bellGain.gain.exponentialRampToValueAtTime(0.0001, now + 4.5);
bellGain.connect(ctx.destination);
frequencies.forEach((f, index) => {
const osc = ctx.createOscillator();
osc.type = "sine";
osc.frequency.setValueAtTime(f, now);
// Detune slightly for realistic metallic resonance
osc.detune.setValueAtTime(index * 4, now);
osc.connect(bellGain);
osc.start(now);
osc.stop(now + 5.0);
});
};
const toggleSound = () => {
if (isPlaying) {
stopDrone();
setIsPlaying(false);
} else {
startDrone();
setIsPlaying(true);
// Play introductory bell chime
setTimeout(() => {
playBellChime();
}, 500);
}
};
// Periodically ring temple bell (every 22 seconds) for ambient realism
useEffect(() => {
if (!isPlaying) return;
const interval = setInterval(() => {
playBellChime();
}, 22000);
return () => clearInterval(interval);
}, [isPlaying]);
// Stop sound on unmount
useEffect(() => {
return () => {
stopDrone();
};
}, []);
return (
<div className="fixed bottom-6 right-6 z-[999] flex items-center gap-3">
{/* Soundwave animation */}
{isPlaying && (
<div className="hidden sm:flex items-end gap-[3px] h-4 px-2">
<div className="w-[3px] bg-maroon h-full rounded-full animate-[soundwave_1.2s_ease-in-out_infinite_alternate]" style={{ animationDelay: "0.1s" }} />
<div className="w-[3px] bg-maroon h-2/3 rounded-full animate-[soundwave_0.8s_ease-in-out_infinite_alternate]" style={{ animationDelay: "0.4s" }} />
<div className="w-[3px] bg-maroon h-4/5 rounded-full animate-[soundwave_1.0s_ease-in-out_infinite_alternate]" style={{ animationDelay: "0.2s" }} />
<div className="w-[3px] bg-maroon h-1/2 rounded-full animate-[soundwave_0.7s_ease-in-out_infinite_alternate]" style={{ animationDelay: "0.6s" }} />
</div>
)}
{/* Sound Toggle Button */}
<button
onClick={toggleSound}
className="clickable w-12 h-12 rounded-full bg-white border border-maroon/20 flex items-center justify-center text-maroon hover:text-white hover:border-maroon-dark shadow-md hover:shadow-lg transition-all duration-300 relative group overflow-hidden"
aria-label={isPlaying ? "Mute Background Music" : "Play Background Music"}
>
<span className="absolute inset-0 bg-maroon translate-y-full group-hover:translate-y-0 transition-transform duration-300 ease-out z-0" />
<span className="relative z-10">
{isPlaying ? <Volume2 size={20} className="animate-pulse" /> : <VolumeX size={20} />}
</span>
</button>
{/* Tailwind Soundwave Keyframes definition embedded in a style block */}
<style jsx global>{`
@keyframes soundwave {
0% { height: 4px; }
100% { height: 16px; }
}
`}</style>
</div>
);
}

View File

@ -0,0 +1,148 @@
"use client";
import { useState, useRef } from "react";
import { motion } from "framer-motion";
import { Star, Quote, Sparkles } from "lucide-react";
export default function Testimonials() {
const containerRef = useRef<HTMLDivElement>(null);
// Testimonials database
const reviews = [
{
name: "Aishwarya Krishnan",
role: "Culinary Critic, Madras Digest",
quote: "My Dosa Place is not merely a restaurant; it is a culinary sanctuary. The ghee roast dosa had the exact golden ghee crunch that I've only ever tasted in heritage kitchens. The visual focus onto our set table completes this extraordinary sensory theatre.",
rating: 5,
},
{
name: "Dr. Raghavan Pillai",
role: "Heritage Historian",
quote: "Dining here felt like attending a grand royal feast in a Chola palace. The attention to traditional serving etiquette, the stone-paved floor, and the hand-ground masalas are a testament to true preservation of culture.",
rating: 5,
},
{
name: "Siddharth Malhotra",
role: "Luxury Connoisseur",
quote: "The WebGL 3D interface was impressive, but the real-life experience surpassed it. The Chef's storytelling through spices, the live flute tunes, and the incredible banana-leaf thali is unmatched. A true 5-star experience.",
rating: 5,
},
];
// 3D Card Hover Tilt logic (programmatic per card)
const Card = ({ review }: { review: typeof reviews[0] }) => {
const cardRef = useRef<HTMLDivElement>(null);
const [rotateX, setRotateX] = useState(0);
const [rotateY, setRotateY] = useState(0);
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
const card = cardRef.current;
if (!card) return;
const rect = card.getBoundingClientRect();
const width = rect.width;
const height = rect.height;
const mouseX = e.clientX - rect.left - width / 2;
const mouseY = e.clientY - rect.top - height / 2;
// Calculate rotation angles (max 12 degrees)
const rX = -(mouseY / (height / 2)) * 10;
const rY = (mouseX / (width / 2)) * 10;
setRotateX(rX);
setRotateY(rY);
};
const handleMouseLeave = () => {
setRotateX(0);
setRotateY(0);
};
return (
<div
ref={cardRef}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
className="w-full flex-shrink-0 lg:flex-1 h-full perspective-[1000px] z-10"
>
<motion.div
animate={{ rotateX, rotateY }}
transition={{ type: "spring", stiffness: 300, damping: 25 }}
className="clickable h-full flex flex-col justify-between p-8 rounded-xl glass border border-gold/15 shadow-[0_20px_50px_rgba(0,0,0,0.6)] relative overflow-hidden group"
style={{ transformStyle: "preserve-3d" }}
>
{/* Decorative Corner Borders */}
<div className="absolute top-2 left-2 w-3 h-3 border-t border-l border-gold/30" />
<div className="absolute top-2 right-2 w-3 h-3 border-t border-r border-gold/30" />
<div className="absolute bottom-2 left-2 w-3 h-3 border-b border-l border-gold/30" />
<div className="absolute bottom-2 right-2 w-3 h-3 border-b border-r border-gold/30" />
{/* Background Ambient Glow */}
<div className="absolute inset-0 bg-gradient-to-br from-gold/5 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none" />
{/* Quote Icon */}
<div className="flex justify-between items-center mb-6">
<div className="flex gap-1">
{Array.from({ length: review.rating }).map((_, i) => (
<Star
key={i}
size={14}
className="text-gold fill-gold drop-shadow-[0_0_5px_rgba(212,175,55,0.6)] animate-pulse"
style={{ animationDelay: `${i * 0.2}s` }}
/>
))}
</div>
<Quote size={24} className="text-gold/25 group-hover:text-gold/50 transition-colors duration-300" />
</div>
{/* Review Text */}
<p className="font-sans text-xs md:text-sm text-stone-700 leading-relaxed tracking-wider mb-8 italic">
"{review.quote}"
</p>
{/* Reviewer Details */}
<div className="border-t border-gold/15 pt-4 flex flex-col">
<span className="font-serif text-sm font-bold text-stone-900 tracking-widest uppercase">
{review.name}
</span>
<span className="font-sans text-[10px] text-gold-dark font-bold tracking-widest uppercase mt-0.5">
{review.role}
</span>
</div>
</motion.div>
</div>
);
};
return (
<section
ref={containerRef}
className="relative min-h-screen py-24 px-6 md:px-12 flex flex-col justify-center overflow-hidden bg-darkbg border-b border-gold/10"
>
{/* Background Graphic elements */}
<div className="absolute top-[20%] right-[-10%] w-96 h-96 rounded-full bg-maroon/10 blur-[150px] pointer-events-none" />
<div className="absolute bottom-[20%] left-[-10%] w-96 h-96 rounded-full bg-gold/5 blur-[150px] pointer-events-none" />
<div className="container max-w-6xl mx-auto relative z-10">
{/* Title */}
<div className="mb-16 flex flex-col items-center text-center">
<span className="font-sans text-[10px] tracking-[0.4em] uppercase text-gold font-semibold flex items-center gap-2 mb-3">
<Sparkles size={12} className="animate-pulse" /> H O N O R E D P A T R O N S
</span>
<h2 className="font-serif text-4xl md:text-6xl font-bold tracking-wide text-foreground uppercase text-glow">
GUEST CRITICS & DIARIES
</h2>
<div className="w-20 h-[1.5px] bg-gold mt-4" />
</div>
{/* Reviews Cards Grid */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 items-stretch min-h-[380px]">
{reviews.map((review, idx) => (
<Card key={idx} review={review} />
))}
</div>
</div>
</section>
);
}