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>
644 lines
48 KiB
JavaScript
644 lines
48 KiB
JavaScript
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:00–3:00", eligibility: "Up to 2 yrs, no instructor/pro background", type: "solo" },
|
||
{ name: "Amateur", duration: "2:30–3:00", eligibility: "2–4 yrs, no instructor/pro background", type: "solo" },
|
||
{ name: "Semi-Pro", duration: "2:50–3:20", eligibility: "3+ yrs, instructor OR pro OR prizes in Amateur", type: "solo" },
|
||
{ name: "Profi", duration: "3:00–3:30", eligibility: "4+ yrs, instructor OR pro OR prizes in Semi-Pro", type: "solo" },
|
||
{ name: "Elite", duration: "3:00–4:00", eligibility: "3+ prizes in Profi OR widely known", type: "solo" },
|
||
{ name: "Duets & Groups", duration: "3:00–4:20", eligibility: "Open to all levels", type: "group" },
|
||
]},
|
||
{ name: "Pole Art", performanceReq: "60% floor & mid-level, 40% upper level", categories: [
|
||
{ name: "Amateur", duration: "2:30–3:00", eligibility: "Up to 2 yrs, no instructor/pro background", type: "solo" },
|
||
{ name: "Semi-Pro", duration: "2:50–3:20", eligibility: "3+ yrs, instructor OR pro OR prizes in Amateur", type: "solo" },
|
||
{ name: "Profi", duration: "3:00–3: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: ["1st–3rd 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 12–13, 2026", location: "Moscow, Russia", venue: "Crystal Hall",
|
||
disciplines: [{ name: "Exotic Pole Dance", categories: [
|
||
{ name: "Amateur", duration: "2:30–3:00", eligibility: "2–4 years", type: "solo" },
|
||
{ name: "Profi", duration: "3:00–3: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 (0–10 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>;
|
||
}
|