import { useRef, useState, useCallback, useContext, createContext } from "react"; import { PURPLE, PURPLE_LIGHT, EMOJI_OPTIONS } from "../../constants/index.js"; import { useApp } from "../../context/AppContext.jsx"; const DragContext = createContext(null); // ── VCard ───────────────────────────────────────────────────────────────────── export function VCard({ children, style = {} }) { return (
{children}
); } // ── VBadge ──────────────────────────────────────────────────────────────────── export function VBadge({ label, color = PURPLE }) { return ( {label} ); } // ── TabBtn ──────────────────────────────────────────────────────────────────── export function TabBtn({ active, onClick, children, darkMode = true }) { return ( ); } // ── VProgressBar ────────────────────────────────────────────────────────────── export function VProgressBar({ value, max, color = PURPLE }) { const p = Math.min((value / (max || 1)) * 100, 100); return (
); } // ── DonutChart ──────────────────────────────────────────────────────────────── export function DonutChart({ segments, total, size = 120, label, centerVal }) { const { darkMode, T } = useApp(); const wrapRef = useRef(null); const [hover, setHover] = useState(null); // { i, x, y } — actieve categorie + cursor const strokeW = size * 0.155; const r = (size - strokeW) / 2 - 1; const cx = size / 2, cy = size / 2; const circ = 2 * Math.PI * r; const active = segments.filter((s) => s.val > 0); const multi = active.length > 1; // Middenwaarde schaalt mee met de beschikbare ruimte binnen de ring, zodat // lange bedragen niet uit verhouding raken. const innerW = (2 * r - strokeW) * 0.92; const valLen = centerVal ? String(centerVal).length : 0; const valFont = valLen ? Math.min(size * 0.125, innerW / (valLen * 0.6)) : 0; // Segmenten sluiten naadloos aan (geen tussenruimte). let off = 0; const slices = active.map((s) => { const raw = (s.val / total) * circ; const sl = { ...s, dash: Math.max(raw, 1), offset: off, share: total ? s.val / total : 0 }; off += raw; return sl; }); const track = (i) => (e) => { const box = wrapRef.current?.getBoundingClientRect(); setHover({ i, x: box ? e.clientX - box.left : cx, y: box ? e.clientY - box.top : cy, }); }; const hovered = hover ? slices[hover.i] : null; const fmtPct = (v) => `${(v * 100).toFixed(1).replace(".", ",")}%`; const fmtEur = (v) => new Intl.NumberFormat("nl-NL", { style: "currency", currency: "EUR", maximumFractionDigits: 0 }).format(v || 0); return (
{/* Baan — zelfde toon als de voortgangsbalken */} {/* Segmenten */} {slices.map((s, i) => ( setHover(null)} /> ))} {/* Center label */} {label && ( {label} )} {/* Center value */} {centerVal && ( {centerVal} )} {/* Tooltip: categorie, bedrag en aandeel */} {hovered && (
{hovered.naam || hovered.label || "—"}
{fmtEur(hovered.val)} · {fmtPct(hovered.share)}
)}
); } // ── SvgLineChart ────────────────────────────────────────────────────────────── export function SvgLineChart({ pts, lines, T, H = 180 }) { if (pts.length < 2) { return (
Minimaal 2 datapunten nodig
); } const W = 580, pl = 64, pr = 14, pt2 = 16, pb = 30; const iW = W - pl - pr, iH = H - pt2 - pb; const allVals = lines.flatMap((l) => pts.map((p) => p[l.key])); const mn = Math.min(0, ...allVals); const mx = Math.max(...allVals) * 1.08 || 1; const rng = mx - mn || 1; const xp = (i) => pl + (i / (pts.length - 1)) * iW; const yp = (v) => pt2 + (1 - (v - mn) / rng) * iH; const gc = T.bg === "#0f1117" ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.05)"; return (
{lines.map((l) => ( ))} {[0, 0.25, 0.5, 0.75, 1].map((f) => { const gy = pt2 + (1 - f) * iH; const v = mn + f * rng; return ( {new Intl.NumberFormat("nl-NL", { style: "currency", currency: "EUR", maximumFractionDigits: 0 }).format(v)} ); })} {lines.map((l) => { const xs = pts.map((_, i) => xp(i)); const ys = pts.map((p) => yp(p[l.key])); const ls = xs.map((x, i) => `${x},${ys[i]}`).join(" "); const ar = `${xs[0]},${pt2 + iH} ${ls} ${xs[xs.length - 1]},${pt2 + iH}`; return ( {xs.map((x, i) => ( ))} ); })} {pts.map((p, i) => { const [y, m, d] = (p.datum || "").split("-"); const label = d ? `${d}-${m}-${y}` : p.datum; return ( {label} ); })}
{lines.map((l) => (
{l.label}
))}
); } // ── EthIcon ─────────────────────────────────────────────────────────────────── export function EthIcon({ size = 18, color = "#3b82f6" }) { const w = size * 0.6, h = size; return ( ); } // ── BtcIcon ─────────────────────────────────────────────────────────────────── export function BtcIcon({ size = 18, color = "#f59e0b" }) { return ( ₿ ); } // ── GoldIcon ────────────────────────────────────────────────────────────────── export function GoldIcon({ size = 24 }) { return ( {/* Bovenkant */} {/* Voorkant */} {/* Rechterkant */} {/* Linkeronderkant */} {/* Onderkant schaduw */} {/* Streep highlight bovenkant */} {/* GOLD tekst */} GOLD ); } // ── Modal primitieven ───────────────────────────────────────────────────────── export function Overlay({ children, onClose, zIndex = 400 }) { return (
{ if (e.target === e.currentTarget) onClose(); }} style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.75)", zIndex, }} > {children}
); } export function ModalBox({ children, maxWidth = 480 }) { const { T } = useApp(); const ref = useRef(null); const [pos, setPos] = useState(null); const dragState = useRef(null); const startDrag = useCallback((e) => { if (e.button !== 0) return; e.preventDefault(); const rect = ref.current.getBoundingClientRect(); dragState.current = { startX: e.clientX, startY: e.clientY, startLeft: rect.left, startTop: rect.top, }; const onMove = (e) => { if (!dragState.current) return; setPos({ x: dragState.current.startLeft + (e.clientX - dragState.current.startX), y: dragState.current.startTop + (e.clientY - dragState.current.startY), }); }; const onUp = () => { dragState.current = null; window.removeEventListener("mousemove", onMove); window.removeEventListener("mouseup", onUp); }; window.addEventListener("mousemove", onMove); window.addEventListener("mouseup", onUp); }, []); const posStyle = pos ? { left: pos.x, top: pos.y, transform: "none" } : { left: "50%", top: "50%", transform: "translate(-50%, -50%)" }; return (
{children}
); } export function ModalHeader({ icon, iconBg, title, subtitle, subtitleColor, onClose, headerBg, T }) { const startDrag = useContext(DragContext); return (
{icon}
{title}
{subtitle}
); } export function ModalFooter({ children, T }) { return (
{children}
); } // ── AppIcon — rendert emoji-string of speciale SVG-iconen ──────────────────── export function AppIcon({ value, fallback = "🏷", size = 20 }) { if (value === "__BTC__") return ; if (value === "__ETH__") return ; if (value === "__GOLD__") return ; if (value) return {value}; return {fallback}; } // ── EmojiPicker ─────────────────────────────────────────────────────────────── const SPECIAL_ICONS = [ { key: "__BTC__", node: }, { key: "__ETH__", node: }, { key: "__GOLD__", node: }, ]; export function EmojiPicker({ selected, T, accentColor, onSelect, onClear }) { const ac = accentColor || "#f59e0b"; const btnStyle = (active) => ({ width: 34, height: 34, borderRadius: 8, border: `2px solid ${active ? ac : T.border}`, background: active ? `${ac}22` : "transparent", cursor: "pointer", fontSize: 18, display: "flex", alignItems: "center", justifyContent: "center", }); return (
{selected && ( )} {SPECIAL_ICONS.map(({ key, node }) => ( ))} {EMOJI_OPTIONS.map((e) => ( ))}
); } // ── ConfirmDeleteDialog ─────────────────────────────────────────────────────── export function ConfirmDeleteDialog({ naam, T, onCancel, onConfirm }) { const RED = "#ef4444"; return (
Weet je zeker dat je {naam} wilt verwijderen?
); } // ── EmptyState ──────────────────────────────────────────────────────────────── export function EmptyState({ icon, text, hint, onClick, locked, T }) { return (
{icon}
{text}
{!locked && hint && (
{hint}
)}
); }