diff --git a/src/pages/WikipediaPage.tsx b/src/pages/WikipediaPage.tsx index c4dceca5..f348a003 100644 --- a/src/pages/WikipediaPage.tsx +++ b/src/pages/WikipediaPage.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState, type RefObject } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { useSeoMeta } from '@unhead/react'; import { @@ -11,7 +11,6 @@ import { Loader2, Newspaper, Search, - Sparkles, Star, TrendingUp, X, @@ -33,7 +32,7 @@ import { cn } from '@/lib/utils'; // Types // --------------------------------------------------------------------------- -type Section = 'all' | 'featured' | 'mostread' | 'onthisday' | 'news'; +type Section = 'featured' | 'mostread' | 'news' | 'onthisday'; interface SectionMeta { label: string; @@ -44,14 +43,14 @@ interface SectionMeta { // Constants // --------------------------------------------------------------------------- -const SECTIONS: Record, SectionMeta> = { +const SECTIONS: Record = { featured: { label: 'Featured', icon: }, mostread: { label: 'Trending', icon: }, - onthisday: { label: 'On This Day', icon: }, news: { label: 'In the News', icon: }, + onthisday: { label: 'On This Day', icon: }, }; -const SECTION_ORDER: Section[] = ['all', 'featured', 'mostread', 'news', 'onthisday']; +const SECTION_ORDER: Section[] = ['featured', 'mostread', 'news', 'onthisday']; // --------------------------------------------------------------------------- // Helpers @@ -80,6 +79,61 @@ function truncateExtract(text: string, maxLen = 150): string { return text.slice(0, maxLen).replace(/\s+\S*$/, '') + '\u2026'; } +// --------------------------------------------------------------------------- +// Scrollspy hook +// --------------------------------------------------------------------------- + +function useScrollspy( + sectionRefs: Record>, + navBarRef: RefObject, +) { + const [active, setActive] = useState
(SECTION_ORDER[0]); + // Guard against scroll-into-view triggering the observer + const isScrollingRef = useRef(false); + + useEffect(() => { + const navBarHeight = navBarRef.current?.offsetHeight ?? 48; + // Trigger when a section crosses just below the sticky nav bar + const rootMargin = `-${navBarHeight + 8}px 0px -60% 0px`; + + const observer = new IntersectionObserver( + (entries) => { + if (isScrollingRef.current) return; + // Pick the first visible section in DOM order + const visible = entries + .filter((e) => e.isIntersecting) + .sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top); + if (visible.length > 0) { + const id = visible[0].target.getAttribute('data-section') as Section; + if (id) setActive(id); + } + }, + { rootMargin, threshold: 0 }, + ); + + for (const key of SECTION_ORDER) { + const el = sectionRefs[key].current; + if (el) observer.observe(el); + } + + return () => observer.disconnect(); + }, [sectionRefs, navBarRef]); + + const scrollTo = useCallback((section: Section) => { + const el = sectionRefs[section].current; + if (!el) return; + const navBarHeight = navBarRef.current?.offsetHeight ?? 48; + const top = el.getBoundingClientRect().top + window.scrollY - navBarHeight - 8; + isScrollingRef.current = true; + setActive(section); + window.scrollTo({ top, behavior: 'smooth' }); + // Release the guard after the smooth scroll finishes + setTimeout(() => { isScrollingRef.current = false; }, 800); + }, [sectionRefs, navBarRef]); + + return { active, scrollTo }; +} + // --------------------------------------------------------------------------- // Section pill // --------------------------------------------------------------------------- @@ -89,12 +143,19 @@ function SectionPill({ section, active, onClick }: { active: boolean; onClick: () => void; }) { - const meta = section === 'all' - ? { label: 'All', icon: } - : SECTIONS[section]; + const meta = SECTIONS[section]; + const pillRef = useRef(null); + + // Auto-scroll the pill into view when it becomes active + useEffect(() => { + if (active && pillRef.current) { + pillRef.current.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' }); + } + }, [active]); return (