// M09 — Client portal: dashboard, project, files, messages, billing const Portal = ({ setPage }) => { const [view, setView] = React.useState('dashboard'); const [projectId, setProjectId] = React.useState('ledgerline'); return (
{view === 'dashboard' && } {view === 'project' && } {view === 'files' && } {view === 'messages' && } {view === 'billing' && }
); }; const PortalSidebar = ({ view, setView, setPage }) => { const NAV = [ ['dashboard','◐','Overview'], ['project','▤','Your project'], ['files','◇','Files & deliverables', 12], ['messages','◌','Messages', 3], ['billing','◯','Billing & invoices'], ]; return ( ); }; const PortalTopbar = () => (
Project on track
NEXT MILESTONE · MAY 22 · CMS HANDOFF
); // ─── DASHBOARD ───────────────────────────────────────────── const PortalDashboard = ({ setView }) => (

Welcome back, Jane.

Here's where Ledgerline stands today — May 14, 2026.
{/* Big timeline */}
● ON TRACK · LAUNCH JUNE 28

Ledgerline rebuild · Sprint 04 of 8

62%
COMPLETE
{/* Timeline bars */}
{['Discovery','Design','Frontend','API + Data','Auth','Payments','QA + Polish','Launch'].map((s,i) => (
S{i+1}
{s}
))}
{/* Stat row */}
{/* Two-col */}
setView('files')}> {[ {t:'Auth flow — final mockups',k:'Design',w:'2 days waiting',c:'#ff8a3a'}, {t:'Brand mark v3',k:'Brand',w:'5 days waiting',c:'#7c9bff'}, ].map((r,i) => (
{r.t}
{r.k.toUpperCase()}
{r.w}
))}
{TEAM.slice(0,4).map((t,i) => (
{t.avatar}
{t.name}
{t.role}
))}
{/* Activity */} {[ ['HA','#d4ff3a','pushed','Frontend dashboard refactor','25m ago'], ['SR','#ff8a3a','uploaded','Auth flow — final mockups','2h ago'], ['AQ','#7c9bff','merged','PR #142 · Webhook reliability','3h ago'], ['HA','#d4ff3a','scheduled','Demo for Friday, 4pm','yesterday'], ['BK','#a78bfa','commented','on Brand mark v3','2d ago'], ].map(([w,c,a,t,d],i) => (
{w}
{a} {t}
{d}
))}
{[ ['Thu, May 16','Sprint demo · 4:00 pm','#d4ff3a','demo'], ['Fri, May 17','Auth flow review · 11:00 am','#ff8a3a','review'], ['Mon, May 20','Sprint 05 kickoff · 10:00 am','#7c9bff','kickoff'], ['Wed, May 22','CMS handoff milestone','#a78bfa','milestone'], ].map(([d,n,c,k],i) => (
{d}
{n}
{k}
))}
); const PortalStat = ({ label, value, sub, hot, ok }) => { const c = hot ? 'var(--warn)' : ok ? 'var(--ok)' : 'var(--ink-3)'; return (
{label}
{value}
{sub}
); }; // ─── PROJECT DETAIL ──────────────────────────────────────── const PortalProject = ({ id }) => { const sprints = [ {n:'S1',name:'Discovery',w:'W18',status:'done',pts:24}, {n:'S2',name:'Design system',w:'W19',status:'done',pts:32}, {n:'S3',name:'Frontend',w:'W20',status:'done',pts:34}, {n:'S4',name:'API + Data',w:'W21',status:'done',pts:30}, {n:'S5',name:'Auth',w:'W22',status:'active',pts:32,progress:62}, {n:'S6',name:'Payments',w:'W23',status:'next',pts:28}, {n:'S7',name:'QA + Polish',w:'W24',status:'planned',pts:22}, {n:'S8',name:'Launch',w:'W25',status:'planned',pts:18}, ]; return (

Ledgerline rebuild

SP04 · Sprint 04 of 8 · 220 total story points · Launch June 28
{/* Sprint board */}
{sprints.map(sp => (
{sp.n} {sp.w}
{sp.name}
{sp.status}
{sp.pts}P
))}
{/* Current sprint detail */}
● SPRINT 04 · ACTIVE · DAY 6 OF 10
32 / 32 PTS COMMITTED

Auth & sessions

{[ ['DONE','Done',[ {t:'OAuth provider integration',pts:8,w:'AQ',c:'#7c9bff'}, {t:'Session token refresh logic',pts:5,w:'AQ',c:'#7c9bff'}, {t:'Login + signup flows (front)',pts:6,w:'NM',c:'#7ee787'}, ]], ['DOING','In progress',[ {t:'Multi-factor auth flow',pts:8,w:'AQ',c:'#7c9bff'}, {t:'Account settings UI',pts:3,w:'NM',c:'#7ee787'}, ]], ['TODO','To do',[ {t:'Password reset emails',pts:2,w:'AQ',c:'#7c9bff'}, ]], ].map(([k,l,items]) => (
{l.toUpperCase()} · {items.length}
{items.map(it => (
{k==='DONE' ? Icon.check() : k==='DOING' ? '◐' : '○'} {it.t} {it.w} {it.pts}P
))}
))}
BURNDOWN
{/* Ideal */} {/* Actual */} {/* Predicted */}
D1 D5 D10
RISKS
{[ ['low','OAuth provider rate limits','#7ee787'], ['med','MFA carrier compatibility','#ffb43a'], ['low','Email deliverability','#7ee787'], ].map(([l,t,c]) => (
{t} {l}
))}
NEXT DEMO
Friday, May 17 · 4:00 PM
Auth & sessions walkthrough. Bring your team.
); }; // ─── FILES ───────────────────────────────────────────────── const PortalFiles = () => { const groups = [ {label:'Awaiting your approval', tone:'warn', items:[ ['Auth flow — final mockups','Figma','SR','2 days','review'], ['Brand mark v3','PDF','SR','5 days','review'], ]}, {label:'Sprint 04 — Auth', tone:'normal', items:[ ['OAuth flow diagram','PDF','AQ','today','final'], ['Session model','Doc','AQ','today','final'], ['MFA wireframes','Figma','SR','2 days','draft'], ]}, {label:'Sprint 03 — Frontend', tone:'normal', items:[ ['Dashboard.tsx','Code','HA','1 wk','final'], ['Components export','Storybook','NM','1 wk','final'], ['Lighthouse report','PDF','HA','1 wk','final'], ]}, {label:'Sprint 02 — Design system', tone:'normal', items:[ ['Tokens.json','JSON','SR','3 wks','final'], ['Brand guide','PDF','SR','3 wks','final'], ['Component library','Figma','SR','3 wks','final'], ['Type specimens','PDF','SR','3 wks','final'], ]}, ]; return (

Files & deliverables

Every artifact, every revision, in one place.
{groups.map((g, gi) => (

{g.label}

{g.items.length} FILES {g.tone==='warn' && }
{g.items.map(([n,type,by,when,status],i) => (
0 ? '1px solid var(--line)' : 'none'}}> {type.slice(0,3).toUpperCase()} {n} {type} {by} {when} ago {status}
{status==='review' ? ( ) : ( )}
))}
))}
); }; // ─── MESSAGES ────────────────────────────────────────────── const PortalMessages = () => { const threads = [ {id:'t1',name:'Hassan A.',role:'Engineering lead',c:'#d4ff3a',last:'Just pushed the new dashboard branch — preview link in #demo',when:'12m',unread:1,active:true}, {id:'t2',name:'Sana R.',role:'Design lead',c:'#ff8a3a',last:'New auth mockups are ready for review whenever you have a minute',when:'2h',unread:1}, {id:'t3',name:'Amir Q.',role:'Backend',c:'#7c9bff',last:'Webhook retry logic merged. Watching the production logs.',when:'4h',unread:1}, {id:'t4',name:'#general',role:'Team channel',c:'#a78bfa',last:'Hassan A.: nice — also worth running the perf tests',when:'yesterday',unread:0}, {id:'t5',name:'Bilal K.',role:'AI engineer',c:'#a78bfa',last:'Got the eval pipeline set up if you want a walkthrough later',when:'2d',unread:0}, ]; const messages = [ {who:'Hassan A.',c:'#d4ff3a',self:false,t:'Morning Jane. Quick heads up — I just pushed the new dashboard branch.',w:'09:34'}, {who:'Hassan A.',c:'#d4ff3a',self:false,t:'Preview is at preview-ledgerline-frontend-r4f.atoms.dev. Same login as staging.',w:'09:34'}, {who:'Jane',c:'#7c9bff',self:true,t:'Amazing, taking a look now',w:'09:38'}, {who:'Jane',c:'#7c9bff',self:true,t:'The metric cards are gorgeous. Question — is the % delta calculated against the same period last month, or trailing 30d?',w:'09:41'}, {who:'Hassan A.',c:'#d4ff3a',self:false,t:'Trailing 30 by default. We can swap to same-period-last-month with a toggle if you prefer — it\'s a 5-min change.',w:'09:42'}, {who:'Hassan A.',c:'#d4ff3a',self:false,t:'Curious what your accounting team is used to seeing.',w:'09:42'}, {who:'Jane',c:'#7c9bff',self:true,t:'They\'re used to same-period-last-month. Let\'s add the toggle and default to that.',w:'09:45'}, ]; return (
{/* Thread list */} {/* Conversation */}
HA
Hassan A.
● ONLINE · ENGINEERING LEAD
TODAY
{messages.map((m,i) => (
{(!m.self || (i > 0 && messages[i-1].self !== m.self)) && ( {m.who.split(' ').map(w=>w[0]).join('').slice(0,2)} )}
{m.t}
{m.w}
))}
Hassan is typing…
); }; // ─── BILLING ─────────────────────────────────────────────── const PortalBilling = () => (

Billing & invoices

● PROJECT ON ACCOUNT
Ledgerline rebuild · Studio tier
$62,400
contract value
$31,200}/>
PAYMENT METHOD
VISA
•••• •••• •••• 4242
EXP 12/28
{/* Invoices */}
Invoices
{['Invoice','Date','Description','Amount','Status',''].map(h => (
{h}
))}
{[ ['INV-0042','May 1, 2026','Sprint 04 milestone',7800,'paid'], ['INV-0041','Apr 17, 2026','Sprint 03 milestone',7800,'paid'], ['INV-0040','Apr 3, 2026','Sprint 02 milestone',7800,'paid'], ['INV-0039','Mar 20, 2026','Sprint 01 milestone',7800,'paid'], ['INV-0038','Mar 1, 2026','Project kickoff · 50%',31200,'paid'], ['INV-0043','May 22, 2026','CMS handoff milestone',7800,'upcoming'], ['INV-0044','Jun 28, 2026','Launch · final 50%',23400,'upcoming'], ].map(([n,d,desc,amt,s],i) => (
0 ? '1px solid var(--line)' : 'none'}}> {n} {d} {desc} ${amt.toLocaleString()} {s}
))}
); window.Portal = Portal;