277 lines
12 KiB
React
277 lines
12 KiB
React
import { useState } from "react";
|
||
import { TABS } from "../constants/index.js";
|
||
import { version } from "../version.js";
|
||
import { useUpdateCheck } from "../hooks/useUpdateCheck.js";
|
||
import { useApp } from "../context/AppContext.jsx";
|
||
|
||
const TAB_ICONS = [
|
||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||
<rect x="1" y="1" width="6" height="6" rx="1.5" stroke="currentColor" strokeWidth="1.5"/>
|
||
<rect x="9" y="1" width="6" height="6" rx="1.5" stroke="currentColor" strokeWidth="1.5"/>
|
||
<rect x="1" y="9" width="6" height="6" rx="1.5" stroke="currentColor" strokeWidth="1.5"/>
|
||
<rect x="9" y="9" width="6" height="6" rx="1.5" stroke="currentColor" strokeWidth="1.5"/>
|
||
</svg>,
|
||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||
<polyline points="1,12 5,7 8,10 11,4 15,6" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="round"/>
|
||
<polyline points="11,4 15,4 15,8" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="round"/>
|
||
</svg>,
|
||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||
<rect x="1" y="3" width="14" height="10" rx="2" stroke="currentColor" strokeWidth="1.5"/>
|
||
<line x1="1" y1="6.5" x2="15" y2="6.5" stroke="currentColor" strokeWidth="1.5"/>
|
||
<line x1="3" y1="10" x2="6" y2="10" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
|
||
</svg>,
|
||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||
<rect x="1.5" y="8" width="3" height="6" rx="1" fill="currentColor"/>
|
||
<rect x="6.5" y="5" width="3" height="9" rx="1" fill="currentColor"/>
|
||
<rect x="11.5" y="2" width="3" height="12" rx="1" fill="currentColor"/>
|
||
</svg>,
|
||
];
|
||
|
||
const GEBRUIKERS_ICON = (
|
||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||
<circle cx="6" cy="5" r="2.5" stroke="currentColor" strokeWidth="1.5"/>
|
||
<path d="M1 13c0-2.5 2-4 5-4s5 1.5 5 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
|
||
<circle cx="12.5" cy="5" r="2" stroke="currentColor" strokeWidth="1.4"/>
|
||
<path d="M14.5 13c0-1.5-.8-2.7-2-3.3" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round"/>
|
||
</svg>
|
||
);
|
||
|
||
const INSTELLINGEN_ICON = (
|
||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||
<path fillRule="evenodd" clipRule="evenodd" d="M6.5 1a.75.75 0 0 0-.743.648l-.16 1.112a5.07 5.07 0 0 0-.88.51l-1.054-.424a.75.75 0 0 0-.9.31L1.263 4.95a.75.75 0 0 0 .165.96l.888.717a5.14 5.14 0 0 0 0 1.018l-.888.717a.75.75 0 0 0-.165.96l1.5 2.598a.75.75 0 0 0 .9.31l1.053-.424c.276.19.572.36.881.51l.16 1.112A.75.75 0 0 0 6.5 14h3a.75.75 0 0 0 .743-.648l.16-1.112c.309-.15.605-.32.88-.51l1.054.424a.75.75 0 0 0 .9-.31l1.5-2.598a.75.75 0 0 0-.165-.96l-.888-.717a5.14 5.14 0 0 0 0-1.018l.888-.717a.75.75 0 0 0 .165-.96l-1.5-2.598a.75.75 0 0 0-.9-.31l-1.053.424a5.07 5.07 0 0 0-.881-.51L9.743 1.648A.75.75 0 0 0 9.5 1h-3Zm1.5 4.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5Z"/>
|
||
</svg>
|
||
);
|
||
|
||
const LOGOUT_ICON = (
|
||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||
<path d="M6 2H3a1 1 0 00-1 1v10a1 1 0 001 1h3" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
|
||
<polyline points="10,5 13,8 10,11" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||
<line x1="13" y1="8" x2="6" y2="8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
|
||
</svg>
|
||
);
|
||
|
||
const INFO_ICON = (
|
||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||
<circle cx="8" cy="8" r="6.5" stroke="currentColor" strokeWidth="1.5"/>
|
||
<line x1="8" y1="7" x2="8" y2="11.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
|
||
<circle cx="8" cy="4.5" r="0.8" fill="currentColor"/>
|
||
</svg>
|
||
);
|
||
|
||
const MOON_ICON = (
|
||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||
<path d="M13.5 9.5A6 6 0 016.5 2.5a6 6 0 100 11 6 6 0 007-4z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||
</svg>
|
||
);
|
||
|
||
function initials(naam = "") {
|
||
const parts = naam.trim().split(/\s+/);
|
||
if (parts.length >= 2) return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
||
return naam.slice(0, 2).toUpperCase();
|
||
}
|
||
|
||
function SectionLabel({ children }) {
|
||
return (
|
||
<div style={{
|
||
fontSize: 11, fontWeight: 700, letterSpacing: "0.12em",
|
||
textTransform: "uppercase", color: "#3d4966",
|
||
padding: "0 12px", marginBottom: 2, marginTop: 20,
|
||
}}>
|
||
{children}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const ACTIVE_BG = "#7c3aed";
|
||
const ACTIVE_SHADOW = "0 4px 14px rgba(124,58,237,0.4)";
|
||
|
||
function NavItem({ active, onClick, icon, label, T, danger }) {
|
||
const [hovered, setHovered] = useState(false);
|
||
const isLit = active || hovered;
|
||
|
||
return (
|
||
<div
|
||
onClick={onClick}
|
||
onMouseEnter={() => setHovered(true)}
|
||
onMouseLeave={() => setHovered(false)}
|
||
style={{
|
||
display: "flex", alignItems: "center", gap: 10,
|
||
padding: "11px 12px", borderRadius: 9, cursor: "pointer", marginBottom: 4,
|
||
background: active
|
||
? ACTIVE_BG
|
||
: hovered ? "rgba(139,92,246,0.08)" : "transparent",
|
||
color: active ? "#fff" : hovered ? T.text : (danger ? "#ef4444" : T.muted),
|
||
fontWeight: active ? 600 : 400,
|
||
fontSize: 15,
|
||
boxShadow: active ? ACTIVE_SHADOW : "none",
|
||
transition: "all 0.15s",
|
||
}}
|
||
>
|
||
<span style={{ flexShrink: 0, display: "flex", color: "inherit", opacity: active ? 1 : (isLit ? 0.85 : 0.55) }}>{icon}</span>
|
||
<span style={{ flex: 1 }}>{label}</span>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function OverPopup({ onClose, nieuweVersie, T }) {
|
||
return (
|
||
<>
|
||
{/* Overlay */}
|
||
<div onClick={onClose} style={{
|
||
position: "fixed", inset: 0, zIndex: 490,
|
||
background: "rgba(0,0,0,0.55)", backdropFilter: "blur(4px)",
|
||
display: "flex", alignItems: "center", justifyContent: "center",
|
||
}}>
|
||
{/* Modal */}
|
||
<div onClick={(e) => e.stopPropagation()} style={{
|
||
background: T.popupBg,
|
||
border: `1px solid ${T.border}`,
|
||
borderRadius: 16, padding: "28px 28px 24px", width: 300,
|
||
boxShadow: "0 24px 60px rgba(0,0,0,0.5)",
|
||
position: "relative",
|
||
}}>
|
||
{/* Sluitknop */}
|
||
<button onClick={onClose} style={{
|
||
position: "absolute", top: 14, right: 14,
|
||
background: "transparent", border: "none", cursor: "pointer",
|
||
color: T.muted, fontSize: 20, lineHeight: 1, padding: 4,
|
||
}}>×</button>
|
||
|
||
{/* Logo + naam */}
|
||
<div style={{ display: "flex", alignItems: "center", gap: 12, marginBottom: 20 }}>
|
||
<div style={{
|
||
width: 44, height: 44, borderRadius: 12, flexShrink: 0,
|
||
background: "rgba(124,58,237,0.15)", border: "1px solid rgba(124,58,237,0.3)",
|
||
display: "flex", alignItems: "center", justifyContent: "center",
|
||
}}>
|
||
<svg width="22" height="22" viewBox="0 0 20 20" fill="none">
|
||
<polyline points="2,15 6,9 10,12 14,5 18,8" stroke="#a855f7" strokeWidth="2.2" strokeLinejoin="round" strokeLinecap="round"/>
|
||
<line x1="2" y1="17" x2="18" y2="17" stroke="rgba(168,85,247,0.35)" strokeWidth="1.4"/>
|
||
</svg>
|
||
</div>
|
||
<div>
|
||
<div style={{ fontSize: 15, fontWeight: 700, color: T.text }}>VibeFinance</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{ height: 1, background: T.border, marginBottom: 16 }} />
|
||
|
||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 10 }}>
|
||
<span style={{ fontSize: 13, color: T.muted }}>Versie</span>
|
||
<span style={{ fontSize: 12, fontWeight: 700, color: "#a855f7", background: "rgba(124,58,237,0.15)", borderRadius: 6, padding: "3px 10px" }}>
|
||
v{version}
|
||
</span>
|
||
</div>
|
||
|
||
{nieuweVersie && (
|
||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 10 }}>
|
||
<span style={{ fontSize: 13, color: T.muted }}>Nieuwe versie</span>
|
||
<span style={{ fontSize: 12, fontWeight: 700, color: "#f97316", background: "rgba(249,115,22,0.12)", borderRadius: 6, padding: "3px 10px" }}>
|
||
v{nieuweVersie.latest}
|
||
</span>
|
||
</div>
|
||
)}
|
||
|
||
<div style={{ height: 1, background: T.border, margin: "12px 0" }} />
|
||
<div style={{ fontSize: 12, color: T.muted, textAlign: "center" }}>© {new Date().getFullYear()} VibeFinance</div>
|
||
</div>
|
||
</div>
|
||
</>
|
||
);
|
||
}
|
||
|
||
function DarkModeToggle({ darkMode, setDarkMode, T }) {
|
||
return (
|
||
<div
|
||
onClick={() => setDarkMode(!darkMode)}
|
||
style={{
|
||
display: "flex", alignItems: "center", gap: 10,
|
||
padding: "11px 12px", borderRadius: 9, cursor: "pointer", marginBottom: 4,
|
||
color: T.muted, fontSize: 13.5, fontWeight: 500,
|
||
transition: "background 0.12s",
|
||
}}
|
||
onMouseEnter={e => e.currentTarget.style.background = "rgba(255,255,255,0.04)"}
|
||
onMouseLeave={e => e.currentTarget.style.background = "transparent"}
|
||
>
|
||
<span style={{ flexShrink: 0, display: "flex", opacity: 0.6 }}>{MOON_ICON}</span>
|
||
<span style={{ flex: 1 }}>Dark Mode</span>
|
||
{/* Toggle pill */}
|
||
<div style={{
|
||
width: 36, height: 20, borderRadius: 99, flexShrink: 0,
|
||
background: darkMode ? "#7c3aed" : "rgba(255,255,255,0.15)",
|
||
position: "relative", transition: "background 0.2s",
|
||
}}>
|
||
<div style={{
|
||
position: "absolute", top: 3, left: darkMode ? 19 : 3,
|
||
width: 14, height: 14, borderRadius: "50%",
|
||
background: "#fff", transition: "left 0.2s",
|
||
boxShadow: "0 1px 4px rgba(0,0,0,0.3)",
|
||
}} />
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default function NavBar() {
|
||
const { tab, setTab, setShowUsers, setShowInstellingen, T, logout, darkMode, setDarkMode, currentUser, avatar } = useApp();
|
||
const nieuweVersie = useUpdateCheck();
|
||
const [overOpen, setOverOpen] = useState(false);
|
||
|
||
return (
|
||
<div style={{
|
||
width: 260, minWidth: 260, flexShrink: 0,
|
||
height: "100vh", position: "sticky", top: 0,
|
||
background: T.sidebar ?? T.card,
|
||
borderRight: `1px solid ${T.border}`,
|
||
display: "flex", flexDirection: "column",
|
||
zIndex: 100,
|
||
}}>
|
||
|
||
{/* Logo */}
|
||
<div style={{ padding: "22px 16px 28px", display: "flex", alignItems: "center", gap: 10 }}>
|
||
<div style={{
|
||
width: 32, height: 32, flexShrink: 0, borderRadius: 9,
|
||
background: "rgba(124,58,237,0.15)",
|
||
border: "1px solid rgba(124,58,237,0.3)",
|
||
display: "flex", alignItems: "center", justifyContent: "center",
|
||
}}>
|
||
<svg width="18" height="18" viewBox="0 0 20 20" fill="none">
|
||
<polyline points="2,15 6,9 10,12 14,5 18,8"
|
||
stroke="#a855f7" strokeWidth="2.2"
|
||
strokeLinejoin="round" strokeLinecap="round"/>
|
||
<line x1="2" y1="17" x2="18" y2="17"
|
||
stroke="rgba(168,85,247,0.35)" strokeWidth="1.4"/>
|
||
</svg>
|
||
</div>
|
||
<span style={{ fontSize: 17, fontWeight: 700, color: T.text, letterSpacing: "-0.3px" }}>VibeFinance</span>
|
||
</div>
|
||
|
||
{/* Nav */}
|
||
<nav style={{ flex: 1, padding: "0 8px", overflowY: "auto" }}>
|
||
|
||
{TABS.map((t, i) => (
|
||
<NavItem key={t} active={tab === i} onClick={() => setTab(i)} icon={TAB_ICONS[i]} label={t} T={T} />
|
||
))}
|
||
|
||
|
||
<NavItem onClick={() => setShowUsers(true)} icon={GEBRUIKERS_ICON} label="Gebruikers" T={T} />
|
||
<NavItem onClick={() => setShowInstellingen(true)} icon={INSTELLINGEN_ICON} label="Instellingen" T={T} />
|
||
|
||
|
||
<div style={{ position: "relative" }}>
|
||
<NavItem onClick={() => setOverOpen(o => !o)} icon={INFO_ICON} label="Over VibeFinance" active={false} T={T} />
|
||
{overOpen && <OverPopup onClose={() => setOverOpen(false)} nieuweVersie={nieuweVersie} T={T} />}
|
||
{nieuweVersie && !overOpen && (
|
||
<div style={{ position: "absolute", top: 10, right: 26, width: 6, height: 6, borderRadius: "50%", background: "#f97316", pointerEvents: "none" }} />
|
||
)}
|
||
</div>
|
||
</nav>
|
||
|
||
{/* Uitloggen */}
|
||
<div style={{ padding: "8px 8px 12px", borderTop: `1px solid ${T.border}` }}>
|
||
<NavItem onClick={logout} icon={LOGOUT_ICON} label="Log Out" T={T} />
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|