diff --git a/src/components/landing-page.tsx b/src/components/landing-page.tsx index 42edfd5..bc27275 100644 --- a/src/components/landing-page.tsx +++ b/src/components/landing-page.tsx @@ -51,7 +51,7 @@ function NewSection({ after?: ReactElement; }) { return (
-
+
{children} {after}
diff --git a/src/components/nav.tsx b/src/components/nav.tsx index e714728..51ced98 100644 --- a/src/components/nav.tsx +++ b/src/components/nav.tsx @@ -1,6 +1,6 @@ "use client" import { useColorSections } from "@/hooks/color-sections" -import { type translations } from "@/i18n/translations" +import { Sections, type translations } from "@/i18n/translations" import { cn } from "@/lib/utils" import { useRef } from "react" import { MobileNav } from "./mobile-nav" @@ -20,7 +20,19 @@ export function MainpageNav({ t: typeof translations.pl }) { const parent = useRef(null); - useColorSections(parent); + const previous = useRef(linksOrder[0]) + const forceIgnore = useRef(false); + useColorSections(parent, previous, forceIgnore); + + function setCurrentUnderline(id: typeof linksOrder[number]) { + forceIgnore.current = true; + previous.current = id; + + document.querySelectorAll('[data-sub]').forEach(x => x.classList.remove('scale-x-100')); + document.querySelectorAll('[data-link]').forEach(x => x.classList.remove('text-primary')); + document.querySelector(`[data-sub="${id}"]`)?.classList.add('scale-x-100'); + document.querySelector(`[data-link="${id}"]`)?.classList.add('text-primary'); + } return ( @@ -31,7 +43,11 @@ export function MainpageNav({ { + setCurrentUnderline(value) + }} className="text-sm md:text-md hover:text-primary transition-colors relative group will-change-[color]" + data-link={value} > {t.nav[value]} = [ "about", "tickets", "cfp", "details", - "contact" + "contact", ] -export function useColorSections(parent: React.RefObject) { - const previous = useRef(linksOrder[0]) +/** + * Those links need to be reverted to account for the smallest section at the bottom. + * This way the intersection still pops the event at correct time, but now + * we account for 'contact' too! + */ +const reversedLinks = linksOrder.toReversed(); + +export function useColorSections(parent: React.RefObject, previous: React.RefObject, forceIgnore: React.MutableRefObject) { + const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)'); useLayoutEffect(() => { + if (prefersReducedMotion.matches) { + return; + } if (parent.current === null) return; const options = { root: null, - rootMargin: "-10px", - threshold: 0.5, // Adjust the visibility threshold as needed + rootMargin: "80px 0px 0px 0px", // Top 60% of viewport should matter + threshold: 0.4, // Adjust the visibility threshold as needed }; @@ -32,27 +42,48 @@ export function useColorSections(parent: React.RefObject) return acc; }, {} as Record); - console.log(links) + + // Set of currently intersecting sections by ID. + let intersecting: Set = new Set; + + function setCurrentUnderline(id: typeof linksOrder[number]) { + for (const sub of Object.values(subs)) { + sub.classList.remove('scale-x-100'); + } + for (const link of Object.values(links)) { + link.classList.remove('text-primary'); + } + previous.current = id; + + subs[previous.current]?.classList.add('scale-x-100'); + links[previous.current]?.classList.add('text-primary'); + } const observer = new IntersectionObserver((entries) => { - + if (forceIgnore.current) { + window.onscrollend = () => { + forceIgnore.current = false + } + return; + } + // Update intersection set based on diff. + const startedIntersecting: Set = new Set; + const stoppedIntersecting: Set = new Set; for (const entry of entries) { - const target = entry.target.id as Sections if (entry.isIntersecting) { - // FIXME: This seems to be VERY broken on firefox. - // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1250972 - // It basically spikes up CPU usage to some enormous values just to update the hash, like WTF firefox. - // if (history.replaceState) { - // timeout = setTimeout(() => { - // history.replaceState(null, "", `#${target}`) - // }, 150) - // } - subs[previous.current]?.classList.remove('scale-x-100'); - links[previous.current]?.classList.remove('text-primary'); - previous.current = target; + startedIntersecting.add(entry.target.id); + } else { + stoppedIntersecting.add(entry.target.id); + } + } + intersecting = intersecting.difference(stoppedIntersecting); + intersecting = intersecting.union(startedIntersecting); - subs[previous.current]?.classList.add('scale-x-100'); - links[previous.current]?.classList.add('text-primary'); + // Act upon intersection set to find the lowest intersecting section - + // that's our 'active' section. + for (const id of reversedLinks) { + if (intersecting.has(id)) { + setCurrentUnderline(id); break; } } @@ -67,6 +98,19 @@ export function useColorSections(parent: React.RefObject) return () => { observer.disconnect() }; - }, [parent]); + }, [forceIgnore, parent, prefersReducedMotion.matches, previous]); + + // Initialize the colors once + useLayoutEffect(() => { + if (prefersReducedMotion.matches) { + return; + } + const sub = document.querySelector('[data-sub="' + linksOrder[0] + '"]'); + const link = document.querySelector('[href="#' + linksOrder[0] + '"]'); + if (sub && link) { + sub.classList.add('scale-x-100'); + link.classList.add('text-primary'); + } + }, [prefersReducedMotion.matches]); }