Files
PoleDanceApp/dancechamp-claude-code/prototypes/member-app.jsx
Dianaka123 789d2bf0a6 Full app rebuild: FastAPI backend + React Native mobile with auth, championships, admin
Backend (FastAPI + SQLAlchemy + SQLite):
- JWT auth with access/refresh tokens, bcrypt password hashing
- User model with member/organizer/admin roles, auto-approve members
- Championship, Registration, ParticipantList, Notification models
- Alembic async migrations, seed data with test users
- Registration endpoint returns tokens for members, pending for organizers
- /registrations/my returns championship title/date/location via eager loading
- Admin endpoints: list users, approve/reject organizers

Mobile (React Native + Expo + TypeScript):
- Zustand auth store, Axios client with token refresh interceptor
- Role-based registration (Member vs Organizer) with contextual form labels
- Tab navigation with Ionicons, safe area headers, admin tab for admin role
- Championships list with status badges, detail screen with registration progress
- My Registrations with championship title, progress bar, and tap-to-navigate
- Admin panel with pending/all filter, approve/reject with confirmation
- Profile screen with role badge, Ionicons info rows, sign out
- Password visibility toggle (Ionicons), keyboard flow hints (returnKeyType)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 22:46:50 +03:00

644 lines
48 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from "react";
/* ── Data ── */
const CHAMPS = [
{
id: "1", name: "Zero Gravity", subtitle: "International Pole Exotic Championship",
org: "Zero Gravity Team", dates: "May 30, 2026", location: "Minsk, Belarus",
venue: "Prime Hall", address: "Pr. Pobeditelei, 65",
disciplines: [
{ name: "Exotic Pole Dance", performanceReq: "70% floor & mid-level, 30% upper level", categories: [
{ name: "Beginners", duration: "2:003:00", eligibility: "Up to 2 yrs, no instructor/pro background", type: "solo" },
{ name: "Amateur", duration: "2:303:00", eligibility: "24 yrs, no instructor/pro background", type: "solo" },
{ name: "Semi-Pro", duration: "2:503:20", eligibility: "3+ yrs, instructor OR pro OR prizes in Amateur", type: "solo" },
{ name: "Profi", duration: "3:003:30", eligibility: "4+ yrs, instructor OR pro OR prizes in Semi-Pro", type: "solo" },
{ name: "Elite", duration: "3:004:00", eligibility: "3+ prizes in Profi OR widely known", type: "solo" },
{ name: "Duets & Groups", duration: "3:004:20", eligibility: "Open to all levels", type: "group" },
]},
{ name: "Pole Art", performanceReq: "60% floor & mid-level, 40% upper level", categories: [
{ name: "Amateur", duration: "2:303:00", eligibility: "Up to 2 yrs, no instructor/pro background", type: "solo" },
{ name: "Semi-Pro", duration: "2:503:20", eligibility: "3+ yrs, instructor OR pro OR prizes in Amateur", type: "solo" },
{ name: "Profi", duration: "3:003:30", eligibility: "4+ yrs, instructor OR pro OR prizes in Semi-Pro", type: "solo" },
]},
],
fees: { videoSelection: "50 BYN / 1,500 RUB", championship: { solo: "280 BYN / 7,500 RUB", duet: "210 BYN / 5,800 RUB pp", group: "190 BYN / 4,500 RUB pp" }, refundNote: "Non-refundable. All fees are charitable contributions." },
videoReqs: { minDuration: "1:30", editing: "No editing or splicing", maxAge: "Less than 1 year old", note: "Must reflect your level" },
judging: [
{ name: "Image", max: 10, desc: "Costume, hair, makeup, originality" },
{ name: "Artistry", max: 10, desc: "Charisma, stage presence, emotion" },
{ name: "Choreography", max: 10, desc: "Body control, complexity, originality" },
{ name: "Musicality", max: 10, desc: "Timing, feeling, accent play" },
{ name: "Technique", max: 10, desc: "Clean execution, transitions, tricks" },
{ name: "Overall", max: 10, desc: "General impression" },
{ name: "Synchronicity", max: 10, desc: "Duets only" },
],
penalties: [
{ name: "Missed element", points: -2 }, { name: "Fall", points: -2 },
{ name: "Leaving stage", consequence: "DQ" }, { name: "Exposure", consequence: "DQ" },
{ name: "Substance influence", consequence: "DQ" }, { name: "No special shoes", consequence: "DQ" },
],
venueSpecs: { poles: "2 (Static & Spinning)", poleHeight: "3.5 m", poleDiameter: "42 mm", stageSize: "6m × 14m" },
costumeRules: ["Neat and well-fitted", "No advertising", "No spikes/sharp objects", "No thongs/sheer/pasties", "Specialized shoes for Exotic", "Creativity is scored"],
generalRules: ["Must be 18+", "No medical contraindications", "Valid life & health insurance", "No lotions/bronzers 24h before", "Grip aids allowed (no wax/rosin)", "Judges' decision is final", "Organizers may change your category"],
prizes: ["1st3rd in each category", "Nominations per block", "Medals, diplomas, sponsor prizes", "All get participation diplomas", "1st Elite → judge next champ"],
resultsChannels: ["Email", "Instagram", "Telegram"],
applicationDeadline: "August 22, 2026",
formUrl: "https://docs.google.com/forms/d/e/1FAIpQLSfLaNg5Sf2QMAI6anpMrnLu-2qYfT3tdwh0dsynQFn8xMhi2g/viewform",
status: "registration_open", accent: "#D4145A", image: "💃",
},
{
id: "2", name: "Pole Star", subtitle: "National Pole Championship",
org: "Pole Star Events", dates: "Jul 1213, 2026", location: "Moscow, Russia", venue: "Crystal Hall",
disciplines: [{ name: "Exotic Pole Dance", categories: [
{ name: "Amateur", duration: "2:303:00", eligibility: "24 years", type: "solo" },
{ name: "Profi", duration: "3:003:30", eligibility: "4+ years", type: "solo" },
]}],
fees: { videoSelection: "2,000 RUB", championship: { solo: "8,000 RUB" } },
videoReqs: { minDuration: "1:00", editing: "No editing", maxAge: "6 months" },
status: "upcoming", applicationDeadline: "Jun 1, 2026", accent: "#7C3AED", image: "⭐",
},
];
const STEPS = [
{ id: "s1", label: "Review rules & eligibility", icon: "📋", detect: "auto", detectLabel: "Auto: tracked in app" },
{ id: "s2", label: "Select category", icon: "🏷️", detect: "auto", detectLabel: "Auto: saved in app" },
{ id: "s3", label: "Record video (min 1:30)", icon: "🎬", detect: "manual", detectLabel: "You confirm" },
{ id: "s4", label: "Submit video selection form", icon: "📤", detect: "email", detectLabel: "Auto: Gmail confirmation" },
{ id: "s5", label: "Pay video selection fee", icon: "💳", warn: true, detect: "receipt", detectLabel: "Upload receipt → Org confirms" },
{ id: "s6", label: "Results (auto-detected)", icon: "🤖", detect: "auto", detectLabel: "Auto: Instagram OCR" },
{ id: "s7", label: "Pay championship fee", icon: "💰", warn: true, detect: "receipt", detectLabel: "Upload receipt → Org confirms" },
{ id: "s8", label: 'Fill "About Me" form', icon: "👤", detect: "email", detectLabel: "Auto: Gmail confirmation" },
{ id: "s9", label: "Confirm insurance", icon: "🛡️", detect: "receipt", detectLabel: "Upload doc → Org confirms" },
{ id: "s10", label: "Submit music & performance", icon: "🎶", detect: "email", detectLabel: "Auto: Gmail confirmation" },
];
const USER = { name: "Alex", city: "Moscow", disciplines: ["Pole Exotic", "Pole Art"], experienceYears: 3, isInstructor: false, instagram: "@alex_pole" };
const NOTIFICATIONS = [
{ id: "n1", type: "category_change", champ: "Zero Gravity", from: "Amateur", to: "Semi-Pro", field: "Level", date: "Feb 24, 2026", read: false, message: "Your level was changed from Amateur to Semi-Pro by the organizer." },
{ id: "n2", type: "payment_confirmed", champ: "Zero Gravity", date: "Feb 23, 2026", read: false, message: "Your video selection fee payment has been confirmed." },
{ id: "n3", type: "result", champ: "Zero Gravity", date: "Feb 22, 2026", read: true, message: "Video selection results are out! You passed! 🎉" },
{ id: "n4", type: "deadline", champ: "Zero Gravity", date: "Feb 20, 2026", read: true, message: "Reminder: registration deadline is Aug 22, 2026." },
];
const MY_REGISTRATIONS = [
{ champId: "1", discipline: "Exotic Pole Dance", category: "Semi-Pro", status: "in_progress", currentStep: 4, stepsCompleted: 3, nextAction: "Submit video selection form", deadline: "Aug 22, 2026" },
{ champId: "2", discipline: "Exotic Pole Dance", category: "Profi", status: "planned", currentStep: 1, stepsCompleted: 0, nextAction: "Review rules & eligibility", deadline: "Jun 1, 2026" },
];
/* ── Theme ── */
const c = { bg: "#08070D", card: "#12111A", cardH: "#1A1926", brd: "#1F1E2E", text: "#F2F0FA", dim: "#5E5C72", mid: "#8F8DA6", accent: "#D4145A", accentS: "rgba(212,20,90,0.10)", green: "#10B981", greenS: "rgba(16,185,129,0.10)", yellow: "#F59E0B", yellowS: "rgba(245,158,11,0.10)", purple: "#8B5CF6" };
const f = { d: "'Playfair Display',Georgia,serif", b: "'DM Sans','Segoe UI',sans-serif", m: "'JetBrains Mono',monospace" };
/* ── Shared ── */
const Badge = ({ status }) => { const m = { registration_open: { l: "REG OPEN", c: c.green, b: c.greenS }, upcoming: { l: "UPCOMING", c: c.yellow, b: c.yellowS } }; const s = m[status] || m.upcoming; return <span style={{ fontFamily: f.m, fontSize: 9, fontWeight: 700, letterSpacing: 1.2, color: s.c, background: s.b, padding: "3px 8px", borderRadius: 4 }}>{s.l}</span>; };
const Chip = ({ text, color = c.mid, bg = c.card, border = c.brd }) => <span style={{ fontFamily: f.b, fontSize: 11, fontWeight: 600, color, background: bg, border: `1px solid ${border}`, padding: "4px 10px", borderRadius: 16, whiteSpace: "nowrap" }}>{text}</span>;
const Info = ({ icon, text }) => <span style={{ fontFamily: f.b, fontSize: 12, color: c.mid, display: "flex", alignItems: "center", gap: 5 }}><span style={{ fontSize: 13 }}>{icon}</span> {text}</span>;
const ST = ({ children, right }) => <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", margin: "0 0 10px" }}><h3 style={{ fontFamily: f.d, fontSize: 14, fontWeight: 700, color: c.mid, margin: 0 }}>{children}</h3>{right && <span style={{ fontFamily: f.m, fontSize: 10, color: c.dim }}>{right}</span>}</div>;
const Cd = ({ children, style: s }) => <div style={{ background: c.card, border: `1px solid ${c.brd}`, borderRadius: 14, padding: 16, ...s }}>{children}</div>;
function Tabs({ tabs, active, onChange, accent: ac }) {
return <div style={{ display: "flex", gap: 4, overflowX: "auto", paddingBottom: 2, marginBottom: 14, scrollbarWidth: "none", msOverflowStyle: "none" }}>
{tabs.map(t => <div key={t} onClick={() => onChange(t)} style={{ fontFamily: f.m, fontSize: 10, fontWeight: 600, letterSpacing: 0.4, color: active === t ? ac || c.accent : c.dim, background: active === t ? `${ac || c.accent}15` : "transparent", border: `1px solid ${active === t ? `${ac || c.accent}30` : "transparent"}`, padding: "5px 12px", borderRadius: 16, cursor: "pointer", whiteSpace: "nowrap" }}>{t}</div>)}
</div>;
}
function Nav({ active, onChange }) {
return <div style={{ display: "flex", justifyContent: "space-around", padding: "10px 0 8px", borderTop: `1px solid ${c.brd}`, background: c.bg, flexShrink: 0 }}>
{[{ id: "home", i: "🏠", l: "Home" }, { id: "my", i: "🎯", l: "My Champs" }, { id: "search", i: "🔍", l: "Search" }, { id: "profile", i: "👤", l: "Profile" }].map(x =>
<div key={x.id} onClick={() => onChange(x.id)} style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 2, cursor: "pointer", opacity: active === x.id ? 1 : 0.35 }}><span style={{ fontSize: 18 }}>{x.i}</span><span style={{ fontFamily: f.m, fontSize: 8, color: c.text, letterSpacing: 0.3 }}>{x.l}</span></div>
)}
</div>;
}
function Hdr({ title, subtitle, onBack, right }) {
return <div style={{ padding: "14px 20px 6px", display: "flex", alignItems: "center", gap: 12 }}>
{onBack && <div onClick={onBack} style={{ width: 32, height: 32, borderRadius: 8, background: c.card, border: `1px solid ${c.brd}`, display: "flex", alignItems: "center", justifyContent: "center", cursor: "pointer", fontSize: 15, color: c.text }}></div>}
<div style={{ flex: 1 }}><h1 style={{ fontFamily: f.d, fontSize: 20, fontWeight: 700, color: c.text, margin: 0 }}>{title}</h1>{subtitle && <p style={{ fontFamily: f.b, fontSize: 11, color: c.dim, margin: "2px 0 0" }}>{subtitle}</p>}</div>
{right}
</div>;
}
/* ── Home ── */
function Home({ onTap, onNotifications }) {
const unread = NOTIFICATIONS.filter(n => !n.read).length;
return <div>
<Hdr title="Dance Hub" subtitle={`Hey ${USER.name} 👋`} right={
<div onClick={onNotifications} style={{ position: "relative", width: 36, height: 36, borderRadius: 10, background: c.card, border: `1px solid ${c.brd}`, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 18, cursor: "pointer" }}>
🔔
{unread > 0 && <div style={{ position: "absolute", top: -4, right: -4, width: 18, height: 18, borderRadius: 9, background: c.accent, display: "flex", alignItems: "center", justifyContent: "center" }}>
<span style={{ fontFamily: f.m, fontSize: 9, fontWeight: 700, color: "#fff" }}>{unread}</span>
</div>}
</div>
} />
<div style={{ padding: "6px 16px 16px", display: "flex", flexDirection: "column", gap: 12 }}>
<div style={{ background: `linear-gradient(135deg,${c.accent}15,${c.accent}05)`, border: `1px solid ${c.accent}25`, borderRadius: 14, padding: "12px 16px" }}>
<p style={{ fontFamily: f.b, fontSize: 12, color: c.accent, margin: 0, fontWeight: 600 }}>🔔 Zero Gravity Deadline: Aug 22!</p>
</div>
<ST right={`${CHAMPS.length} events`}>Championships</ST>
{CHAMPS.map(ch => <ChampCard key={ch.id} ch={ch} onTap={onTap} />)}
</div>
</div>;
}
function ChampCard({ ch, onTap }) {
const [h, setH] = useState(false);
return <div onClick={() => onTap(ch)} onMouseEnter={() => setH(true)} onMouseLeave={() => setH(false)} style={{ background: h ? c.cardH : c.card, border: `1px solid ${c.brd}`, borderRadius: 16, padding: 16, cursor: "pointer", transition: "all 0.2s", transform: h ? "translateY(-2px)" : "none", boxShadow: h ? "0 8px 24px rgba(0,0,0,0.3)" : "none" }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", marginBottom: 10 }}>
<div style={{ width: 46, height: 46, borderRadius: 12, background: `linear-gradient(135deg,${ch.accent}20,${ch.accent}40)`, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 22 }}>{ch.image}</div>
<Badge status={ch.status} />
</div>
<h3 style={{ fontFamily: f.d, fontSize: 16, fontWeight: 700, color: c.text, margin: "0 0 2px" }}>{ch.name}</h3>
<p style={{ fontFamily: f.b, fontSize: 11, color: c.dim, margin: "0 0 10px" }}>{ch.subtitle}</p>
<div style={{ display: "flex", gap: 12, flexWrap: "wrap" }}><Info icon="📅" text={ch.dates} /><Info icon="📍" text={ch.location} /></div>
{ch.disciplines && <div style={{ display: "flex", gap: 6, marginTop: 10, flexWrap: "wrap" }}>{ch.disciplines.map(d => <Chip key={d.name} text={d.name} color={ch.accent} bg={`${ch.accent}10`} border={`${ch.accent}25`} />)}</div>}
</div>;
}
/* ── Championship Detail ── */
function Detail({ ch, onBack, onProgress }) {
const [tab, setTab] = useState("Info");
const tabs = ["Info", "Categories", "Fees", "Judging", "Rules"];
const completedCount = 3; // mock
return <div style={{ flex: 1, overflow: "auto" }}>
<Hdr title={ch.name} subtitle={ch.subtitle} onBack={onBack} />
<div style={{ padding: "6px 16px 20px" }}>
{/* Hero */}
<div style={{ background: `linear-gradient(135deg,${ch.accent}15,${ch.accent}05)`, border: `1px solid ${ch.accent}25`, borderRadius: 16, padding: 16, marginBottom: 14 }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 12 }}>
<span style={{ fontSize: 32 }}>{ch.image}</span><Badge status={ch.status} />
</div>
<div style={{ display: "flex", flexWrap: "wrap", gap: "8px 18px" }}>
<Info icon="📅" text={ch.dates} />
<Info icon="📍" text={`${ch.venue ? ch.venue + ", " : ""}${ch.location}`} />
{ch.applicationDeadline && <Info icon="⏰" text={`Deadline: ${ch.applicationDeadline}`} />}
{ch.resultsChannels && <Info icon="📢" text={`Results: ${ch.resultsChannels.join(", ")}`} />}
</div>
</div>
{/* Register + Progress buttons */}
<div style={{ display: "flex", gap: 8, marginBottom: 14 }}>
<div onClick={() => onProgress(ch)} style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center", gap: 6, padding: "11px 12px", borderRadius: 12, background: c.card, border: `1px solid ${c.brd}`, cursor: "pointer" }}>
<span style={{ fontSize: 14 }}>📋</span>
<span style={{ fontFamily: f.b, fontSize: 12, fontWeight: 600, color: c.text }}>Progress</span>
<span style={{ fontFamily: f.m, fontSize: 10, color: c.dim }}>{completedCount}/{STEPS.length}</span>
</div>
{ch.formUrl && ch.status === "registration_open" && <a href={ch.formUrl} target="_blank" rel="noopener noreferrer" style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center", gap: 6, padding: "11px 12px", borderRadius: 12, background: ch.accent, fontFamily: f.b, fontSize: 12, fontWeight: 700, color: "#fff", textDecoration: "none", cursor: "pointer" }}>
Register
</a>}
</div>
<Tabs tabs={tabs} active={tab} onChange={setTab} accent={ch.accent} />
{/* Info */}
{tab === "Info" && <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
{ch.disciplines && <Cd><ST>Disciplines</ST>{ch.disciplines.map(d => <div key={d.name} style={{ marginBottom: 10 }}>
<p style={{ fontFamily: f.b, fontSize: 13, fontWeight: 600, color: c.text, margin: "0 0 4px" }}>{d.name}</p>
{d.performanceReq && <p style={{ fontFamily: f.b, fontSize: 11, color: c.dim, margin: 0 }}>{d.performanceReq}</p>}
<div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginTop: 6 }}>{d.categories.map(cat => <Chip key={cat.name} text={`${cat.name}${cat.type === "group" ? " 👥" : ""}`} />)}</div>
</div>)}</Cd>}
{ch.videoReqs && <Cd><ST>Video Requirements</ST><div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
<Info icon="⏱" text={`Min duration: ${ch.videoReqs.minDuration}`} />
<Info icon="🚫" text={ch.videoReqs.editing} />
<Info icon="📅" text={ch.videoReqs.maxAge} />
<Info icon="📊" text={ch.videoReqs.note} />
</div></Cd>}
{ch.prizes && <Cd><ST>Prizes</ST>{ch.prizes.map((p, i) => <p key={i} style={{ fontFamily: f.b, fontSize: 12, color: c.mid, margin: "0 0 4px" }}>🏆 {p}</p>)}</Cd>}
</div>}
{/* Categories */}
{tab === "Categories" && ch.disciplines && <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
{ch.disciplines.map(d => <Cd key={d.name}><ST>{d.name}</ST>{d.categories.map(cat => {
const match = (USER.experienceYears >= 2 && USER.experienceYears <= 4 && !USER.isInstructor && cat.name === "Amateur") || (USER.experienceYears >= 3 && cat.name === "Semi-Pro") || cat.name === "Duets & Groups";
return <div key={cat.name} style={{ padding: "10px 0", borderBottom: `1px solid ${c.brd}` }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<span style={{ fontFamily: f.b, fontSize: 13, fontWeight: 600, color: c.text }}>{cat.name}</span>
<div style={{ display: "flex", gap: 6, alignItems: "center" }}>
<span style={{ fontFamily: f.m, fontSize: 10, color: c.dim }}>{cat.duration}</span>
{match && <span style={{ fontFamily: f.m, fontSize: 9, fontWeight: 700, color: c.green, background: c.greenS, padding: "2px 7px", borderRadius: 4 }}>MATCH</span>}
</div>
</div>
<p style={{ fontFamily: f.b, fontSize: 11, color: c.dim, margin: "4px 0 0" }}>{cat.eligibility}</p>
</div>;
})}</Cd>)}
<Cd style={{ background: `${c.yellow}08`, border: `1px solid ${c.yellow}20` }}><p style={{ fontFamily: f.b, fontSize: 11, color: c.yellow, margin: 0 }}> Organizers may change your category if level doesn't match</p></Cd>
</div>}
{/* Fees */}
{tab === "Fees" && ch.fees && <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
<Cd><ST>Stage 1: Video Selection</ST>
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: 6 }}><span style={{ fontFamily: f.b, fontSize: 13, color: c.text }}>Fee</span><span style={{ fontFamily: f.m, fontSize: 13, fontWeight: 700, color: ch.accent }}>{ch.fees.videoSelection}</span></div>
<p style={{ fontFamily: f.b, fontSize: 11, color: c.yellow, margin: 0 }}>⚠️ Non-refundable even if you don't pass</p>
</Cd>
<Cd><ST>Stage 2: Championship (after passing)</ST>
{Object.entries(ch.fees.championship).map(([t, a]) => <div key={t} style={{ display: "flex", justifyContent: "space-between", padding: "6px 0", borderBottom: `1px solid ${c.brd}` }}><span style={{ fontFamily: f.b, fontSize: 13, color: c.text, textTransform: "capitalize" }}>{t}</span><span style={{ fontFamily: f.m, fontSize: 13, fontWeight: 700, color: ch.accent }}>{a}</span></div>)}
{ch.fees.refundNote && <p style={{ fontFamily: f.b, fontSize: 11, color: c.yellow, margin: "8px 0 0" }}> {ch.fees.refundNote}</p>}
</Cd>
</div>}
{/* Judging */}
{tab === "Judging" && ch.judging && <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
<Cd><ST>Scoring (010 each)</ST>{ch.judging.map(j => <div key={j.name} style={{ padding: "8px 0", borderBottom: `1px solid ${c.brd}` }}>
<div style={{ display: "flex", justifyContent: "space-between" }}><span style={{ fontFamily: f.b, fontSize: 13, fontWeight: 600, color: c.text }}>{j.name}</span><span style={{ fontFamily: f.m, fontSize: 11, color: c.purple }}>0{j.max}</span></div>
<p style={{ fontFamily: f.b, fontSize: 11, color: c.dim, margin: "2px 0 0" }}>{j.desc}</p>
</div>)}</Cd>
{ch.penalties && <Cd><ST>Penalties</ST>{ch.penalties.map((p, i) => <div key={i} style={{ display: "flex", justifyContent: "space-between", alignItems: "center", padding: "6px 0", borderBottom: `1px solid ${c.brd}` }}>
<span style={{ fontFamily: f.b, fontSize: 12, color: c.text }}>{p.name}</span>
<span style={{ fontFamily: f.m, fontSize: 10, fontWeight: 700, color: p.consequence ? "#EF4444" : c.yellow, background: p.consequence ? "rgba(239,68,68,0.1)" : c.yellowS, padding: "2px 8px", borderRadius: 4 }}>{p.consequence || `${p.points}`}</span>
</div>)}</Cd>}
</div>}
{/* Rules + Venue */}
{tab === "Rules" && <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
{ch.generalRules && <Cd><ST>General</ST>{ch.generalRules.map((r, i) => <p key={i} style={{ fontFamily: f.b, fontSize: 12, color: c.mid, margin: "0 0 6px" }}> {r}</p>)}</Cd>}
{ch.costumeRules && <Cd><ST>Costume & Shoes</ST>{ch.costumeRules.map((r, i) => <p key={i} style={{ fontFamily: f.b, fontSize: 12, color: c.mid, margin: "0 0 6px" }}> {r}</p>)}</Cd>}
{ch.venueSpecs && <Cd><ST>Stage & Equipment</ST>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>{Object.entries(ch.venueSpecs).map(([k, v]) => <div key={k} style={{ background: c.bg, borderRadius: 10, padding: 12 }}><p style={{ fontFamily: f.m, fontSize: 9, color: c.dim, margin: "0 0 4px", letterSpacing: 0.5, textTransform: "uppercase" }}>{k.replace(/([A-Z])/g, " $1")}</p><p style={{ fontFamily: f.b, fontSize: 13, fontWeight: 600, color: c.text, margin: 0 }}>{v}</p></div>)}</div>
</Cd>}
</div>}
</div>
</div>;
}
/* ── Progress Screen (separate full view) ── */
function Progress({ ch, onBack }) {
const [done, setDone] = useState({ s1: true, s2: true, s3: true });
const [uploads, setUploads] = useState({});
const [orgConfirmed, setOrgConfirmed] = useState({});
const cnt = Object.values(done).filter(Boolean).length;
const pct = (cnt / STEPS.length) * 100;
const detectColors = { auto: { c: c.green, bg: c.greenS, label: "AUTO" }, email: { c: "#60A5FA", bg: "rgba(96,165,250,0.10)", label: "GMAIL" }, receipt: { c: c.yellow, bg: c.yellowS, label: "UPLOAD" }, manual: { c: c.mid, bg: `${c.mid}15`, label: "MANUAL" } };
const handleUpload = (stepId) => {
setUploads(p => ({ ...p, [stepId]: true }));
};
return <div style={{ flex: 1, overflow: "auto" }}>
<Hdr title="Progress" subtitle={ch.name} onBack={onBack} />
<div style={{ padding: "6px 16px 20px", display: "flex", flexDirection: "column", gap: 12 }}>
{/* Summary */}
<div style={{ background: `linear-gradient(135deg,${ch.accent}15,${ch.accent}05)`, border: `1px solid ${ch.accent}25`, borderRadius: 16, padding: 16 }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 10 }}>
<span style={{ fontFamily: f.d, fontSize: 32, fontWeight: 700, color: ch.accent }}>{Math.round(pct)}%</span>
<span style={{ fontFamily: f.m, fontSize: 11, color: c.dim }}>{cnt} of {STEPS.length} steps</span>
</div>
<div style={{ height: 6, background: `${ch.accent}20`, borderRadius: 3, overflow: "hidden" }}>
<div style={{ height: "100%", width: `${pct}%`, background: `linear-gradient(90deg,${ch.accent},${ch.accent}BB)`, borderRadius: 3, transition: "width 0.3s" }} />
</div>
</div>
{/* Legend */}
<div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
{Object.entries(detectColors).map(([k, v]) =>
<span key={k} style={{ fontFamily: f.m, fontSize: 8, fontWeight: 700, color: v.c, background: v.bg, padding: "3px 8px", borderRadius: 4, letterSpacing: 0.5 }}>{v.label}</span>
)}
<span style={{ fontFamily: f.m, fontSize: 8, fontWeight: 700, color: c.purple, background: `${c.purple}15`, padding: "3px 8px", borderRadius: 4, letterSpacing: 0.5 }}>ORG </span>
</div>
{/* Steps */}
<Cd style={{ padding: "4px 10px" }}>
{STEPS.map((s, i) => {
const d = done[s.id];
const isN = !d && cnt === i;
const uploaded = uploads[s.id];
const confirmed = orgConfirmed[s.id];
const dc = detectColors[s.detect];
return <div key={s.id} style={{ padding: "10px 4px", borderBottom: i < STEPS.length - 1 ? `1px solid ${c.brd}` : "none" }}>
{/* Main row */}
<div onClick={() => { if (s.detect === "manual" || s.detect === "auto") setDone(p => ({ ...p, [s.id]: !p[s.id] })); }} style={{ display: "flex", alignItems: "center", gap: 10, cursor: s.detect === "manual" || s.detect === "auto" ? "pointer" : "default", background: isN ? `${ch.accent}06` : "transparent", borderRadius: 8, padding: "2px 0" }}>
<div style={{
width: 26, height: 26, borderRadius: 8, flexShrink: 0,
border: `2px solid ${d ? c.green : isN ? ch.accent : c.brd}`,
background: d ? c.greenS : "transparent",
display: "flex", alignItems: "center", justifyContent: "center",
fontFamily: f.m, fontSize: 11, fontWeight: 700,
color: d ? c.green : isN ? ch.accent : c.dim,
}}>{d ? "✓" : i + 1}</div>
<span style={{ fontSize: 15 }}>{s.icon}</span>
<div style={{ flex: 1 }}>
<span style={{ fontFamily: f.b, fontSize: 12, color: d ? c.dim : isN ? c.text : c.mid, textDecoration: d ? "line-through" : "none", fontWeight: isN ? 600 : 400 }}>{s.label}</span>
</div>
{s.warn && !d && <span style={{ fontSize: 10 }}></span>}
{isN && <span style={{ fontFamily: f.m, fontSize: 8, color: ch.accent, background: c.accentS, padding: "2px 8px", borderRadius: 4, fontWeight: 700 }}>NEXT</span>}
</div>
{/* Detection method + action */}
{!d && <div style={{ marginLeft: 36, marginTop: 6, display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap" }}>
<span style={{ fontFamily: f.m, fontSize: 8, fontWeight: 700, color: dc.c, background: dc.bg, padding: "2px 7px", borderRadius: 4, letterSpacing: 0.3 }}>{dc.label}</span>
<span style={{ fontFamily: f.b, fontSize: 10, color: c.dim }}>{s.detectLabel}</span>
</div>}
{/* Upload action for receipt steps */}
{!d && isN && s.detect === "receipt" && <div style={{ marginLeft: 36, marginTop: 8 }}>
{!uploaded ? (
<div onClick={() => handleUpload(s.id)} style={{ display: "inline-flex", alignItems: "center", gap: 6, padding: "7px 14px", borderRadius: 8, background: `${c.yellow}15`, border: `1px solid ${c.yellow}30`, cursor: "pointer" }}>
<span style={{ fontSize: 13 }}>📸</span>
<span style={{ fontFamily: f.b, fontSize: 11, fontWeight: 600, color: c.yellow }}>Upload receipt</span>
</div>
) : !confirmed ? (
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<span style={{ fontFamily: f.m, fontSize: 9, fontWeight: 700, color: c.green, background: c.greenS, padding: "2px 8px", borderRadius: 4 }}>📸 Uploaded</span>
<span style={{ fontFamily: f.b, fontSize: 10, color: c.dim }}>Waiting for org to confirm...</span>
{/* Demo: simulate org confirm */}
<span onClick={() => { setOrgConfirmed(p => ({ ...p, [s.id]: true })); setDone(p => ({ ...p, [s.id]: true })); }} style={{ fontFamily: f.m, fontSize: 8, fontWeight: 700, color: c.purple, background: `${c.purple}15`, padding: "2px 8px", borderRadius: 4, cursor: "pointer" }}>Demo: Org </span>
</div>
) : null}
</div>}
{/* Email detection indicator */}
{!d && isN && s.detect === "email" && <div style={{ marginLeft: 36, marginTop: 8, display: "flex", alignItems: "center", gap: 6 }}>
<span style={{ fontSize: 12 }}>📧</span>
<span style={{ fontFamily: f.b, fontSize: 10, color: c.dim }}>Monitoring Gmail for confirmation...</span>
{/* Demo: simulate detection */}
<span onClick={() => setDone(p => ({ ...p, [s.id]: true }))} style={{ fontFamily: f.m, fontSize: 8, fontWeight: 700, color: "#60A5FA", background: "rgba(96,165,250,0.10)", padding: "2px 8px", borderRadius: 4, cursor: "pointer" }}>Demo: Detected</span>
</div>}
{/* Auto completed indicator */}
{d && s.detect === "auto" && <div style={{ marginLeft: 36, marginTop: 4 }}>
<span style={{ fontFamily: f.m, fontSize: 9, color: c.green }}> Auto-detected</span>
</div>}
{d && s.detect === "email" && <div style={{ marginLeft: 36, marginTop: 4 }}>
<span style={{ fontFamily: f.m, fontSize: 9, color: "#60A5FA" }}> Gmail confirmation received</span>
</div>}
{d && s.detect === "receipt" && <div style={{ marginLeft: 36, marginTop: 4 }}>
<span style={{ fontFamily: f.m, fontSize: 9, color: c.purple }}> Receipt uploaded · Org confirmed</span>
</div>}
</div>;
})}
</Cd>
{/* Auto-detection monitoring */}
<Cd style={{ background: `${c.purple}08`, border: `1px solid ${c.purple}20` }}>
<div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 12 }}>
<span style={{ fontSize: 20 }}>🤖</span>
<div>
<p style={{ fontFamily: f.b, fontSize: 14, fontWeight: 600, color: c.text, margin: 0 }}>Auto-Detection Active</p>
<p style={{ fontFamily: f.b, fontSize: 11, color: c.dim, margin: "2px 0 0" }}>Monitoring multiple channels</p>
</div>
</div>
{[
{ ch: "Instagram", icon: "📸", desc: "Results photo OCR", status: "Monitoring" },
{ ch: "Gmail", icon: "📧", desc: "Form confirmations & results", status: "Connected" },
{ ch: "Telegram", icon: "💬", desc: "Championship chat", status: "Monitoring" },
].map(x => <div key={x.ch} style={{ display: "flex", alignItems: "center", gap: 10, padding: "10px 12px", background: c.card, borderRadius: 10, marginBottom: 6 }}>
<span style={{ fontSize: 16 }}>{x.icon}</span>
<div style={{ flex: 1 }}>
<p style={{ fontFamily: f.b, fontSize: 12, fontWeight: 500, color: c.text, margin: 0 }}>{x.ch}</p>
<p style={{ fontFamily: f.b, fontSize: 10, color: c.dim, margin: "2px 0 0" }}>{x.desc}</p>
</div>
<span style={{ fontFamily: f.m, fontSize: 9, fontWeight: 700, color: x.status === "Connected" ? c.green : c.yellow, background: x.status === "Connected" ? c.greenS : c.yellowS, padding: "3px 8px", borderRadius: 4 }}>{x.status}</span>
</div>)}
</Cd>
{/* Register */}
{ch.formUrl && ch.status === "registration_open" && <a href={ch.formUrl} target="_blank" rel="noopener noreferrer" style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: 6, padding: "14px", borderRadius: 12, background: ch.accent, fontFamily: f.b, fontSize: 14, fontWeight: 700, color: "#fff", textDecoration: "none" }}>
Register Now
</a>}
</div>
</div>;
}
/* ── My Championships ── */
function MyChamps({ onTap, onProgress }) {
const active = MY_REGISTRATIONS.filter(r => r.status === "in_progress");
const planned = MY_REGISTRATIONS.filter(r => r.status === "planned");
const completed = MY_REGISTRATIONS.filter(r => r.status === "completed");
const RegCard = ({ reg }) => {
const ch = CHAMPS.find(c2 => c2.id === reg.champId);
if (!ch) return null;
const pct = (reg.stepsCompleted / STEPS.length) * 100;
const statusMap = { in_progress: { label: "IN PROGRESS", color: c.green, bg: c.greenS }, planned: { label: "PLANNED", color: c.yellow, bg: c.yellowS }, completed: { label: "COMPLETED", color: c.purple, bg: `${c.purple}15` } };
const st = statusMap[reg.status];
return <Cd style={{ padding: 0, overflow: "hidden" }}>
{/* Color accent bar */}
<div style={{ height: 3, background: `linear-gradient(90deg,${ch.accent},${ch.accent}88)` }} />
<div style={{ padding: 14 }}>
{/* Header */}
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", marginBottom: 10 }}>
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<div style={{ width: 40, height: 40, borderRadius: 10, background: `linear-gradient(135deg,${ch.accent}20,${ch.accent}40)`, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 18 }}>{ch.image}</div>
<div>
<h3 style={{ fontFamily: f.d, fontSize: 15, fontWeight: 700, color: c.text, margin: 0 }}>{ch.name}</h3>
<p style={{ fontFamily: f.b, fontSize: 11, color: c.dim, margin: "1px 0 0" }}>{ch.dates} · {ch.location}</p>
</div>
</div>
<span style={{ fontFamily: f.m, fontSize: 8, fontWeight: 700, letterSpacing: 0.8, color: st.color, background: st.bg, padding: "3px 8px", borderRadius: 4 }}>{st.label}</span>
</div>
{/* Category */}
<div style={{ display: "flex", gap: 6, marginBottom: 12 }}>
<Chip text={reg.discipline} color={ch.accent} bg={`${ch.accent}10`} border={`${ch.accent}25`} />
<Chip text={reg.category} />
</div>
{/* Progress bar */}
<div style={{ marginBottom: 10 }}>
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: 4 }}>
<span style={{ fontFamily: f.m, fontSize: 10, color: c.dim }}>{reg.stepsCompleted}/{STEPS.length} steps</span>
<span style={{ fontFamily: f.m, fontSize: 10, color: ch.accent }}>{Math.round(pct)}%</span>
</div>
<div style={{ height: 4, background: c.brd, borderRadius: 2, overflow: "hidden" }}>
<div style={{ height: "100%", width: `${pct}%`, background: ch.accent, borderRadius: 2, transition: "width 0.3s" }} />
</div>
</div>
{/* Next action */}
{reg.nextAction && <div style={{ background: `${ch.accent}08`, border: `1px solid ${ch.accent}15`, borderRadius: 10, padding: "10px 12px", marginBottom: 10 }}>
<p style={{ fontFamily: f.m, fontSize: 9, color: c.dim, margin: "0 0 3px", letterSpacing: 0.5, textTransform: "uppercase" }}>Next step</p>
<p style={{ fontFamily: f.b, fontSize: 12, fontWeight: 600, color: c.text, margin: 0 }}>{STEPS[reg.currentStep - 1]?.icon} {reg.nextAction}</p>
</div>}
{/* Deadline */}
{reg.deadline && <div style={{ display: "flex", alignItems: "center", gap: 6, marginBottom: 12 }}>
<span style={{ fontSize: 12 }}></span>
<span style={{ fontFamily: f.b, fontSize: 11, color: c.yellow }}>Deadline: {reg.deadline}</span>
</div>}
{/* Actions */}
<div style={{ display: "flex", gap: 8 }}>
<div onClick={() => onTap(ch)} style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center", gap: 5, padding: "9px", borderRadius: 10, background: c.bg, border: `1px solid ${c.brd}`, cursor: "pointer" }}>
<span style={{ fontSize: 12 }}></span>
<span style={{ fontFamily: f.b, fontSize: 11, fontWeight: 600, color: c.mid }}>Details</span>
</div>
<div onClick={() => onProgress(ch)} style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center", gap: 5, padding: "9px", borderRadius: 10, background: ch.accent, cursor: "pointer" }}>
<span style={{ fontSize: 12 }}>📋</span>
<span style={{ fontFamily: f.b, fontSize: 11, fontWeight: 700, color: "#fff" }}>Progress</span>
</div>
</div>
</div>
</Cd>;
};
return <div>
<Hdr title="My Championships" subtitle={`${MY_REGISTRATIONS.length} registrations`} />
<div style={{ padding: "6px 16px 16px", display: "flex", flexDirection: "column", gap: 12 }}>
{active.length > 0 && <>
<ST right={`${active.length}`}>Active</ST>
{active.map(r => <RegCard key={r.champId} reg={r} />)}
</>}
{planned.length > 0 && <>
<ST right={`${planned.length}`}>Planned</ST>
{planned.map(r => <RegCard key={r.champId} reg={r} />)}
</>}
{completed.length > 0 && <>
<ST right={`${completed.length}`}>Completed</ST>
{completed.map(r => <RegCard key={r.champId} reg={r} />)}
</>}
{MY_REGISTRATIONS.length === 0 && <div style={{ textAlign: "center", padding: "60px 20px" }}>
<span style={{ fontSize: 40 }}>🔍</span>
<p style={{ fontFamily: f.b, fontSize: 14, color: c.mid, margin: "12px 0 4px" }}>No championships yet</p>
<p style={{ fontFamily: f.b, fontSize: 12, color: c.dim }}>Browse championships and start your journey!</p>
</div>}
</div>
</div>;
}
/* ── Notifications ── */
function Notifications({ onBack }) {
const [notifs, setNotifs] = useState(NOTIFICATIONS);
const markRead = (id) => setNotifs(p => p.map(n => n.id === id ? { ...n, read: true } : n));
const markAllRead = () => setNotifs(p => p.map(n => ({ ...n, read: true })));
const unread = notifs.filter(n => !n.read).length;
const typeConfig = {
category_change: { icon: "🔄", color: c.yellow, label: "Category Changed" },
payment_confirmed: { icon: "✅", color: c.green, label: "Payment Confirmed" },
result: { icon: "🏆", color: c.accent, label: "Results" },
deadline: { icon: "⏰", color: c.yellow, label: "Deadline Reminder" },
style_change: { icon: "🔄", color: c.purple, label: "Style Changed" },
registration_confirmed: { icon: "📋", color: c.green, label: "Registration" },
announcement: { icon: "📢", color: c.blue, label: "Announcement" },
};
return <div>
<Hdr title="Notifications" subtitle={unread > 0 ? `${unread} unread` : "All caught up ✓"} onBack={onBack} right={
unread > 0 ? <div onClick={markAllRead} style={{ fontFamily: f.b, fontSize: 11, color: c.accent, cursor: "pointer", padding: "4px 8px" }}>Read all</div> : null
} />
<div style={{ padding: "6px 16px 16px", display: "flex", flexDirection: "column", gap: 6 }}>
{notifs.length === 0 && <div style={{ textAlign: "center", padding: "60px 20px" }}>
<span style={{ fontSize: 40 }}>🔕</span>
<p style={{ fontFamily: f.b, fontSize: 14, color: c.mid, margin: "12px 0 4px" }}>No notifications</p>
<p style={{ fontFamily: f.b, fontSize: 12, color: c.dim }}>You'll see updates from championships here</p>
</div>}
{notifs.map(n => {
const tc = typeConfig[n.type] || typeConfig.announcement;
return <div key={n.id} onClick={() => markRead(n.id)} style={{
display: "flex", gap: 12, padding: "12px 14px", borderRadius: 12, cursor: "pointer",
background: n.read ? c.card : `${tc.color}08`,
border: `1px solid ${n.read ? c.brd : `${tc.color}20`}`,
}}>
<div style={{ width: 36, height: 36, borderRadius: 10, background: `${tc.color}15`, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 16, flexShrink: 0 }}>{tc.icon}</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 3 }}>
<span style={{ fontFamily: f.m, fontSize: 9, fontWeight: 700, color: tc.color, letterSpacing: 0.5 }}>{tc.label}</span>
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
<span style={{ fontFamily: f.m, fontSize: 9, color: c.dim }}>{n.date}</span>
{!n.read && <div style={{ width: 7, height: 7, borderRadius: 4, background: c.accent }} />}
</div>
</div>
<p style={{ fontFamily: f.b, fontSize: 12, color: n.read ? c.mid : c.text, margin: 0, lineHeight: 1.4 }}>{n.message}</p>
<p style={{ fontFamily: f.m, fontSize: 10, color: c.dim, margin: "4px 0 0" }}>{n.champ}</p>
</div>
</div>;
})}
</div>
</div>;
}
/* ── Search ── */
function Search({ onTap }) {
const [q, setQ] = useState("");
const [fl, setFl] = useState("all");
const fs = [{ id: "all", l: "All" }, { id: "registration_open", l: "Open" }, { id: "upcoming", l: "Upcoming" }];
const res = CHAMPS.filter(ch => (!q || ch.name.toLowerCase().includes(q.toLowerCase()) || ch.location.toLowerCase().includes(q.toLowerCase())) && (fl === "all" || ch.status === fl));
return <div>
<Hdr title="Discover" subtitle="Find your next championship" />
<div style={{ padding: "4px 16px 16px" }}>
<div style={{ background: c.card, border: `1px solid ${c.brd}`, borderRadius: 12, padding: "10px 14px", display: "flex", alignItems: "center", gap: 10, marginBottom: 12 }}>
<span style={{ fontSize: 15, opacity: 0.4 }}>🔍</span>
<input type="text" placeholder="Search..." value={q} onChange={e => setQ(e.target.value)} style={{ background: "transparent", border: "none", outline: "none", color: c.text, fontFamily: f.b, fontSize: 13, width: "100%" }} />
</div>
<div style={{ display: "flex", gap: 6, marginBottom: 14 }}>{fs.map(x => <div key={x.id} onClick={() => setFl(x.id)} style={{ fontFamily: f.m, fontSize: 10, fontWeight: 600, color: fl === x.id ? c.accent : c.dim, background: fl === x.id ? c.accentS : c.card, border: `1px solid ${fl === x.id ? `${c.accent}30` : c.brd}`, padding: "5px 12px", borderRadius: 16, cursor: "pointer" }}>{x.l}</div>)}</div>
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>{res.length ? res.map(ch => <ChampCard key={ch.id} ch={ch} onTap={onTap} />) : <div style={{ textAlign: "center", padding: 40 }}><span style={{ fontSize: 28 }}>🤷</span><p style={{ fontFamily: f.b, fontSize: 13, color: c.dim, marginTop: 8 }}>No results</p></div>}</div>
</div>
</div>;
}
/* ── Profile ── */
function Profile() {
return <div>
<Hdr title="Profile" />
<div style={{ padding: "6px 20px 20px" }}>
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", marginBottom: 24 }}>
<div style={{ width: 68, height: 68, borderRadius: 18, background: `linear-gradient(135deg,${c.accent}25,${c.accent}10)`, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 30, marginBottom: 10, border: `2px solid ${c.accent}35` }}>💃</div>
<h2 style={{ fontFamily: f.d, fontSize: 19, fontWeight: 700, color: c.text, margin: "0 0 2px" }}>{USER.name}</h2>
<p style={{ fontFamily: f.m, fontSize: 11, color: c.accent, margin: 0 }}>{USER.instagram}</p>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: 8, marginBottom: 20 }}>
{[{ i: "📍", l: "City", v: USER.city }, { i: "💃", l: "Disciplines", v: USER.disciplines.join(", ") }, { i: "📅", l: "Experience", v: `${USER.experienceYears} years` }, { i: "🎓", l: "Instructor", v: USER.isInstructor ? "Yes" : "No" }].map(r =>
<Cd key={r.l} style={{ padding: "11px 14px", display: "flex", alignItems: "center", gap: 12 }}><span style={{ fontSize: 17 }}>{r.i}</span><div><p style={{ fontFamily: f.m, fontSize: 9, color: c.dim, margin: 0, letterSpacing: 0.5, textTransform: "uppercase" }}>{r.l}</p><p style={{ fontFamily: f.b, fontSize: 13, color: c.text, margin: "2px 0 0" }}>{r.v}</p></div></Cd>
)}
</div>
<ST>Eligible Categories</ST>
<Cd style={{ marginBottom: 20 }}>{["Amateur (Exotic)", "Semi-Pro (Exotic)", "Duets & Groups", "Amateur (Pole Art)", "Semi-Pro (Pole Art)"].map(cat =>
<div key={cat} style={{ display: "flex", alignItems: "center", gap: 8, padding: "7px 0", borderBottom: `1px solid ${c.brd}` }}><span style={{ fontFamily: f.m, fontSize: 9, fontWeight: 700, color: c.green, background: c.greenS, padding: "2px 7px", borderRadius: 4 }}></span><span style={{ fontFamily: f.b, fontSize: 12, color: c.text }}>{cat}</span></div>
)}</Cd>
<ST>Stats</ST>
<div style={{ display: "flex", gap: 8, marginBottom: 20 }}>{[{ n: "2", l: "Champs", co: c.accent }, { n: "1", l: "Passed", co: c.green }, { n: "1", l: "Pending", co: c.yellow }].map(s =>
<div key={s.l} style={{ flex: 1, background: c.card, border: `1px solid ${c.brd}`, borderRadius: 12, padding: "14px 8px", textAlign: "center" }}><p style={{ fontFamily: f.d, fontSize: 24, fontWeight: 700, color: s.co, margin: 0 }}>{s.n}</p><p style={{ fontFamily: f.m, fontSize: 9, color: c.dim, margin: "4px 0 0", textTransform: "uppercase" }}>{s.l}</p></div>
)}</div>
<div style={{ background: c.card, border: `1px solid ${c.brd}`, borderRadius: 12, overflow: "hidden" }}>{["Edit Profile", "Competition History", "Notifications", "Settings", "Log Out"].map((x, i, a) =>
<div key={x} style={{ padding: "13px 16px", fontFamily: f.b, fontSize: 13, color: x === "Log Out" ? "#EF4444" : c.text, borderBottom: i < a.length - 1 ? `1px solid ${c.brd}` : "none", cursor: "pointer", display: "flex", justifyContent: "space-between" }}>{x}<span style={{ color: c.dim }}></span></div>
)}</div>
</div>
</div>;
}
/* ── App Shell ── */
export default function App() {
const [scr, setScr] = useState("home");
const [sel, setSel] = useState(null);
const [prev, setPrev] = useState("home");
const go = (s, ch) => { setPrev(scr); setScr(s); if (ch) setSel(ch); };
const goBack = () => { setScr(prev || "home"); setSel(null); };
const render = () => {
if (scr === "progress" && sel) return <Progress ch={sel} onBack={() => go("detail")} />;
if (scr === "detail" && sel) return <Detail ch={sel} onBack={goBack} onProgress={ch => go("progress", ch)} />;
if (scr === "notifications") return <Notifications onBack={() => go("home")} />;
if (scr === "my") return <MyChamps onTap={ch => go("detail", ch)} onProgress={ch => go("progress", ch)} />;
if (scr === "search") return <Search onTap={ch => go("detail", ch)} />;
if (scr === "profile") return <Profile />;
return <Home onTap={ch => go("detail", ch)} onNotifications={() => go("notifications")} />;
};
const showNav = scr === "home" || scr === "search" || scr === "profile" || scr === "my";
return <div style={{ display: "flex", justifyContent: "center", alignItems: "center", minHeight: "100vh", background: "#030206", padding: 20, fontFamily: f.b }}>
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;600;700&family=DM+Sans:wght@400;500;600&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet" />
<style>{`*::-webkit-scrollbar{display:none}*{scrollbar-width:none}`}</style>
<div style={{ width: 375, height: 740, background: c.bg, borderRadius: 36, overflow: "hidden", display: "flex", flexDirection: "column", border: `1.5px solid ${c.brd}`, boxShadow: `0 0 80px rgba(212,20,90,0.06),0 20px 40px rgba(0,0,0,0.5)` }}>
<div style={{ padding: "8px 24px", display: "flex", justifyContent: "space-between", alignItems: "center", flexShrink: 0 }}>
<span style={{ fontFamily: f.m, fontSize: 11, color: c.dim }}>9:41</span>
<div style={{ width: 100, height: 28, background: "#000", borderRadius: 14 }} />
<span style={{ fontFamily: f.m, fontSize: 11, color: c.dim }}></span>
</div>
<div style={{ flex: 1, overflow: "auto", minHeight: 0 }}>{render()}</div>
{showNav && <Nav active={scr} onChange={s => { setScr(s); setSel(null); }} />}
</div>
</div>;
}