// Chart Growth Hub — Student "Upcoming events" calendar (preview + booking). // Visible to EVERY student. Reads the public `live_calendar` view (published events, // no join links). Booking a slot requires ACTIVE Live Trading Room access. // Exposes window.EventsPanel. (function(){ const { useState, useEffect, useMemo } = React; const TYPE_META={ live: { label:'Live session', dot:'var(--gold-2)', soft:'rgba(212,175,55,.16)', text:'var(--gold-2)', border:'rgba(212,175,55,.45)' }, training:{ label:'Training', dot:'#34d399', soft:'rgba(52,211,153,.14)', text:'#5fe3b4', border:'rgba(52,211,153,.45)' }, meeting: { label:'Meeting', dot:'#8ab4ff', soft:'rgba(122,162,247,.16)', text:'#a9c4ff', border:'rgba(122,162,247,.45)' }, maintenance:{ label:'Maintenance', dot:'#f4a52a', soft:'rgba(244,165,42,.16)', text:'#f6bd5c', border:'rgba(244,165,42,.5)' }, }; const WD=['Sun','Mon','Tue','Wed','Thu','Fri','Sat']; const MO=['January','February','March','April','May','June','July','August','September','October','November','December']; const dayKey = d => { const x=new Date(d); return x.getFullYear()+'-'+x.getMonth()+'-'+x.getDate(); }; const sameDay = (a,b) => a&&b&&dayKey(a)===dayKey(b); const fmtTime = d => d ? new Date(d).toLocaleTimeString(undefined,{hour:'numeric',minute:'2-digit'}) : 'Time TBA'; const ebadge={display:'inline-block',padding:'4px 11px',borderRadius:999,fontFamily:'var(--font-mono)',fontSize:'.7rem',border:'1px solid var(--border-gold)'}; function EventsPanel({client, session}){ const [events,setEvents]=useState(null); const [bookings,setBookings]=useState({}); const [active,setActive]=useState(false); const [me,setMe]=useState(null); const today=new Date(); const [cursor,setCursor]=useState(new Date(today.getFullYear(),today.getMonth(),1)); const [selDay,setSelDay]=useState(new Date()); const [busy,setBusy]=useState(null); const [err,setErr]=useState(""); async function load(){ const { data:ev, error } = await client.from('live_calendar').select('*').order('starts_at',{ascending:true}); if(error){ setEvents([]); return; } setEvents(ev||[]); const { data:bk } = await client.from('live_bookings').select('session_id').eq('student_id',session.user.id); const bm={}; (bk||[]).forEach(r=>bm[r.session_id]=true); setBookings(bm); const { data:la } = await client.from('live_access').select('status').eq('student_id',session.user.id).maybeSingle(); setActive(!!(la && la.status==='active')); const { data:s } = await client.from('students').select('full_name,email').eq('id',session.user.id).maybeSingle(); setMe(s); } useEffect(()=>{ load(); },[]); async function book(ev){ setBusy(ev.id); setErr(""); const { error } = await client.from('live_bookings').insert({ session_id:ev.id, student_id:session.user.id, student_email:(me&&me.email)||session.user.email, student_name:(me&&me.full_name)||null }); if(error){ setErr(error.message); } else { setBookings(b=>({...b,[ev.id]:true})); try{ client.functions.invoke('notify-event',{ body:{ kind:'booked', session_id:ev.id } }); }catch(e){} if(window.cgLog) window.cgLog(client,'book_event', ev.title||'event'); setEvents(es=>es.map(x=>x.id===ev.id?{...x,attendee_count:(+x.attendee_count||0)+1}:x)); } setBusy(null); } async function cancel(ev){ setBusy(ev.id); await client.from('live_bookings').delete().eq('session_id',ev.id).eq('student_id',session.user.id); setBookings(b=>{ const n={...b}; delete n[ev.id]; return n; }); setEvents(es=>es.map(x=>x.id===ev.id?{...x,attendee_count:Math.max(0,(+x.attendee_count||1)-1)}:x)); setBusy(null); } const monthCells = useMemo(()=>{ const y=cursor.getFullYear(), m=cursor.getMonth(); const start=new Date(y,m,1).getDay(); const dim=new Date(y,m+1,0).getDate(); const cells=[]; for(let i=0;i{ const map={}; (events||[]).forEach(s=>{ if(!s.starts_at) return; (map[dayKey(s.starts_at)]=map[dayKey(s.starts_at)]||[]).push(s); }); return map; },[events]); const dayEvents = (selDay && byDay[dayKey(selDay)]) || []; const nextUp = useMemo(()=> (events||[]).filter(s=>s.starts_at && new Date(s.starts_at).getTime()>Date.now()-3*3600e3).slice(0,3), [events]); if(events===null) return null; if(events.length===0) return null; // nothing published yet — stay quiet return

Upcoming events & live sessions

{!active && Booking needs Live Trading Room access}

Trainings, live sessions and meetings your coaches have scheduled. Pick a day to see what's on{active?' and book your slot.':' — get Live access to book your slot.'}

{/* legend */}
{Object.entries(TYPE_META).map(([k,m])=>( {m.label}))}
{MO[cursor.getMonth()]} {cursor.getFullYear()}
setCursor(new Date(cursor.getFullYear(),cursor.getMonth()-1,1))}>← {const n=new Date(); setCursor(new Date(n.getFullYear(),n.getMonth(),1)); setSelDay(n);}}>Today setCursor(new Date(cursor.getFullYear(),cursor.getMonth()+1,1))}>→
{WD.map(d=>
{d}
)} {monthCells.map((d,i)=>{ if(!d) return
; const evs=byDay[dayKey(d)]||[]; const isToday=sameDay(d,today); const isSel=sameDay(d,selDay); return ; })}
{/* day detail */}
{selDay? selDay.toLocaleDateString(undefined,{weekday:'long',month:'long',day:'numeric'}) : 'Select a day'}
{err &&
{err}
} {dayEvents.length===0 &&
Nothing on this day.{nextUp.length>0 &&
Next: {const d=new Date(nextUp[0].starts_at); setCursor(new Date(d.getFullYear(),d.getMonth(),1)); setSelDay(d);}}>{nextUp[0].title} →
}
}
{dayEvents.map(ev=>{ const m=TYPE_META[ev.session_type]||TYPE_META.live; const booked=!!bookings[ev.id]; const b=busy===ev.id; return
{m.label} {booked && ✓ Booked}
{ev.title}
{fmtTime(ev.starts_at)}{ev.duration?' · '+ev.duration:''}{ev.coach_name?' · '+ev.coach_name:''}{(+ev.attendee_count>0)?' · '+ev.attendee_count+' attending':''}
{ev.description &&

{ev.description}

}
{ev.session_type==='maintenance' ?
⚠ Scheduled maintenance — the platform may be briefly unavailable. No booking needed.
: active ? (booked ? : ) :
🔒 Live Trading Room access needed to book.
}
; })}
; } function EBtn({children,onClick}){ return ; } window.EventsPanel = EventsPanel; })();