// evaluations.jsx // Vintage Music Group A&R evaluation system: // • EvaluationsBlock — Profile section listing past evals // • EvaluationDetail — full rubric breakdown for one evaluation // • RubricScreen — public rubric reference + A&R input mode // • CreditsCard — Home/Profile widget showing XP → A&R credits const { useState: useStateE, useEffect: useEffectE } = React; const { Card, Chip, TopBar, DisplayHeading } = window.LAUNCH_SCREENS_1; // ──────────────────────────────────────────────────────── // Quick Score Rubric — Vintage Music Group v1.0 // 5 categories × 10 pts = 50 total · 40+ unlocks rewards // ──────────────────────────────────────────────────────── const RUBRIC = [ { id: 'structure', label: 'SONG STRUCTURE', desc: 'Form, flow, arrangement — does the song carry the listener?' }, { id: 'hook', label: 'TITLE / HOOK', desc: 'The line you remember after one listen. Sticky, earned, owned.' }, { id: 'idea', label: 'IDEA', desc: 'The concept underneath the song. Specific point of view.' }, { id: 'lyrics', label: 'LYRICS', desc: 'Word choice, imagery, payoff, rhyme craft.' }, { id: 'melody', label: 'MELODY', desc: 'Top-line shape, intervals, singability, contour.' }, ]; const REWARD_THRESHOLD = 40; // What each score level signals — 40+ is the reward-unlock line const SCORE_BANDS = [ { min: 0, max: 19, label: 'FOUNDATION', sub: 'Core craft needs work', tone: '#7a7a7a' }, { min: 20, max: 29, label: 'BUILDING', sub: 'Real ideas, rough execution', tone: '#8A6A3B' }, { min: 30, max: 39, label: 'ALMOST THERE', sub: 'One revision from unlocking', tone: '#E8A736' }, { min: 40, max: 44, label: 'REWARDS UNLOCKED', sub: 'Bonus XP, mentor tag, ★ badge', tone: '#4A7FD8' }, { min: 45, max: 50, label: 'TOP CUT', sub: 'Front-of-line A&R + features', tone: '#C72820' }, ]; function bandFor(total) { return SCORE_BANDS.find(b => total >= b.min && total <= b.max) || SCORE_BANDS[0]; } // Sample stored evaluations — these would normally come from an API const SAMPLE_EVALS = [ { id: 'eval-001', track: 'WINTER LIGHT', coverPal: ['#C72820', '#7A5AE0'], coverLbl: 'WL', submitted: 'APR 18, 2026', reviewer: { lbl: 'JB', name: 'Jay Brunswick', role: 'PROD · A&R VINTAGE MUSIC GROUP', color: '#C72820' }, tier: 'IN-DEPTH · $40', scores: { structure: 9, hook: 9, idea: 8, lyrics: 8, melody: 9 }, notes: { structure: 'Form holds across the runtime. Pre-chorus does its job — tension lands. Consider trimming the intro from 4 bars to 2 so the first hook arrives sooner.', hook: '"Winter light bends low" is the line. Title earns its keep — make it the streaming card and the first lyric. Owns its space.', idea: 'Clear point of view: missing a person through a season. Specific enough to feel personal, universal enough to land. Don\'t over-explain it.', lyrics: 'Verse 2 is the strongest writing. Pre-chorus rhyme scheme feels predictable on second listen — try ending one line short. Imagery in bridge is excellent.', melody: 'Chorus contour climbs naturally. Pre-chorus melody is the weak link — sits in one octave too long. Push one phrase up a fourth.', }, verdict: '"Real promise. This is the closest you\'ve gotten to your sound. Cut the intro, sharpen the pre-chorus melody, and resubmit — we\'ll talk."', }, { id: 'eval-002', track: 'BLUE DOORS', coverPal: ['#4A7FD8', '#FBBF24'], coverLbl: 'BD', submitted: 'MAR 02, 2026', reviewer: { lbl: 'PG', name: 'PG Banker', role: 'SONGWRITER · NASHVILLE', color: '#7A5AE0' }, tier: 'QUICK SCORE · $11', scores: { structure: 8, hook: 9, idea: 7, lyrics: 7, melody: 8 }, notes: { structure: 'Form is balanced. Bridge restates the chorus — bridges should reframe, not repeat. Try a left-field melodic move there.', hook: 'Title carries the song. "Blue doors" is concrete, visual, repeatable. Don\'t bury it under the vocal layer.', idea: 'Decent concept but the listener has to do too much work to find it. Spell the through-line one notch clearer.', lyrics: 'Voice is the strongest asset here. Lead with it. Verse 1 line 3 is the only weak rhyme — easy fix.', melody: 'Chorus is the sticky part. Verse melody is forgettable — too step-wise. Add one jump.', }, verdict: '"Solid demo. You\'re writing better than you\'re producing. Lift the verse melody and the whole thing climbs."', }, { id: 'eval-003', track: 'HALF SLEEP', coverPal: ['#FBBF24', '#0A0A0A'], coverLbl: 'HS', submitted: 'JAN 22, 2026', reviewer: { lbl: 'PG', name: 'PG Banker', role: 'SONGWRITER · NASHVILLE', color: '#7A5AE0' }, tier: 'QUICK SCORE · $11', scores: { structure: 7, hook: 6, idea: 8, lyrics: 7, melody: 6 }, notes: { structure: 'Hook arrives too late — bring it to 0:35 instead of 1:05. Listener won\'t wait.', hook: 'Title is OK but you can do better. "Half sleep" is a phrase, not yet a hook. Find the image inside it.', idea: 'Concept is strong. The in-between state of half-sleep is fertile ground — lean into the dream logic.', lyrics: 'Honest writing. Verse 2 line 2 is the keeper line — build the song around it next time.', melody: 'Familiar territory. What\'s the melodic move nobody else would make? Try the one that scares you.', }, verdict: '"Get the hook in faster. Trust the listener less. There\'s a song here — find it in the first 30 seconds."', }, ]; // XP → credits accounting (every 100 XP = 1 evaluation credit) const XP_TOTAL = 8372; // lifetime const XP_PER_CREDIT = 100; const CREDITS_USED = 80; // mock — increments per evaluation submitted function creditsState() { const earned = Math.floor(XP_TOTAL / XP_PER_CREDIT); const available = earned - CREDITS_USED; const xpToNext = XP_PER_CREDIT - (XP_TOTAL % XP_PER_CREDIT); const pct = ((XP_TOTAL % XP_PER_CREDIT) / XP_PER_CREDIT) * 100; return { earned, available, xpToNext, pct }; } // ──────────────────────────────────────────────────────── // CreditsCard — XP progress to next A&R credit // ──────────────────────────────────────────────────────── function CreditsCard({ theme, onUse, compact = false }) { const c = creditsState(); return ( {/* deep-field backdrop */}
★ A&R EVALUATION CREDITS
VINTAGE MUSIC GROUP · 100 XP = 1 CREDIT
{c.available} UNLOCKED
{/* Progress bar */}
XP TO NEXT CREDIT {c.xpToNext} XP
{!compact && ( )}
); } // ──────────────────────────────────────────────────────── // EvaluationsBlock — Profile section list view // ──────────────────────────────────────────────────────── function EvaluationsBlock({ theme, onOpenEval, onOpenRubric, onUseCredit, evals = SAMPLE_EVALS }) { return (
EVALUATIONS · {evals.length}
{evals.length === 0 && (
NO REVIEWS YET
Submit a track for Pro Review — your coach's score lands here.
)} {evals.map(e => { const total = Object.values(e.scores).reduce((a, b) => a + b, 0); const band = bandFor(total); return ( ); })}
); } // ──────────────────────────────────────────────────────── // EvaluationDetail — full rubric breakdown // ──────────────────────────────────────────────────────── function EvaluationDetail({ theme, evalId, onBack, onOpenRubric, evals = SAMPLE_EVALS }) { const e = evals.find(x => x.id === evalId) || evals[0]; const total = Object.values(e.scores).reduce((a, b) => a + b, 0); const band = bandFor(total); // Coach voice note — demo carries an inline data URL; live mints a signed URL // (the member is the only non-coach who can, per RLS). const [voiceUrl, setVoiceUrl] = useStateE(e.voiceUrl || null); useEffectE(() => { let alive = true; setVoiceUrl(e.voiceUrl || null); if (!e.voiceUrl && e.voicePath && window.LAUNCH_USER && window.LAUNCH_USER.evalVoiceUrl) { window.LAUNCH_USER.evalVoiceUrl(e.voicePath).then(u => { if (alive) setVoiceUrl(u); }); } return () => { alive = false; }; }, [e.id, e.voicePath, e.voiceUrl]); return (
{/* hero with deep field + cover */}
{/* top bar */}
{e.tier}
{/* title block bottom-left */}
★ EVALUATED {e.submitted}
{e.track}
{/* Score gauge */}
VINTAGE A&R · OVERALL SCORE
{total}/50
★ {band.label}
{band.sub}
{/* 40+ reward unlock indicator */}
= REWARD_THRESHOLD ? band.tone : theme.border}`, }}>
= REWARD_THRESHOLD ? `${band.tone}22` : theme.surface2, textAlign: 'left', }}>
REWARD THRESHOLD · 40 / 50
= REWARD_THRESHOLD ? band.tone : theme.text, marginTop: 3, textTransform: 'uppercase', }}> {total >= REWARD_THRESHOLD ? `★ UNLOCKED · +${total - REWARD_THRESHOLD + 5} OVER` : `${REWARD_THRESHOLD - total} POINTS TO UNLOCK`}
= REWARD_THRESHOLD ? band.tone : theme.surface, color: total >= REWARD_THRESHOLD ? '#fff' : theme.faint, }}>
{total >= REWARD_THRESHOLD ? '★' : '·'}
{total >= REWARD_THRESHOLD ? 'CLAIM' : 'LOCKED'}
{/* Rubric breakdown */}
SCORE BY CATEGORY
{RUBRIC.map(r => { const score = e.scores[r.id]; const pct = (score / 10) * 100; return (
{r.label}
{score}/10
{e.notes[r.id]}
); })}
{/* Coach voice note — private to this member */} {(voiceUrl || e.voicePath) && (
🎙 COACH VOICE NOTE · PRIVATE TO YOU
{voiceUrl ? (
)} {/* Final verdict */}
A&R VERDICT
{e.verdict}
{e.reviewer.lbl}
{e.reviewer.name}
{e.reviewer.role} · VINTAGE A&R
); } // ──────────────────────────────────────────────────────── // RubricScreen — read mode + A&R input mode // ──────────────────────────────────────────────────────── function RubricScreen({ theme, onBack }) { const [mode, setMode] = useStateE('reference'); // 'reference' | 'input' const [scores, setScores] = useStateE( RUBRIC.reduce((acc, r) => ({ ...acc, [r.id]: 7 }), {}) ); const [notes, setNotes] = useStateE( RUBRIC.reduce((acc, r) => ({ ...acc, [r.id]: '' }), {}) ); const [verdict, setVerdict] = useStateE(''); const total = Object.values(scores).reduce((a, b) => a + b, 0); const band = bandFor(total); return (
{/* Top bar */}
VINTAGE MUSIC GROUP
★ HOW WE SCORE
THE RUBRIC
v1.0
Every track gets scored across 5 categories, 1–10 each. Total out of 50. Hit 40+ to unlock rewards — bonus XP, a ★ badge on your profile, and your next submission moves to the front of the A&R queue.
{/* Mode toggle */}
{[ { id: 'reference', l: 'REFERENCE' }, { id: 'input', l: 'INPUT MODE · A&R' }, ].map(m => ( ))}
{/* Score band guide */}
VERDICT BANDS
{SCORE_BANDS.map((b, i) => (
{b.min}–{b.max}
{b.label}
{b.sub.toUpperCase()}
))}
{/* Categories — different content per mode */}
{mode === 'reference' ? 'CATEGORIES' : 'SCORE THIS TRACK'}
{RUBRIC.map(r => (
{r.label}
{r.desc}
{mode === 'input' && ( <>
setScores({ ...scores, [r.id]: parseInt(e.target.value, 10) })} style={{ flex: 1, accentColor: theme.accent }} />
{scores[r.id]}/10