feat: update navigation scroll-spy grouping, fix active links under Lenis scroll, and update footer categories
This commit is contained in:
@@ -20,16 +20,16 @@ const NAV = [
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Platform',
|
||||
title: 'Spend',
|
||||
color: '#FF2E88', // pink
|
||||
links: [
|
||||
{ label: 'Why Now', href: '#why-now' },
|
||||
{ label: 'The Difference', href: '#difference' },
|
||||
{ label: 'How It Works', href: '#how-it-works' },
|
||||
{ label: 'The Loop', href: '#the-loop' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Company',
|
||||
title: 'Live',
|
||||
color: '#8B3DFF', // purple
|
||||
links: [
|
||||
{ label: 'The Vision', href: '#the-vision' },
|
||||
|
||||
@@ -11,18 +11,39 @@ const CREAM = '#FFFDF5';
|
||||
/* Scroll distance (px) before the bar solidifies into the sticky sticker. */
|
||||
const STICK_AFTER = 500;
|
||||
|
||||
const NAV_LINKS = [
|
||||
{ label: 'Earn', href: '#how-it-works', color: '#C8FF37' }, // lime
|
||||
{ label: 'Spend', href: '#difference', color: '#FF2E88' }, // pink
|
||||
{ label: 'Live', href: '#the-vision', color: '#8B3DFF' }, // purple
|
||||
{ label: 'Why Now', href: '#why-now', color: GOLD },
|
||||
/* The page's 15 sections split into three ordered acts. Each nav tab jumps to
|
||||
the start of its act, and the scroll-spy highlights whichever act you're in
|
||||
(any section in `sections` lights that tab). Keep `sections` in page order. */
|
||||
const NAV_GROUPS = [
|
||||
{
|
||||
label: 'Earn',
|
||||
color: '#C8FF37', // lime
|
||||
target: '#the-why',
|
||||
sections: ['intro', 'the-why', 'difference', 'the-problem', 'the-shift'],
|
||||
},
|
||||
{
|
||||
label: 'Spend',
|
||||
color: '#FF2E88', // pink
|
||||
target: '#lyts',
|
||||
sections: ['lyts', 'one-day', 'feelings', 'the-loop', 'why-now'],
|
||||
},
|
||||
{
|
||||
label: 'Live',
|
||||
color: '#8B3DFF', // purple
|
||||
target: '#how-it-works',
|
||||
sections: ['how-it-works', 'old-vs-new', 'the-vision', 'our-belief', 'the-promise'],
|
||||
},
|
||||
];
|
||||
|
||||
/** Map a section id → the index of the act (nav group) that owns it. */
|
||||
const groupForSection = (id: string) =>
|
||||
NAV_GROUPS.findIndex((g) => g.sections.includes(id));
|
||||
|
||||
export default function Header() {
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
const [hidden, setHidden] = useState(false);
|
||||
const [stuck, setStuck] = useState(false);
|
||||
const [activeHash, setActiveHash] = useState('');
|
||||
const [activeGroup, setActiveGroup] = useState(-1);
|
||||
const mobileMenuRef = useRef<HTMLDivElement>(null);
|
||||
const lastScrollYRef = useRef(0);
|
||||
|
||||
@@ -45,9 +66,9 @@ export default function Header() {
|
||||
return () => window.removeEventListener('scroll', onScroll);
|
||||
}, []);
|
||||
|
||||
// Lightweight scroll-spy so the current section's nav link lights up.
|
||||
// Scroll-spy across every section → highlight whichever act (group) it's in.
|
||||
useEffect(() => {
|
||||
const ids = NAV_LINKS.map((l) => l.href.replace('#', ''));
|
||||
const ids = NAV_GROUPS.flatMap((g) => g.sections);
|
||||
const targets = ids
|
||||
.map((id) => document.getElementById(id))
|
||||
.filter((el): el is HTMLElement => !!el);
|
||||
@@ -58,7 +79,7 @@ export default function Header() {
|
||||
const visible = entries
|
||||
.filter((e) => e.isIntersecting)
|
||||
.sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0];
|
||||
if (visible) setActiveHash(`#${visible.target.id}`);
|
||||
if (visible) setActiveGroup(groupForSection(visible.target.id));
|
||||
},
|
||||
{ rootMargin: '-45% 0px -45% 0px', threshold: [0, 0.25, 0.5] },
|
||||
);
|
||||
@@ -86,7 +107,20 @@ export default function Header() {
|
||||
return () => { document.body.style.overflow = ''; };
|
||||
}, [mobileOpen]);
|
||||
|
||||
const isActive = (href: string) => href === activeHash;
|
||||
const isActive = (i: number) => i === activeGroup;
|
||||
|
||||
/* Lenis smooth-scroll owns the page, so a native hash jump gets overridden
|
||||
for far-away targets (that's why "Live" appeared dead). Drive the scroll
|
||||
through Lenis directly, offsetting for the fixed header. */
|
||||
const handleNavClick = (e: React.MouseEvent, target: string) => {
|
||||
const el = document.querySelector(target) as HTMLElement | null;
|
||||
if (!el) return; // let the browser handle it if the section isn't mounted
|
||||
e.preventDefault();
|
||||
setMobileOpen(false);
|
||||
const lenis = (window as unknown as { lenis?: { scrollTo: (t: HTMLElement, o?: object) => void } }).lenis;
|
||||
if (lenis?.scrollTo) lenis.scrollTo(el, { offset: -90, duration: 1.2 });
|
||||
else el.scrollIntoView({ behavior: 'smooth' });
|
||||
};
|
||||
|
||||
return (
|
||||
<div suppressHydrationWarning>
|
||||
@@ -140,11 +174,11 @@ export default function Header() {
|
||||
|
||||
<nav>
|
||||
<ul style={{ listStyle: 'none', margin: 0, padding: 0, display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
{NAV_LINKS.map(({ label, href, color }) => (
|
||||
<li key={href}>
|
||||
{NAV_GROUPS.map(({ label, target, color }) => (
|
||||
<li key={label}>
|
||||
<Link
|
||||
href={href}
|
||||
onClick={() => setMobileOpen(false)}
|
||||
href={target}
|
||||
onClick={(e) => handleNavClick(e, target)}
|
||||
className="font-sora"
|
||||
style={{
|
||||
display: 'block',
|
||||
@@ -282,13 +316,14 @@ export default function Header() {
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
}}>
|
||||
{NAV_LINKS.map(({ label, href, color }, i) => {
|
||||
const active = isActive(href);
|
||||
{NAV_GROUPS.map(({ label, target, color }, i) => {
|
||||
const active = isActive(i);
|
||||
const tilt = i % 2 === 0 ? -2.5 : 2.5;
|
||||
return (
|
||||
<li key={href}>
|
||||
<li key={label}>
|
||||
<Link
|
||||
href={href}
|
||||
href={target}
|
||||
onClick={(e) => handleNavClick(e, target)}
|
||||
className={`header-nav-tab${active ? ' is-active' : ''}`}
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
|
||||
Reference in New Issue
Block a user