// Chart Growth Hub — Student "Live Trading Room" panel. // Access model v2 (2026-07-16): the old $88 separate paywall is RETIRED. // VIP members -> full live access automatically, bundled into VIP tuition. No payment here. // Free Trial -> limited live access, gated on a manually admin-verified broker account // (VT Markets, under our IB) + an admin-set session cap. No payment either — // this is earned by having a funded broker account, not by paying us directly. // Source of truth: students.vip (VIP) and live_access.broker_verified / session_cap / sessions_used (Free Trial). // Exposes window.LivePanel. (function(){ const { useState, useEffect } = React; // ---- video helpers ---- function ytId(u){ const m=(u||'').match(/(?:youtu\.be\/|v=|embed\/)([\w-]{11})/); return m?m[1]:null; } function vimeoId(u){ const m=(u||'').match(/vimeo\.com\/(?:video\/)?(\d+)/); return m?m[1]:null; } function recEmbed(s){ if(s.recording_type==='youtube'){ const id=ytId(s.recording_url); return id?`https://www.youtube.com/embed/${id}?rel=0`:null; } if(s.recording_type==='vimeo'){ const id=vimeoId(s.recording_url); return id?`https://player.vimeo.com/video/${id}`:null; } return null; } const fmtWhen = d => d ? new Date(d).toLocaleString(undefined,{weekday:'short',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}) : ''; function Card({children, style}){ return
{children}
; } const Bullet = ({children}) =>
  • {children}
  • ; function LivePanel({client, session}){ const [vip,setVip]=useState(undefined); // undefined=loading, true/false once known const [la,setLa]=useState(undefined); // live_access row (Free Trial verification/session cap) — undefined=loading, null=none yet const [sessions,setSessions]=useState([]); const [open,setOpen]=useState(null); // recording modal const [err,setErr]=useState(""); async function load(){ const { data:stu } = await client.from('students').select('vip').eq('id',session.user.id).maybeSingle(); const isVip = !!(stu && stu.vip); setVip(isVip); const { data:row } = await client.from('live_access').select('*').eq('student_id',session.user.id).maybeSingle(); setLa(row||null); const trialCanJoin = !!(row && row.broker_verified && row.session_cap!=null && (row.sessions_used||0) < row.session_cap); if(isVip || trialCanJoin){ const { data:s } = await client.from('live_sessions').select('*').order('starts_at',{ascending:true}); setSessions(s||[]); } } useEffect(()=>{ load(); /* eslint-disable-next-line */ },[]); if(vip===undefined || la===undefined) return null; // loading — stay quiet const cap = la ? la.session_cap : null; const used = la ? (la.sessions_used||0) : 0; const verified = !!(la && la.broker_verified); const trialCanJoin = verified && cap!=null && used < cap; // ---------- ROOM: VIP (unlimited) or Free Trial with sessions remaining ---------- if(vip || trialCanJoin){ const now=Date.now(); const upcoming=sessions.filter(s=>!s.recording_url && (!s.starts_at || new Date(s.starts_at).getTime()>now-3*3600e3)); const recordings=sessions.filter(s=>s.recording_url); return
    Upcoming sessions
    {upcoming.length===0 &&

    No sessions scheduled right now — we'll post the next one here.

    }
    {upcoming.map(s=>{ const soon = s.starts_at && new Date(s.starts_at).getTime()-now < 30*60e3 && new Date(s.starts_at).getTime()-now > -3*3600e3; return
    {s.title}
    {fmtWhen(s.starts_at)||'Time TBA'}{soon&&' · live now / soon'}
    {s.join_url ? Join session ↗ : link soon}
    ; })}
    Session recordings
    {recordings.length===0 &&

    Recordings of past sessions will appear here.

    }
    {recordings.map(s=>(
    setOpen(s)}>
    {s.title}
    {s.starts_at?new Date(s.starts_at).toLocaleDateString(undefined,{month:'short',day:'numeric',year:'numeric'}):'Recording'}{s.duration?' · '+s.duration:''}
    Watch ↗
    ))}
    {open && setOpen(null)} />}
    ; } // ---------- Free Trial, sessions used up ---------- if(!vip && verified && cap!=null && used>=cap){ return

    You've used your free live sessions

    You joined {used} of {cap} free live session{cap===1?'':'s'} on your Free Trial. Become a VIP member for unlimited live trading, plus the Calculator, Journal, Signals and the Bot.

    ; } // ---------- Free Trial, broker verification pending ---------- if(!vip && la && la.broker_account_id && !verified){ return
    Confirming

    We're verifying your broker account

    Your coach is checking your account under our broker. Once confirmed, your free live sessions will unlock here — no action needed from you.

    Account: {la.broker_account_id}

    ; } // ---------- Free Trial, not yet submitted ---------- return
    Free Trial

    Trade live with a broker account under Chart Growth Hub

    Free Trial members get a limited number of live trading sessions once we've confirmed you have an account under our broker relationship. Submit your account ID below and your coach will verify it.

      Join live sessions with the coaches, limited number while on Free Trial Full recordings of every session to rewatch Upgrade to VIP anytime for unlimited live trading
    ; } // ---- Free Trial broker-account submission (admin verifies manually, same review pattern as GCash) ---- function BrokerForm({client, session, onDone, err, setErr}){ const [id,setId]=useState(""); const [busy,setBusy]=useState(false); async function submit(e){ e.preventDefault(); setErr(""); if(!id.trim()){ setErr("Enter your broker account ID/number."); return; } setBusy(true); try{ const { error } = await client.from('live_access').upsert({ student_id:session.user.id, broker_account_id:id.trim(), broker_verified:false },{ onConflict:'student_id' }); if(error) throw error; onDone(); }catch(e){ setErr(e.message||String(e)); setBusy(false); } } return
    setId(e.target.value)} placeholder="e.g. VT-123456"/>
    {err &&
    {err}
    }
    ; } function RecModal({s,onClose}){ const url=recEmbed(s); return
    e.stopPropagation()}> {s.recording_type==='file' && s.recording_url ?
    ; } function RoomHead({vip,remaining}){ return

    Live Trading Room

    {vip?'VIP · Unlimited':'Free Trial'} {!vip && remaining!=null && {remaining} free session{remaining===1?'':'s'} left}
    ; } function Section({children}){ return
    {children}
    ; } window.LivePanel = LivePanel; })();