Skip to content
import { useState, useEffect } from “react”; import { LineChart, Line, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from “recharts”; const C = { blue: “#0077b5”, dark: “#0f172a”, card: “#1e293b”, border: “#334155”, muted: “#94a3b8”, text: “#e2e8f0”, }; const field: React.CSSProperties = { width: “100%”, padding: “9px 12px”, background: C.dark, border: 1px solid ${C.border}, borderRadius: 6, color: C.text, fontSize: 14, boxSizing: “border-box”, colorScheme: “dark” as any, }; interface Entry { id: number; date: string; posts: string; views: string; likes: string; comments: string; impressions: string; dms_count: string; } interface Gesprek { id: number; date: string; naam: string; bedrijf: string; bron: string; fase: string; notitie: string; } interface EvalResult { c: string; icon: string; t: string; d: string; } const FASEN = [“eerste contact”,”demo/kennismaking”,”offerte”,”klant geworden”,”niet doorgegaan”]; const FASE_C: Record = { “eerste contact”:”#3b82f6″,”demo/kennismaking”:”#f59e0b”, “offerte”:”#8b5cf6″,”klant geworden”:”#10b981″,”niet doorgegaan”:”#6b7280″, }; // ── Evaluatie (volledig lokaal, geen API) ───────────────────── function evaluate(entries: Entry[], gesprekken: Gesprek[]): EvalResult[] { const n = entries.length; if (!n) return []; const totP = entries.reduce((s,e)=>s+(+e.posts||0),0); const totV = entries.reduce((s,e)=>s+(+e.views||0),0); const totL = entries.reduce((s,e)=>s+(+e.likes||0),0); const totC = entries.reduce((s,e)=>s+(+e.comments||0),0); const totD = entries.reduce((s,e)=>s+(+e.dms_count||0),0); const vpp = totP > 0 ? Math.round(totV/totP) : 0; const eng = totV > 0 ? ((totL+totC)/totV*100).toFixed(1) : “0”; const ppd = (totP/n).toFixed(1); const dpd = (totD/n).toFixed(1); const half = Math.floor(n/2); const v1 = entries.slice(0,half).reduce((s,e)=>s+(+e.views||0),0); const v2 = entries.slice(half).reduce((s,e)=>s+(+e.views||0),0); const trend = n>=4 ? (v2>v1?”📈 stijgend”:v2=300) res.push({c:”#10b981″,icon:”✅”,t:Sterk bereik — ${vpp} views per post,d:”Je posts bereiken veel mensen. Houd de regelmaat vast.”}); else if (vpp>=100) res.push({c:”#f59e0b”,icon:”🟡”,t:Gemiddeld bereik — ${vpp} views per post,d:”Probeer meer persoonlijke of opiniërende posts voor meer views.”}); else res.push({c:”#ef4444″,icon:”⚠️”,t:Laag bereik — ${vpp} views per post,d:”Post op di–do ochtenden (7–9u) en gebruik een sterke openingszin.”}); // Engagement if (totV > 0) { if (+eng>=3) res.push({c:”#10b981″,icon:”✅”,t:Hoge engagement — ${eng}%,d:”Mensen reageren actief. Je raakt de juiste snaar bij je doelgroep.”}); else if (+eng>=1) res.push({c:”#f59e0b”,icon:”🟡”,t:Gemiddelde engagement — ${eng}%,d:”Stel vragen in je posts of eindig met een oproep tot reactie.”}); else res.push({c:”#ef4444″,icon:”⚠️”,t:Lage engagement — ${eng}%,d:”Reageer ook op anderen en maak posts die uitnodigen tot gesprek.”}); } // Frequentie if (+ppd>=0.8) res.push({c:”#10b981″,icon:”✅”,t:Goede frequentie — ${ppd} posts/dag,d:”Je bent actief zichtbaar. Consistentie is key — ga zo door.”}); else if (+ppd>=0.4) res.push({c:”#f59e0b”,icon:”🟡”,t:Matige frequentie — ${ppd} posts/dag,d:”Streef naar minimaal 3–4 posts per week voor meer bereik.”}); else res.push({c:”#ef4444″,icon:”⚠️”,t:Lage frequentie — ${ppd} posts/dag,d:”Plan posts vooruit en gebruik AI om ideeën sneller uit te werken.”}); // DMs if (+dpd>=2) res.push({c:”#10b981″,icon:”✅”,t:Actief in DMs — ${dpd}/dag,d:”Goed bezig. Zorg dat DMs leiden naar een concreet gesprek of afspraak.”}); else if (+dpd>=0.5) res.push({c:”#f59e0b”,icon:”🟡”,t:Weinig DMs — ${dpd}/dag,d:”Reageer op mensen die je post liken — dát is het beste moment voor een DM.”}); else res.push({c:”#ef4444″,icon:”⚠️”,t:”Nauwelijks DMs verstuurd”,d:”Start dagelijks 1–2 persoonlijke gesprekken. Zonder DMs geen klantgesprekken.”}); // Trend if (trend) { const tc = trend.includes(“stijgend”)?”#10b981″:trend.includes(“dalend”)?”#ef4444″:”#f59e0b”; const td = trend.includes(“stijgend”)?”Je bereik groeit. De aanpak werkt — bouw hierop voort.” :trend.includes(“dalend”)?”Je bereik daalt. Analyseer welke posts goed scoorden en herhaal dat format.” :”Je bereik groeit niet. Experimenteer met een nieuw format (video, carrousel, poll).”; res.push({c:tc,icon:trend.slice(0,2),t:Trend: ${trend.slice(3)},d:td}); } // Gesprekken if (!gesprekken.length) { res.push({c:”#f59e0b”,icon:”🤝”,t:”Nog geen klantgesprekken”,d:”Registreer gesprekken in het tabblad 🤝 Gesprekken om je conversie bij te houden.”}); } else { const kl = gesprekken.filter(g=>g.fase===”klant geworden”).length; const cv = Math.round(kl/gesprekken.length*100); res.push(cv>=30 ? {c:”#10b981″,icon:”🎯”,t:Sterke conversie — ${cv}%,d:${kl} van ${gesprekken.length} contacten zijn klant geworden. Uitstekend.} : {c:”#f59e0b”,icon:”🎯”,t:Conversie — ${cv}%,d:${kl} van ${gesprekken.length} zijn klant geworden. Werk aan je opvolging na het eerste contact.} ); } return res; } // ── Opslag ─────────────────────────────────────────────────── async function storageGet(k: string): Promise { try { const r = await (window as any).storage?.get(k); if (r?.value) return r.value; } catch {} try { return localStorage.getItem(k); } catch {} return null; } async function storageSave(k: string, v: string) { try { localStorage.setItem(k,v); } catch {} try { await (window as any).storage?.set(k,v); } catch {} } // ── WHITE LABEL CONFIG ──────────────────────────────────────── // Pas deze waarden aan per klant: const WL = { naam: “Richard ST de Vries”, bedrijf: “De Vries Advies & Training”, stad: “Groningen”, sleutel: “ld9”, // unieke sleutel per klant (voorkomt data-mix) }; // ── Hoofdcomponent ─────────────────────────────────────────── export default function LinkedInDashboard() { const today = new Date().toISOString().split(“T”)[0]; const [tab, setTab] = useState(“invoer”); const [entries, setEntries] = useState([]); const [gesprekken, setGesprekken] = useState([]); const [saved, setSaved] = useState(false); const [evalResults, setEvalResults] = useState([]); const [showExport, setShowExport] = useState(false); const [form, setForm] = useState>({ date:today,posts:””,views:””,likes:””,comments:””,impressions:””,dms_count:”” }); const [gf, setGf] = useState>({ date:today,naam:””,bedrijf:””,bron:”LinkedIn DM”,fase:”eerste contact”,notitie:”” }); const KEY_E = ${WL.sleutel}_e; const KEY_G = ${WL.sleutel}_g; useEffect(() => { (async () => { const ev = await storageGet(KEY_E); if (ev) setEntries(JSON.parse(ev)); const gv = await storageGet(KEY_G); if (gv) setGesprekken(JSON.parse(gv)); })(); }, []); const saveEntry = async () => { const e = {…form,id:Date.now()}; const u = […entries.filter(x=>x.date!==form.date),e].sort((a,b)=>a.date.localeCompare(b.date)); setEntries(u); await storageSave(KEY_E,JSON.stringify(u)); setSaved(true); setTimeout(()=>setSaved(false),2000); }; const delEntry = async (id: number) => { const u = entries.filter(e=>e.id!==id); setEntries(u); await storageSave(KEY_E,JSON.stringify(u)); }; const saveG = async () => { if (!gf.naam) return; const u = […gesprekken,{…gf,id:Date.now()}].sort((a,b)=>b.date.localeCompare(a.date)); setGesprekken(u); await storageSave(KEY_G,JSON.stringify(u)); setGf(f=>({…f,naam:””,bedrijf:””,notitie:””})); }; const updFase = async (id: number, fase: string) => { const u = gesprekken.map(g=>g.id===id?{…g,fase}:g); setGesprekken(u); await storageSave(KEY_G,JSON.stringify(u)); }; const delG = async (id: number) => { const u = gesprekken.filter(g=>g.id!==id); setGesprekken(u); await storageSave(KEY_G,JSON.stringify(u)); }; const tot = (k: keyof Entry) => entries.reduce((s,e)=>s+(+e[k]||0),0); const avg7 = (k: keyof Entry) => { const d = entries.slice(-7).filter(e=>e[k]); return d.length?(d.reduce((s,e)=>s+(+e[k]||0),0)/d.length).toFixed(1):”—”; }; const csvData = () => { const sep=”;”; const a1=[[“Datum”,”Posts”,”Views”,”Likes”,”Comments”,”Impressies”,”DMs”],…entries.map(e=>[e.date,+e.posts||0,+e.views||0,+e.likes||0,+e.comments||0,+e.impressions||0,+e.dms_count||0])].map(r=>r.join(sep)).join(“\n”); const a2=[[“Datum”,”Naam”,”Bedrijf”,”Bron”,”Fase”,”Notitie”],…gesprekken.map(g=>[g.date,g.naam,g.bedrijf,g.bron,g.fase,g.notitie])].map(r=>r.join(sep)).join(“\n”); const a3=[[“Statistiek”,”Waarde”],[“Views”,tot(“views”)],[“Impressies”,tot(“impressions”)],[“Likes”,tot(“likes”)],[“Comments”,tot(“comments”)],[“Gesprekken”,gesprekken.length]].map(r=>r.join(sep)).join(“\n”); return ACTIVITEITEN\n${a1}\n\nGESPREKKEN\n${a2}\n\nTOTALEN\n${a3}; }; const chart = entries.slice(-14).map(e=>({ d:e.date.slice(5),Views:+e.views||0,Impressies:+e.impressions||0,Likes:+e.likes||0,Comments:+e.comments||0 })); return ( {/* ── HEADER — pas WL aan voor white label ── */}
📊 LinkedIn Dashboard
{WL.naam} · {WL.bedrijf} · {WL.stad}
{/* ── TABS ── */}
{[[“invoer”,”📝 Invoer”],[“overzicht”,”📈 Overzicht”],[“gesprekken”,”🤝 Gesprekken”],[“evaluatie”,”🔍 Evaluatie”]].map(([id,lbl])=>( ))}
{/* ── INVOER ── */} {tab===”invoer” && (

Dagelijkse activiteit invoeren

Datum
setForm(f=>({…f,date:e.target.value}))} style={field}/>
{([[“posts”,”Posts”],[“views”,”Profielbezoeken”],[“likes”,”Likes”],[“comments”,”Comments”],[“impressions”,”Impressies”],[“dms_count”,”DMs”]] as [keyof typeof form,string][]).map(([k,l])=>(
{l}
setForm(f=>({…f,[k]:e.target.value}))} style={field}/>
))}
{saved && ✓ Opgeslagen!}
{entries.length > 0 ?
{[“Datum”,”Posts”,”Views”,”Likes”,”Comments”,”Impressies”,”DMs”,””].map(h=>( ))} {[…entries].reverse().slice(0,10).map(e=>( {[e.date,e.posts,e.views,e.likes,e.comments,e.impressions,e.dms_count].map((v,i)=>( ))} ))}
{h}
{v||”—”}
:
Nog geen data. Voer je eerste dag in.
}
)} {/* ── OVERZICHT ── */} {tab===”overzicht” && (

Overzicht & statistieken

{[ {l:”Totaal views”,v:tot(“views”).toLocaleString(“nl-NL”),c:”#3b82f6″,i:”👁”}, {l:”Gem. views/dag (7d)”,v:avg7(“views”),c:”#06b6d4″,i:”📊”}, {l:”Totaal impressies”,v:tot(“impressions”).toLocaleString(“nl-NL”),c:”#8b5cf6″,i:”📣”}, {l:”Totaal likes”,v:tot(“likes”),c:”#10b981″,i:”👍”}, {l:”Totaal comments”,v:tot(“comments”),c:”#f59e0b”,i:”💬”}, {l:”Klantgesprekken”,v:gesprekken.length,c:”#ef4444″,i:”🤝”}, ].map(({l,v,c,i})=>(
{i} {l}
{v}
))}
{chart.length > 1 ? <>
Views & Impressies — 14 dagen
Likes & Comments — 14 dagen
:
Voer minimaal 2 dagen in voor grafieken.
}
)} {/* ── GESPREKKEN ── */} {tab===”gesprekken” && (

🤝 Klantgesprekken

Datum
setGf(f=>({…f,date:e.target.value}))} style={field}/>
Naam
setGf(f=>({…f,naam:e.target.value}))} style={field}/>
Bedrijf
setGf(f=>({…f,bedrijf:e.target.value}))} style={field}/>
Bron
Fase
Notitie
setGf(f=>({…f,notitie:e.target.value}))} style={field}/>
{[[“eerste contact”,”#3b82f6″,”👋”],[“demo/kennismaking”,”#f59e0b”,”☕”],[“offerte”,”#8b5cf6″,”📄”],[“klant geworden”,”#10b981″,”🎉”],[“niet doorgegaan”,”#6b7280″,”❌”]].map(([f,c,ic])=>(
{ic} {f}
{gesprekken.filter(g=>g.fase===f).length}
))}
{gesprekken.length > 0 && (
Conversie → klant: {Math.round(gesprekken.filter(g=>g.fase===”klant geworden”).length/gesprekken.length*100)}% {gesprekken.filter(g=>g.fase===”klant geworden”).length} van {gesprekken.length}
)} {gesprekken.length > 0 ?
{gesprekken.map(g=>(
{g.date}
{g.naam}{g.bedrijf&& · {g.bedrijf}}
{g.bron} {g.notitie&&{g.notitie}}
))}
:
Nog geen gesprekken. Voeg je eerste contact toe.
}
)} {/* ── EVALUATIE ── */} {tab===”evaluatie” && (

🔍 Evaluatie

ℹ️
Gebaseerd op je ingevoerde cijfers en LinkedIn-benchmarks. Geen AI — altijd direct resultaat.
{entries.length > 0 ? <>Data: {entries.length} dag{entries.length!==1?”en”:””} — {entries[0]?.date} t/m {entries[entries.length-1]?.date} : Nog geen data. Voer eerst activiteiten in. }
{evalResults.length > 0 && (
{evalResults.map((r,i)=>(
{r.icon}
{r.t}
{r.d}
))}
)}
)}
{/* ── EXPORT MODAL ── */} {showExport && (
📋 Data exporteren
Klik “Kopieer” en plak in Excel of Google Sheets (Ctrl+V).