// Homepage-only section compositions for The Vivaan Hotel & Resorts.
// Depends on shared/vivaan-sections.jsx being loaded first.
const { Button } = window.VivaanDesignSystem_b1c8ba;
const { PhotoBlock, PhotoSlideshow, HoverImageSwap, SectionLabel, EditorialHeading, BodyCopy } = window;
/* ---------------------------------------------------------------------- */
/* 2. Welcome */
/* ---------------------------------------------------------------------- */
const METRICS = window.METRICS;
// Count-up animation for the metrics row: parses "100,000+", "5.5", "43" etc.
// into a numeric target + decimal precision + suffix, then animates from 0
// once the row scrolls into view (fires once, eased, ~1.8s).
function CountUpMetric({ value, style }) {
const ref = React.useRef(null);
const [display, setDisplay] = React.useState(null);
const parsed = React.useMemo(() => {
const m = String(value).match(/^([\d,]*\.?\d+)(\+?)$/);
if (!m) return null;
const numStr = m[1].replace(/,/g, "");
const decimals = numStr.includes(".") ? numStr.split(".")[1].length : 0;
return { target: parseFloat(numStr), decimals, suffix: m[2] };
}, [value]);
React.useEffect(() => {
if (!parsed || !ref.current) return;
setDisplay(parsed.target === Math.round(parsed.target) && parsed.decimals === 0 ? "0" : (0).toFixed(parsed.decimals));
const el = ref.current;
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const duration = 1800;
const start = performance.now();
const animate = (now) => {
const t = Math.min((now - start) / duration, 1);
const eased = 1 - Math.pow(1 - t, 3);
const current = parsed.target * eased;
setDisplay(
parsed.decimals > 0
? current.toFixed(parsed.decimals)
: Math.round(current).toLocaleString("en-US")
);
if (t < 1) requestAnimationFrame(animate);
};
requestAnimationFrame(animate);
observer.disconnect();
}
});
}, { threshold: 0.4 });
observer.observe(el);
return () => observer.disconnect();
}, [parsed]);
if (!parsed) return
{value}
;
return {display}{parsed.suffix}
;
}
function WelcomeSection() {
const wrap = { padding: "var(--space-2xl) var(--space-xl)", backgroundColor: "var(--color-surface-page)" };
const metricsRow = { display: "flex", gap: "var(--space-xl)", marginTop: "var(--space-xl)", flexWrap: "wrap", borderTop: "1px solid var(--color-border-hairline)", paddingTop: "var(--space-lg)" };
const metricValue = { fontFamily: "var(--font-serif)", fontSize: 36, color: "var(--vivaan-gold)" };
const metricLabel = { fontFamily: "var(--font-serif)", fontSize: 12, letterSpacing: "0.08em", textTransform: "uppercase", color: "var(--color-text-secondary)", marginTop: 6 };
return (
Welcome to The Vivaan
Set across 5.5 acres in Karnal, The Vivaan brings together quiet luxury stays,
grand celebrations and thoughtful hospitality — a private estate built for
occasions that deserve to be remembered.
{METRICS.map((m) => (
))}
);
}
/* ---------------------------------------------------------------------- */
/* 3. Wedding */
/* ---------------------------------------------------------------------- */
const VENUES = window.VENUES;
function VenueCard({ v }) {
const [hover, setHover] = React.useState(false);
return (
setHover(true)}
onMouseLeave={() => setHover(false)}
style={{ display: "flex", flexDirection: "column", gap: 14, cursor: "pointer" }}
>
{v.name}
{v.tag}
{v.capacity} · {v.area}
);
}
function WeddingSection() {
const wrap = { padding: "var(--space-2xl) var(--space-xl)", backgroundColor: "var(--vivaan-cream)" };
const head = { display: "flex", justifyContent: "space-between", alignItems: "flex-end", gap: 40, marginBottom: "var(--space-xl)", flexWrap: "wrap" };
return (
Wedding & Events
Eight distinct venues, from candlelit ballrooms to open lawns beneath the sky —
each one shaped around the story you want to tell.
View All Venues
{VENUES.map((v) => )}
);
}
/* ---------------------------------------------------------------------- */
/* 4. Stay */
/* ---------------------------------------------------------------------- */
const ROOMS = window.ROOMS;
function StaySection() {
const wrap = { padding: "var(--space-2xl) var(--space-xl)", backgroundColor: "var(--color-surface-page)", display: "flex", flexDirection: "column", gap: "var(--space-2xl)" };
const head = { display: "flex", flexDirection: "column", gap: 20, maxWidth: 560, marginBottom: "var(--space-md)" };
return (
Stay
Six room categories, each composed for a different rhythm of travel — from
family suites to intimate singles, every stay begins with stillness.
{[0, 1, 2].map((row) => (
{[ROOMS[row * 2], ROOMS[row * 2 + 1]].map((r) => (
))}
))}
);
}
Object.assign(window, { METRICS, WelcomeSection, VENUES, WeddingSection, ROOMS, StaySection });