// screens.jsx — LAUNCH app screens
// All five screens + shared primitives. Exports to window for app.jsx.
const { useState } = React;
// ─────────────────────────────────────────────────────────────
// Social links — edit the URLs here to point at the real profiles.
// (TikTok not added yet — drop a new entry here with its profile URL.)
// ─────────────────────────────────────────────────────────────
const SOCIAL_LINKS = [
{
id: 'facebook',
label: 'Facebook',
url: 'https://www.facebook.com/profile.php?id=61588404807330',
icon: () => (
),
},
{
id: 'instagram',
label: 'Instagram',
url: 'https://www.instagram.com/launchnash/',
icon: () => (
),
},
];
// Reusable row of social icons — used at the top and bottom of Home.
function SocialBar({ theme }) {
return (
);
}
// ─────────────────────────────────────────────────────────────
// Theme helpers
// ─────────────────────────────────────────────────────────────
function useTheme(t) {
const dark = t.dark;
return {
dark,
bg: dark ? '#0A0A0A' : '#EDE7DC',
surface: dark ? '#161616' : '#FFFFFF',
surface2: dark ? '#1F1F1F' : '#DDD4C2',
border: dark ? 'rgba(255,255,255,0.08)' : 'rgba(10,10,10,0.10)',
text: dark ? '#FFFFFF' : '#0A0A0A',
muted: dark ? 'rgba(255,255,255,0.55)' : 'rgba(10,10,10,0.55)',
faint: dark ? 'rgba(255,255,255,0.30)' : 'rgba(10,10,10,0.30)',
accent: t.accent,
onAccent: '#0A0A0A',
accent2: '#CAFF33',
fontDisplay: t.fontDisplay,
fontBody: t.fontBody,
fontMono: '"JetBrains Mono", ui-monospace, monospace',
pad: t.density === 'compact' ? 12 : t.density === 'comfy' ? 22 : 16,
gap: t.density === 'compact' ? 10 : t.density === 'comfy' ? 18 : 14,
radius: 18
};
}
// Copy bank — toggles between hype and pro voice
const COPY = {
hype: {
greeting: "WHAT'S GOOD",
streakCaption: 'DAY STREAK · DON\'T BREAK IT',
todayKicker: "TODAY'S FOCUS",
todayCTA: 'BOOK SESSION',
upNext: 'UP NEXT',
drops: 'DROPS FROM THE SQUAD',
completeCTA: 'SUBMIT FOR REVIEW · CLAIM XP',
submit: 'DROP YOUR TAKE',
roadmapKicker: 'YOUR LAUNCH PATH',
roadmapTitle: 'FROM BEDROOM TO BILLBOARD',
communityTitle: 'THE FEED',
profileTier: 'RISING ARTIST',
profileSub: '11 months in · keep cooking'
},
pro: {
greeting: 'Good morning',
streakCaption: 'consecutive practice days',
todayKicker: "Today's focus",
todayCTA: 'Book session',
upNext: 'Up next',
drops: 'Recent releases',
completeCTA: 'Submit for review',
submit: 'Submit your work',
roadmapKicker: 'Career roadmap',
roadmapTitle: 'Your development path',
communityTitle: 'Community',
profileTier: 'Rising Artist',
profileSub: 'Member since June 2025'
}
};
// ─────────────────────────────────────────────────────────────
// Shared bits
// ─────────────────────────────────────────────────────────────
// Deterministic hash → keeps the same celestial body for the same label
function hashLabel(s) {
let h = 0;
const str = String(s || '');
for (let i = 0; i < str.length; i++) h = ((h << 5) - h + str.charCodeAt(i)) | 0;
return Math.abs(h);
}
function CoverArt({ palette, label, sub, size = 56, radius = 10 }) {
// Space-themed cover backed by the JWST deep-field photo.
// Each label gets a deterministic crop offset + tint so tiles feel varied
// but stable — same artist always looks the same.
const h = hashLabel(label);
const variant = h % 5; // 0=planet, 1=ringed, 2=crescent moon, 3=constellation, 4=binary
const [c1, c2] = palette;
const isLarge = size > 90;
// Pan + scale across the deep-field so adjacent tiles don't repeat
const bgScale = 2.6;
const bgX = -((h % 71) / 71) * (bgScale - 1) * 100;
const bgY = -(((h >>> 5) % 79) / 79) * (bgScale - 1) * 100;
const rotate = ((h >>> 9) % 360);
const bodyId = `cv-${h}`;
const haloId = `hl-${h}`;
return (
{/* JWST deep-field background — scaled + rotated for variety */}
{/* Color tint overlay — uses the artist palette so every cover still
reads as "their" tile while sharing the cosmic backdrop. */}
{/* Bottom darkening for text legibility */}
{/* Celestial subject — sits on top of the deep field */}
{/* Label */}
{tabs.map((t) => {
const on = active === t.id;
return (
);
})}
);
}
// ─────────────────────────────────────────────────────────────
// Top bar — used per screen (kicker + actions)
// ─────────────────────────────────────────────────────────────
function TopBar({ left, right, theme }) {
return (
{left}
{right}
);
}
function IconBtn({ children, theme, onClick }) {
return (
);
}
// ─────────────────────────────────────────────────────────────
// 1. HOME
// ─────────────────────────────────────────────────────────────
function HomeScreen({ theme, copy, onOpenLesson, logged, onToggleToday, onOpenReview, onOpenMessages, onOpenLeaderboard, onOpenCoachQueue, onOpenEvaluations, onOpenPlans, unreadCount = 0 }) {
const HabitTracker = window.LAUNCH_SCREENS_2 && window.LAUNCH_SCREENS_2.HabitTracker;
const ProReviewCTA = window.LAUNCH_SCREENS_3 && window.LAUNCH_SCREENS_3.ProReviewCTA;
const LeaderboardCard = window.LAUNCH_SCREENS_6 && window.LAUNCH_SCREENS_6.LeaderboardCard;
const ProfilePlanCard = window.LAUNCH_SCREENS_4 && window.LAUNCH_SCREENS_4.ProfilePlanCard;
const user = window.LAUNCH_USER.useUser();
const plan = window.LAUNCH_USER.usePlan ? window.LAUNCH_USER.usePlan() : { plan: 'free', active: false, expiresAt: null };
const isCoach = user.role === 'coach' || user.role === 'admin';
// Notice when a new coach review has arrived (vs the last one the user opened).
const [newReview, setNewReview] = React.useState(null); // null | { id }
React.useEffect(() => {
let alive = true;
if (window.LAUNCH_USER.fetchMyEvaluations) {
window.LAUNCH_USER.fetchMyEvaluations().then(list => {
if (!alive || !Array.isArray(list) || !list.length) return;
const latestId = String(list[0].id);
let seen = '';
try { seen = localStorage.getItem('launch.seenReview') || ''; } catch (e) {}
if (latestId !== seen) setNewReview({ id: latestId });
});
}
return () => { alive = false; };
}, []);
const openReviews = () => {
if (newReview) {
try { localStorage.setItem('launch.seenReview', newReview.id); } catch (e) {}
setNewReview(null);
}
onOpenEvaluations && onOpenEvaluations();
};
return (
MON · MAY 11
{copy.greeting}, {user.firstName.toUpperCase()}
}
right={<>
{Icon.search(theme.text)}
{unreadCount > 0 && (
{unreadCount}
)}
>} />
{/* Membership — surfaced near the top so it's one of the first things seen */}
{ProfilePlanCard && onOpenPlans && !isCoach &&
{plan.active ? '★ YOUR MEMBERSHIP' : '★ UNLOCK EVERYTHING'}