diff --git a/frontend/index.html b/frontend/index.html index 660e319..36c42b8 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -8,7 +8,7 @@ VibeFinance - +
diff --git a/frontend/package.json b/frontend/package.json index ea0e4bb..0a6e51c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "vibefinance-frontend", - "version": "0.1.4", + "version": "0.1.5-dev", "updateCheckUrl": "https://vibehoogie.duckdns.org/api/v1/repos/vibe/VibeFinance/releases/latest", "private": true, "type": "module", diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg index 682a971..d57f17b 100644 --- a/frontend/public/favicon.svg +++ b/frontend/public/favicon.svg @@ -1,8 +1,8 @@ - - + + diff --git a/frontend/vibefinance-logo-wordmark.svg b/frontend/public/icons/vibefinance-logo-wordmark.svg similarity index 100% rename from frontend/vibefinance-logo-wordmark.svg rename to frontend/public/icons/vibefinance-logo-wordmark.svg diff --git a/frontend/vibefinance-logo.svg b/frontend/public/icons/vibefinance-logo.svg similarity index 100% rename from frontend/vibefinance-logo.svg rename to frontend/public/icons/vibefinance-logo.svg diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index d231b8f..478170c 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -8,7 +8,9 @@ import DashboardTab from "./pages/DashboardTab.jsx"; import EigenVermogenTab from "./pages/EigenVermogenTab.jsx"; import SchuldTab from "./pages/SchuldTab.jsx"; import VoortgangTab from "./pages/VoortgangTab.jsx"; -import { RED, PURPLE, PURPLE_LIGHT } from "./constants/index.js"; +import { RED, PURPLE_LIGHT } from "./constants/index.js"; +import { fmt } from "./utils/format.js"; +import { version, updateCheckUrl } from "../package.json"; import ProfielPopup from "./components/ProfielPopup.jsx"; import InstellingenModal from "./components/InstellingenModal.jsx"; @@ -21,8 +23,23 @@ function initials(naam = "") { const PAGES = [DashboardTab, EigenVermogenTab, SchuldTab, VoortgangTab]; function AppInner() { - const { loggedIn, login, logout, locked, tab, showUsers, setShowUsers, showInstellingen, setShowInstellingen, loading, T, currentUser, avatar, idleMinutes } = useApp(); - const [profielOpen, setProfielOpen] = useState(false); + const { loggedIn, login, logout, locked, tab, showUsers, setShowUsers, showInstellingen, setShowInstellingen, loading, T, currentUser, avatar, idleMinutes, totEV } = useApp(); + const [profielOpen, setProfielOpen] = useState(false); + const [searchOpen, setSearchOpen] = useState(false); + const [nieuweVersie, setNieuweVersie] = useState(null); + const [bellOpen, setBellOpen] = useState(false); + const searchRef = useRef(null); + + 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(() => {}); + }, []); const idleTimer = useRef(null); const IDLE_MS = (idleMinutes ?? 30) * 60 * 1000; @@ -46,6 +63,19 @@ function AppInner() { }; }, [loggedIn]); + useEffect(() => { + const handler = (e) => { + if ((e.metaKey || e.ctrlKey) && e.key === "k") { + e.preventDefault(); + setSearchOpen(true); + setTimeout(() => searchRef.current?.focus(), 50); + } + if (e.key === "Escape") setSearchOpen(false); + }; + window.addEventListener("keydown", handler); + return () => window.removeEventListener("keydown", handler); + }, []); + if (!loggedIn) { if (sessionVerlopen) { return ( @@ -103,35 +133,134 @@ function AppInner() { const ActivePage = PAGES[tab] ?? DashboardTab; return ( -
+
{/* Hoofdinhoud */} -
- {/* Topbalk rechts */} -
+
+ {/* Topbalk */} +
+
+ + {/* Notificatiebel */}
- + + {/* Badge */} + {nieuweVersie && ( +
1
+ )} + + {/* Popup */} + {bellOpen && nieuweVersie && ( + <> +
setBellOpen(false)} style={{ position: "fixed", inset: 0, zIndex: 190 }} /> +
+
+ Meldingen +
+
+
+ + + + +
+
+
+ Nieuwe versie beschikbaar +
+
+ v{nieuweVersie} is uitgebracht. Je gebruikt momenteel v{version}. +
+
+
+
+ + )} +
+ + + {/* Gebruikersprofiel */} +
+ {profielOpen && setProfielOpen(false)} />}
@@ -160,7 +289,7 @@ function AppInner() { {showInstellingen && setShowInstellingen(false)} />} {/* Actieve pagina */} -
+
diff --git a/frontend/src/components/GebruikersBeheer.jsx b/frontend/src/components/GebruikersBeheer.jsx index 83ac7d2..c6ffdb2 100644 --- a/frontend/src/components/GebruikersBeheer.jsx +++ b/frontend/src/components/GebruikersBeheer.jsx @@ -1,5 +1,10 @@ import { useState, useEffect, useRef } from "react"; -import { PURPLE, PURPLE_LIGHT, RED, GREEN } from "../constants/index.js"; +import { RED, GREEN } from "../constants/index.js"; + +const TEAL = "linear-gradient(135deg, #7c3aed, #a855f7)"; +const TEAL_COLOR = "#a855f7"; +const TEAL_BG = "rgba(124,58,237,0.15)"; +const TEAL_BORDER = "rgba(124,58,237,0.35)"; import { useApp } from "../context/AppContext.jsx"; const getToken = () => localStorage.getItem("vf_token"); @@ -51,11 +56,11 @@ function RolSelect({ value, onChange, opties, T, darkMode }) { {opties.map((o) => (
{ onChange(o); setOpen(false); }} style={{ padding: "9px 12px", fontSize: 13, cursor: "pointer", - color: o === value ? PURPLE_LIGHT : T.text, - background: o === value ? `${PURPLE}22` : "transparent", + color: o === value ? TEAL_COLOR : T.text, + background: o === value ? TEAL_BG : "transparent", }} - onMouseEnter={(e) => e.currentTarget.style.background = `${PURPLE}18`} - onMouseLeave={(e) => e.currentTarget.style.background = o === value ? `${PURPLE}22` : "transparent"} + onMouseEnter={(e) => e.currentTarget.style.background = "rgba(14,221,212,0.10)"} + onMouseLeave={(e) => e.currentTarget.style.background = o === value ? TEAL_BG : "transparent"} > {o}
@@ -192,8 +197,8 @@ export default function GebruikersBeheer({ onClose }) { }}>
Gebruikersbeheer - {users.length} gebruiker{users.length !== 1 ? "s" : ""} @@ -231,18 +236,18 @@ export default function GebruikersBeheer({ onClose }) {
{initials}
{u.naam} {isSelf && ( jij )}
@@ -258,9 +263,9 @@ export default function GebruikersBeheer({ onClose }) {
{!isSelf && ( <> @@ -298,10 +303,10 @@ export default function GebruikersBeheer({ onClose }) { {/* Bewerken modal */} {editUser && (
-
+
{editUser.naam} bewerken
@@ -317,7 +322,7 @@ export default function GebruikersBeheer({ onClose }) { }}>Annuleren @@ -328,7 +333,7 @@ export default function GebruikersBeheer({ onClose }) { {/* Nieuwe gebruiker */} {showAdd ? (
@@ -348,7 +353,7 @@ export default function GebruikersBeheer({ onClose }) { }}>Annuleren diff --git a/frontend/src/components/InstellingenModal.jsx b/frontend/src/components/InstellingenModal.jsx index 4bdb1c5..004a2f8 100644 --- a/frontend/src/components/InstellingenModal.jsx +++ b/frontend/src/components/InstellingenModal.jsx @@ -1,5 +1,6 @@ import { useApp } from "../context/AppContext.jsx"; -import { PURPLE } from "../constants/index.js"; + +const TEAL = "linear-gradient(135deg, #7c3aed, #a855f7)"; const BACKUP_ICON = ( @@ -73,7 +74,7 @@ export default function InstellingenModal({ onClose }) { onClick={() => setDarkMode((d) => !d)} style={{ width: 44, height: 24, borderRadius: 12, cursor: "pointer", - background: darkMode ? `linear-gradient(135deg, ${PURPLE}, #a855f7)` : "#334155", + background: darkMode ? TEAL : "#334155", position: "relative", transition: "background 0.2s", flexShrink: 0, }} > diff --git a/frontend/src/components/LoginPage.jsx b/frontend/src/components/LoginPage.jsx index 88ee923..b56a5a8 100644 --- a/frontend/src/components/LoginPage.jsx +++ b/frontend/src/components/LoginPage.jsx @@ -1,12 +1,6 @@ import { useState } from "react"; -import { PURPLE, PURPLE_LIGHT, RED } from "../constants/index.js"; +import { RED } from "../constants/index.js"; -const PersonIcon = () => ( - - - - -); const MailIcon = () => ( @@ -19,15 +13,18 @@ const LockIcon = () => ( ); +const PersonIcon = () => ( + + + + +); -function EyeBtn({ show, onToggle, absolute = false }) { - const style = absolute - ? { position: "absolute", right: 12, top: "50%", transform: "translateY(-50%)" } - : { position: "absolute", right: 10, top: "50%", transform: "translateY(-50%)" }; +function EyeBtn({ show, onToggle }) { return (
- {icon} -
-
{label}
-
- {children} - {extra} +
+
+ +
+ {icon && ( + + {icon} + + )} + setFocused(true)} onBlur={() => setFocused(false)} + style={{ + flex: 1, background: "transparent", border: "none", outline: "none", + color: "#f1f5f9", fontSize: 14, + fontFamily: "'Poppins', system-ui, sans-serif", + }} + /> + {rightElement}
); } +function MiniBarChart() { + const bars = [38, 52, 45, 68, 74, 61, 89]; + const months = ["nov", "dec", "jan", "feb", "mrt", "apr", "mei"]; + return ( +
+ {bars.map((h, i) => ( +
+
+ {months[i]} +
+ ))} +
+ ); +} + export default function LoginPage({ onLogin }) { - const [mode, setMode] = useState("login"); - const [naam, setNaam] = useState(""); - const [email, setEmail] = useState(""); - const [wachtwoord, setWachtwoord] = useState(""); - const [herhaalWw, setHerhaalWw] = useState(""); - const [showPass, setShowPass] = useState(false); - const [showPass2, setShowPass2] = useState(false); - const [err, setErr] = useState(""); - const [loading, setLoading] = useState(false); + const [mode, setMode] = useState("login"); + const [naam, setNaam] = useState(""); + const [email, setEmail] = useState(""); + const [wachtwoord, setWachtwoord] = useState(""); + const [herhaalWw, setHerhaalWw] = useState(""); + const [showPass, setShowPass] = useState(false); + const [showPass2, setShowPass2] = useState(false); + const [err, setErr] = useState(""); + const [loading, setLoading] = useState(false); const switchMode = (m) => { setMode(m); setNaam(""); setEmail(""); setWachtwoord(""); setHerhaalWw(""); setErr(""); }; @@ -94,298 +134,244 @@ export default function LoginPage({ onLogin }) { } }; - const iStyle = { - width: "100%", background: "transparent", border: "none", - color: "#f1f5f9", padding: "0", fontSize: 13, - outline: "none", boxSizing: "border-box", - }; + const onEnter = (e) => e.key === "Enter" && handleSubmit(); return (
- {/* Achtergrond gloeden */} -
-
- - {/* Links: formulier */} + {/* Floating card — larger */}
- {/* Logo */} -
+ + {/* LEFT: Form panel */} +
+ {/* Logo */} +
+
+ + + + +
+ VibeFinance +
+ + {/* Heading */} +
+

+ Welkom terug! +

+

+ {mode === "login" + ? "Log in om je budgetoverzicht te bekijken." + : "Maak een account aan om je vermogen bij te houden."} +

+
+ + {/* Tab switcher */}
- - - - + {[["login", "Inloggen"], ["register", "Registreren"]].map(([m, lbl]) => ( + + ))}
- VibeFinance -
- {/* Heading */} -
-

- {mode === "login" ? "Welkom terug!" : "Account aanmaken"} -

-

- {mode === "login" - ? "Voer je gegevens in om in te loggen op je dashboard." - : "Maak een account aan om je vermogen bij te houden."} -

-
- - {/* Tab switcher */} -
- {[["login", "Inloggen"], ["register", "Registreren"]].map(([m, label]) => ( - - ))} -
- - {/* Formulier */} -
- {mode === "register" ? ( - <> - {/* Volledige naam */} - } label="Volledige naam"> - { setNaam(e.target.value); setErr(""); }} - onKeyDown={(e) => e.key === "Enter" && handleSubmit()} - style={iStyle} /> - - - {/* E-mail */} - } label="E-mailadres"> - { setEmail(e.target.value); setErr(""); }} - onKeyDown={(e) => e.key === "Enter" && handleSubmit()} - style={iStyle} /> - - - {/* Wachtwoord */} - } label="Wachtwoord" extra={ - setShowPass((s) => !s)} /> - }> - { setWachtwoord(e.target.value); setErr(""); }} - onKeyDown={(e) => e.key === "Enter" && handleSubmit()} - style={{ ...iStyle, paddingRight: 36 }} /> - - - {/* Herhaal wachtwoord */} - } label="Herhaal wachtwoord" extra={ - setShowPass2((s) => !s)} /> - }> - + {mode === "register" && ( + } + onChange={(e) => { setNaam(e.target.value); setErr(""); }} + onKeyDown={onEnter} + /> + )} + } + onChange={(e) => { setEmail(e.target.value); setErr(""); }} + onKeyDown={onEnter} + /> + } + onChange={(e) => { setWachtwoord(e.target.value); setErr(""); }} + onKeyDown={onEnter} + rightElement={ setShowPass(s => !s)} />} + /> + {mode === "register" && ( + <> + } onChange={(e) => { setHerhaalWw(e.target.value); setErr(""); }} - onKeyDown={(e) => e.key === "Enter" && handleSubmit()} - style={{ ...iStyle, paddingRight: 36 }} /> - + onKeyDown={onEnter} + rightElement={ setShowPass2(s => !s)} />} + /> +
+ Min. 8 tekens · één hoofdletter · één cijfer +
+ + )} +
-
- Min. 8 tekens · één hoofdletter · één cijfer -
- - ) : ( - <> - } label="E-mailadres"> - { setEmail(e.target.value); setErr(""); }} - onKeyDown={(e) => e.key === "Enter" && handleSubmit()} - style={iStyle} /> - - - } label="Wachtwoord" extra={ - setShowPass((s) => !s)} /> - }> - { setWachtwoord(e.target.value); setErr(""); }} - onKeyDown={(e) => e.key === "Enter" && handleSubmit()} - style={{ ...iStyle, paddingRight: 36 }} /> - - + {err && ( +
{err}
)} + + {/* Submit */} +
- {err && ( -
{err}
- )} - - {/* Submit knop */} - - - -
- - {/* Rechts: app mockup — flat, groot, licht zwevend */} -
+ {/* RIGHT: Purple mockup panel */}
- {/* Browser chrome */} -
-
-
-
-
vibefinance.app
+ {/* Decorative circles */} +
+
+
+
+ + {/* Top badge */} +
+ + Jouw vermogen in één oogopslag
- {/* App */} -
+ {/* Main glassmorphism card */} +
+ {/* Card header */} +
+
+
Vermogensoverzicht
+
The Road naar € 500.000
+
+
Mei 2026
+
- {/* Sidebar */} -
-
- -
- {[ - { icon: , active: true }, - { icon: , active: false }, - { icon: , active: false }, - { icon: , active: false }, - ].map((item, i) => ( -
- {item.icon} + {/* KPI rows */} + {[ + { label: "Eigen Vermogen", val: "€ 445.500", color: "#10b981", icon: "↑" }, + { label: "Schuld", val: "€ 54.500", color: "#f97316", icon: "↓" }, + { label: "Winst", val: "€ 142.500", color: "#a855f7", icon: "↑" }, + ].map(({ label, val, color, icon }) => ( +
+
+
{icon}
+ {label}
- ))} -
-
- + {val}
-
-
- + ))} + + {/* Progress toward goal */} +
+
+ Voortgang naar doel + 89% +
+
+
+
+
+ € 0 + € 500.000
- {/* Inhoud */} -
- {/* Topbalk */} -
-
JH
- -
🌙
-
- - {/* Paginatitel */} -
-
The Road naar € 500.000
-
Persoonlijk vermogensdashboard
-
- - {/* 6 KPI kaarten */} -
- {[ - { label: "Start Eigen Vermogen", val: "€ 0", color: PURPLE }, - { label: "Extra Geïnv. Fiat", val: "€ 303.000", color: PURPLE }, - { label: "Totaal Geïnv. Fiat", val: "€ 303.000", color: PURPLE }, - { label: "Totaal Eigen Vermogen", val: "€ 445.500", color: "#10b981" }, - { label: "Winst op Investering", val: "€ 142.500", color: "#10b981" }, - { label: "ROI %", val: "+47,03%", color: "#10b981" }, - ].map((k) => ( -
-
{k.label}
-
{k.val}
-
- ))} -
- - {/* Progressie balk */} -
-
-
- 🎯 - Doel & Bijbehorende Progressie - € 500.000 -
- 89,10% behaald -
-
-
- {[25, 50, 75].map(p =>
)} -
-
- {["€ 0", "€ 125k", "€ 250k", "€ 375k", "€ 500k"].map(m => ( - {m} - ))} -
-
- - {/* Grafiek */} -
-
Ontwikkeling van Eigen Vermogen
- - - - - - - - {[0,1,2,3,4].map(i => )} - {["500k","375k","250k","125k","0"].map((l, i) => ( - {l} - ))} - - - {[[44,112],[114,102],[184,90],[254,74],[324,56],[394,38],[464,22],[534,10],[558,7]].map(([x,y],i) => ( - - ))} - {["jan","feb","mrt","apr","mei","jun","jul","aug","sep"].map((m, i) => ( - {m} - ))} - -
+ {/* Mini bar chart */} +
+
Groei per maand
+
+ + {/* Floating badges */} +
+
+
ROI
+
+47,03%
+
+
+
Doel
+
€ 500.000
+
+
+ + {/* Bottom tagline */} +
+ Beheer je vermogen, volg je groei en bereik je financiële doelen — allemaal op één plek. +
+
); diff --git a/frontend/src/components/NavBar.jsx b/frontend/src/components/NavBar.jsx index 4acaef4..f636630 100644 --- a/frontend/src/components/NavBar.jsx +++ b/frontend/src/components/NavBar.jsx @@ -1,5 +1,5 @@ import { useState, useEffect } from "react"; -import { PURPLE, PURPLE_LIGHT, TABS } from "../constants/index.js"; +import { TABS } from "../constants/index.js"; import { version, updateCheckUrl } from "../../package.json"; import { useApp } from "../context/AppContext.jsx"; @@ -49,66 +49,178 @@ const LOGOUT_ICON = ( ); -function Tooltip({ label, collapsed }) { - if (!collapsed) return null; +const INFO_ICON = ( + + + + + +); + +const MOON_ICON = ( + + + +); + +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 (
{label}
+ fontSize: 11, fontWeight: 700, letterSpacing: "0.12em", + textTransform: "uppercase", color: "#3d4966", + padding: "0 12px", marginBottom: 2, marginTop: 20, + }}> + {children} +
); } -function SidebarItem({ active, onClick, icon, label, T, collapsed }) { +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 (
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", + 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", + }} + > + {icon} + {label} +
+ ); +} + +function OverPopup({ onClose, nieuweVersie, T }) { + return ( + <> + {/* Overlay */} +
- {icon} - {!collapsed && {label}} - {hovered && } + {/* Modal */} +
e.stopPropagation()} style={{ + background: T.card, + 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 */} + + + {/* Logo + naam */} +
+
+ + + + +
+
+
VibeFinance
+
+
+ +
+ +
+ Versie + + v{version} + +
+ + {nieuweVersie && ( +
+ Nieuw beschikbaar + + v{nieuweVersie} + +
+ )} + +
+
© {new Date().getFullYear()} VibeFinance
+
+
+ + ); +} + +function DarkModeToggle({ darkMode, setDarkMode, T }) { + return ( +
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"} + > + {MOON_ICON} + Dark Mode + {/* Toggle pill */} +
+
+
); } export default function NavBar() { - const { - tab, setTab, - setShowUsers, - setShowInstellingen, - T, - logout, - } = useApp(); - + const { tab, setTab, setShowUsers, setShowInstellingen, T, logout, darkMode, setDarkMode, currentUser, avatar } = useApp(); const [nieuweVersie, setNieuweVersie] = useState(null); - const [collapsed, setCollapsed] = useState(false); - - const W = collapsed ? 56 : 220; + const [overOpen, setOverOpen] = useState(false); useEffect(() => { if (!updateCheckUrl) return; fetch(updateCheckUrl) - .then((r) => r.json()) - .then((data) => { + .then(r => r.json()) + .then(data => { const latest = data?.tag_name?.replace(/^v/, ""); if (latest && latest !== version) setNieuweVersie(latest); }) @@ -117,143 +229,57 @@ export default function NavBar() { return (
- {/* Logo + collapse knop */} - {!collapsed ? ( -
-
-
- - - - -
- VibeFinance -
- -
- ) : ( -
- -
- )} + {/* Logo */} +
+
+ + + + +
+ VibeFinance +
+ + {/* Nav */} +