The dropdown was anchored to right-0 of the <details> element. On mobile, the trigger button sits near the left side of the viewport so right-0 caused the panel to extend leftward off-screen. Changing to left-0 opens the panel rightward from the button, keeping it fully visible. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
173 lines
7.7 KiB
JavaScript
173 lines
7.7 KiB
JavaScript
import NotificationBell from './NotificationBell';
|
|
import ThemeToggle from './ui/ThemeToggle';
|
|
import AuthWidget from './AuthWidget';
|
|
import UserDisplay from './ui/UserDisplay';
|
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
|
import { useSpring, animated } from '@react-spring/web';
|
|
import { hydrateAuthFromStorage } from '../stores/authStore';
|
|
|
|
const ITEMS = [
|
|
{ id: 'dashboard', label: 'Dashboard', href: '/' },
|
|
{ id: 'work-orders', label: 'Putni nalozi', href: '/putni-nalozi' },
|
|
{ id: 'service-records', label: 'Servisni zapisi', href: '/servisni-zapisi' },
|
|
{ id: 'vehicles', label: 'Vozila', href: '/vehicles' },
|
|
{ id: 'clients', label: 'Klijenti', href: '/clients' },
|
|
];
|
|
|
|
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(() => {
|
|
hydrateAuthFromStorage();
|
|
|
|
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);
|
|
};
|
|
}, []);
|
|
|
|
// 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 (
|
|
<nav className="sticky top-0 z-30 mb-4 rounded-lg border border-border-hairline bg-canvas-elevated/95 px-4 py-3 backdrop-blur overflow-visible">
|
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
{/* Logo + korisnik */}
|
|
<a href="/" className="flex flex-col hover:opacity-80">
|
|
<span className="text-base font-bold tracking-tight text-text-main">ERP</span>
|
|
{!minimal && <UserDisplay field="ime_prezime" className="text-xs text-text-muted" />}
|
|
</a>
|
|
|
|
{/* Navigacijski linkovi s react-spring klizačem */}
|
|
<ul className="relative hidden items-center gap-1 text-sm sm:flex">
|
|
{/* Animirani pozadinski klizač */}
|
|
<animated.li
|
|
aria-hidden="true"
|
|
style={{
|
|
position: 'absolute',
|
|
top: 0,
|
|
bottom: 0,
|
|
borderRadius: '0.5rem',
|
|
backgroundColor: 'rgb(238 242 255)',
|
|
pointerEvents: 'none',
|
|
...springStyle,
|
|
}}
|
|
/>
|
|
{ITEMS.map((item, idx) => (
|
|
<li key={item.id} ref={(el) => (itemRefs.current[idx] = el)}>
|
|
<a
|
|
href={item.href}
|
|
className={
|
|
pathname === item.href
|
|
? 'relative z-10 block rounded-lg px-3 py-2 font-medium text-indigo-700'
|
|
: 'relative z-10 block rounded-lg px-3 py-2 text-text-main hover:text-indigo-600'
|
|
}
|
|
>
|
|
{item.label}
|
|
</a>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
|
|
{/* Desna strana */}
|
|
<div className="flex items-center gap-2">
|
|
{/* Mobilni dropdown */}
|
|
<details className="relative sm:hidden">
|
|
<summary className="list-none cursor-pointer rounded-lg border border-border-hairline bg-canvas-base px-3 py-1.5 text-xs font-medium text-text-main hover:bg-canvas-deep select-none">
|
|
☰ {currentItem?.label || 'Izbornik'}
|
|
</summary>
|
|
{/* Dropdown je positioniran fixed-like kroz visoki z-index kako ne bi bio odsječen */}
|
|
<div className="absolute left-0 top-full mt-1 z-50 min-w-52 rounded-lg border border-border-hairline bg-canvas-elevated shadow-xl">
|
|
<div className="border-b border-border-hairline px-3 py-2 text-[11px] uppercase tracking-wide text-text-muted">
|
|
Navigacija
|
|
</div>
|
|
<ul className="py-1">
|
|
{ITEMS.map((item) => (
|
|
<li key={`mobile-${item.id}`}>
|
|
<a
|
|
href={item.href}
|
|
className={
|
|
pathname === item.href
|
|
? 'flex items-center gap-2 bg-indigo-50 px-3 py-2.5 text-sm font-medium text-indigo-700'
|
|
: 'flex items-center gap-2 px-3 py-2.5 text-sm text-text-main hover:bg-canvas-deep'
|
|
}
|
|
>
|
|
{pathname === item.href && <span className="h-1.5 w-1.5 rounded-full bg-indigo-500" />}
|
|
{item.label}
|
|
</a>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
</details>
|
|
<ThemeToggle />
|
|
<NotificationBell />
|
|
<AuthWidget />
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
);
|
|
}
|