291 lines
14 KiB
JavaScript
291 lines
14 KiB
JavaScript
import { useState, useEffect } from "react";
|
|
import { PURPLE, PURPLE_LIGHT, TABS } from "../constants/index.js";
|
|
import { version, updateCheckUrl } from "../../package.json";
|
|
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 DATA_ICON = (
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
<path d="M8 1v8M5 6l3 3 3-3" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
|
<path d="M2 11v2a1 1 0 001 1h10a1 1 0 001-1v-2" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
|
|
</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>
|
|
);
|
|
|
|
function Tooltip({ label, collapsed }) {
|
|
if (!collapsed) return null;
|
|
return (
|
|
<div style={{
|
|
position: "absolute", left: "calc(100% + 10px)", top: "50%",
|
|
transform: "translateY(-50%)",
|
|
background: "#1e2130", border: "1px solid rgba(255,255,255,0.1)",
|
|
borderRadius: 6, padding: "5px 10px",
|
|
fontSize: 12, fontWeight: 500, color: "#fff",
|
|
whiteSpace: "nowrap", pointerEvents: "none", zIndex: 200,
|
|
}}>{label}</div>
|
|
);
|
|
}
|
|
|
|
function SidebarItem({ active, onClick, icon, label, T, collapsed }) {
|
|
const [hovered, setHovered] = useState(false);
|
|
return (
|
|
<div
|
|
onClick={onClick}
|
|
onMouseEnter={() => setHovered(true)}
|
|
onMouseLeave={() => setHovered(false)}
|
|
title={collapsed ? label : undefined}
|
|
style={{
|
|
position: "relative",
|
|
display: "flex", alignItems: "center",
|
|
gap: collapsed ? 0 : 10,
|
|
padding: "9px 12px", borderRadius: 8, cursor: "pointer", marginBottom: 6,
|
|
justifyContent: collapsed ? "center" : "flex-start",
|
|
background: active ? `${PURPLE}18` : "transparent",
|
|
color: active ? PURPLE_LIGHT : T.muted,
|
|
fontWeight: active ? 600 : 500,
|
|
fontSize: 13,
|
|
transition: "background 0.15s, color 0.15s",
|
|
}}>
|
|
<span style={{ flexShrink: 0, display: "flex" }}>{icon}</span>
|
|
{!collapsed && <span style={{ flex: 1 }}>{label}</span>}
|
|
{hovered && <Tooltip label={label} collapsed={collapsed} />}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function NavBar() {
|
|
const {
|
|
tab, setTab,
|
|
setShowUsers,
|
|
T,
|
|
logout,
|
|
doBackup, doRestore,
|
|
} = useApp();
|
|
|
|
const [nieuweVersie, setNieuweVersie] = useState(null);
|
|
const [dataOpen, setDataOpen] = useState(false);
|
|
const [collapsed, setCollapsed] = useState(false);
|
|
|
|
const W = collapsed ? 56 : 220;
|
|
|
|
useEffect(() => {
|
|
if (!updateCheckUrl) return;
|
|
fetch(updateCheckUrl)
|
|
.then((r) => r.json())
|
|
.then((data) => {
|
|
const latest = data?.tag_name?.replace(/^v/, "");
|
|
if (latest && latest !== version) setNieuweVersie(latest);
|
|
})
|
|
.catch(() => {});
|
|
}, []);
|
|
|
|
return (
|
|
<div style={{
|
|
width: W, minWidth: W, flexShrink: 0,
|
|
height: "100vh", position: "sticky", top: 0,
|
|
background: T.card, borderRight: `1px solid ${T.border}`,
|
|
display: "flex", flexDirection: "column",
|
|
zIndex: 100, transition: "width 0.2s ease, min-width 0.2s ease",
|
|
overflow: "hidden",
|
|
}}>
|
|
{/* Logo + collapse knop */}
|
|
{!collapsed ? (
|
|
<div style={{ padding: "20px 12px 16px", display: "flex", alignItems: "center", gap: 8, justifyContent: "space-between" }}>
|
|
<div style={{ display: "flex", alignItems: "center", gap: 8, overflow: "hidden" }}>
|
|
<div style={{
|
|
width: 42, height: 42, flexShrink: 0,
|
|
background: `linear-gradient(135deg, ${PURPLE}, #a855f7)`,
|
|
borderRadius: 10, display: "flex", alignItems: "center",
|
|
justifyContent: "center", boxShadow: `0 0 12px ${PURPLE}66`,
|
|
}}>
|
|
<svg width="26" height="26" viewBox="0 0 20 20" fill="none">
|
|
<polyline points="2,15 6,9 10,12 14,5 18,8" stroke="#fff"
|
|
strokeWidth="2" strokeLinejoin="round" strokeLinecap="round" />
|
|
<line x1="2" y1="17" x2="18" y2="17"
|
|
stroke="rgba(255,255,255,0.35)" strokeWidth="1.2" />
|
|
</svg>
|
|
</div>
|
|
<span style={{
|
|
fontWeight: 700, fontSize: 17, whiteSpace: "nowrap",
|
|
background: `linear-gradient(90deg, ${PURPLE}, #a855f7)`,
|
|
WebkitBackgroundClip: "text", WebkitTextFillColor: "transparent",
|
|
}}>VibeFinance</span>
|
|
</div>
|
|
<button onClick={() => setCollapsed(true)} style={{
|
|
background: "transparent", border: `1px solid ${T.border}`,
|
|
borderRadius: 6, cursor: "pointer", color: T.muted,
|
|
width: 28, height: 28, display: "flex", alignItems: "center", justifyContent: "center",
|
|
flexShrink: 0, padding: 0,
|
|
}}>
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
<rect x="1" y="1" width="14" height="14" rx="2.5" stroke="currentColor" strokeWidth="1.4"/>
|
|
<line x1="5" y1="1.7" x2="5" y2="14.3" stroke="currentColor" strokeWidth="1.4"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div style={{ padding: "20px 12px 16px", display: "flex", justifyContent: "center" }}>
|
|
<button onClick={() => setCollapsed(false)} style={{
|
|
background: "transparent", border: `1px solid ${T.border}`,
|
|
borderRadius: 6, cursor: "pointer", color: T.muted,
|
|
width: 28, height: 28, display: "flex", alignItems: "center", justifyContent: "center",
|
|
padding: 0,
|
|
}}>
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
<rect x="1" y="1" width="14" height="14" rx="2.5" stroke="currentColor" strokeWidth="1.4"/>
|
|
<line x1="5" y1="1.7" x2="5" y2="14.3" stroke="currentColor" strokeWidth="1.4"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
|
|
{/* Navigatie */}
|
|
<nav style={{ flex: 1, padding: `16px ${collapsed ? 8 : 10}px`, overflowY: "auto" }}>
|
|
{TABS.map((t, i) => (
|
|
<SidebarItem
|
|
key={t} active={tab === i} onClick={() => setTab(i)}
|
|
icon={TAB_ICONS[i]} label={t} T={T} collapsed={collapsed}
|
|
/>
|
|
))}
|
|
|
|
<div style={{ height: 1, background: T.border, margin: "12px 2px" }} />
|
|
|
|
<SidebarItem
|
|
onClick={() => setShowUsers(true)}
|
|
icon={GEBRUIKERS_ICON}
|
|
label="Gebruikers"
|
|
T={T}
|
|
collapsed={collapsed}
|
|
/>
|
|
|
|
{/* Data */}
|
|
{collapsed ? (
|
|
<div style={{ position: "relative" }}>
|
|
<SidebarItem
|
|
onClick={() => setDataOpen((o) => !o)}
|
|
icon={DATA_ICON}
|
|
label="Data"
|
|
T={T}
|
|
collapsed={collapsed}
|
|
/>
|
|
{dataOpen && (
|
|
<div style={{
|
|
position: "absolute", left: "calc(100% + 10px)", top: 0,
|
|
background: T.card, border: `1px solid ${T.border}`,
|
|
borderRadius: 8, padding: "4px 0", minWidth: 160, zIndex: 200,
|
|
boxShadow: "0 4px 16px rgba(0,0,0,0.3)",
|
|
}}>
|
|
<div onClick={() => { doBackup(); setDataOpen(false); }} style={{ display: "flex", alignItems: "center", gap: 8, padding: "8px 14px", cursor: "pointer", color: T.muted, fontSize: 12 }}>
|
|
<svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M8 1v8M5 6l3 3 3-3" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/><path d="M2 11v2a1 1 0 001 1h10a1 1 0 001-1v-2" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/></svg>
|
|
Backup maken
|
|
</div>
|
|
<div onClick={() => { doRestore(); setDataOpen(false); }} style={{ display: "flex", alignItems: "center", gap: 8, padding: "8px 14px", cursor: "pointer", color: T.muted, fontSize: 12 }}>
|
|
<svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M2 8a6 6 0 106 -6" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/><polyline points="2,4 2,8 6,8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/></svg>
|
|
Backup herstellen
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div>
|
|
<div onClick={() => setDataOpen((o) => !o)} style={{
|
|
display: "flex", alignItems: "center", gap: 10,
|
|
padding: "9px 12px", borderRadius: 8, cursor: "pointer", marginBottom: 6,
|
|
color: T.muted, fontSize: 13, fontWeight: 500,
|
|
}}>
|
|
{DATA_ICON}
|
|
<span style={{ flex: 1 }}>Data</span>
|
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"
|
|
style={{ transform: dataOpen ? "rotate(180deg)" : "none", transition: "transform 0.2s", flexShrink: 0 }}>
|
|
<polyline points="2,4 6,8 10,4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
|
</svg>
|
|
</div>
|
|
{dataOpen && (
|
|
<div style={{ paddingLeft: 14, marginBottom: 4 }}>
|
|
<div onClick={() => { doBackup(); setDataOpen(false); }} style={{ display: "flex", alignItems: "center", gap: 8, padding: "7px 12px", borderRadius: 8, cursor: "pointer", color: T.muted, fontSize: 12 }}>
|
|
<svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M8 1v8M5 6l3 3 3-3" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/><path d="M2 11v2a1 1 0 001 1h10a1 1 0 001-1v-2" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/></svg>
|
|
Backup maken
|
|
</div>
|
|
<div onClick={() => { doRestore(); setDataOpen(false); }} style={{ display: "flex", alignItems: "center", gap: 8, padding: "7px 12px", borderRadius: 8, cursor: "pointer", color: T.muted, fontSize: 12 }}>
|
|
<svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M2 8a6 6 0 106 -6" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/><polyline points="2,4 2,8 6,8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/></svg>
|
|
Backup herstellen
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</nav>
|
|
|
|
{/* Onderste sectie */}
|
|
<div style={{ padding: `10px ${collapsed ? 8 : 10}px 24px`, borderTop: `1px solid ${T.border}` }}>
|
|
|
|
{/* Update melding */}
|
|
{nieuweVersie && !collapsed && (
|
|
<div style={{
|
|
margin: "8px 0", padding: "8px 12px", borderRadius: 8,
|
|
background: "rgba(249,115,22,0.1)", border: "1px solid rgba(249,115,22,0.25)",
|
|
}}>
|
|
<div style={{ fontSize: 11, color: "#f97316", fontWeight: 700, marginBottom: 2 }}>
|
|
Nieuwe versie: v{nieuweVersie}
|
|
</div>
|
|
<div style={{ fontSize: 11, color: T.muted }}>Huidig: v{version}</div>
|
|
</div>
|
|
)}
|
|
{nieuweVersie && collapsed && (
|
|
<div style={{ display: "flex", justifyContent: "center", marginBottom: 6 }}>
|
|
<div style={{ width: 8, height: 8, borderRadius: "50%", background: "#f97316" }} title={`v${nieuweVersie} beschikbaar`} />
|
|
</div>
|
|
)}
|
|
|
|
{/* Uitloggen */}
|
|
<SidebarItem
|
|
onClick={logout}
|
|
icon={<span style={{ color: T.muted, display: "flex" }}>{LOGOUT_ICON}</span>}
|
|
label="Log out"
|
|
T={T}
|
|
collapsed={collapsed}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|