first commit
This commit is contained in:
+69
-16
@@ -1,26 +1,79 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
@theme {
|
||||
/* Rotary brand colors — generates bg-rotary-navy, text-rotary-gold, etc. */
|
||||
--color-rotary-navy: #0b2545;
|
||||
--color-rotary-blue: #12468c;
|
||||
--color-rotary-azure: #2e7cd6;
|
||||
--color-rotary-gold: #f2a900;
|
||||
--color-rotary-goldlight: #ffd166;
|
||||
--color-rotary-amber: #c2660c;
|
||||
--color-rotary-paper: #f4f6fa;
|
||||
--color-rotary-ink: #101b2d;
|
||||
|
||||
/* Fonts — generates font-serif / font-sans overrides.
|
||||
Works even without next/font: falls back to system stacks. */
|
||||
--font-serif: var(--font-display, ui-serif), Georgia, serif;
|
||||
--font-sans: var(--font-body, ui-sans-serif), system-ui, sans-serif;
|
||||
|
||||
/* Custom animation utilities — generates animate-spin-slow, animate-fade-up, etc. */
|
||||
--animate-spin-slow: spin-slow 50s linear infinite;
|
||||
--animate-spin-slower: spin-slow 90s linear infinite;
|
||||
--animate-fade-up: fade-up 0.7s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
--animate-shimmer: shimmer 2.5s linear infinite;
|
||||
--animate-ring-pulse: ring-pulse 2.4s ease-out infinite;
|
||||
--animate-shine: shine 3.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
@keyframes spin-slow {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
@keyframes fade-up {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(16px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes ring-pulse {
|
||||
0%,
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(242, 169, 0, 0.45);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 0 10px rgba(242, 169, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shine {
|
||||
0% {
|
||||
transform: translateX(-120%);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(220%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(220%);
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
background-color: var(--color-rotary-paper);
|
||||
color: var(--color-rotary-ink);
|
||||
}
|
||||
+2
-2
@@ -13,8 +13,8 @@ const geistMono = Geist_Mono({
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
title: "Rotary Club of Vellore — Installation Ceremony 2026",
|
||||
description: "The President and Members cordially invite you, with family, to witness the installation of the incoming Board and to Create Lasting Impact together.",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
|
||||
+18
-62
@@ -1,65 +1,21 @@
|
||||
import Image from "next/image";
|
||||
import Hero from "@/components/Hero";
|
||||
import EventDetails from "@/components/EventDetails";
|
||||
import Handover from "@/components/Handover";
|
||||
import Guests from "@/components/Guests";
|
||||
import OfficeBearers from "@/components/OfficeBearers";
|
||||
import CtaBand from "@/components/CtaBand";
|
||||
import Footer from "@/components/Footer";
|
||||
|
||||
export default function Home() {
|
||||
export default function Page() {
|
||||
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="bg-rotary-paper">
|
||||
<Hero />
|
||||
<EventDetails />
|
||||
<Handover />
|
||||
<Guests />
|
||||
<OfficeBearers />
|
||||
<CtaBand />
|
||||
<Footer />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const TARGET = new Date("2026-07-03T18:30:00+05:30").getTime();
|
||||
|
||||
function getParts(msLeft: number) {
|
||||
const clamped = Math.max(msLeft, 0);
|
||||
return {
|
||||
days: Math.floor(clamped / (1000 * 60 * 60 * 24)),
|
||||
hours: Math.floor((clamped / (1000 * 60 * 60)) % 24),
|
||||
minutes: Math.floor((clamped / (1000 * 60)) % 60),
|
||||
seconds: Math.floor((clamped / 1000) % 60),
|
||||
};
|
||||
}
|
||||
|
||||
export default function Countdown() {
|
||||
const [parts, setParts] = useState<ReturnType<typeof getParts> | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const tick = () => setParts(getParts(TARGET - Date.now()));
|
||||
tick();
|
||||
const id = setInterval(tick, 1000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
const units = [
|
||||
{ label: "Days", value: parts?.days },
|
||||
{ label: "Hours", value: parts?.hours },
|
||||
{ label: "Min", value: parts?.minutes },
|
||||
{ label: "Sec", value: parts?.seconds },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex gap-2.5 sm:gap-3" aria-live="polite">
|
||||
{units.map((u) => (
|
||||
<div
|
||||
key={u.label}
|
||||
className="flex w-14 flex-col items-center rounded-xl border border-white/10 bg-white/5 py-2 backdrop-blur-sm sm:w-16"
|
||||
>
|
||||
<span className="font-serif text-xl font-semibold tabular-nums text-white sm:text-2xl">
|
||||
{u.value !== undefined ? String(u.value).padStart(2, "0") : "--"}
|
||||
</span>
|
||||
<span className="text-[10px] uppercase tracking-wider text-white/60">
|
||||
{u.label}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import Reveal from "./Reveal";
|
||||
|
||||
export default function CtaBand() {
|
||||
return (
|
||||
<section className="bg-rotary-navy px-4 py-14 sm:px-6 lg:px-8">
|
||||
<Reveal>
|
||||
<div className="relative mx-auto max-w-4xl overflow-hidden rounded-3xl bg-gradient-to-r from-rotary-gold via-rotary-goldlight to-rotary-gold bg-[length:200%_100%] p-8 text-center shadow-xl animate-shimmer sm:p-10">
|
||||
<p className="font-serif text-xl font-semibold text-rotary-navy sm:text-2xl">
|
||||
Dear Rotarians, your presence is most essential.
|
||||
</p>
|
||||
<p className="mx-auto mt-2 max-w-md text-sm text-rotary-navy/70">
|
||||
Join the President and Members of Rotary Club of Vellore, with
|
||||
family, for the Installation Ceremony on 3rd July 2026.
|
||||
</p>
|
||||
</div>
|
||||
</Reveal>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import Reveal from "./Reveal";
|
||||
|
||||
const DETAILS = [
|
||||
{
|
||||
label: "Date",
|
||||
value: "3rd July 2026",
|
||||
sub: "Friday",
|
||||
icon: (
|
||||
<path d="M8 2v4M16 2v4M3 10h18M5 4h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Z" strokeLinecap="round" strokeLinejoin="round" />
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Time",
|
||||
value: "6:30 PM",
|
||||
sub: "Doors open at 6:00 PM",
|
||||
icon: <><circle cx="12" cy="12" r="9" /><path d="M12 7v5l3 3" strokeLinecap="round" strokeLinejoin="round" /></>,
|
||||
},
|
||||
{
|
||||
label: "Venue",
|
||||
value: "Hotel Khanna, Laya Hall",
|
||||
sub: "Anna Salai, Vellore",
|
||||
icon: (
|
||||
<><path d="M12 21s7-6.2 7-11.5A7 7 0 0 0 5 9.5C5 14.8 12 21 12 21Z" strokeLinecap="round" strokeLinejoin="round" /><circle cx="12" cy="9.5" r="2.5" /></>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
export default function EventDetails() {
|
||||
return (
|
||||
<section id="event" className="bg-rotary-paper px-4 py-6 sm:px-6 sm:py-8 lg:px-8">
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<Reveal>
|
||||
<p className="text-center text-xs font-semibold uppercase tracking-[0.28em] text-rotary-azure">
|
||||
Save the date
|
||||
</p>
|
||||
<h2 className="mt-2 text-center font-serif text-3xl font-semibold text-rotary-navy sm:text-4xl">
|
||||
Join us for the evening
|
||||
</h2>
|
||||
</Reveal>
|
||||
|
||||
<div className="mt-8 grid gap-3 sm:grid-cols-3">
|
||||
{DETAILS.map((d, i) => (
|
||||
<Reveal key={d.label} delay={i * 100}>
|
||||
<div className="group flex h-full items-center gap-3 rounded-xl border border-rotary-navy/[0.06] bg-white p-3.5 shadow-sm transition-all duration-300 hover:-translate-y-0.5 hover:shadow-md hover:shadow-rotary-blue/10">
|
||||
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-rotary-blue/[0.08] text-rotary-blue transition-colors duration-300 group-hover:bg-rotary-gold/15 group-hover:text-rotary-gold">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="h-4 w-4">
|
||||
{d.icon}
|
||||
</svg>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-[10px] font-semibold uppercase tracking-wider text-rotary-azure">
|
||||
{d.label}
|
||||
</p>
|
||||
<p className="truncate font-serif text-base font-semibold leading-tight text-rotary-navy">
|
||||
{d.value}
|
||||
</p>
|
||||
<p className="truncate text-xs text-rotary-ink/55">{d.sub}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import Image from "next/image";
|
||||
import GearMotif from "./GearMotif";
|
||||
|
||||
const LINKS = [
|
||||
{ href: "#event", label: "The Ceremony" },
|
||||
{ href: "#handover", label: "Handover" },
|
||||
{ href: "#guests", label: "Guests" },
|
||||
{ href: "#bearers", label: "Office Bearers" },
|
||||
];
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="relative overflow-hidden bg-rotary-navy px-4 pb-8 pt-16 text-white sm:px-6 lg:px-8">
|
||||
<GearMotif className="pointer-events-none absolute -bottom-16 -right-16 h-64 w-64 text-white/[0.04]" />
|
||||
|
||||
<div className="relative mx-auto grid max-w-7xl gap-10 border-b border-white/10 pb-10 sm:grid-cols-3">
|
||||
{/* Brand */}
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="rounded-lg bg-white px-2.5 py-1.5 shadow-md shadow-black/20">
|
||||
<Image
|
||||
src="/images/rotary-logo.png"
|
||||
alt="Rotary International — Create Lasting Impact"
|
||||
width={180}
|
||||
height={52}
|
||||
className="h-7 w-auto object-contain"
|
||||
/>
|
||||
</div>
|
||||
<span className="font-serif text-base font-semibold">
|
||||
Rotary Club of Vellore
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-4 max-w-xs text-sm leading-relaxed text-white/60">
|
||||
Club No. 15900 · Chartered 5th March 1948 · District 3231.
|
||||
Serving Vellore since generations, one project at a time.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Quick links */}
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-rotary-goldlight">
|
||||
Quick links
|
||||
</p>
|
||||
<ul className="mt-4 space-y-2.5">
|
||||
{LINKS.map((l) => (
|
||||
<li key={l.href}>
|
||||
<a
|
||||
href={l.href}
|
||||
className="text-sm text-white/65 transition-colors duration-200 hover:text-white"
|
||||
>
|
||||
{l.label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Contact */}
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-rotary-goldlight">
|
||||
The evening
|
||||
</p>
|
||||
<ul className="mt-4 space-y-2.5 text-sm text-white/65">
|
||||
<li>3rd July 2026 · Friday · 6:30 PM</li>
|
||||
<li>Hotel Khanna, Laya Hall</li>
|
||||
<li>Anna Salai, Vellore</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative mx-auto flex max-w-7xl flex-col-reverse items-center gap-3 pt-6 text-xs text-white/45 sm:flex-row sm:justify-between">
|
||||
<p>© 2026 Rotary Club of Vellore. All are welcome.</p>
|
||||
<div className="flex items-center gap-5">
|
||||
<p className="text-white/35">
|
||||
Powered by{" "}
|
||||
<span className="font-medium text-white/60 transition-colors duration-200 hover:text-rotary-goldlight">
|
||||
Rootxwire
|
||||
</span>
|
||||
</p>
|
||||
<a
|
||||
href="#top"
|
||||
className="group flex items-center gap-1.5 text-white/60 transition-colors duration-200 hover:text-white"
|
||||
>
|
||||
Back to top
|
||||
<svg className="h-3.5 w-3.5 transition-transform duration-300 group-hover:-translate-y-0.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M12 19V5M6 11l6-6 6 6" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
type Props = {
|
||||
className?: string;
|
||||
duration?: "spin-slow" | "spin-slower";
|
||||
};
|
||||
|
||||
/** A single-color outline gear, meant to sit behind content at low opacity. */
|
||||
export default function GearMotif({ className = "", duration = "spin-slow" }: Props) {
|
||||
const spin = duration === "spin-slower" ? "animate-spin-slower" : "animate-spin-slow";
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 200 200"
|
||||
className={`${className} ${spin}`}
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle cx="100" cy="100" r="46" stroke="currentColor" strokeWidth="6" />
|
||||
<circle cx="100" cy="100" r="14" stroke="currentColor" strokeWidth="6" />
|
||||
{Array.from({ length: 12 }).map((_, i) => {
|
||||
const angle = (i * 360) / 12;
|
||||
return (
|
||||
<rect
|
||||
key={i}
|
||||
x="94"
|
||||
y="4"
|
||||
width="12"
|
||||
height="26"
|
||||
rx="2"
|
||||
fill="currentColor"
|
||||
transform={`rotate(${angle} 100 100)`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import Image from "next/image";
|
||||
import Reveal from "./Reveal";
|
||||
|
||||
const GUESTS = [
|
||||
{
|
||||
name: "Rtn. PDG. M. Rajan Babu",
|
||||
role: "Chief Guest",
|
||||
subrole: "Past District Governor, 3231",
|
||||
photo: "/images/chief-guest-couple.png",
|
||||
},
|
||||
{
|
||||
name: "Rtn. Yelagiri V. Selvam",
|
||||
role: "Guest of Honour",
|
||||
subrole: "Chairman, Kodai Vizha 2026 – 27",
|
||||
photo: "/images/guest-of-honour.jpg",
|
||||
},
|
||||
];
|
||||
|
||||
export default function Guests() {
|
||||
return (
|
||||
<section id="guests" className="bg-rotary-paper px-4 py-6 sm:px-6 sm:py-8 lg:px-8">
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<Reveal>
|
||||
<p className="text-center text-xs font-semibold uppercase tracking-[0.28em] text-rotary-azure">
|
||||
With us this evening
|
||||
</p>
|
||||
<h2 className="mt-2 text-center font-serif text-3xl font-semibold text-rotary-navy sm:text-4xl">
|
||||
Distinguished Guests
|
||||
</h2>
|
||||
</Reveal>
|
||||
|
||||
<div className="mt-10 grid gap-6 sm:grid-cols-2">
|
||||
{GUESTS.map((g, i) => (
|
||||
<Reveal key={g.name} delay={i * 120}>
|
||||
<div className="group flex items-center gap-5 rounded-2xl border border-rotary-navy/[0.06] bg-white p-5 shadow-sm transition-all duration-300 hover:-translate-y-1 hover:shadow-lg hover:shadow-rotary-blue/10 sm:p-6">
|
||||
<div className="relative h-20 w-20 shrink-0 overflow-hidden rounded-full ring-4 ring-rotary-gold/20 transition-all duration-300 group-hover:ring-rotary-gold/50 sm:h-24 sm:w-24">
|
||||
<Image
|
||||
src={g.photo}
|
||||
alt={g.name}
|
||||
fill
|
||||
sizes="96px"
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-rotary-gold">
|
||||
{g.role}
|
||||
</p>
|
||||
<p className="mt-1 font-serif text-lg font-semibold text-rotary-navy sm:text-xl">
|
||||
{g.name}
|
||||
</p>
|
||||
<p className="mt-0.5 text-sm text-rotary-ink/60">{g.subrole}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import Reveal from "./Reveal";
|
||||
|
||||
const OUTGOING = {
|
||||
label: "2025 – 26",
|
||||
officers: [
|
||||
{ role: "President", name: "Rtn. Prof. S. Thiyagukumar" },
|
||||
{ role: "Secretary", name: "Rtn. A.S. Abdul Rahaman" },
|
||||
{ role: "Treasurer", name: "Rtn. Er. G. Muralidharan" },
|
||||
],
|
||||
};
|
||||
|
||||
const INCOMING = {
|
||||
label: "2026 – 27",
|
||||
officers: [
|
||||
{ role: "President", name: "Rtn. Er. G. Muralidharan" },
|
||||
{ role: "Secretary", name: "Rtn. M. Bhimarao" },
|
||||
{ role: "Treasurer", name: "Rtn. K. Kumar" },
|
||||
],
|
||||
};
|
||||
|
||||
function OfficerCard({
|
||||
group,
|
||||
tone,
|
||||
}: {
|
||||
group: typeof OUTGOING;
|
||||
tone: "outgoing" | "incoming";
|
||||
}) {
|
||||
const incoming = tone === "incoming";
|
||||
return (
|
||||
<div
|
||||
className={`relative overflow-hidden rounded-2xl border p-6 sm:p-7 ${
|
||||
incoming
|
||||
? "border-rotary-gold/30 bg-rotary-navy text-white shadow-lg shadow-rotary-navy/20"
|
||||
: "border-rotary-navy/[0.08] bg-white"
|
||||
}`}
|
||||
>
|
||||
{incoming && (
|
||||
<div className="pointer-events-none absolute inset-0 overflow-hidden rounded-2xl">
|
||||
<div className="absolute inset-y-0 -left-1/4 w-1/3 -skew-x-12 bg-gradient-to-r from-transparent via-rotary-amber/35 to-transparent blur-2xl animate-shine" />
|
||||
</div>
|
||||
)}
|
||||
<p
|
||||
className={`text-xs font-semibold uppercase tracking-[0.2em] ${
|
||||
incoming ? "text-rotary-goldlight" : "text-rotary-azure"
|
||||
}`}
|
||||
>
|
||||
{incoming ? "Incoming Board" : "Outgoing Board"}
|
||||
</p>
|
||||
<p className={`mt-1 font-serif text-2xl font-semibold ${incoming ? "text-white" : "text-rotary-navy"}`}>
|
||||
{group.label}
|
||||
</p>
|
||||
|
||||
<ul className="mt-5 space-y-3.5">
|
||||
{group.officers.map((o) => (
|
||||
<li
|
||||
key={o.role}
|
||||
className={`flex items-center justify-between gap-4 border-b pb-3.5 last:border-b-0 last:pb-0 ${
|
||||
incoming ? "border-white/10" : "border-rotary-navy/[0.06]"
|
||||
}`}
|
||||
>
|
||||
<span className={`text-sm ${incoming ? "text-white/60" : "text-rotary-ink/55"}`}>
|
||||
{o.role}
|
||||
</span>
|
||||
<span className={`text-right text-sm font-medium ${incoming ? "text-white" : "text-rotary-navy"}`}>
|
||||
{o.name}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Handover() {
|
||||
return (
|
||||
<section id="handover" className="bg-white px-4 py-6 sm:px-6 sm:py-8 lg:px-8">
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<Reveal>
|
||||
<p className="text-center text-xs font-semibold uppercase tracking-[0.28em] text-rotary-azure">
|
||||
The gavel passes
|
||||
</p>
|
||||
<h2 className="mt-2 text-center font-serif text-3xl font-semibold text-rotary-navy sm:text-4xl">
|
||||
A year of service, handed forward
|
||||
</h2>
|
||||
</Reveal>
|
||||
|
||||
<div className="mt-10 grid items-center gap-6 sm:grid-cols-[1fr_auto_1fr]">
|
||||
<Reveal>
|
||||
<OfficerCard group={OUTGOING} tone="outgoing" />
|
||||
</Reveal>
|
||||
|
||||
<Reveal delay={100}>
|
||||
<div className="flex justify-center py-2 sm:py-0">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-rotary-gold/15 text-rotary-gold transition-transform duration-500 hover:rotate-180 sm:h-14 sm:w-14">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="h-6 w-6 rotate-90 sm:rotate-0">
|
||||
<path d="M5 12h14M13 6l6 6-6 6" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
<Reveal delay={200}>
|
||||
<OfficerCard group={INCOMING} tone="incoming" />
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
const NAV_LINKS = [
|
||||
{ href: "#event", label: "The Ceremony" },
|
||||
{ href: "#handover", label: "Handover" },
|
||||
{ href: "#guests", label: "Guests" },
|
||||
{ href: "#bearers", label: "Office Bearers" },
|
||||
];
|
||||
|
||||
export default function Header() {
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const onScroll = () => setScrolled(window.scrollY > 12);
|
||||
onScroll();
|
||||
window.addEventListener("scroll", onScroll, { passive: true });
|
||||
return () => window.removeEventListener("scroll", onScroll);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<header
|
||||
className={`sticky top-0 z-50 w-full transition-all duration-300 ${
|
||||
scrolled
|
||||
? "bg-rotary-navy/95 shadow-lg shadow-rotary-navy/20 backdrop-blur"
|
||||
: "bg-rotary-navy"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`mx-auto flex max-w-7xl items-center justify-between px-4 transition-all duration-300 ${
|
||||
scrolled ? "py-2.5" : "py-4"
|
||||
}`}
|
||||
>
|
||||
{/* Brand */}
|
||||
<a href="#top" className="group flex items-center gap-3">
|
||||
<span className="relative flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-rotary-gold/15 ring-1 ring-rotary-gold/40 transition-transform duration-300 group-hover:scale-105 sm:h-10 sm:w-10">
|
||||
<Image
|
||||
src="/images/rotary-logo.png"
|
||||
alt="Rotary International District 3231"
|
||||
width={28}
|
||||
height={28}
|
||||
className="h-6 w-6 object-contain sm:h-7 sm:w-7"
|
||||
/>
|
||||
</span>
|
||||
<span className="flex flex-col leading-tight">
|
||||
<span className="font-serif text-[15px] font-semibold tracking-tight text-white sm:text-base">
|
||||
Rotary Club of Vellore
|
||||
</span>
|
||||
<span className="text-[11px] uppercase tracking-[0.18em] text-rotary-goldlight/90">
|
||||
District 3231 · Est. 1948
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
{/* Desktop nav (hidden) */}
|
||||
<nav className="hidden">
|
||||
{NAV_LINKS.map((link) => (
|
||||
<a
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="group relative py-1 text-sm font-medium text-white/85 transition-colors hover:text-white"
|
||||
>
|
||||
{link.label}
|
||||
<span className="absolute -bottom-0.5 left-0 h-[2px] w-full origin-left scale-x-0 bg-rotary-gold transition-transform duration-300 ease-out group-hover:scale-x-100" />
|
||||
</a>
|
||||
))}
|
||||
<a
|
||||
href="#event"
|
||||
className="rounded-full bg-rotary-gold px-4 py-2 text-sm font-semibold text-rotary-navy shadow-sm transition-all duration-300 hover:-translate-y-0.5 hover:bg-rotary-goldlight hover:shadow-md active:translate-y-0"
|
||||
>
|
||||
RSVP
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
{/* Mobile toggle (hidden) */}
|
||||
<button
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
aria-label="Toggle menu"
|
||||
aria-expanded={open}
|
||||
className="hidden"
|
||||
>
|
||||
<span
|
||||
className={`h-0.5 w-6 bg-white transition-transform duration-300 ${
|
||||
open ? "translate-y-2 rotate-45" : ""
|
||||
}`}
|
||||
/>
|
||||
<span
|
||||
className={`h-0.5 w-6 bg-white transition-opacity duration-300 ${
|
||||
open ? "opacity-0" : "opacity-100"
|
||||
}`}
|
||||
/>
|
||||
<span
|
||||
className={`h-0.5 w-6 bg-white transition-transform duration-300 ${
|
||||
open ? "-translate-y-2 -rotate-45" : ""
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Mobile menu (hidden) */}
|
||||
<div className="hidden">
|
||||
<nav className="flex flex-col gap-1 border-t border-white/10 bg-rotary-navy px-4 pb-4 pt-2 sm:px-6">
|
||||
{NAV_LINKS.map((link) => (
|
||||
<a
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
onClick={() => setOpen(false)}
|
||||
className="rounded-lg px-3 py-2.5 text-sm font-medium text-white/85 transition-colors hover:bg-white/5 hover:text-white"
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
))}
|
||||
<a
|
||||
href="#event"
|
||||
onClick={() => setOpen(false)}
|
||||
className="mt-2 rounded-full bg-rotary-gold px-4 py-2.5 text-center text-sm font-semibold text-rotary-navy"
|
||||
>
|
||||
RSVP
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import Image from "next/image";
|
||||
import GearMotif from "./GearMotif";
|
||||
import Countdown from "./Countdown";
|
||||
|
||||
export default function Hero() {
|
||||
return (
|
||||
<section
|
||||
id="top"
|
||||
className="relative overflow-hidden bg-gradient-to-b from-rotary-navy via-rotary-navy to-rotary-blue px-4 pb-6 pt-4 text-white sm:px-6 sm:pb-6 sm:pt-6 lg:px-8"
|
||||
>
|
||||
{/* signature watermark */}
|
||||
<GearMotif className="pointer-events-none absolute -right-24 -top-24 h-[420px] w-[420px] text-white/[0.05] sm:h-[520px] sm:w-[520px]" />
|
||||
<GearMotif
|
||||
duration="spin-slower"
|
||||
className="pointer-events-none absolute -bottom-32 -left-20 h-72 w-72 text-rotary-gold/[0.06]"
|
||||
/>
|
||||
|
||||
<div className="relative mx-auto max-w-6xl">
|
||||
{/* Brand row — replaces the old header's logo now that it's gone */}
|
||||
<div className="mb-12 flex flex-wrap items-center gap-4 sm:mb-16">
|
||||
<div className="rounded-xl bg-white px-3 py-1.5 shadow-lg shadow-black/20">
|
||||
<Image
|
||||
src="/images/rotary-logo.png"
|
||||
alt="Rotary International — Create Lasting Impact"
|
||||
width={220}
|
||||
height={64}
|
||||
className="h-9 w-auto object-contain sm:h-11"
|
||||
/>
|
||||
</div>
|
||||
<span className="hidden h-8 w-px bg-white/15 sm:block" />
|
||||
<span className="flex flex-col leading-tight">
|
||||
<span className="font-serif text-base font-semibold tracking-tight text-white sm:text-lg">
|
||||
Rotary Club of Vellore
|
||||
</span>
|
||||
<span className="text-[11px] uppercase tracking-[0.18em] text-rotary-goldlight/90">
|
||||
District 3231 · Est. 1948
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid items-center gap-12 lg:grid-cols-[1.15fr_0.85fr]">
|
||||
{/* Copy */}
|
||||
<div className="animate-fade-up">
|
||||
{/* Advisor + President thumbnails */}
|
||||
<div className="mb-6 flex items-center gap-4">
|
||||
<div className="relative h-20 w-20 overflow-hidden rounded-2xl ring-2 ring-white/15 sm:h-24 sm:w-24">
|
||||
<Image
|
||||
src="/images/club-advisor.jpg"
|
||||
alt="Rtn. Dr. T. Jacob John, Club Advisor"
|
||||
fill
|
||||
sizes="96px"
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative h-20 w-20 overflow-hidden rounded-2xl ring-2 ring-rotary-gold/60 sm:h-24 sm:w-24">
|
||||
<Image
|
||||
src="/images/president.png"
|
||||
alt="Rtn. Er. G. Muralidharan, President 2026-27"
|
||||
fill
|
||||
sizes="96px"
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.28em] text-rotary-goldlight">
|
||||
Rotary Club of Vellore · Club No. 15900 · Charter 05.03.1948
|
||||
</p>
|
||||
|
||||
<h1 className="mt-4 font-serif text-4xl font-semibold leading-[1.08] tracking-tight sm:text-5xl lg:text-6xl">
|
||||
Installation
|
||||
<span className="block text-rotary-goldlight">Ceremony 2026 – 27</span>
|
||||
</h1>
|
||||
|
||||
<p className="mt-5 max-w-md text-[15px] leading-relaxed text-white/75 sm:text-base">
|
||||
The President and Members cordially invite you, with family, to
|
||||
witness the installation of the incoming Board and to
|
||||
<span className="text-white"> Create Lasting Impact</span> together.
|
||||
</p>
|
||||
|
||||
{/* date / venue chips */}
|
||||
<div className="mt-7 flex flex-wrap gap-3">
|
||||
<div className="flex items-center gap-2 rounded-full border border-white/15 bg-white/5 px-4 py-2 text-sm">
|
||||
<svg className="h-4 w-4 text-rotary-gold" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<rect x="3" y="5" width="18" height="16" rx="2" />
|
||||
<path d="M16 3v4M8 3v4M3 10h18" strokeLinecap="round" />
|
||||
</svg>
|
||||
3 July 2026 · Friday · 6:30 PM
|
||||
</div>
|
||||
<div className="flex items-center gap-2 rounded-full border border-white/15 bg-white/5 px-4 py-2 text-sm">
|
||||
<svg className="h-4 w-4 text-rotary-gold" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M12 21s7-6.2 7-11.5A7 7 0 0 0 5 9.5C5 14.8 12 21 12 21Z" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<circle cx="12" cy="9.5" r="2.5" />
|
||||
</svg>
|
||||
Hotel Khanna, Laya Hall, Vellore
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8">
|
||||
<p className="mb-2.5 text-xs font-medium uppercase tracking-wider text-white/50">
|
||||
Counting down to installation
|
||||
</p>
|
||||
<Countdown />
|
||||
</div>
|
||||
|
||||
<a
|
||||
href="#event"
|
||||
className="group mt-9 inline-flex items-center gap-2 rounded-full bg-rotary-gold px-6 py-3 text-sm font-semibold text-rotary-navy shadow-[0_8px_24px_-6px_rgba(242,169,0,0.5)] transition-all duration-300 hover:-translate-y-0.5 hover:bg-rotary-goldlight animate-ring-pulse"
|
||||
>
|
||||
View Ceremony Details
|
||||
<svg className="h-4 w-4 transition-transform duration-300 group-hover:translate-x-1" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M5 12h14M13 6l6 6-6 6" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* President portrait */}
|
||||
<div className="animate-fade-up [animation-delay:150ms] justify-self-center lg:justify-self-end">
|
||||
<div className="relative">
|
||||
<div className="absolute -inset-3 rounded-[2rem] border border-rotary-gold/30" />
|
||||
<div className="relative h-92 w-80 overflow-hidden rounded-[1.75rem] ring-4 ring-white/10 sm:h-100 sm:w-84">
|
||||
<Image
|
||||
src="/images/president.jpg"
|
||||
alt="Rtn. Er. G. Muralidharan, President 2026-27"
|
||||
fill
|
||||
sizes="256px"
|
||||
className="object-cover"
|
||||
priority
|
||||
/>
|
||||
<div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-rotary-navy/90 to-transparent px-4 pb-4 pt-10">
|
||||
<p className="text-[11px] uppercase tracking-widest text-rotary-goldlight">
|
||||
President, 2026 – 27
|
||||
</p>
|
||||
<p className="font-serif text-lg font-semibold leading-tight">
|
||||
Rtn. Er. G. Muralidharan
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Reveal from "./Reveal";
|
||||
|
||||
const GROUPS = [
|
||||
{
|
||||
label: "Leadership",
|
||||
roster: [
|
||||
{ role: "Club Advisor", name: "Rtn. Dr. T. Jacob John" },
|
||||
{ role: "Club Mentor", name: "Rtn. Dr. T. Daniel Ezhilarasu" },
|
||||
{ role: "Club Trainer", name: "Rtn. Dr. T. Jacob John" },
|
||||
{ role: "President", name: "Rtn. Er. G. Muralidharan" },
|
||||
{ role: "Secretary", name: "Rtn. M. Bhimarao" },
|
||||
{ role: "Immediate Past President", name: "Rtn. Prof. S. Thiyagukumar" },
|
||||
{ role: "President Elect (2027-28)", name: "Rtn. B. Nirmala Gopinath" },
|
||||
{ role: "President Nominee (2028-29)", name: "Rtn. A.S. Abdul Rahiman" },
|
||||
{ role: "Treasurer", name: "Rtn. K. Kumar" },
|
||||
{ role: "Sergeant at Arms – 1", name: "Rtn. Dr. D. Daniel Ezhilarasu" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Directors",
|
||||
roster: [
|
||||
{ role: "Club Administration", name: "Rtn. P. Srinivasan" },
|
||||
{ role: "Vocational Service", name: "Rtn. Joseph Annaiah" },
|
||||
{ role: "Community Service — Health", name: "Rtn. Dr. Thiagarajan" },
|
||||
{ role: "Community Service — Development", name: "Rtn. Emperumal M." },
|
||||
{ role: "International Service", name: "Rtn. Suthakarraj" },
|
||||
{ role: "Youth Service", name: "Rtn. A.S. Abdul Rahiman" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Committee Chairs",
|
||||
roster: [
|
||||
{ role: "Rotary Foundation", name: "Rtn. Subash Chandaran" },
|
||||
{ role: "Membership Development", name: "Rtn. Dr. M. Thangamani" },
|
||||
{ role: "Public Image", name: "Rtn. Boominatthan" },
|
||||
{ role: "Family Participation", name: "Rtn. P. Srinivasan" },
|
||||
{ role: "DE-RYLA", name: "Rtn. Shyam Sundar Jaising" },
|
||||
{ role: "Blood Camp", name: "Rtn. Prof. D. Thirumaran" },
|
||||
{ role: "School into Smiles", name: "Rtn. Gopinath" },
|
||||
{ role: "Award Team", name: "Rtn. ASA. Shanmugam" },
|
||||
{ role: "Polio Plus", name: "Rtn. Viswanathan" },
|
||||
{ role: "Water and Sanitation", name: "Rtn. R. Ravi" },
|
||||
{ role: "Literacy", name: "Rtn. Dr. K. Saravanan" },
|
||||
{ role: "Bulletin", name: "Rtn. Dr. M. Thangamani" },
|
||||
{ role: "Eye Camps", name: "Rtn. A.S. Abdul Rahiman" },
|
||||
{ role: "Dental Camps", name: "Rtn. Dr. Bavithara" },
|
||||
{ role: "General Medical Camps", name: "Rtn. Emperumal M." },
|
||||
{ role: "Dist. Special Project", name: "Rtn. Emperumal M." },
|
||||
{ role: "Pongal Vizha & Festivals", name: "Rtn. M. Bhima Rao" },
|
||||
{ role: "Environment", name: "Rtn. TKT Srinivasan" },
|
||||
{ role: "Skill India", name: "Rtn. K.V. Elangovan" },
|
||||
{ role: "Rotract", name: "Rtn. Prof. B. Nirmal Prasad" },
|
||||
{ role: "Interact", name: "Rtn. P. Duraisamy Padmanaban" },
|
||||
{ role: "Go Green", name: "Rtn. Dr. M. Kumaran" },
|
||||
{ role: "WinS", name: "Rtn. Balaji" },
|
||||
{ role: "Low Cost Shelter", name: "Rtn. Chandru" },
|
||||
{ role: "Be Safety Town", name: "Rtn. R. Ravi" },
|
||||
{ role: "Clean Town — Swachh Bharat", name: "Rtn. M. Bhima Rao" },
|
||||
{ role: "Job Fair", name: "Rtn. TKT Srinivasan" },
|
||||
{ role: "Rally (Car, Bike, Walk)", name: "Rtn. Joseph Annaiah" },
|
||||
{ role: "IWC Ann Club", name: "Rtn. Nirmala Gopinath" },
|
||||
{ role: "Competition & Sports", name: "Rtn. P. Duraisamy" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default function OfficeBearers() {
|
||||
const [active, setActive] = useState(0);
|
||||
|
||||
return (
|
||||
<section id="bearers" className="bg-white px-4 py-6 sm:px-6 sm:py-8 lg:px-8">
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<Reveal>
|
||||
<p className="text-center text-xs font-semibold uppercase tracking-[0.28em] text-rotary-azure">
|
||||
Office Bearers of 2026 – 27
|
||||
</p>
|
||||
<h2 className="mt-2 text-center font-serif text-3xl font-semibold text-rotary-navy sm:text-4xl">
|
||||
The Directory
|
||||
</h2>
|
||||
</Reveal>
|
||||
|
||||
{/* Tabs */}
|
||||
<Reveal delay={100}>
|
||||
<div className="mt-9 flex justify-center gap-1 rounded-full border border-rotary-navy/[0.08] bg-rotary-paper p-1.5 sm:inline-flex sm:gap-1">
|
||||
{GROUPS.map((g, i) => (
|
||||
<button
|
||||
key={g.label}
|
||||
onClick={() => setActive(i)}
|
||||
className={`relative flex-1 rounded-full px-3 py-2 text-xs font-semibold transition-all duration-300 sm:flex-none sm:px-5 sm:text-sm ${
|
||||
active === i
|
||||
? "bg-rotary-navy text-white shadow-sm"
|
||||
: "text-rotary-ink/60 hover:text-rotary-navy"
|
||||
}`}
|
||||
>
|
||||
{g.label}
|
||||
<span className="ml-1.5 text-[10px] opacity-60">
|
||||
{g.roster.length}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
{/* Roster */}
|
||||
<div className="mt-8 grid gap-x-10 sm:grid-cols-2">
|
||||
{GROUPS[active].roster.map((b, i) => (
|
||||
<div
|
||||
key={b.role}
|
||||
style={{ animationDelay: `${(i % 15) * 40}ms` }}
|
||||
className="group flex animate-fade-up items-baseline justify-between gap-3 border-b border-dotted border-rotary-navy/15 py-3 transition-colors duration-200 hover:border-rotary-gold"
|
||||
>
|
||||
<span className="text-[13px] text-rotary-ink/55 transition-colors duration-200 group-hover:text-rotary-ink/80 sm:text-sm">
|
||||
{b.role}
|
||||
</span>
|
||||
<span className="shrink-0 text-right text-[13px] font-medium text-rotary-navy sm:text-sm">
|
||||
{b.name}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
delay?: number; // ms
|
||||
className?: string;
|
||||
variant?: "fade" | "glow";
|
||||
};
|
||||
|
||||
export default function Reveal({ children, delay = 0, className = "", variant = "fade" }: Props) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const node = ref.current;
|
||||
if (!node) return;
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setVisible(true);
|
||||
observer.disconnect();
|
||||
}
|
||||
},
|
||||
{ threshold: 0.15 }
|
||||
);
|
||||
observer.observe(node);
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const hiddenState =
|
||||
variant === "glow"
|
||||
? "opacity-0 translate-y-6 scale-95 drop-shadow-none"
|
||||
: "opacity-0 translate-y-6";
|
||||
|
||||
const visibleState =
|
||||
variant === "glow"
|
||||
? "opacity-100 translate-y-0 scale-100 drop-shadow-[0_0_36px_rgba(242,169,0,0.28)]"
|
||||
: "opacity-100 translate-y-0";
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
style={{ transitionDelay: visible ? `${delay}ms` : "0ms" }}
|
||||
className={`transition-all duration-700 ease-out motion-reduce:transition-none ${
|
||||
visible ? visibleState : hiddenState
|
||||
} ${className}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user