// screens3.jsx — Pro Review (paid evaluation service) const { useState: useState3 } = React; const { Card, Chip, CoverArt, Waveform, TopBar, IconBtn, DisplayHeading, Icon } = window.LAUNCH_SCREENS_1; const TIERS = [ { id: 'quick', name: 'QUICK SCORE', price: 11, eta: '48 HOURS', blurb: 'Fast point-based score. 5 categories, 1\u201310 each. Hit 40+/50 to unlock rewards.', perks: [ 'Song Structure \u00b7 1\u201310', 'Title / Hook \u00b7 1\u201310', 'Idea \u00b7 1\u201310', 'Lyrics \u00b7 1\u201310', 'Melody \u00b7 1\u201310', 'Score 40+ \u2192 rewards & bonus XP' ], accent: false }, { id: 'full', name: 'IN-DEPTH EVAL', price: 40, eta: '5 DAYS', blurb: 'Deep written breakdown + voice note from a Launch mentor. Mix, songwriting, vocals, arrangement.', perks: [ 'Mentor-matched to your genre', 'Written breakdown (~600 words)', '5-min voice walkthrough', 'Stems-level feedback', 'Full rubric (out of 50)' ], accent: true, badge: 'MOST PICKED' }]; const REVIEWERS = [ { lbl: 'JB', color: '#C72820', name: 'Jay Brunswick', displayName: 'Jay', role: 'RIAA Gold · 170+ cuts · 2 Billboard Top 40' }, { lbl: 'PG', color: '#7A5AE0', name: 'PG Banker', displayName: 'PG', role: 'Logic Pro certified · 1,000+ placements' }]; const RECENT_REVIEWS = [ { avatar: 'KP', color: '#C72820', name: 'kai.park', track: 'NEON HOURS', tier: 'IN-DEPTH EVAL', quote: '"Top-line is sticky as hell. Pull the 2k from your vocal bus — it\'s fighting the synth lead."', reviewer: 'JAY BRUNSWICK', stars: 5 }, { avatar: 'IL', color: '#FBBF24', name: 'isla.loop', track: 'LOW TIDE', tier: 'A&R SUBMIT', quote: '"Scored 9.1 — pitched to two labels. Hold tight."', reviewer: 'A&R PANEL', stars: 5, ar: true }]; function Stars({ n, color, dim }) { return (
{[0, 1, 2, 3, 4].map((i) => )}
); } // ───────────────────────────────────────────────────────────── // Pro Review screen — 2 modes: 'browse' and 'submit' // ───────────────────────────────────────────────────────────── function ReviewScreen({ theme, copy, onBack, onOpenEvaluations }) { const [mode, setMode] = useState3('browse'); // 'browse' | 'submit' | 'success' const [tier, setTier] = useState3('full'); const [trackTitle, setTrackTitle] = useState3(''); const [notes, setNotes] = useState3(''); const [trackUploaded, setTrackUploaded] = useState3(false); const [audioFile, setAudioFile] = useState3(null); const [coachLbl, setCoachLbl] = useState3('JB'); const [reviewers, setReviewers] = useState3(REVIEWERS); // demo fallback until the table loads const [submitting, setSubmitting] = useState3(false); const [submitErr, setSubmitErr] = useState3(null); const [agreementGate, setAgreementGate] = useState3(false); // consent gate before upload const [consented, setConsented] = useState3(false); // read+agreed THIS submission — re-prompts on every upload const [creditCents, setCreditCents] = useState3(0); // review-only credit from redeemed points React.useEffect(() => { let alive = true; if (window.LAUNCH_USER.fetchMyReviewCredit) window.LAUNCH_USER.fetchMyReviewCredit().then(c => { if (alive) setCreditCents(c || 0); }); return () => { alive = false; }; }, []); const fileRef = React.useRef(null); // Load the admin-managed coach roster (Supabase `coaches` table). Falls back // to the built-in REVIEWERS list in demo mode / on error. React.useEffect(() => { let alive = true; window.LAUNCH_USER.fetchCoaches().then((list) => { if (!alive || !list || !list.length) return; setReviewers(list); setCoachLbl((prev) => list.some((r) => r.lbl === prev) ? prev : list[0].lbl); }); return () => { alive = false; }; }, []); const selectedTier = TIERS.find((t) => t.id === tier); // Plan benefits: basic → 1 free quick ($11); vip → 2 free in-depth ($40). const ent = window.LAUNCH_USER.usePlan ? window.LAUNCH_USER.usePlan() : { plan: 'free', quickEval: 0, fullEval: 0 }; const freeEvalLeft = tier === 'quick' ? (ent.quickEval || 0) : tier === 'full' ? (ent.fullEval || 0) : 0; const planCoversThis = freeEvalLeft > 0; // Review-only credit (from redeemed points) applies ONLY to the $40 tier, and // only when the plan doesn't already cover it (plan benefit takes priority). const priceCents = (selectedTier ? selectedTier.price : 0) * 100; const isReviewTier = tier === 'full'; const creditApplied = (!planCoversThis && isReviewTier) ? Math.min(Math.max(0, creditCents), priceCents) : 0; const dueCents = planCoversThis ? 0 : Math.max(0, priceCents - creditApplied); const onPickFile = (e) => { const f = e.target.files && e.target.files[0]; if (f) { setAudioFile(f); setTrackUploaded(true); setSubmitErr(null); } }; // Show the Song Submission Agreement BEFORE the file picker opens — the user // can't even choose an audio file until they've read it and ticked "I agree". // We prompt on EVERY upload (per-submission `consented` flag), not just the // first time, even though the acceptance is still recorded in Supabase. const openFilePicker = () => { if (!consented) { setAgreementGate(true); return; } if (fileRef.current) fileRef.current.click(); }; const fmtSize = (b) => b < 1024 * 1024 ? (b / 1024).toFixed(0) + ' KB' : (b / (1024 * 1024)).toFixed(1) + ' MB'; const doSubmit = async () => { // Consent gate: the user must accept the Song Submission Agreement first. if (!consented) { setAgreementGate(true); return; } setSubmitting(true); setSubmitErr(null); const res = await window.LAUNCH_USER.submitSongForReview({ title: trackTitle, notes, tier, coach: coachLbl, audioFile, }); if (res && res.error) { setSubmitting(false); setSubmitErr(res.error); return; } const subId = res.submission && res.submission.id; // 1) Plan benefit: a free Quick (basic) or In-Depth (vip) eval — no payment. // Works in both live and demo (claimFreeEval handles both). if (planCoversThis && subId && window.LAUNCH_USER.claimFreeEval) { const fr = await window.LAUNCH_USER.claimFreeEval(subId, tier); if (fr && fr.ok) { setSubmitting(false); (setConsented(false), setMode('success')); return; } // claim failed → fall through to the normal paid flow } // Live mode → charge via Stripe Checkout (redirects out and back). if (window.LAUNCH_USER.isConfigured()) { // If account credit (from redeemed points) covers the full price, apply it // directly — no Stripe, no payment page. if (subId && priceCents > 0 && creditApplied >= priceCents && window.LAUNCH_USER.payReviewWithCredit) { const pc = await window.LAUNCH_USER.payReviewWithCredit(subId, priceCents); if (pc && pc.ok && pc.covered) { setSubmitting(false); (setConsented(false), setMode('success')); return; } // credit didn't cover (e.g. stale balance) → fall through to checkout } const itemMap = { quick: 'quick-score', full: 'full-review' }; const co = await window.LAUNCH_USER.startCheckout({ item: itemMap[tier] || 'quick-score', submissionId: subId, }); if (co && co.error) { setSubmitting(false); setSubmitErr('Saved, but checkout failed: ' + co.error); return; } // Account credit fully covered it server-side → no Stripe redirect. if (co && co.done) { setSubmitting(false); (setConsented(false), setMode('success')); return; } return; // leaving the page for Stripe } // Demo mode → simulated success. setSubmitting(false); (setConsented(false), setMode('success')); }; // ───────── SUCCESS ───────── if (mode === 'success') { return (
} />
SUBMITTED · {dueCents <= 0 ? 'COVERED BY CREDIT' : '$' + (dueCents / 100).toFixed(0) + ' CHARGED'}
YOUR TRACK
IS IN THE QUEUE
Expect your review within {selectedTier.eta.toLowerCase()}. We'll notify you the moment it drops.
ORDER · LCH-2487
Track {trackTitle || 'Untitled Demo v3'}
Tier {selectedTier.name}
ETA {selectedTier.eta}
); } // ───────── SUBMIT FORM ───────── if (mode === 'submit') { const canSubmit = trackTitle.trim() && audioFile && !submitting; return (
{/* Top bar with back */}
STEP 2 / 2
{selectedTier.name} · ${selectedTier.price}
DROP YOUR
TRACK
{/* Upload area */}
{/* Form fields */}
setTrackTitle(e.target.value)} placeholder="e.g. NEON HOURS" style={{ width: '100%', padding: '14px 16px', borderRadius: 12, background: theme.surface, border: `1px solid ${theme.border}`, color: theme.text, fontFamily: theme.fontBody, fontSize: 15, outline: 'none' }} onFocus={(e) => e.target.style.borderColor = theme.accent} onBlur={(e) => e.target.style.borderColor = theme.border} />