// Chart Growth Hub — Student Trading Journal panel (VIP-only). // Table: journal_entries (student_id, trade_date, pair, direction, entry_price, exit_price, pnl, notes). // RLS restricts every row to student_id = auth.uid() AND my_vip(), so the panel itself just needs to // be shown behind the isVIP gate already used elsewhere in the dashboard. // Exposes window.JournalPanel. (function(){ const { useState, useEffect, useMemo } = React; const fmtDate = d => d ? new Date(d+'T00:00:00').toLocaleDateString(undefined,{month:'short',day:'numeric',year:'numeric'}) : '—'; const money = n => (n==null||n==='') ? '—' : (Number(n)>=0?'+':'') + '$' + Number(n).toFixed(2); function blankForm(){ return { trade_date:new Date().toISOString().slice(0,10), pair:'', direction:'long', entry_price:'', exit_price:'', pnl:'', notes:'' }; } function JournalPanel({client, session}){ const [rows,setRows]=useState(null); const [err,setErr]=useState(""); const [form,setForm]=useState(null); // null = closed, object = add/edit form open const [busy,setBusy]=useState(false); async function load(){ const { data, error } = await client.from('journal_entries').select('*').eq('student_id',session.user.id).order('trade_date',{ascending:false}).order('created_at',{ascending:false}); if(error) setErr(error.message); else setRows(data||[]); } useEffect(()=>{ load(); /* eslint-disable-next-line */ },[]); const stats = useMemo(()=>{ const r=rows||[]; const withPnl=r.filter(x=>x.pnl!=null && x.pnl!==''); const wins=withPnl.filter(x=>Number(x.pnl)>0).length; const total=withPnl.reduce((s,x)=>s+Number(x.pnl),0); return { count:r.length, winRate: withPnl.length? Math.round(wins/withPnl.length*100):null, total }; },[rows]); function openAdd(){ setForm(blankForm()); } function openEdit(r){ setForm({ ...r, entry_price:r.entry_price??'', exit_price:r.exit_price??'', pnl:r.pnl??'' }); } async function save(){ if(!form.pair.trim()){ setErr('Enter the pair/asset.'); return; } setErr(""); setBusy(true); const payload = { student_id: session.user.id, trade_date: form.trade_date, pair: form.pair.trim().toUpperCase(), direction: form.direction, entry_price: form.entry_price===''?null:Number(form.entry_price), exit_price: form.exit_price===''?null:Number(form.exit_price), pnl: form.pnl===''?null:Number(form.pnl), notes: form.notes||null, }; let error; if(form.id){ ({ error } = await client.from('journal_entries').update(payload).eq('id',form.id)); } else { ({ error } = await client.from('journal_entries').insert(payload)); } setBusy(false); if(error){ setErr(error.message); return; } setForm(null); load(); } async function del(r){ if(!confirm('Delete this journal entry?')) return; setRows(rs=>rs.filter(x=>x.id!==r.id)); const { error } = await client.from('journal_entries').delete().eq('id',r.id); if(error){ setErr(error.message); load(); } } if(!rows) return
; return

Trading Journal

Log every trade and track your progress over time. Only you can see this.

{[['Entries',stats.count],['Win rate',stats.winRate==null?'—':stats.winRate+'%'],['Total P&L',money(stats.total)]].map(([l,n])=>(
{n}
{l}
))}
{err &&
{err}
} {form &&
setForm(f=>({...f,trade_date:e.target.value}))}/>
setForm(f=>({...f,pair:e.target.value}))} placeholder="XAU/USD"/>
setForm(f=>({...f,entry_price:e.target.value}))}/>
setForm(f=>({...f,exit_price:e.target.value}))}/>
setForm(f=>({...f,pnl:e.target.value}))} placeholder="e.g. 120 or -45"/>
setForm(f=>({...f,notes:e.target.value}))} placeholder="Setup, reasoning, lesson learned…"/>
} {rows.length===0 && !form &&
No entries yet. Click + Log a trade to start your journal.
}
{rows.map(r=>(
{r.pair} {r.direction} {fmtDate(r.trade_date)}
{r.notes &&
{r.notes}
} {(r.entry_price!=null||r.exit_price!=null) &&
{r.entry_price!=null?'entry '+r.entry_price:''}{r.entry_price!=null&&r.exit_price!=null?' → ':''}{r.exit_price!=null?'exit '+r.exit_price:''}
}
0?'var(--green)':r.pnl<0?'var(--red)':'var(--ink-faint)'}}>{money(r.pnl)}
))}
; } window.JournalPanel = JournalPanel; })();