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
|
color: '#FF2E88', // pink
|
||||||
links: [
|
links: [
|
||||||
{ label: 'Why Now', href: '#why-now' },
|
{ label: 'The Difference', href: '#difference' },
|
||||||
{ label: 'How It Works', href: '#how-it-works' },
|
{ label: 'How It Works', href: '#how-it-works' },
|
||||||
{ label: 'The Loop', href: '#the-loop' },
|
{ label: 'The Loop', href: '#the-loop' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Company',
|
title: 'Live',
|
||||||
color: '#8B3DFF', // purple
|
color: '#8B3DFF', // purple
|
||||||
links: [
|
links: [
|
||||||
{ label: 'The Vision', href: '#the-vision' },
|
{ label: 'The Vision', href: '#the-vision' },
|
||||||
|
|||||||
@@ -11,18 +11,39 @@ const CREAM = '#FFFDF5';
|
|||||||
/* Scroll distance (px) before the bar solidifies into the sticky sticker. */
|
/* Scroll distance (px) before the bar solidifies into the sticky sticker. */
|
||||||
const STICK_AFTER = 500;
|
const STICK_AFTER = 500;
|
||||||
|
|
||||||
const NAV_LINKS = [
|
/* The page's 15 sections split into three ordered acts. Each nav tab jumps to
|
||||||
{ label: 'Earn', href: '#how-it-works', color: '#C8FF37' }, // lime
|
the start of its act, and the scroll-spy highlights whichever act you're in
|
||||||
{ label: 'Spend', href: '#difference', color: '#FF2E88' }, // pink
|
(any section in `sections` lights that tab). Keep `sections` in page order. */
|
||||||
{ label: 'Live', href: '#the-vision', color: '#8B3DFF' }, // purple
|
const NAV_GROUPS = [
|
||||||
{ label: 'Why Now', href: '#why-now', color: GOLD },
|
{
|
||||||
|
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() {
|
export default function Header() {
|
||||||
const [mobileOpen, setMobileOpen] = useState(false);
|
const [mobileOpen, setMobileOpen] = useState(false);
|
||||||
const [hidden, setHidden] = useState(false);
|
const [hidden, setHidden] = useState(false);
|
||||||
const [stuck, setStuck] = useState(false);
|
const [stuck, setStuck] = useState(false);
|
||||||
const [activeHash, setActiveHash] = useState('');
|
const [activeGroup, setActiveGroup] = useState(-1);
|
||||||
const mobileMenuRef = useRef<HTMLDivElement>(null);
|
const mobileMenuRef = useRef<HTMLDivElement>(null);
|
||||||
const lastScrollYRef = useRef(0);
|
const lastScrollYRef = useRef(0);
|
||||||
|
|
||||||
@@ -45,9 +66,9 @@ export default function Header() {
|
|||||||
return () => window.removeEventListener('scroll', onScroll);
|
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(() => {
|
useEffect(() => {
|
||||||
const ids = NAV_LINKS.map((l) => l.href.replace('#', ''));
|
const ids = NAV_GROUPS.flatMap((g) => g.sections);
|
||||||
const targets = ids
|
const targets = ids
|
||||||
.map((id) => document.getElementById(id))
|
.map((id) => document.getElementById(id))
|
||||||
.filter((el): el is HTMLElement => !!el);
|
.filter((el): el is HTMLElement => !!el);
|
||||||
@@ -58,7 +79,7 @@ export default function Header() {
|
|||||||
const visible = entries
|
const visible = entries
|
||||||
.filter((e) => e.isIntersecting)
|
.filter((e) => e.isIntersecting)
|
||||||
.sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0];
|
.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] },
|
{ rootMargin: '-45% 0px -45% 0px', threshold: [0, 0.25, 0.5] },
|
||||||
);
|
);
|
||||||
@@ -86,7 +107,20 @@ export default function Header() {
|
|||||||
return () => { document.body.style.overflow = ''; };
|
return () => { document.body.style.overflow = ''; };
|
||||||
}, [mobileOpen]);
|
}, [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 (
|
return (
|
||||||
<div suppressHydrationWarning>
|
<div suppressHydrationWarning>
|
||||||
@@ -140,11 +174,11 @@ export default function Header() {
|
|||||||
|
|
||||||
<nav>
|
<nav>
|
||||||
<ul style={{ listStyle: 'none', margin: 0, padding: 0, display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
<ul style={{ listStyle: 'none', margin: 0, padding: 0, display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||||
{NAV_LINKS.map(({ label, href, color }) => (
|
{NAV_GROUPS.map(({ label, target, color }) => (
|
||||||
<li key={href}>
|
<li key={label}>
|
||||||
<Link
|
<Link
|
||||||
href={href}
|
href={target}
|
||||||
onClick={() => setMobileOpen(false)}
|
onClick={(e) => handleNavClick(e, target)}
|
||||||
className="font-sora"
|
className="font-sora"
|
||||||
style={{
|
style={{
|
||||||
display: 'block',
|
display: 'block',
|
||||||
@@ -282,13 +316,14 @@ export default function Header() {
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 10,
|
gap: 10,
|
||||||
}}>
|
}}>
|
||||||
{NAV_LINKS.map(({ label, href, color }, i) => {
|
{NAV_GROUPS.map(({ label, target, color }, i) => {
|
||||||
const active = isActive(href);
|
const active = isActive(i);
|
||||||
const tilt = i % 2 === 0 ? -2.5 : 2.5;
|
const tilt = i % 2 === 0 ? -2.5 : 2.5;
|
||||||
return (
|
return (
|
||||||
<li key={href}>
|
<li key={label}>
|
||||||
<Link
|
<Link
|
||||||
href={href}
|
href={target}
|
||||||
|
onClick={(e) => handleNavClick(e, target)}
|
||||||
className={`header-nav-tab${active ? ' is-active' : ''}`}
|
className={`header-nav-tab${active ? ' is-active' : ''}`}
|
||||||
style={{
|
style={{
|
||||||
display: 'inline-flex',
|
display: 'inline-flex',
|
||||||
|
|||||||
Reference in New Issue
Block a user