From 0a21de187958220ce4780c16a9e1f6ee1a9c1830 Mon Sep 17 00:00:00 2001 From: omarscoding Date: Tue, 13 Oct 2026 13:52:37 +1300 Subject: [PATCH 1/3] made nav bar sticky --- client/src/main/Header.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/main/Header.tsx b/client/src/main/Header.tsx index eac7036..edd0a9e 100644 --- a/client/src/main/Header.tsx +++ b/client/src/main/Header.tsx @@ -142,7 +142,7 @@ const Header = () => { }, [isProfileMenuOpen]); return ( -
+
{/** MOBILE ONLY - profile shortcut, balances the hamburger on the right */}
{!loading && From f9ee545e1e06d247499a16617ab6b740a37b7e79 Mon Sep 17 00:00:00 2001 From: omarscoding Date: Tue, 13 Oct 2026 14:07:21 +1300 Subject: [PATCH 2/3] fixed navbar pill jumping on page change --- client/src/main/App.tsx | 7 ++- client/src/main/Header.tsx | 111 +++++++++++++++++++++++++++---------- 2 files changed, 87 insertions(+), 31 deletions(-) diff --git a/client/src/main/App.tsx b/client/src/main/App.tsx index 9f66a3c..4948b20 100644 --- a/client/src/main/App.tsx +++ b/client/src/main/App.tsx @@ -6,7 +6,7 @@ import { Outlet, useLocation, } from "react-router-dom"; -import { useEffect } from "react"; +import { useLayoutEffect } from "react"; import { AuthProvider } from "../auth/AuthProvider.tsx"; import Header from "./Header.tsx"; import Footer from "./Footer.tsx"; @@ -67,8 +67,9 @@ const Layout = () => { const { pathname } = useLocation(); // Reset scroll position on every route change - BrowserRouter doesn't do - // this automatically the way a full-page navigation would. - useEffect(() => { + // this automatically the way a full-page navigation would. A layout effect + // runs before paint, so the new page never flashes at the old scroll offset. + useLayoutEffect(() => { window.scrollTo(0, 0); }, [pathname]); diff --git a/client/src/main/Header.tsx b/client/src/main/Header.tsx index edd0a9e..0375225 100644 --- a/client/src/main/Header.tsx +++ b/client/src/main/Header.tsx @@ -1,5 +1,11 @@ -import { AnimatePresence, motion } from "motion/react"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { AnimatePresence, animate, motion, useMotionValue } from "motion/react"; +import { + useCallback, + useEffect, + useLayoutEffect, + useRef, + useState, +} from "react"; import { ChevronDown, CircleUserRound, @@ -20,6 +26,7 @@ import "../style/common.css"; const tabs = ["Home", "About", "Events", "Sponsors", "Contact", "Faq"]; const profileImageUpdatedEvent = "profile-image-updated"; +const pillTransition = { type: "spring", bounce: 0, duration: 0.4 } as const; // "Home" lives at "/" rather than "/home". const routeForTab = (tab: string) => { @@ -38,6 +45,14 @@ const Header = () => { const [isProfileMenuOpen, setIsProfileMenuOpen] = useState(false); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [menuPath, setMenuPath] = useState(location.pathname); + const navRef = useRef(null); + const tabRefs = useRef<(HTMLAnchorElement | null)[]>([]); + const pillX = useMotionValue(0); + const pillWidth = useMotionValue(0); + const isPillPlaced = useRef(false); + const activeTabIndex = tabs.findIndex( + (tab) => routeForTab(tab) === location.pathname + ); // Navigating away (including via browser back/forward) closes the drawer. if (menuPath !== location.pathname) { @@ -114,6 +129,47 @@ const Header = () => { setFailedProfileImageUrls(new Set()); }, [user?.id]); + // The nav pill is one persistent element positioned from the active link's + // offsetLeft/offsetWidth. Those are relative to the nav, so scroll position + // (which a sticky header's page offset depends on) never affects it. It + // slides only when the active link changes, and snaps into place on first + // paint, when re-appearing after a non-tab page, or on resize. + useLayoutEffect(() => { + const nav = navRef.current; + const activeTab = tabRefs.current[activeTabIndex]; + if (!nav || !activeTab) { + isPillPlaced.current = false; + return; + } + + let target = { left: -1, width: -1 }; + const place = (shouldAnimate: boolean) => { + const left = activeTab.offsetLeft; + const width = activeTab.offsetWidth; + if (left === target.left && width === target.width) return; + target = { left, width }; + + if (shouldAnimate) { + animate(pillX, left, pillTransition); + animate(pillWidth, width, pillTransition); + } else { + pillX.jump(left); + pillWidth.jump(width); + } + }; + + place(isPillPlaced.current); + isPillPlaced.current = true; + + const observer = new ResizeObserver(() => place(false)); + observer.observe(nav); + for (const tab of tabRefs.current) { + if (tab) observer.observe(tab); + } + + return () => observer.disconnect(); + }, [activeTabIndex, pillX, pillWidth]); + useEffect(() => { if (!isProfileMenuOpen) return; @@ -198,32 +254,31 @@ const Header = () => {
-
- - {tabs.map((tab) => { - const actualRoute = routeForTab(tab); - const isSelected = location.pathname === actualRoute; - - return ( - - - {tab} - - {isSelected && ( - - )} - - ); - })} - +
+ {activeTabIndex !== -1 && ( +
From 7dffbe1469882a70f8051a83845a541e68175905 Mon Sep 17 00:00:00 2001 From: omarscoding Date: Tue, 20 Oct 2026 13:36:08 +1300 Subject: [PATCH 3/3] made left side div in pfp and admin dashboard stick to top of page, no more following page when scrolling down --- client/src/components/admin/AdminSidebar.tsx | 2 +- client/src/components/profile/ProfileSidebar.tsx | 2 +- client/src/pages/Profile.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/src/components/admin/AdminSidebar.tsx b/client/src/components/admin/AdminSidebar.tsx index 65de0f2..8fdb283 100644 --- a/client/src/components/admin/AdminSidebar.tsx +++ b/client/src/components/admin/AdminSidebar.tsx @@ -17,7 +17,7 @@ export default function AdminSidebar({ ]; return ( -