From bbdd02c9e9996f42822a547e048a454eca087afe Mon Sep 17 00:00:00 2001 From: mariomitte Date: Sun, 19 Jul 2026 16:18:15 +0200 Subject: [PATCH] fix: stabiliziraj navbar tranziciju i ukloni white splash Zadrzava Navbar i ostale UI otoke kroz Astro tranzicije te sinkronizira aktivni path preko astro:page-load kako bi react-spring indikator animirao iz trenutne pozicije. Uklanja bijeli splash pri promjeni stranice i prelasku teme postavljanjem html background boje inline, prilagodbom ThemeToggle logike i uklanjanjem body color tranzicije. Dodano je i TTL kesiranje za dashboard/task dohvat kako bi se smanjili redundantni API pozivi pri navigaciji. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- frontend/src/components/Navbar.jsx | 120 ++++++++++++++---- .../dashboard/FleetDashboardShell.jsx | 5 - frontend/src/components/ui/ThemeToggle.jsx | 2 + frontend/src/layouts/Layout.astro | 15 ++- frontend/src/stores/fleetDashboardStore.js | 13 +- frontend/src/stores/taskStore.js | 9 +- frontend/src/styles/global.css | 13 ++ 7 files changed, 139 insertions(+), 38 deletions(-) diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx index 31950f4..e412f28 100644 --- a/frontend/src/components/Navbar.jsx +++ b/frontend/src/components/Navbar.jsx @@ -2,7 +2,8 @@ import NotificationBell from './NotificationBell'; import ThemeToggle from './ui/ThemeToggle'; import AuthWidget from './AuthWidget'; import UserDisplay from './ui/UserDisplay'; -import { useEffect, useMemo, useState } from 'preact/hooks'; +import { useEffect, useRef, useState } from 'preact/hooks'; +import { useSpring, animated } from '@react-spring/web'; import { hydrateAuthFromStorage } from '../stores/authStore'; const ITEMS = [ @@ -13,27 +14,82 @@ const ITEMS = [ { id: 'clients', label: 'Klijenti', href: '/clients' }, ]; -export default function Navbar({ minimal = false }) { - const [pathname, setPathname] = useState('/'); - const normalizedPath = useMemo(() => { - const value = String(pathname || '/'); - if (value.length > 1 && value.endsWith('/')) return value.slice(0, -1); - return value; - }, [pathname]); +function normalizePath(p) { + const value = String(p || '/'); + if (value.length > 1 && value.endsWith('/')) return value.slice(0, -1); + return value; +} +function getActiveIndex(path) { + const idx = ITEMS.findIndex((item) => item.href === path); + return idx >= 0 ? idx : 0; +} + +export default function Navbar({ minimal = false }) { + // Navbar ostaje montiran (transition:persist) — pratimo promjene pathname preko astro:page-load + const [pathname, setPathname] = useState(() => + typeof window !== 'undefined' ? normalizePath(window.location.pathname) : '/' + ); + + const itemRefs = useRef([]); + const [indicatorStyle, setIndicatorStyle] = useState({ left: 0, width: 0 }); + // Na prvom renderu preskočimo animaciju (pill se pojavljuje na ispravnoj poziciji bez slide-a od 0) + const [skipAnimation, setSkipAnimation] = useState(true); + + const activeIndex = getActiveIndex(pathname); + + const measureIndicator = (currentIndex) => { + const idx = currentIndex ?? activeIndex; + const el = itemRefs.current[idx]; + if (!el) return; + const parent = el.closest('ul'); + if (!parent) return; + const parentRect = parent.getBoundingClientRect(); + const elRect = el.getBoundingClientRect(); + setIndicatorStyle({ + left: elRect.left - parentRect.left, + width: elRect.width, + }); + }; + + // Mount: izmjeri početnu poziciju bez animacije, pa uključi animaciju za buduće navigacije useEffect(() => { - const updatePath = () => setPathname(window.location.pathname || '/'); - updatePath(); - window.addEventListener('astro:page-load', updatePath); - // Hidratira auth iz storage pri prvi puta učitavanja Navbar-a hydrateAuthFromStorage(); - return () => window.removeEventListener('astro:page-load', updatePath); + + const timer = setTimeout(() => { + measureIndicator(); + setSkipAnimation(false); + }, 30); + + // Sluša Astro view-transition navigaciju (Navbar ostaje montiran zahvaljujući transition:persist) + const handlePageLoad = () => { + setPathname(normalizePath(window.location.pathname)); + }; + document.addEventListener('astro:page-load', handlePageLoad); + + return () => { + clearTimeout(timer); + document.removeEventListener('astro:page-load', handlePageLoad); + }; }, []); - const currentItem = useMemo( - () => ITEMS.find((item) => item.href === normalizedPath) || ITEMS[0], - [normalizedPath] - ); + // Kad se promijeni activeIndex (nova stranica), animiraj pill na novu poziciju + useEffect(() => { + if (!skipAnimation) { + measureIndicator(); + } + }, [activeIndex]); + + // react-spring: animirani klizač + // immediate=true na prvom renderu → pill se ne animira od lijevog ruba, već skače na pravo mjesto + const springStyle = useSpring({ + left: indicatorStyle.left, + width: indicatorStyle.width, + immediate: skipAnimation, + config: { tension: 340, friction: 28 }, + }); + + const currentItem = ITEMS[activeIndex]; return (