Brightsmile
Dental clinic site starring Molly, an animated tooth mascot whose eyes follow your cursor — with services, whitening before/after, team, reviews and online booking.
app/page.tsx
"use client";
import { AnimatePresence, motion, useMotionValue, useSpring, useTransform } from "motion/react";
import {
Baby,
CalendarCheck,
Check,
ChevronsLeftRight,
Clock,
HeartPulse,
MapPin,
Menu,
Phone,
Plus,
ShieldCheck,
Smile,
Sparkles,
Star,
Stethoscope,
X,
} from "lucide-react";
import { useEffect, useState, type FormEvent } from "react";
const ease = [0.22, 1, 0.36, 1] as const;
const reveal = {
initial: { opacity: 0, y: 24 },
whileInView: { opacity: 1, y: 0 },
viewport: { once: true, amount: 0.25 },
transition: { duration: 0.7, ease },
};
/* ------------------------------------------------------------------ mascot */
/** "Molly" — an original tooth mascot. Eyes follow the cursor, she blinks, waves and floats. */
function ToothMascot({ className = "" }: { className?: string }) {
const mx = useMotionValue(0);
const my = useMotionValue(0);
const eyeX = useSpring(useTransform(mx, [-1, 1], [-4, 4]), { stiffness: 200, damping: 20 });
const eyeY = useSpring(useTransform(my, [-1, 1], [-3, 3]), { stiffness: 200, damping: 20 });
useEffect(() => {
const onMove = (e: PointerEvent) => {
mx.set((e.clientX / window.innerWidth) * 2 - 1);
my.set((e.clientY / window.innerHeight) * 2 - 1);
};
window.addEventListener("pointermove", onMove);
return () => window.removeEventListener("pointermove", onMove);
}, [mx, my]);
const blink = { scaleY: [1, 1, 0.1, 1] };
const blinkT = { duration: 4, times: [0, 0.92, 0.96, 1], repeat: Infinity, ease: "easeInOut" as const };
return (
<motion.svg viewBox="0 0 240 260" className={className} role="img" aria-label="Molly the tooth mascot waving" animate={{ y: [0, -12, 0] }} transition={{ duration: 4, repeat: Infinity, ease: "easeInOut" }}>
<defs>
<linearGradient id="bs-tooth" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#ffffff" />
<stop offset="100%" stopColor="#e6f6f4" />
</linearGradient>
</defs>
{/* shadow */}
<motion.ellipse cx="120" cy="248" rx="58" ry="8" fill="#0f766e" opacity="0.12" animate={{ rx: [58, 48, 58], opacity: [0.12, 0.07, 0.12] }} transition={{ duration: 4, repeat: Infinity, ease: "easeInOut" }} />
{/* waving arm (behind body) */}
<motion.g style={{ transformBox: "view-box", transformOrigin: "52px 120px" }} animate={{ rotate: [0, -28, 6, -28, 0, 0] }} transition={{ duration: 2.4, repeat: Infinity, repeatDelay: 1.6, ease: "easeInOut" }}>
<path d="M54 122 C 34 112 22 94 18 76" stroke="#5eead4" strokeWidth="11" strokeLinecap="round" fill="none" />
<circle cx="17" cy="72" r="10" fill="#5eead4" />
</motion.g>
{/* arm holding toothbrush */}
<motion.g style={{ transformBox: "view-box", transformOrigin: "186px 128px" }} animate={{ rotate: [0, 6, 0] }} transition={{ duration: 1.6, repeat: Infinity, ease: "easeInOut" }}>
<path d="M186 128 C 204 136 214 146 220 158" stroke="#5eead4" strokeWidth="11" strokeLinecap="round" fill="none" />
<rect x="206" y="112" width="10" height="72" rx="5" fill="#0d9488" transform="rotate(-24 211 148)" />
<rect x="198" y="96" width="20" height="16" rx="4" fill="#ffffff" stroke="#99f6e4" strokeWidth="3" transform="rotate(-24 211 148)" />
<circle cx="221" cy="160" r="10" fill="#5eead4" />
</motion.g>
{/* body */}
<path
d="M78 30 C 40 30 26 62 32 98 C 38 134 46 190 62 214 C 72 230 86 222 88 198 C 91 170 98 156 120 156 C 142 156 149 170 152 198 C 154 222 168 230 178 214 C 194 190 202 134 208 98 C 214 62 200 30 162 30 C 140 30 132 42 120 42 C 108 42 100 30 78 30 Z"
fill="url(#bs-tooth)"
stroke="#99f6e4"
strokeWidth="4"
/>
{/* shine */}
<path d="M62 56 C 52 66 50 82 52 94" stroke="#ffffff" strokeWidth="8" strokeLinecap="round" fill="none" opacity="0.9" />
{/* cheeks */}
<circle cx="78" cy="118" r="11" fill="#fda4af" opacity="0.55" />
<circle cx="162" cy="118" r="11" fill="#fda4af" opacity="0.55" />
{/* eyes */}
{[92, 148].map((cx) => (
<g key={cx}>
<motion.ellipse cx={cx} cy="94" rx="13" ry="15" fill="#ffffff" stroke="#134e4a" strokeWidth="3" style={{ transformBox: "fill-box", transformOrigin: "center" }} animate={blink} transition={blinkT} />
<motion.g style={{ x: eyeX, y: eyeY }}>
<motion.circle cx={cx} cy="96" r="6.5" fill="#134e4a" style={{ transformBox: "fill-box", transformOrigin: "center" }} animate={blink} transition={blinkT} />
<circle cx={cx + 2.5} cy="93" r="2" fill="#ffffff" />
</motion.g>
</g>
))}
{/* smile */}
<path d="M100 124 Q 120 144 140 124" stroke="#134e4a" strokeWidth="4.5" strokeLinecap="round" fill="none" />
{/* sparkles */}
{[
{ x: 206, y: 50, d: 0 },
{ x: 34, y: 34, d: 0.8 },
{ x: 190, y: 22, d: 1.5 },
].map((s) => (
<motion.path
key={`${s.x}-${s.y}`}
d={`M${s.x} ${s.y - 9} L${s.x + 3} ${s.y - 3} L${s.x + 9} ${s.y} L${s.x + 3} ${s.y + 3} L${s.x} ${s.y + 9} L${s.x - 3} ${s.y + 3} L${s.x - 9} ${s.y} L${s.x - 3} ${s.y - 3} Z`}
fill="#facc15"
style={{ transformBox: "fill-box", transformOrigin: "center" }}
animate={{ scale: [0, 1, 0], rotate: [0, 90, 180] }}
transition={{ duration: 2, delay: s.d, repeat: Infinity, repeatDelay: 0.6 }}
/>
))}
</motion.svg>
);
}
/* --------------------------------------------------------------------- nav */
function Nav() {
const [open, setOpen] = useState(false);
const links = [
{ href: "#services", label: "Services" },
{ href: "#results", label: "Results" },
{ href: "#team", label: "Dentists" },
{ href: "#faq", label: "FAQ" },
];
return (
<header className="sticky top-0 z-40 border-b border-teal-900/5 bg-white/80 backdrop-blur-lg">
<div className="mx-auto flex h-16 max-w-6xl items-center justify-between px-5">
<a href="#" className="flex items-center gap-2 font-semibold text-teal-950">
<span className="grid size-9 place-items-center rounded-xl bg-teal-600 text-white"><Smile className="size-5" /></span>
Brightsmile
</a>
<nav className="hidden items-center gap-7 text-sm text-teal-900/70 md:flex">
{links.map((l) => (
<a key={l.href} href={l.href} className="transition-colors hover:text-teal-950">{l.label}</a>
))}
</nav>
<div className="flex items-center gap-2">
<a href="tel:+911234567890" className="hidden items-center gap-1.5 text-sm font-medium text-teal-900 lg:flex"><Phone className="size-4" /> +91 12345 67890</a>
<a href="#book" className="rounded-full bg-teal-600 px-4 py-2 text-sm font-medium text-white shadow-lg shadow-teal-600/25 transition hover:bg-teal-700">Book visit</a>
<button onClick={() => setOpen((o) => !o)} aria-label={open ? "Close menu" : "Open menu"} className="grid size-9 place-items-center rounded-lg text-teal-900 md:hidden">
{open ? <X className="size-5" /> : <Menu className="size-5" />}
</button>
</div>
</div>
<AnimatePresence>
{open && (
<motion.nav initial={{ height: 0 }} animate={{ height: "auto" }} exit={{ height: 0 }} className="overflow-hidden border-t border-teal-900/5 md:hidden">
<div className="space-y-1 px-5 py-3">
{links.map((l) => (
<a key={l.href} href={l.href} onClick={() => setOpen(false)} className="block rounded-lg px-2 py-2 text-sm text-teal-900">{l.label}</a>
))}
</div>
</motion.nav>
)}
</AnimatePresence>
</header>
);
}
/* -------------------------------------------------------------------- hero */
function Hero() {
return (
<section className="relative overflow-hidden">
<div aria-hidden className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,#ccfbf1,transparent_55%),radial-gradient(ellipse_at_bottom_left,#e0f2fe,transparent_50%)]" />
<motion.div aria-hidden className="absolute -right-24 top-20 size-80 rounded-full bg-teal-200/40 blur-3xl" animate={{ scale: [1, 1.15, 1] }} transition={{ duration: 9, repeat: Infinity }} />
<div className="relative mx-auto grid max-w-6xl items-center gap-12 px-5 pb-20 pt-14 lg:grid-cols-[1.1fr_1fr] lg:pt-20">
<div>
<motion.span initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} className="inline-flex items-center gap-2 rounded-full bg-white px-3 py-1 text-xs font-medium text-teal-800 shadow-sm ring-1 ring-teal-100">
<span className="flex text-amber-400">{Array.from({ length: 5 }).map((_, i) => <Star key={i} className="size-3 fill-current" />)}</span>
4.9 from 2,300+ happy patients
</motion.span>
<motion.h1 initial={{ opacity: 0, y: 24, filter: "blur(8px)" }} animate={{ opacity: 1, y: 0, filter: "blur(0px)" }} transition={{ duration: 0.9, ease }} className="mt-6 text-balance text-5xl font-semibold leading-[1.05] tracking-tight text-teal-950 sm:text-6xl">
Gentle dentistry for a{" "}
<span className="relative whitespace-nowrap text-teal-600">
brighter smile
<svg aria-hidden viewBox="0 0 300 20" className="absolute -bottom-2 left-0 w-full">
<motion.path d="M4 14 Q 150 -2 296 12" stroke="#5eead4" strokeWidth="6" strokeLinecap="round" fill="none" initial={{ pathLength: 0 }} animate={{ pathLength: 1 }} transition={{ duration: 1, delay: 0.7, ease }} />
</svg>
</span>
</motion.h1>
<motion.p initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.2, duration: 0.8, ease }} className="mt-6 max-w-lg text-lg text-teal-900/70">
Cleanings, whitening, aligners and implants in a calm, modern clinic — with same-week appointments and no surprises on the bill.
</motion.p>
<motion.div initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.35, duration: 0.8, ease }} className="mt-9 flex flex-wrap gap-3">
<a href="#book" className="inline-flex items-center gap-2 rounded-full bg-teal-600 px-6 py-3.5 text-sm font-medium text-white shadow-xl shadow-teal-600/25 transition hover:-translate-y-0.5 hover:bg-teal-700">
<CalendarCheck className="size-4" /> Book an appointment
</a>
<a href="#services" className="rounded-full bg-white px-6 py-3.5 text-sm font-medium text-teal-900 ring-1 ring-teal-200 transition hover:bg-teal-50">Our services</a>
</motion.div>
<div className="mt-10 grid max-w-md grid-cols-3 gap-4">
{[
{ n: "15+", l: "Years of care" },
{ n: "8", l: "Specialists" },
{ n: "0%", l: "EMI options" },
].map((s) => (
<div key={s.l}>
<p className="text-2xl font-semibold text-teal-950">{s.n}</p>
<p className="text-xs text-teal-900/60">{s.l}</p>
</div>
))}
</div>
</div>
<motion.div initial={{ opacity: 0, scale: 0.9 }} animate={{ opacity: 1, scale: 1 }} transition={{ duration: 0.9, delay: 0.2, ease }} className="relative mx-auto w-full max-w-sm">
<div className="absolute inset-6 rounded-full bg-gradient-to-br from-teal-100 to-sky-100" />
<ToothMascot className="relative w-full drop-shadow-xl" />
<motion.div initial={{ opacity: 0, x: -20 }} animate={{ opacity: 1, x: 0 }} transition={{ delay: 1 }} className="absolute -left-4 top-10 rounded-2xl bg-white p-3 shadow-xl shadow-teal-900/10 sm:-left-10">
<p className="flex items-center gap-2 text-xs font-medium text-teal-950"><Sparkles className="size-4 text-amber-400" /> Hi! I'm Molly 👋</p>
</motion.div>
<motion.div initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} transition={{ delay: 1.2 }} className="absolute -right-2 bottom-16 rounded-2xl bg-white p-3 shadow-xl shadow-teal-900/10 sm:-right-8">
<p className="text-xs text-teal-900/60">Next available</p>
<p className="flex items-center gap-1.5 text-sm font-semibold text-teal-950"><Clock className="size-4 text-teal-600" /> Today, 4:30 PM</p>
</motion.div>
</motion.div>
</div>
</section>
);
}
/* ---------------------------------------------------------------- services */
const SERVICES = [
{ icon: Sparkles, title: "Teeth whitening", text: "Up to 8 shades brighter in one 60-minute session.", price: "from ₹6,999" },
{ icon: Smile, title: "Clear aligners", text: "Invisible aligners with a 3D preview of your new smile.", price: "from ₹59,999" },
{ icon: ShieldCheck, title: "Cleaning & check-up", text: "Ultrasonic scaling, polish and a full digital X-ray.", price: "from ₹1,499" },
{ icon: Stethoscope, title: "Dental implants", text: "Natural-looking, lifelong replacements for missing teeth.", price: "from ₹34,999" },
{ icon: Baby, title: "Kids dentistry", text: "Friendly first visits, sealants and fluoride care.", price: "from ₹999" },
{ icon: HeartPulse, title: "Emergency care", text: "Same-day appointments for pain, chips and swelling.", price: "Same day" },
];
function Services() {
return (
<section id="services" className="scroll-mt-20 px-5 py-24">
<div className="mx-auto max-w-6xl">
<motion.div {...reveal} className="max-w-2xl">
<p className="text-sm font-semibold text-teal-600">Services</p>
<h2 className="mt-2 text-4xl font-semibold tracking-tight text-teal-950">Everything your smile needs, under one roof</h2>
</motion.div>
<div className="mt-12 grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
{SERVICES.map((s, i) => (
<motion.div key={s.title} {...reveal} transition={{ ...reveal.transition, delay: (i % 3) * 0.08 }} whileHover={{ y: -6 }} className="group rounded-3xl border border-teal-100 bg-white p-7 transition-shadow hover:shadow-xl hover:shadow-teal-900/5">
<span className="grid size-12 place-items-center rounded-2xl bg-teal-50 text-teal-600 transition-colors group-hover:bg-teal-600 group-hover:text-white">
<s.icon className="size-6" />
</span>
<h3 className="mt-6 text-lg font-semibold text-teal-950">{s.title}</h3>
<p className="mt-2 text-sm leading-relaxed text-teal-900/65">{s.text}</p>
<p className="mt-5 text-sm font-medium text-teal-700">{s.price}</p>
</motion.div>
))}
</div>
</div>
</section>
);
}
/* ---------------------------------------------------------------- results */
function SmileRow({ shade }: { shade: string }) {
return (
<svg viewBox="0 0 400 220" className="size-full" aria-hidden>
<rect width="400" height="220" fill="#fecdd3" />
<path d="M20 70 Q 200 -10 380 70 L 380 150 Q 200 230 20 150 Z" fill="#9f1239" />
<path d="M40 78 Q 200 10 360 78 L 360 112 Q 200 100 40 112 Z" fill={shade} />
<path d="M40 120 Q 200 108 360 120 L 360 140 Q 200 200 40 140 Z" fill={shade} />
{[80, 125, 170, 215, 260, 305].map((x) => (
<line key={x} x1={x + 10} y1={x === 170 || x === 215 ? 52 : 64} x2={x + 10} y2="112" stroke="#00000014" strokeWidth="2" />
))}
</svg>
);
}
function Results() {
const [pos, setPos] = useState(50);
return (
<section id="results" className="scroll-mt-20 bg-teal-950 px-5 py-24 text-white">
<div className="mx-auto grid max-w-6xl items-center gap-12 lg:grid-cols-2">
<motion.div {...reveal}>
<p className="text-sm font-semibold text-teal-300">Real results</p>
<h2 className="mt-2 text-4xl font-semibold tracking-tight">See the difference in one visit</h2>
<p className="mt-4 max-w-md text-teal-100/70">Drag the slider to compare before and after our in-clinic whitening. Results shown are typical for a single 60-minute session.</p>
<ul className="mt-8 space-y-3 text-sm text-teal-50">
{["Enamel-safe, dentist-supervised gel", "No sensitivity for 9 in 10 patients", "Lasts 1–3 years with simple care"].map((t) => (
<li key={t} className="flex items-center gap-3"><span className="grid size-5 place-items-center rounded-full bg-teal-400 text-teal-950"><Check className="size-3" /></span>{t}</li>
))}
</ul>
</motion.div>
<motion.div {...reveal} className="relative aspect-[20/11] select-none overflow-hidden rounded-3xl ring-1 ring-white/10">
<div className="absolute inset-0"><SmileRow shade="#ffffff" /></div>
<div className="absolute inset-0" style={{ clipPath: `inset(0 ${100 - pos}% 0 0)` }}><SmileRow shade="#e9d8a6" /></div>
<span className="absolute left-3 top-3 rounded-full bg-black/50 px-2.5 py-1 text-xs font-medium">Before</span>
<span className="absolute right-3 top-3 rounded-full bg-white px-2.5 py-1 text-xs font-medium text-teal-950">After</span>
<div className="pointer-events-none absolute inset-y-0 w-0.5 -translate-x-1/2 bg-white" style={{ left: `${pos}%` }}>
<span className="absolute left-1/2 top-1/2 grid size-10 -translate-x-1/2 -translate-y-1/2 place-items-center rounded-full bg-white text-teal-900 shadow-lg"><ChevronsLeftRight className="size-4" /></span>
</div>
<input type="range" min={0} max={100} value={pos} onChange={(e) => setPos(Number(e.target.value))} aria-label="Compare before and after whitening" className="absolute inset-0 h-full w-full cursor-ew-resize opacity-0" />
</motion.div>
</div>
</section>
);
}
/* -------------------------------------------------------------------- team */
const TEAM = [
{ name: "Dr. Ananya Rao", role: "Cosmetic dentist", years: 12, tone: "from-teal-400 to-emerald-600" },
{ name: "Dr. Vikram Shah", role: "Implantologist", years: 15, tone: "from-sky-400 to-blue-600" },
{ name: "Dr. Meera Iyer", role: "Orthodontist", years: 9, tone: "from-violet-400 to-fuchsia-600" },
{ name: "Dr. Rohan Das", role: "Pediatric dentist", years: 7, tone: "from-amber-400 to-orange-600" },
];
function Team() {
return (
<section id="team" className="scroll-mt-20 px-5 py-24">
<div className="mx-auto max-w-6xl">
<motion.h2 {...reveal} className="text-4xl font-semibold tracking-tight text-teal-950">Meet your dentists</motion.h2>
<div className="mt-12 grid gap-5 sm:grid-cols-2 lg:grid-cols-4">
{TEAM.map((d, i) => (
<motion.div key={d.name} {...reveal} transition={{ ...reveal.transition, delay: i * 0.08 }} className="group">
<div className={`relative aspect-[4/5] overflow-hidden rounded-3xl bg-gradient-to-br ${d.tone}`}>
<span className="absolute inset-0 grid place-items-center text-6xl font-semibold text-white/90 transition-transform duration-500 group-hover:scale-110">
{d.name.replace("Dr. ", "").split(" ").map((n) => n[0]).join("")}
</span>
<span className="absolute bottom-3 left-3 rounded-full bg-white/90 px-2.5 py-1 text-xs font-medium text-teal-950 backdrop-blur">{d.years} yrs experience</span>
</div>
<p className="mt-4 font-semibold text-teal-950">{d.name}</p>
<p className="text-sm text-teal-900/60">{d.role}</p>
</motion.div>
))}
</div>
</div>
</section>
);
}
/* --------------------------------------------------------------- reviews */
const REVIEWS = [
{ name: "Kavya M.", text: "I used to dread the dentist. Dr. Ananya explained every step and I felt nothing. My kids now ask when we're going back!" },
{ name: "Arjun P.", text: "Whitening results were incredible — friends asked if I'd been on holiday. Clear pricing upfront, no surprises." },
{ name: "Sara K.", text: "Booked online at 9am, seen by 4pm for a broken tooth. Calm clinic, kind staff, spotless rooms." },
];
function Reviews() {
return (
<section className="bg-teal-50/60 px-5 py-24">
<div className="mx-auto max-w-6xl">
<motion.h2 {...reveal} className="text-center text-4xl font-semibold tracking-tight text-teal-950">Smiles we've made</motion.h2>
<div className="mt-12 grid gap-5 md:grid-cols-3">
{REVIEWS.map((r, i) => (
<motion.figure key={r.name} {...reveal} transition={{ ...reveal.transition, delay: i * 0.1 }} className="rounded-3xl bg-white p-7 shadow-sm ring-1 ring-teal-100">
<div className="flex text-amber-400">{Array.from({ length: 5 }).map((_, k) => <Star key={k} className="size-4 fill-current" />)}</div>
<blockquote className="mt-4 leading-relaxed text-teal-900/80">“{r.text}”</blockquote>
<figcaption className="mt-6 text-sm font-semibold text-teal-950">{r.name}</figcaption>
</motion.figure>
))}
</div>
<motion.div {...reveal} className="mt-12 flex flex-wrap items-center justify-center gap-x-10 gap-y-3 text-sm font-medium text-teal-900/50">
<span>We accept:</span>
{["Star Health", "HDFC ERGO", "ICICI Lombard", "Niva Bupa", "Care Health"].map((n) => <span key={n}>{n}</span>)}
</motion.div>
</div>
</section>
);
}
/* ---------------------------------------------------------------- booking */
const TIMES = ["10:00", "11:30", "13:00", "15:00", "16:30", "18:00"];
function Booking() {
const [days, setDays] = useState<{ key: string; dow: string; date: number; month: string }[]>([]);
const [day, setDay] = useState<string | null>(null);
const [time, setTime] = useState<string | null>(null);
const [errors, setErrors] = useState<Record<string, string>>({});
const [state, setState] = useState<"idle" | "loading" | "done">("idle");
const [summary, setSummary] = useState("");
// Computed after mount so the server and browser render the same HTML.
useEffect(() => {
const out = [];
const now = new Date();
for (let i = 0; i < 14 && out.length < 7; i++) {
const d = new Date(now.getFullYear(), now.getMonth(), now.getDate() + i);
if (d.getDay() === 0) continue; // closed on Sundays
out.push({ key: d.toDateString(), dow: d.toLocaleDateString("en-IN", { weekday: "short" }), date: d.getDate(), month: d.toLocaleDateString("en-IN", { month: "short" }) });
}
setDays(out);
}, []);
async function submit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
const f = new FormData(e.currentTarget);
const errs: Record<string, string> = {};
if (String(f.get("name")).trim().length < 2) errs.name = "Please enter your name.";
if (!/^[+\d][\d\s-]{8,}$/.test(String(f.get("phone")).trim())) errs.phone = "Please enter a valid phone number.";
if (!day) errs.day = "Choose a day.";
if (!time) errs.time = "Choose a time.";
setErrors(errs);
if (Object.keys(errs).length) return;
setState("loading");
// Replace with your booking API (Cal.com, Google Calendar, your clinic system…).
await new Promise((r) => setTimeout(r, 900));
setSummary(`${f.get("service")} · ${day} at ${time}`);
setState("done");
}
return (
<section id="book" className="scroll-mt-20 px-5 py-24">
<div className="mx-auto grid max-w-6xl gap-12 lg:grid-cols-[1fr_1.2fr]">
<motion.div {...reveal}>
<p className="text-sm font-semibold text-teal-600">Book online</p>
<h2 className="mt-2 text-4xl font-semibold tracking-tight text-teal-950">Pick a time that suits you</h2>
<p className="mt-4 max-w-md text-teal-900/70">Same-week appointments, evening slots and a reminder on WhatsApp the day before.</p>
<div className="mt-8 space-y-4 text-sm text-teal-900/80">
<p className="flex items-center gap-3"><MapPin className="size-5 text-teal-600" /> 12 Lake View Road, Bengaluru 560001</p>
<p className="flex items-center gap-3"><Clock className="size-5 text-teal-600" /> Mon–Sat, 10:00 AM – 8:00 PM</p>
<p className="flex items-center gap-3"><Phone className="size-5 text-teal-600" /> +91 12345 67890</p>
</div>
<div className="mt-10 hidden w-40 lg:block"><ToothMascot className="w-full" /></div>
</motion.div>
<motion.div {...reveal} className="rounded-3xl bg-white p-6 shadow-2xl shadow-teal-900/10 ring-1 ring-teal-100 sm:p-8">
<AnimatePresence mode="wait">
{state === "done" ? (
<motion.div key="done" initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} className="py-10 text-center">
<motion.span initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ type: "spring", delay: 0.1 }} className="mx-auto grid size-16 place-items-center rounded-full bg-teal-600 text-white"><Check className="size-8" /></motion.span>
<h3 className="mt-6 text-2xl font-semibold text-teal-950">You're booked!</h3>
<p className="mt-2 text-teal-900/70">{summary}</p>
<p className="mt-1 text-sm text-teal-900/50">We'll confirm on WhatsApp shortly.</p>
<button onClick={() => { setState("idle"); setDay(null); setTime(null); }} className="mt-6 text-sm font-medium text-teal-700 hover:underline">Book another visit</button>
</motion.div>
) : (
<motion.form key="form" onSubmit={submit} noValidate exit={{ opacity: 0 }} className="space-y-6">
<div className="grid gap-4 sm:grid-cols-2">
<label className="block text-sm font-medium text-teal-950">
Full name
<input name="name" autoComplete="name" aria-invalid={Boolean(errors.name)} className="mt-1.5 h-11 w-full rounded-xl border border-teal-100 px-3 text-sm font-normal outline-none focus:border-teal-500 focus:ring-4 focus:ring-teal-500/10" />
{errors.name && <span className="mt-1 block text-xs font-normal text-rose-600">{errors.name}</span>}
</label>
<label className="block text-sm font-medium text-teal-950">
Phone
<input name="phone" type="tel" autoComplete="tel" placeholder="+91" aria-invalid={Boolean(errors.phone)} className="mt-1.5 h-11 w-full rounded-xl border border-teal-100 px-3 text-sm font-normal outline-none focus:border-teal-500 focus:ring-4 focus:ring-teal-500/10" />
{errors.phone && <span className="mt-1 block text-xs font-normal text-rose-600">{errors.phone}</span>}
</label>
</div>
<label className="block text-sm font-medium text-teal-950">
Service
<select name="service" className="mt-1.5 h-11 w-full rounded-xl border border-teal-100 bg-white px-3 text-sm font-normal outline-none focus:border-teal-500">
{SERVICES.map((s) => <option key={s.title}>{s.title}</option>)}
</select>
</label>
<fieldset>
<legend className="text-sm font-medium text-teal-950">Day</legend>
<div className="mt-2 grid grid-cols-4 gap-2 sm:grid-cols-7">
{days.map((d) => (
<button type="button" key={d.key} onClick={() => setDay(`${d.dow} ${d.date} ${d.month}`)} aria-pressed={day === `${d.dow} ${d.date} ${d.month}`} className={`rounded-xl border py-2 text-center transition ${day === `${d.dow} ${d.date} ${d.month}` ? "border-teal-600 bg-teal-600 text-white" : "border-teal-100 text-teal-950 hover:border-teal-300"}`}>
<span className="block text-[11px] opacity-70">{d.dow}</span>
<span className="block text-lg font-semibold">{d.date}</span>
</button>
))}
{days.length === 0 && Array.from({ length: 7 }).map((_, i) => <span key={i} className="h-[62px] animate-pulse rounded-xl bg-teal-50" />)}
</div>
{errors.day && <p className="mt-1 text-xs text-rose-600">{errors.day}</p>}
</fieldset>
<fieldset>
<legend className="text-sm font-medium text-teal-950">Time</legend>
<div className="mt-2 grid grid-cols-3 gap-2">
{TIMES.map((t, i) => {
const full = i === 2;
return (
<button type="button" key={t} disabled={full} onClick={() => setTime(t)} aria-pressed={time === t} className={`rounded-xl border py-2.5 text-sm font-medium transition ${time === t ? "border-teal-600 bg-teal-600 text-white" : "border-teal-100 text-teal-950 hover:border-teal-300"} disabled:cursor-not-allowed disabled:text-teal-900/30 disabled:line-through`}>
{t}
</button>
);
})}
</div>
{errors.time && <p className="mt-1 text-xs text-rose-600">{errors.time}</p>}
</fieldset>
<button disabled={state === "loading"} className="flex h-12 w-full items-center justify-center gap-2 rounded-xl bg-teal-600 text-sm font-semibold text-white shadow-lg shadow-teal-600/25 transition hover:bg-teal-700 disabled:opacity-60">
{state === "loading" ? "Booking…" : <><CalendarCheck className="size-4" /> Confirm appointment</>}
</button>
</motion.form>
)}
</AnimatePresence>
</motion.div>
</div>
</section>
);
}
/* --------------------------------------------------------------------- faq */
const FAQS = [
{ q: "Does teeth whitening hurt?", a: "No. Most patients feel nothing at all; a few notice mild sensitivity for a day, which we prevent with a desensitising gel." },
{ q: "Do you accept insurance?", a: "Yes — we're cashless with most major insurers and offer 0% EMI on treatments above ₹10,000." },
{ q: "How long does a check-up take?", a: "About 40 minutes, including cleaning, a digital X-ray and a personalised care plan." },
{ q: "Can I bring my child?", a: "Absolutely. Dr. Rohan specialises in gentle first visits and we have a play corner in the waiting room." },
];
function Faq() {
const [open, setOpen] = useState<number | null>(0);
return (
<section id="faq" className="scroll-mt-20 bg-teal-50/60 px-5 py-24">
<div className="mx-auto max-w-3xl">
<motion.h2 {...reveal} className="text-center text-4xl font-semibold tracking-tight text-teal-950">Questions, answered</motion.h2>
<div className="mt-12 space-y-3">
{FAQS.map((f, i) => (
<div key={f.q} className="rounded-2xl bg-white ring-1 ring-teal-100">
<button onClick={() => setOpen(open === i ? null : i)} aria-expanded={open === i} className="flex w-full items-center justify-between gap-4 p-5 text-left font-medium text-teal-950">
{f.q}
<motion.span animate={{ rotate: open === i ? 45 : 0 }} className="grid size-7 shrink-0 place-items-center rounded-full bg-teal-50 text-teal-700"><Plus className="size-4" /></motion.span>
</button>
<AnimatePresence initial={false}>
{open === i && (
<motion.p initial={{ height: 0, opacity: 0 }} animate={{ height: "auto", opacity: 1 }} exit={{ height: 0, opacity: 0 }} className="overflow-hidden px-5 pb-5 text-sm leading-relaxed text-teal-900/70">{f.a}</motion.p>
)}
</AnimatePresence>
</div>
))}
</div>
</div>
</section>
);
}
export default function BrightsmileTemplate() {
return (
<div className="min-h-screen bg-white text-teal-950 antialiased">
<Nav />
<main>
<Hero />
<Services />
<Results />
<Team />
<Reviews />
<Booking />
<Faq />
</main>
<footer className="border-t border-teal-100 px-5 py-10">
<div className="mx-auto flex max-w-6xl flex-wrap items-center justify-between gap-4 text-sm text-teal-900/60">
<span>© {new Date().getFullYear()} Brightsmile Dental Clinic</span>
<span>Registered with the Dental Council of India · Reg. No. 00000</span>
</div>
</footer>
</div>
);
}
What's included
- Navbar
- Hero + animated mascot
- Services
- Before / after slider
- Dentist team
- Reviews + insurers
- Appointment booking
- FAQ
- Footer
Use it in 3 steps
1. Install dependencies in your Next.js + Tailwind project:
$ npm i motion lucide-react- 2. Copy the code into
app/page.tsx. - 3. Replace the copy, colors and links with your own — then deploy.