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>
This commit is contained in:
517
dancechamp-claude-code/prototypes/admin-panel.jsx
Normal file
517
dancechamp-claude-code/prototypes/admin-panel.jsx
Normal file
@@ -0,0 +1,517 @@
|
||||
import { useState } from "react";
|
||||
|
||||
/* ── Platform Data ── */
|
||||
const PLATFORM = { name: "DanceChamp", version: "1.0 MVP", totalRevenue: "12,450 BYN" };
|
||||
|
||||
const ORGS_DATA = [
|
||||
{ id: "o1", name: "Zero Gravity Team", instagram: "@zerogravity_pole", logo: "💃", status: "active", joined: "Jan 15, 2026", champsCount: 2, membersCount: 24, city: "Minsk", email: "team@zerogravity.by", verified: true },
|
||||
{ id: "o2", name: "Pole Universe", instagram: "@pole_universe", logo: "🌌", status: "active", joined: "Feb 2, 2026", champsCount: 1, membersCount: 12, city: "Moscow", email: "info@poleuniverse.ru", verified: true },
|
||||
{ id: "o3", name: "Sky Pole Studio", instagram: "@sky_pole", logo: "☁️", status: "pending", joined: "Feb 20, 2026", champsCount: 0, membersCount: 0, city: "St. Petersburg", email: "hello@skypole.ru", verified: false },
|
||||
{ id: "o4", name: "Dance Flames", instagram: "@dance_flames", logo: "🔥", status: "blocked", joined: "Dec 10, 2025", champsCount: 1, membersCount: 5, city: "Kyiv", email: "admin@danceflames.ua", verified: false, blockReason: "Fake organization — no real events" },
|
||||
];
|
||||
|
||||
const CHAMPS_DATA = [
|
||||
{ id: "c1", orgId: "o1", orgName: "Zero Gravity Team", name: "Zero Gravity", dates: "May 30, 2026", location: "Minsk", status: "live", members: 24, passed: 8, pending: 8, revenue: "4,200 BYN", orgVerified: true },
|
||||
{ id: "c2", orgId: "o1", orgName: "Zero Gravity Team", name: "Pole Star", dates: "Jul 12, 2026", location: "Moscow", status: "draft", members: 1, passed: 0, pending: 0, revenue: "0", orgVerified: true },
|
||||
{ id: "c3", orgId: "o2", orgName: "Pole Universe", name: "Galactic Pole", dates: "Sep 15, 2026", location: "Moscow", status: "live", members: 12, passed: 0, pending: 12, revenue: "1,800 BYN", orgVerified: true },
|
||||
{ id: "c4", orgId: "o3", orgName: "Sky Pole Studio", name: "Sky Open", dates: "Oct 5, 2026", location: "St. Petersburg", status: "pending_approval", members: 0, passed: 0, pending: 0, revenue: "0", orgVerified: false },
|
||||
{ id: "c5", orgId: "o4", orgName: "Dance Flames", name: "Fire Cup", dates: "Mar 1, 2026", location: "Kyiv", status: "blocked", members: 5, passed: 0, pending: 0, revenue: "250 BYN", orgVerified: false },
|
||||
];
|
||||
|
||||
const USERS_DATA = [
|
||||
{ id: "u1", name: "Alex Petrova", instagram: "@alex_pole", email: "alex@mail.ru", city: "Moscow", joined: "Jan 20, 2026", champsJoined: 2, status: "active", role: "member" },
|
||||
{ id: "u2", name: "Maria Ivanova", instagram: "@maria_exotic", email: "maria@gmail.com", city: "Minsk", joined: "Jan 22, 2026", champsJoined: 1, status: "active", role: "member" },
|
||||
{ id: "u3", name: "Elena Kozlova", instagram: "@elena.pole", email: "elena@ya.ru", city: "St. Petersburg", joined: "Feb 1, 2026", champsJoined: 1, status: "active", role: "member" },
|
||||
{ id: "u4", name: "Daria Sokolova", instagram: "@daria_art", email: "daria@mail.ru", city: "Kyiv", joined: "Feb 5, 2026", champsJoined: 1, status: "active", role: "member" },
|
||||
{ id: "u5", name: "Anna Belova", instagram: "@anna.b_pole", email: "anna@gmail.com", city: "Minsk", joined: "Feb 10, 2026", champsJoined: 1, status: "active", role: "member" },
|
||||
{ id: "u6", name: "Olga Morozova", instagram: "@olga_exotic", email: "olga@mail.ru", city: "Moscow", joined: "Feb 12, 2026", champsJoined: 3, status: "warned", role: "member", warnReason: "Disputed payment — under review" },
|
||||
{ id: "u7", name: "Ivan Petrov", instagram: "@ivan_admin", email: "ivan@zerogravity.by", city: "Minsk", joined: "Jan 10, 2026", champsJoined: 0, status: "active", role: "org_admin", org: "Zero Gravity Team" },
|
||||
{ id: "u8", name: "Spam Bot", instagram: "@totally_real", email: "spam@fake.com", city: "Unknown", joined: "Feb 22, 2026", champsJoined: 0, status: "blocked", role: "member", blockReason: "Spam account" },
|
||||
];
|
||||
|
||||
const LOGS_DATA = [
|
||||
{ id: "l1", action: "Org approved & verified", target: "Pole Universe", by: "Admin", date: "Feb 2, 2026", type: "org" },
|
||||
{ id: "l2", action: "User blocked", target: "Spam Bot", by: "Admin", date: "Feb 22, 2026", type: "user" },
|
||||
{ id: "l3", action: "Org blocked", target: "Dance Flames", by: "Admin", date: "Feb 23, 2026", type: "org" },
|
||||
{ id: "l4", action: "Champ auto-approved (verified org)", target: "Zero Gravity", by: "System", date: "Feb 1, 2026", type: "champ" },
|
||||
{ id: "l5", action: "User warned", target: "Olga Morozova", by: "Admin", date: "Feb 20, 2026", type: "user" },
|
||||
{ id: "l6", action: "New org registered (pending)", target: "Sky Pole Studio", by: "System", date: "Feb 20, 2026", type: "org" },
|
||||
{ id: "l7", action: "Champ submitted for review", target: "Sky Open", by: "Sky Pole Studio", date: "Feb 21, 2026", type: "champ" },
|
||||
];
|
||||
|
||||
/* ── Theme (admin = darker, more neutral accent) ── */
|
||||
const c = { bg: "#07060C", card: "#111019", cardH: "#18172290", brd: "#1D1C2B", text: "#F2F0FA", dim: "#5E5C72", mid: "#8F8DA6", accent: "#6366F1", accentS: "rgba(99,102,241,0.10)", green: "#10B981", greenS: "rgba(16,185,129,0.10)", yellow: "#F59E0B", yellowS: "rgba(245,158,11,0.10)", purple: "#8B5CF6", purpleS: "rgba(139,92,246,0.10)", blue: "#60A5FA", blueS: "rgba(96,165,250,0.10)", red: "#EF4444", redS: "rgba(239,68,68,0.10)", orange: "#F97316", orangeS: "rgba(249,115,22,0.10)" };
|
||||
const f = { d: "'Playfair Display',Georgia,serif", b: "'DM Sans','Segoe UI',sans-serif", m: "'JetBrains Mono',monospace" };
|
||||
|
||||
/* ── Shared UI ── */
|
||||
const Cd = ({ children, style: s }) => <div style={{ background: c.card, border: `1px solid ${c.brd}`, borderRadius: 14, padding: 16, ...s }}>{children}</div>;
|
||||
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}</div>;
|
||||
const Bg = ({ label, color, bg }) => <span style={{ fontFamily: f.m, fontSize: 8, fontWeight: 700, letterSpacing: 0.8, color, background: bg, padding: "3px 8px", borderRadius: 4 }}>{label}</span>;
|
||||
|
||||
const statusConfig = {
|
||||
active: { l: "ACTIVE", c: c.green, b: c.greenS }, live: { l: "LIVE", c: c.green, b: c.greenS },
|
||||
pending: { l: "PENDING", c: c.yellow, b: c.yellowS }, pending_approval: { l: "AWAITING REVIEW", c: c.orange, b: c.orangeS },
|
||||
draft: { l: "DRAFT", c: c.dim, b: `${c.dim}15` },
|
||||
blocked: { l: "BLOCKED", c: c.red, b: c.redS }, warned: { l: "WARNED", c: c.orange, b: c.orangeS },
|
||||
};
|
||||
|
||||
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, minWidth: 0 }}><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>;
|
||||
}
|
||||
|
||||
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: "dash", i: "📊", l: "Overview" }, { id: "orgs", i: "🏢", l: "Orgs" }, { id: "champs", i: "🏆", l: "Champs" }, { id: "users", i: "👥", l: "Users" }].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 SearchBar({ value, onChange, placeholder }) {
|
||||
return <div style={{ background: c.card, border: `1px solid ${c.brd}`, borderRadius: 12, padding: "10px 14px", display: "flex", alignItems: "center", gap: 10 }}>
|
||||
<span style={{ fontSize: 14, opacity: 0.4 }}>🔍</span>
|
||||
<input type="text" placeholder={placeholder || "Search..."} value={value} onChange={e => onChange(e.target.value)} style={{ background: "transparent", border: "none", outline: "none", color: c.text, fontFamily: f.b, fontSize: 13, width: "100%" }} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
function FilterChips({ filters, active, onChange, accent }) {
|
||||
return <div style={{ display: "flex", gap: 4, overflowX: "auto", scrollbarWidth: "none" }}>
|
||||
{filters.map(fi => <div key={fi.id} onClick={() => onChange(fi.id)} style={{ fontFamily: f.m, fontSize: 9, fontWeight: 600, whiteSpace: "nowrap", color: active === fi.id ? accent || c.accent : c.dim, background: active === fi.id ? `${accent || c.accent}15` : "transparent", border: `1px solid ${active === fi.id ? `${accent || c.accent}30` : "transparent"}`, padding: "5px 10px", borderRadius: 16, cursor: "pointer" }}>{fi.l}{fi.n !== undefined ? ` (${fi.n})` : ""}</div>)}
|
||||
</div>;
|
||||
}
|
||||
|
||||
function ActionBtn({ label, color, onClick, icon, filled }) {
|
||||
return <div onClick={onClick} style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: 5, padding: "8px 14px", borderRadius: 8, background: filled ? color : `${color}15`, border: `1px solid ${filled ? color : `${color}30`}`, cursor: "pointer" }}>
|
||||
{icon && <span style={{ fontSize: 12 }}>{icon}</span>}
|
||||
<span style={{ fontFamily: f.b, fontSize: 11, fontWeight: 700, color: filled ? "#fff" : color }}>{label}</span>
|
||||
</div>;
|
||||
}
|
||||
|
||||
/* ── Dashboard ── */
|
||||
function Dashboard({ orgs, champs, users, onNav }) {
|
||||
const pendingOrgs = orgs.filter(o => o.status === "pending").length;
|
||||
const pendingChamps = champs.filter(c2 => c2.status === "pending_approval").length;
|
||||
const blockedUsers = users.filter(u => u.status === "blocked").length;
|
||||
|
||||
return <div>
|
||||
<Hdr title="Admin Panel" subtitle={`${PLATFORM.name} · ${PLATFORM.version}`} right={
|
||||
<div style={{ width: 36, height: 36, borderRadius: 10, background: `linear-gradient(135deg,${c.accent}25,${c.accent}10)`, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 14, fontFamily: f.m, fontWeight: 700, color: c.accent }}>⚡</div>
|
||||
} />
|
||||
<div style={{ padding: "6px 16px 16px", display: "flex", flexDirection: "column", gap: 12 }}>
|
||||
|
||||
{/* Platform stats */}
|
||||
<div style={{ display: "flex", gap: 6 }}>
|
||||
{[{ n: orgs.filter(o => o.status === "active").length, l: "Orgs", co: c.accent, go: "orgs" },
|
||||
{ n: champs.filter(c2 => c2.status === "live").length, l: "Live Champs", co: c.green, go: "champs" },
|
||||
{ n: users.length, l: "Users", co: c.blue, go: "users" },
|
||||
].map(s => <div key={s.l} onClick={() => onNav(s.go)} style={{ flex: 1, background: c.card, border: `1px solid ${c.brd}`, borderRadius: 12, padding: "10px 6px", textAlign: "center", cursor: "pointer" }}>
|
||||
<p style={{ fontFamily: f.d, fontSize: 20, fontWeight: 700, color: s.co, margin: 0 }}>{s.n}</p>
|
||||
<p style={{ fontFamily: f.m, fontSize: 7, color: c.dim, margin: "2px 0 0", textTransform: "uppercase" }}>{s.l}</p>
|
||||
</div>)}
|
||||
</div>
|
||||
|
||||
{/* Needs attention */}
|
||||
{(pendingOrgs > 0 || pendingChamps > 0) && <Cd style={{ background: `${c.yellow}06`, border: `1px solid ${c.yellow}20` }}>
|
||||
<ST right={<Bg label="ACTION" color={c.yellow} bg={c.yellowS} />}>⚡ Needs Attention</ST>
|
||||
{pendingOrgs > 0 && <div onClick={() => onNav("orgs")} style={{ display: "flex", alignItems: "center", gap: 10, padding: "10px 0", borderBottom: `1px solid ${c.brd}`, cursor: "pointer" }}>
|
||||
<span style={{ fontSize: 16 }}>🏢</span>
|
||||
<span style={{ fontFamily: f.b, fontSize: 13, color: c.text, flex: 1 }}>Organizations awaiting approval</span>
|
||||
<span style={{ fontFamily: f.m, fontSize: 14, fontWeight: 700, color: c.yellow }}>{pendingOrgs}</span>
|
||||
<span style={{ color: c.dim }}>›</span>
|
||||
</div>}
|
||||
{pendingChamps > 0 && <div onClick={() => onNav("champs")} style={{ display: "flex", alignItems: "center", gap: 10, padding: "10px 0", cursor: "pointer" }}>
|
||||
<span style={{ fontSize: 16 }}>🏆</span>
|
||||
<span style={{ fontFamily: f.b, fontSize: 13, color: c.text, flex: 1 }}>Champs awaiting approval (unverified orgs)</span>
|
||||
<span style={{ fontFamily: f.m, fontSize: 14, fontWeight: 700, color: c.yellow }}>{pendingChamps}</span>
|
||||
<span style={{ color: c.dim }}>›</span>
|
||||
</div>}
|
||||
</Cd>}
|
||||
|
||||
{/* Quick stats */}
|
||||
<Cd>
|
||||
<ST>Platform Health</ST>
|
||||
{[{ l: "Total revenue", v: PLATFORM.totalRevenue, co: c.green },
|
||||
{ l: "Active orgs", v: `${orgs.filter(o => o.status === "active").length}/${orgs.length}`, co: c.accent },
|
||||
{ l: "Blocked users", v: `${blockedUsers}`, co: blockedUsers > 0 ? c.red : c.green },
|
||||
{ l: "Avg members/champ", v: Math.round(users.filter(u => u.role === "member").length / Math.max(champs.filter(c2 => c2.status === "live").length, 1)), co: c.blue },
|
||||
].map(s => <div key={s.l} style={{ display: "flex", justifyContent: "space-between", padding: "8px 0", borderBottom: `1px solid ${c.brd}` }}>
|
||||
<span style={{ fontFamily: f.b, fontSize: 12, color: c.mid }}>{s.l}</span>
|
||||
<span style={{ fontFamily: f.m, fontSize: 13, fontWeight: 700, color: s.co }}>{s.v}</span>
|
||||
</div>)}
|
||||
</Cd>
|
||||
|
||||
{/* Recent activity */}
|
||||
<Cd>
|
||||
<ST right={<span style={{ fontFamily: f.m, fontSize: 10, color: c.dim }}>{LOGS_DATA.length} entries</span>}>Recent Activity</ST>
|
||||
{LOGS_DATA.slice(0, 5).map(log => {
|
||||
const tc = { org: c.accent, user: c.blue, champ: c.green }[log.type] || c.dim;
|
||||
return <div key={log.id} style={{ display: "flex", alignItems: "center", gap: 10, padding: "7px 0", borderBottom: `1px solid ${c.brd}` }}>
|
||||
<div style={{ width: 6, height: 6, borderRadius: 3, background: tc, flexShrink: 0 }} />
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<p style={{ fontFamily: f.b, fontSize: 12, color: c.text, margin: 0 }}>{log.action}: <span style={{ color: tc }}>{log.target}</span></p>
|
||||
<p style={{ fontFamily: f.m, fontSize: 9, color: c.dim, margin: "2px 0 0" }}>{log.date} · {log.by}</p>
|
||||
</div>
|
||||
</div>;
|
||||
})}
|
||||
</Cd>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
/* ── Organizations ── */
|
||||
function OrgsList({ orgs, onOrgTap }) {
|
||||
const [search, setSearch] = useState("");
|
||||
const [filter, setFilter] = useState("all");
|
||||
|
||||
const filters = [
|
||||
{ id: "all", l: "All", n: orgs.length },
|
||||
{ id: "active", l: "✅ Active", n: orgs.filter(o => o.status === "active").length },
|
||||
{ id: "pending", l: "⏳ Pending", n: orgs.filter(o => o.status === "pending").length },
|
||||
{ id: "blocked", l: "🚫 Blocked", n: orgs.filter(o => o.status === "blocked").length },
|
||||
];
|
||||
|
||||
const filtered = orgs.filter(o => {
|
||||
const q = !search || o.name.toLowerCase().includes(search.toLowerCase()) || o.instagram.toLowerCase().includes(search.toLowerCase());
|
||||
if (!q) return false;
|
||||
if (filter === "active") return o.status === "active";
|
||||
if (filter === "pending") return o.status === "pending";
|
||||
if (filter === "blocked") return o.status === "blocked";
|
||||
return true;
|
||||
});
|
||||
|
||||
return <div>
|
||||
<Hdr title="Organizations" subtitle={`${orgs.length} registered`} />
|
||||
<div style={{ padding: "6px 16px 16px", display: "flex", flexDirection: "column", gap: 10 }}>
|
||||
<SearchBar value={search} onChange={setSearch} placeholder="Search org name or @handle..." />
|
||||
<FilterChips filters={filters} active={filter} onChange={setFilter} />
|
||||
{filtered.map(o => {
|
||||
const st = statusConfig[o.status] || statusConfig.active;
|
||||
return <div key={o.id} onClick={() => onOrgTap(o)} style={{ background: c.card, border: `1px solid ${c.brd}`, borderRadius: 14, padding: 14, cursor: "pointer" }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
||||
<div style={{ width: 40, height: 40, borderRadius: 10, background: `${c.accent}15`, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 18 }}>{o.logo}</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<p style={{ fontFamily: f.b, fontSize: 14, fontWeight: 600, color: c.text, margin: 0 }}>{o.name}</p>
|
||||
<Bg label={st.l} color={st.c} bg={st.b} />
|
||||
</div>
|
||||
<p style={{ fontFamily: f.m, fontSize: 10, color: c.accent, margin: "2px 0 0" }}>{o.instagram}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: 12, marginTop: 8, paddingTop: 8, borderTop: `1px solid ${c.brd}` }}>
|
||||
<span style={{ fontFamily: f.b, fontSize: 10, color: c.mid }}>📍 {o.city}</span>
|
||||
<span style={{ fontFamily: f.b, fontSize: 10, color: c.mid }}>🏆 {o.champsCount} champs</span>
|
||||
<span style={{ fontFamily: f.b, fontSize: 10, color: c.mid }}>👥 {o.membersCount} members</span>
|
||||
</div>
|
||||
</div>;
|
||||
})}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
/* ── Org Detail ── */
|
||||
function OrgDetail({ org: initial, onBack, champs }) {
|
||||
const [o, setO] = useState(initial);
|
||||
const st = statusConfig[o.status] || statusConfig.active;
|
||||
const orgChamps = champs.filter(c2 => c2.orgId === o.id);
|
||||
|
||||
return <div style={{ flex: 1, overflow: "auto" }}>
|
||||
<Hdr title={o.name} subtitle={o.instagram} onBack={onBack} />
|
||||
<div style={{ padding: "6px 16px 20px", display: "flex", flexDirection: "column", gap: 12 }}>
|
||||
{/* Profile */}
|
||||
<Cd style={{ display: "flex", alignItems: "center", gap: 14, padding: "14px 16px" }}>
|
||||
<div style={{ width: 54, height: 54, borderRadius: 14, background: `${c.accent}15`, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 24 }}>{o.logo}</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<p style={{ fontFamily: f.b, fontSize: 16, fontWeight: 600, color: c.text, margin: 0 }}>{o.name}</p>
|
||||
<p style={{ fontFamily: f.m, fontSize: 11, color: c.accent, margin: "2px 0 0" }}>{o.instagram}</p>
|
||||
<p style={{ fontFamily: f.b, fontSize: 11, color: c.dim, margin: "2px 0 0" }}>📍 {o.city} · 📧 {o.email}</p>
|
||||
</div>
|
||||
<Bg label={st.l} color={st.c} bg={st.b} />
|
||||
</Cd>
|
||||
|
||||
{/* Info */}
|
||||
<Cd>
|
||||
<ST>Details</ST>
|
||||
{[{ l: "Joined", v: o.joined }, { l: "Championships", v: o.champsCount }, { l: "Total members", v: o.membersCount }, { l: "Verified", v: o.verified ? "✅ Yes" : "❌ No" }].map(r =>
|
||||
<div key={r.l} style={{ display: "flex", justifyContent: "space-between", padding: "7px 0", borderBottom: `1px solid ${c.brd}` }}>
|
||||
<span style={{ fontFamily: f.m, fontSize: 10, color: c.dim, textTransform: "uppercase" }}>{r.l}</span>
|
||||
<span style={{ fontFamily: f.b, fontSize: 12, color: c.text }}>{r.v}</span>
|
||||
</div>
|
||||
)}
|
||||
</Cd>
|
||||
|
||||
{/* Approval policy */}
|
||||
<Cd style={{ background: o.verified ? `${c.green}06` : `${c.yellow}06`, border: `1px solid ${o.verified ? `${c.green}20` : `${c.yellow}20`}` }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
||||
<span style={{ fontSize: 18 }}>{o.verified ? "🛡️" : "⏳"}</span>
|
||||
<div>
|
||||
<p style={{ fontFamily: f.b, fontSize: 12, fontWeight: 600, color: o.verified ? c.green : c.yellow, margin: 0 }}>{o.verified ? "Verified — Auto-approve events" : "Unverified — Events need manual approval"}</p>
|
||||
<p style={{ fontFamily: f.b, fontSize: 10, color: c.dim, margin: "2px 0 0" }}>{o.verified ? "Championships go live instantly when org clicks 'Go Live'" : "Admin must review & approve each championship before it becomes visible"}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Cd>
|
||||
|
||||
{/* Championships */}
|
||||
{orgChamps.length > 0 && <Cd>
|
||||
<ST right={<span style={{ fontFamily: f.m, fontSize: 10, color: c.dim }}>{orgChamps.length}</span>}>Championships</ST>
|
||||
{orgChamps.map(ch => {
|
||||
const cs = statusConfig[ch.status] || statusConfig.draft;
|
||||
return <div key={ch.id} style={{ display: "flex", justifyContent: "space-between", alignItems: "center", padding: "8px 0", borderBottom: `1px solid ${c.brd}` }}>
|
||||
<div><p style={{ fontFamily: f.b, fontSize: 12, color: c.text, margin: 0 }}>{ch.name}</p><p style={{ fontFamily: f.m, fontSize: 10, color: c.dim, margin: "2px 0 0" }}>{ch.dates} · {ch.location}</p></div>
|
||||
<Bg label={cs.l} color={cs.c} bg={cs.b} />
|
||||
</div>;
|
||||
})}
|
||||
</Cd>}
|
||||
|
||||
{/* Block reason */}
|
||||
{o.blockReason && <Cd style={{ background: `${c.red}06`, border: `1px solid ${c.red}20` }}>
|
||||
<p style={{ fontFamily: f.m, fontSize: 9, color: c.red, margin: "0 0 4px", textTransform: "uppercase", letterSpacing: 0.5 }}>Block Reason</p>
|
||||
<p style={{ fontFamily: f.b, fontSize: 12, color: c.text, margin: 0 }}>{o.blockReason}</p>
|
||||
</Cd>}
|
||||
|
||||
{/* Actions */}
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
{o.status === "pending" && <div style={{ display: "flex", gap: 8 }}>
|
||||
<ActionBtn label="Approve" color={c.green} onClick={() => setO(p => ({ ...p, status: "active", verified: true }))} icon="✅" filled />
|
||||
<ActionBtn label="Reject" color={c.red} onClick={() => setO(p => ({ ...p, status: "blocked", blockReason: "Rejected by admin" }))} icon="❌" filled />
|
||||
</div>}
|
||||
{o.status === "active" && <ActionBtn label="Block Organization" color={c.red} onClick={() => setO(p => ({ ...p, status: "blocked", blockReason: "Blocked by admin" }))} icon="🚫" />}
|
||||
{o.status === "blocked" && <ActionBtn label="Unblock Organization" color={c.green} onClick={() => setO(p => ({ ...p, status: "active", blockReason: null }))} icon="✅" />}
|
||||
{!o.verified && o.status !== "blocked" && <ActionBtn label="Verify Organization" color={c.accent} onClick={() => setO(p => ({ ...p, verified: true }))} icon="🛡️" />}
|
||||
<ActionBtn label="Delete Organization" color={c.red} onClick={() => {}} icon="🗑️" />
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
/* ── Championships ── */
|
||||
function ChampsList({ champs, onChampTap }) {
|
||||
const [search, setSearch] = useState("");
|
||||
const [filter, setFilter] = useState("all");
|
||||
|
||||
const filters = [
|
||||
{ id: "all", l: "All", n: champs.length },
|
||||
{ id: "live", l: "🟢 Live", n: champs.filter(c2 => c2.status === "live").length },
|
||||
{ id: "pending_approval", l: "⏳ Awaiting", n: champs.filter(c2 => c2.status === "pending_approval").length },
|
||||
{ id: "draft", l: "📝 Draft", n: champs.filter(c2 => c2.status === "draft").length },
|
||||
{ id: "blocked", l: "🚫 Blocked", n: champs.filter(c2 => c2.status === "blocked").length },
|
||||
];
|
||||
|
||||
const filtered = champs.filter(ch => {
|
||||
const q = !search || ch.name.toLowerCase().includes(search.toLowerCase()) || ch.orgName.toLowerCase().includes(search.toLowerCase());
|
||||
if (!q) return false;
|
||||
if (filter !== "all") return ch.status === filter;
|
||||
return true;
|
||||
});
|
||||
|
||||
return <div>
|
||||
<Hdr title="Championships" subtitle={`${champs.length} total`} />
|
||||
<div style={{ padding: "6px 16px 16px", display: "flex", flexDirection: "column", gap: 10 }}>
|
||||
<SearchBar value={search} onChange={setSearch} placeholder="Search championship or org..." />
|
||||
<FilterChips filters={filters} active={filter} onChange={setFilter} />
|
||||
{filtered.map(ch => {
|
||||
const st = statusConfig[ch.status] || statusConfig.draft;
|
||||
return <div key={ch.id} onClick={() => onChampTap(ch)} style={{ background: c.card, border: `1px solid ${c.brd}`, borderRadius: 14, padding: 14, cursor: "pointer" }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", marginBottom: 4 }}>
|
||||
<div><p style={{ fontFamily: f.b, fontSize: 14, fontWeight: 600, color: c.text, margin: 0 }}>{ch.name}</p><p style={{ fontFamily: f.m, fontSize: 10, color: c.accent, margin: "2px 0 0" }}>by {ch.orgName} {ch.orgVerified ? "🛡️" : ""}</p></div>
|
||||
<Bg label={st.l} color={st.c} bg={st.b} />
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: 12, marginTop: 6 }}>
|
||||
<span style={{ fontFamily: f.b, fontSize: 10, color: c.mid }}>📅 {ch.dates}</span>
|
||||
<span style={{ fontFamily: f.b, fontSize: 10, color: c.mid }}>📍 {ch.location}</span>
|
||||
<span style={{ fontFamily: f.b, fontSize: 10, color: c.mid }}>👥 {ch.members}</span>
|
||||
</div>
|
||||
</div>;
|
||||
})}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
/* ── Championship Detail ── */
|
||||
function ChampDetail({ ch: initial, onBack }) {
|
||||
const [ch, setCh] = useState(initial);
|
||||
const st = statusConfig[ch.status] || statusConfig.draft;
|
||||
|
||||
return <div style={{ flex: 1, overflow: "auto" }}>
|
||||
<Hdr title={ch.name} subtitle={`by ${ch.orgName}`} onBack={onBack} />
|
||||
<div style={{ padding: "6px 16px 20px", display: "flex", flexDirection: "column", gap: 12 }}>
|
||||
<Cd style={{ display: "flex", alignItems: "center", gap: 14, padding: "14px 16px" }}>
|
||||
<div style={{ width: 50, height: 50, borderRadius: 14, background: `${c.accent}15`, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 22 }}>🏆</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<p style={{ fontFamily: f.b, fontSize: 16, fontWeight: 600, color: c.text, margin: 0 }}>{ch.name}</p>
|
||||
<p style={{ fontFamily: f.m, fontSize: 11, color: c.accent, margin: "2px 0 0" }}>{ch.orgName}</p>
|
||||
<p style={{ fontFamily: f.b, fontSize: 11, color: c.dim, margin: "2px 0 0" }}>📅 {ch.dates} · 📍 {ch.location}</p>
|
||||
</div>
|
||||
<Bg label={st.l} color={st.c} bg={st.b} />
|
||||
</Cd>
|
||||
|
||||
<Cd>
|
||||
<ST>Stats</ST>
|
||||
{[{ l: "Members", v: ch.members }, { l: "Passed", v: ch.passed }, { l: "Pending", v: ch.pending }, { l: "Revenue", v: ch.revenue }].map(r =>
|
||||
<div key={r.l} style={{ display: "flex", justifyContent: "space-between", padding: "7px 0", borderBottom: `1px solid ${c.brd}` }}>
|
||||
<span style={{ fontFamily: f.m, fontSize: 10, color: c.dim, textTransform: "uppercase" }}>{r.l}</span>
|
||||
<span style={{ fontFamily: f.b, fontSize: 12, color: c.text }}>{r.v}</span>
|
||||
</div>
|
||||
)}
|
||||
</Cd>
|
||||
|
||||
{/* Approval info */}
|
||||
{ch.orgVerified !== undefined && <Cd>
|
||||
<ST>Approval Policy</ST>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 10, padding: "8px 0" }}>
|
||||
<span style={{ fontSize: 16 }}>{ch.orgVerified ? "🛡️" : "⏳"}</span>
|
||||
<div style={{ flex: 1 }}>
|
||||
<p style={{ fontFamily: f.b, fontSize: 12, color: c.text, margin: 0 }}>{ch.orgVerified ? "Verified org — auto-approved" : "Unverified org — manual review required"}</p>
|
||||
<p style={{ fontFamily: f.b, fontSize: 10, color: c.dim, margin: "2px 0 0" }}>{ch.orgVerified ? "This org can go live without admin approval" : "Admin must approve before members can see this event"}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Cd>}
|
||||
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
{ch.status === "pending_approval" && <div style={{ display: "flex", gap: 8 }}>
|
||||
<ActionBtn label="Approve" color={c.green} onClick={() => setCh(p => ({ ...p, status: "live" }))} icon="✅" filled />
|
||||
<ActionBtn label="Reject" color={c.red} onClick={() => setCh(p => ({ ...p, status: "blocked" }))} icon="❌" filled />
|
||||
</div>}
|
||||
{ch.status === "live" && <ActionBtn label="Suspend Event" color={c.red} onClick={() => setCh(p => ({ ...p, status: "blocked" }))} icon="⏸️" />}
|
||||
{ch.status === "blocked" && <ActionBtn label="Reinstate Event" color={c.green} onClick={() => setCh(p => ({ ...p, status: "live" }))} icon="✅" />}
|
||||
<ActionBtn label="Delete Championship" color={c.red} onClick={() => {}} icon="🗑️" />
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
/* ── Users ── */
|
||||
function UsersList({ users, onUserTap }) {
|
||||
const [search, setSearch] = useState("");
|
||||
const [filter, setFilter] = useState("all");
|
||||
|
||||
const filters = [
|
||||
{ id: "all", l: "All", n: users.length },
|
||||
{ id: "active", l: "✅ Active", n: users.filter(u => u.status === "active").length },
|
||||
{ id: "warned", l: "⚠️ Warned", n: users.filter(u => u.status === "warned").length },
|
||||
{ id: "blocked", l: "🚫 Blocked", n: users.filter(u => u.status === "blocked").length },
|
||||
{ id: "org_admin", l: "🏢 Org Admins", n: users.filter(u => u.role === "org_admin").length },
|
||||
];
|
||||
|
||||
const filtered = users.filter(u => {
|
||||
const q = !search || u.name.toLowerCase().includes(search.toLowerCase()) || u.instagram.toLowerCase().includes(search.toLowerCase()) || u.email.toLowerCase().includes(search.toLowerCase());
|
||||
if (!q) return false;
|
||||
if (filter === "active") return u.status === "active";
|
||||
if (filter === "warned") return u.status === "warned";
|
||||
if (filter === "blocked") return u.status === "blocked";
|
||||
if (filter === "org_admin") return u.role === "org_admin";
|
||||
return true;
|
||||
});
|
||||
|
||||
return <div>
|
||||
<Hdr title="Users" subtitle={`${users.length} total`} />
|
||||
<div style={{ padding: "6px 16px 16px", display: "flex", flexDirection: "column", gap: 10 }}>
|
||||
<SearchBar value={search} onChange={setSearch} placeholder="Search name, @handle, or email..." />
|
||||
<FilterChips filters={filters} active={filter} onChange={setFilter} />
|
||||
{filtered.map(u => {
|
||||
const st = statusConfig[u.status] || statusConfig.active;
|
||||
return <div key={u.id} onClick={() => onUserTap(u)} style={{ background: c.card, border: `1px solid ${c.brd}`, borderRadius: 14, padding: 12, cursor: "pointer" }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
||||
<div style={{ width: 38, height: 38, borderRadius: 10, background: u.role === "org_admin" ? `${c.purple}15` : `${c.blue}15`, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 16 }}>{u.role === "org_admin" ? "🏢" : "👤"}</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<p style={{ fontFamily: f.b, fontSize: 13, fontWeight: 600, color: c.text, margin: 0 }}>{u.name}</p>
|
||||
<Bg label={st.l} color={st.c} bg={st.b} />
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: 8, marginTop: 2 }}>
|
||||
<span style={{ fontFamily: f.m, fontSize: 10, color: c.accent }}>{u.instagram}</span>
|
||||
<span style={{ fontFamily: f.b, fontSize: 10, color: c.dim }}>{u.city}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
})}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
/* ── User Detail ── */
|
||||
function UserDetail({ user: initial, onBack }) {
|
||||
const [u, setU] = useState(initial);
|
||||
const st = statusConfig[u.status] || statusConfig.active;
|
||||
|
||||
return <div style={{ flex: 1, overflow: "auto" }}>
|
||||
<Hdr title={u.name} subtitle={u.instagram} onBack={onBack} />
|
||||
<div style={{ padding: "6px 16px 20px", display: "flex", flexDirection: "column", gap: 12 }}>
|
||||
<Cd style={{ display: "flex", alignItems: "center", gap: 14, padding: "14px 16px" }}>
|
||||
<div style={{ width: 50, height: 50, borderRadius: 14, background: `${c.blue}15`, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 22 }}>👤</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<p style={{ fontFamily: f.b, fontSize: 16, fontWeight: 600, color: c.text, margin: 0 }}>{u.name}</p>
|
||||
<p style={{ fontFamily: f.m, fontSize: 11, color: c.accent, margin: "2px 0 0" }}>{u.instagram}</p>
|
||||
<p style={{ fontFamily: f.b, fontSize: 11, color: c.dim, margin: "2px 0 0" }}>📧 {u.email}</p>
|
||||
</div>
|
||||
<Bg label={st.l} color={st.c} bg={st.b} />
|
||||
</Cd>
|
||||
|
||||
<Cd>
|
||||
<ST>Info</ST>
|
||||
{[{ l: "City", v: u.city }, { l: "Joined", v: u.joined }, { l: "Championships", v: u.champsJoined },
|
||||
{ l: "Role", v: u.role === "org_admin" ? `Org Admin (${u.org})` : "Member" },
|
||||
].map(r => <div key={r.l} style={{ display: "flex", justifyContent: "space-between", padding: "7px 0", borderBottom: `1px solid ${c.brd}` }}>
|
||||
<span style={{ fontFamily: f.m, fontSize: 10, color: c.dim, textTransform: "uppercase" }}>{r.l}</span>
|
||||
<span style={{ fontFamily: f.b, fontSize: 12, color: c.text }}>{r.v}</span>
|
||||
</div>)}
|
||||
</Cd>
|
||||
|
||||
{(u.blockReason || u.warnReason) && <Cd style={{ background: `${u.status === "blocked" ? c.red : c.orange}06`, border: `1px solid ${u.status === "blocked" ? c.red : c.orange}20` }}>
|
||||
<p style={{ fontFamily: f.m, fontSize: 9, color: u.status === "blocked" ? c.red : c.orange, margin: "0 0 4px", textTransform: "uppercase", letterSpacing: 0.5 }}>{u.status === "blocked" ? "Block" : "Warning"} Reason</p>
|
||||
<p style={{ fontFamily: f.b, fontSize: 12, color: c.text, margin: 0 }}>{u.blockReason || u.warnReason}</p>
|
||||
</Cd>}
|
||||
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
{u.status === "active" && <>
|
||||
<ActionBtn label="Warn User" color={c.orange} onClick={() => setU(p => ({ ...p, status: "warned", warnReason: "Warning issued by admin" }))} icon="⚠️" />
|
||||
<ActionBtn label="Block User" color={c.red} onClick={() => setU(p => ({ ...p, status: "blocked", blockReason: "Blocked by admin" }))} icon="🚫" />
|
||||
</>}
|
||||
{u.status === "warned" && <>
|
||||
<ActionBtn label="Remove Warning" color={c.green} onClick={() => setU(p => ({ ...p, status: "active", warnReason: null }))} icon="✅" />
|
||||
<ActionBtn label="Block User" color={c.red} onClick={() => setU(p => ({ ...p, status: "blocked", blockReason: "Blocked after warning" }))} icon="🚫" />
|
||||
</>}
|
||||
{u.status === "blocked" && <ActionBtn label="Unblock User" color={c.green} onClick={() => setU(p => ({ ...p, status: "active", blockReason: null }))} icon="✅" />}
|
||||
<ActionBtn label="Delete User" color={c.red} onClick={() => {}} icon="🗑️" />
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
/* ── App Shell ── */
|
||||
export default function AdminApp() {
|
||||
const [scr, setScr] = useState("dash");
|
||||
const [sel, setSel] = useState(null);
|
||||
|
||||
const go = (screen, data) => { setScr(screen); setSel(data || null); };
|
||||
|
||||
const render = () => {
|
||||
if (scr === "orgDetail" && sel) return <OrgDetail org={sel} onBack={() => go("orgs")} champs={CHAMPS_DATA} />;
|
||||
if (scr === "champDetail" && sel) return <ChampDetail ch={sel} onBack={() => go("champs")} />;
|
||||
if (scr === "userDetail" && sel) return <UserDetail user={sel} onBack={() => go("users")} />;
|
||||
if (scr === "orgs") return <OrgsList orgs={ORGS_DATA} onOrgTap={o => go("orgDetail", o)} />;
|
||||
if (scr === "champs") return <ChampsList champs={CHAMPS_DATA} onChampTap={ch => go("champDetail", ch)} />;
|
||||
if (scr === "users") return <UsersList users={USERS_DATA} onUserTap={u => go("userDetail", u)} />;
|
||||
return <Dashboard orgs={ORGS_DATA} champs={CHAMPS_DATA} users={USERS_DATA} onNav={setScr} />;
|
||||
};
|
||||
|
||||
const showNav = ["dash", "orgs", "champs", "users"].includes(scr);
|
||||
|
||||
return <div style={{ display: "flex", justifyContent: "center", alignItems: "center", minHeight: "100vh", background: "#020106", 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(99,102,241,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={setScr} />}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
643
dancechamp-claude-code/prototypes/member-app.jsx
Normal file
643
dancechamp-claude-code/prototypes/member-app.jsx
Normal file
@@ -0,0 +1,643 @@
|
||||
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>;
|
||||
}
|
||||
683
dancechamp-claude-code/prototypes/org-app.jsx
Normal file
683
dancechamp-claude-code/prototypes/org-app.jsx
Normal file
@@ -0,0 +1,683 @@
|
||||
import { useState } from "react";
|
||||
|
||||
/* ── Data ── */
|
||||
const ORG = { name: "Zero Gravity Team", instagram: "@zerogravity_pole", logo: "💃" };
|
||||
|
||||
const makeCh = (overrides) => ({
|
||||
id: "", name: "", subtitle: "", eventDate: "", regStart: "", regEnd: "", location: "", venue: "", accent: "#D4145A", image: "💃", status: "draft",
|
||||
disciplines: [], styles: [], fees: null, judging: [], penalties: [], judges: [], rules: [], costumeRules: [], members: [],
|
||||
formUrl: "", rulesUrl: "",
|
||||
configured: { info: false, categories: false, fees: false, rules: false, judging: false },
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const INITIAL_CHAMPS = [
|
||||
makeCh({
|
||||
id: "ch1", name: "Zero Gravity", subtitle: "International Pole Exotic Championship",
|
||||
eventDate: "May 30, 2026", regStart: "Feb 1, 2026", regEnd: "Apr 22, 2026", location: "Minsk, Belarus", venue: "Prime Hall", status: "registration_open", accent: "#D4145A", image: "💃",
|
||||
disciplines: [
|
||||
{ name: "Exotic Pole Dance", levels: ["Beginners", "Amateur", "Semi-Pro", "Profi", "Elite", "Duets & Groups"] },
|
||||
{ name: "Pole Art", levels: ["Amateur", "Semi-Pro", "Profi"] },
|
||||
],
|
||||
styles: ["Classic", "Flow", "Theater"],
|
||||
fees: { videoSelection: "50 BYN / 1,500 RUB", solo: "280 BYN / 7,500 RUB", duet: "210 BYN / 5,800 RUB pp", group: "190 BYN / 4,500 RUB pp" },
|
||||
judging: [{ name: "Image", max: 10 }, { name: "Artistry", max: 10 }, { name: "Choreography", max: 10 }, { name: "Musicality", max: 10 }, { name: "Technique", max: 10 }, { name: "Overall", max: 10 }],
|
||||
penalties: [{ name: "Missed element", val: "-2" }, { name: "Fall", val: "-2" }, { name: "Leaving stage", val: "DQ" }, { name: "Exposure", val: "DQ" }],
|
||||
judges: [
|
||||
{ id: "j1", name: "Anastasia Skukhtorova", instagram: "@skukhtorova", bio: "World Pole Art Champion. International judge with 10+ years experience." },
|
||||
{ id: "j2", name: "Marion Crampe", instagram: "@marioncrampe", bio: "Pole Art legend, multiple international championship winner and judge." },
|
||||
{ id: "j3", name: "Dmitry Politov", instagram: "@dmitry_politov", bio: "World Pole Sports Champion. Certified IPSF judge." },
|
||||
],
|
||||
rules: ["Must be 18+", "Valid life & health insurance", "No lotions/bronzers 24h before", "Grip aids allowed (no wax/rosin)", "Judges' decision is final"],
|
||||
costumeRules: ["Neat and well-fitted", "No advertising", "No spikes/sharp objects", "Specialized shoes for Exotic"],
|
||||
configured: { info: true, categories: true, fees: true, rules: true, judging: true },
|
||||
members: [
|
||||
{ id: "m1", name: "Alex Petrova", instagram: "@alex_pole", level: "Semi-Pro", style: "Classic", discipline: "Exotic Pole Dance", type: "solo", stepsCompleted: 3, videoUrl: "https://youtube.com/...", feePaid: false, receiptUploaded: true, insuranceUploaded: false, passed: null, city: "Moscow" },
|
||||
{ id: "m2", name: "Maria Ivanova", instagram: "@maria_exotic", level: "Amateur", style: "Flow", discipline: "Exotic Pole Dance", type: "solo", stepsCompleted: 5, videoUrl: "https://youtube.com/...", feePaid: true, receiptUploaded: true, insuranceUploaded: true, passed: true, city: "Minsk" },
|
||||
{ id: "m3", name: "Elena Kozlova", instagram: "@elena.pole", level: "Profi", style: "Theater", discipline: "Exotic Pole Dance", type: "solo", stepsCompleted: 5, videoUrl: "https://youtube.com/...", feePaid: true, receiptUploaded: true, insuranceUploaded: false, passed: true, city: "St. Petersburg" },
|
||||
{ id: "m4", name: "Daria Sokolova", instagram: "@daria_art", level: "Amateur", style: "Classic", discipline: "Pole Art", type: "solo", stepsCompleted: 4, videoUrl: "https://youtube.com/...", feePaid: false, receiptUploaded: true, insuranceUploaded: false, passed: null, city: "Kyiv" },
|
||||
{ id: "m5", name: "Anna Belova", instagram: "@anna.b_pole", level: "Beginners", style: "Flow", discipline: "Exotic Pole Dance", type: "solo", stepsCompleted: 2, videoUrl: null, feePaid: false, receiptUploaded: false, insuranceUploaded: false, passed: null, city: "Minsk" },
|
||||
{ id: "m6", name: "Olga Morozova", instagram: "@olga_exotic", level: "Elite", style: "Classic", discipline: "Exotic Pole Dance", type: "solo", stepsCompleted: 5, videoUrl: "https://youtube.com/...", feePaid: true, receiptUploaded: true, insuranceUploaded: true, passed: false, city: "Moscow" },
|
||||
{ id: "m7", name: "Katya & Nina", instagram: "@katya_nina", level: "Semi-Pro", style: "Theater", discipline: "Exotic Pole Dance", type: "duet", stepsCompleted: 5, videoUrl: "https://youtube.com/...", feePaid: true, receiptUploaded: true, insuranceUploaded: false, passed: null, city: "Kazan" },
|
||||
],
|
||||
}),
|
||||
makeCh({
|
||||
id: "ch2", name: "Pole Star", subtitle: "National Pole Championship",
|
||||
eventDate: "Jul 12–13, 2026", regStart: "", regEnd: "", location: "Moscow, Russia", venue: "Crystal Hall", status: "draft", accent: "#7C3AED", image: "⭐",
|
||||
configured: { info: true, categories: false, fees: false, rules: false, judging: false },
|
||||
members: [],
|
||||
}),
|
||||
];
|
||||
|
||||
/* ── 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", purpleS: "rgba(139,92,246,0.10)", blue: "#60A5FA", blueS: "rgba(96,165,250,0.10)", red: "#EF4444", redS: "rgba(239,68,68,0.10)" };
|
||||
const f = { d: "'Playfair Display',Georgia,serif", b: "'DM Sans','Segoe UI',sans-serif", m: "'JetBrains Mono',monospace" };
|
||||
|
||||
/* ── Shared UI ── */
|
||||
const Cd = ({ children, style: s }) => <div style={{ background: c.card, border: `1px solid ${c.brd}`, borderRadius: 14, padding: 16, ...s }}>{children}</div>;
|
||||
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}</div>;
|
||||
const Bg = ({ label, color, bg }) => <span style={{ fontFamily: f.m, fontSize: 8, fontWeight: 700, letterSpacing: 0.8, color, background: bg, padding: "3px 8px", borderRadius: 4 }}>{label}</span>;
|
||||
|
||||
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, minWidth: 0 }}><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", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{subtitle}</p>}</div>
|
||||
{right}
|
||||
</div>;
|
||||
}
|
||||
|
||||
function Tabs({ tabs, active, onChange, accent: ac }) {
|
||||
return <div style={{ display: "flex", gap: 3, overflowX: "auto", paddingBottom: 2, marginBottom: 14, scrollbarWidth: "none" }}>
|
||||
{tabs.map(t => <div key={t.id} onClick={() => onChange(t.id)} style={{ fontFamily: f.m, fontSize: 9, fontWeight: 600, letterSpacing: 0.3, display: "flex", alignItems: "center", gap: 4, color: active === t.id ? ac || c.accent : c.dim, background: active === t.id ? `${ac || c.accent}15` : "transparent", border: `1px solid ${active === t.id ? `${ac || c.accent}30` : "transparent"}`, padding: "5px 10px", borderRadius: 16, cursor: "pointer", whiteSpace: "nowrap" }}>
|
||||
{t.done !== undefined && <span style={{ width: 6, height: 6, borderRadius: 3, background: t.done ? c.green : c.yellow, flexShrink: 0 }} />}
|
||||
{t.label}
|
||||
</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: "dash", i: "📊", l: "Dashboard" }, { id: "orgSettings", i: "⚙️", l: "Settings" }].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 Input({ label, value, onChange, placeholder }) {
|
||||
return <div style={{ marginBottom: 12 }}>
|
||||
<p style={{ fontFamily: f.m, fontSize: 9, color: c.dim, margin: "0 0 6px", letterSpacing: 0.5, textTransform: "uppercase" }}>{label}</p>
|
||||
<input type="text" value={value || ""} onChange={e => onChange(e.target.value)} placeholder={placeholder} style={{ width: "100%", padding: "10px 12px", borderRadius: 10, background: c.bg, border: `1px solid ${c.brd}`, color: c.text, fontFamily: f.b, fontSize: 13, outline: "none", boxSizing: "border-box" }} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
function TagEditor({ items, onAdd, onRemove, color, placeholder, addLabel }) {
|
||||
const [val, setVal] = useState("");
|
||||
const submit = () => { if (val.trim()) { onAdd(val.trim()); setVal(""); } };
|
||||
return <div>
|
||||
<div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginBottom: 8 }}>
|
||||
{items.map((item, i) => <div key={item} style={{ display: "flex", alignItems: "center", gap: 4, padding: "4px 10px", borderRadius: 16, background: `${color}10`, border: `1px solid ${color}25` }}>
|
||||
<span style={{ fontFamily: f.b, fontSize: 11, color: c.text }}>{item}</span>
|
||||
<span onClick={() => onRemove(i)} style={{ fontSize: 10, color: c.dim, cursor: "pointer", lineHeight: 1 }}>×</span>
|
||||
</div>)}
|
||||
{items.length === 0 && <span style={{ fontFamily: f.b, fontSize: 11, color: c.dim, fontStyle: "italic" }}>None added yet</span>}
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: 6 }}>
|
||||
<input value={val} onChange={e => setVal(e.target.value)} placeholder={placeholder} onKeyDown={e => e.key === "Enter" && submit()} style={{ flex: 1, padding: "8px 12px", borderRadius: 8, background: c.bg, border: `1px solid ${c.brd}`, color: c.text, fontFamily: f.b, fontSize: 12, outline: "none" }} />
|
||||
<div onClick={submit} style={{ padding: "8px 14px", borderRadius: 8, background: color, color: "#fff", fontFamily: f.b, fontSize: 12, fontWeight: 700, cursor: "pointer" }}>+</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
/* ── Dashboard ── */
|
||||
function Dashboard({ champs, org, onChampTap, onCreateChamp }) {
|
||||
return <div>
|
||||
<Hdr title="Dashboard" subtitle={org.name} right={
|
||||
<div style={{ width: 36, height: 36, borderRadius: 10, background: `linear-gradient(135deg,${c.accent}25,${c.accent}10)`, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 18 }}>{org.logo}</div>
|
||||
} />
|
||||
<div style={{ padding: "6px 16px 16px", display: "flex", flexDirection: "column", gap: 10 }}>
|
||||
<div onClick={onCreateChamp} style={{ display: "flex", alignItems: "center", gap: 12, padding: "14px 16px", borderRadius: 14, background: `linear-gradient(135deg,${c.accent}15,${c.accent}08)`, border: `1px solid ${c.accent}30`, cursor: "pointer" }}>
|
||||
<div style={{ width: 42, height: 42, borderRadius: 12, background: c.accent, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 22, color: "#fff", fontWeight: 700, flexShrink: 0 }}>+</div>
|
||||
<div style={{ flex: 1 }}><p style={{ fontFamily: f.b, fontSize: 14, fontWeight: 700, color: c.text, margin: 0 }}>New Championship</p><p style={{ fontFamily: f.b, fontSize: 11, color: c.dim, margin: "2px 0 0" }}>Quick create — configure later</p></div>
|
||||
<span style={{ color: c.accent, fontSize: 16 }}>›</span>
|
||||
</div>
|
||||
|
||||
<ST right={<span style={{ fontFamily: f.m, fontSize: 10, color: c.dim }}>{champs.length} events</span>}>Your Championships</ST>
|
||||
|
||||
{champs.map(ch => {
|
||||
const cfg = ch.configured;
|
||||
const done = Object.values(cfg).filter(Boolean).length;
|
||||
const total = Object.keys(cfg).length;
|
||||
const ready = done === total;
|
||||
const stMap = { registration_open: { l: "LIVE", c: c.green, b: c.greenS }, draft: { l: `SETUP ${done}/${total}`, c: c.yellow, b: c.yellowS } };
|
||||
const st = stMap[ch.status] || stMap.draft;
|
||||
return <div key={ch.id} onClick={() => onChampTap(ch)} style={{ background: c.card, border: `1px solid ${c.brd}`, borderRadius: 14, overflow: "hidden", cursor: "pointer" }}>
|
||||
<div style={{ height: 3, background: `linear-gradient(90deg,${ch.accent},${ch.accent}88)` }} />
|
||||
<div style={{ padding: "12px 14px" }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 6 }}>
|
||||
<div style={{ width: 38, height: 38, borderRadius: 10, background: `linear-gradient(135deg,${ch.accent}20,${ch.accent}40)`, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 17, flexShrink: 0 }}>{ch.image}</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<h3 style={{ fontFamily: f.d, fontSize: 15, fontWeight: 700, color: c.text, margin: 0 }}>{ch.name}</h3>
|
||||
<Bg label={st.l} color={st.c} bg={st.b} />
|
||||
</div>
|
||||
<p style={{ fontFamily: f.b, fontSize: 11, color: c.dim, margin: "2px 0 0" }}>{ch.eventDate} · {ch.location}</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* Readiness bar */}
|
||||
{!ready && <div style={{ marginTop: 6 }}>
|
||||
<div style={{ height: 4, background: c.brd, borderRadius: 2, overflow: "hidden" }}>
|
||||
<div style={{ height: "100%", width: `${(done / total) * 100}%`, background: ch.accent, borderRadius: 2 }} />
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: 6, marginTop: 6, flexWrap: "wrap" }}>
|
||||
{Object.entries(cfg).map(([k, v]) => <span key={k} style={{ fontFamily: f.m, fontSize: 8, color: v ? c.green : c.yellow, letterSpacing: 0.3 }}>{v ? "✓" : "○"} {k}</span>)}
|
||||
</div>
|
||||
</div>}
|
||||
{/* Stats for live champs */}
|
||||
{ch.status === "registration_open" && <div style={{ display: "flex", gap: 10, paddingTop: 8, marginTop: 6, borderTop: `1px solid ${c.brd}` }}>
|
||||
{[{ n: ch.members.length, l: "Members", co: c.mid }, { n: ch.members.filter(m => m.passed === true).length, l: "Passed", co: c.green }, { n: ch.members.filter(m => m.videoUrl && m.passed === null).length, l: "Pending", co: c.yellow }].map(s =>
|
||||
<div key={s.l} style={{ flex: 1, textAlign: "center" }}><p style={{ fontFamily: f.d, fontSize: 16, fontWeight: 700, color: s.co, margin: 0 }}>{s.n}</p><p style={{ fontFamily: f.m, fontSize: 7, color: c.dim, margin: "2px 0 0", textTransform: "uppercase" }}>{s.l}</p></div>
|
||||
)}
|
||||
</div>}
|
||||
</div>
|
||||
</div>;
|
||||
})}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
/* ── Championship Detail (configurable tabs) ── */
|
||||
function ChampDetail({ ch: initial, onBack, onMemberTap, onUpdate }) {
|
||||
const [ch, setCh] = useState(initial);
|
||||
const [tab, setTab] = useState("Overview");
|
||||
const [members, setMembers] = useState(ch.members);
|
||||
const [memFilter, setMemFilter] = useState("all");
|
||||
const [memSearch, setMemSearch] = useState("");
|
||||
const [editing, setEditing] = useState(null);
|
||||
const [newJudge, setNewJudge] = useState({ name: "", instagram: "", bio: "" });
|
||||
|
||||
const upd = (key, val) => setCh(p => ({ ...p, [key]: val }));
|
||||
const markDone = (section) => setCh(p => ({ ...p, configured: { ...p.configured, [section]: true } }));
|
||||
const allDone = Object.values(ch.configured).every(Boolean);
|
||||
|
||||
const stats = {
|
||||
total: members.length, videoSent: members.filter(m => m.videoUrl).length,
|
||||
passed: members.filter(m => m.passed === true).length, failed: members.filter(m => m.passed === false).length,
|
||||
pending: members.filter(m => m.videoUrl && m.passed === null).length, feePaid: members.filter(m => m.feePaid).length,
|
||||
receipts: members.filter(m => m.receiptUploaded && !m.feePaid).length,
|
||||
};
|
||||
|
||||
const decide = (id, pass) => setMembers(p => p.map(m => m.id === id ? { ...m, passed: pass } : m));
|
||||
|
||||
const tabDefs = [
|
||||
{ id: "Overview", label: "Overview" },
|
||||
{ id: "Categories", label: "Categories", done: ch.configured.categories },
|
||||
{ id: "Fees", label: "Fees", done: ch.configured.fees },
|
||||
{ id: "Rules", label: "Rules", done: ch.configured.rules },
|
||||
{ id: "Judges", label: "Judges", done: ch.configured.judging },
|
||||
...(ch.status === "registration_open" ? [{ id: "Members", label: `Members (${members.length})` }, { id: "Results", label: "Results" }] : []),
|
||||
];
|
||||
|
||||
const memFilters = [
|
||||
{ id: "all", l: "All", n: members.length }, { id: "receipts", l: "📸 Receipts", n: stats.receipts },
|
||||
{ id: "videos", l: "🎬 Videos", n: stats.pending }, { id: "passed", l: "✅ Passed", n: stats.passed },
|
||||
];
|
||||
const filteredMem = members.filter(m => {
|
||||
const q = !memSearch || m.name.toLowerCase().includes(memSearch.toLowerCase()) || m.instagram.toLowerCase().includes(memSearch.toLowerCase());
|
||||
if (!q) return false;
|
||||
if (memFilter === "receipts") return m.receiptUploaded && !m.feePaid;
|
||||
if (memFilter === "videos") return m.videoUrl && m.passed === null;
|
||||
if (memFilter === "passed") return m.passed === true;
|
||||
return true;
|
||||
});
|
||||
|
||||
return <div style={{ flex: 1, overflow: "auto" }}>
|
||||
<Hdr title={ch.name} subtitle={ch.subtitle || ch.location} onBack={onBack} right={
|
||||
ch.status === "draft" && allDone ? <div onClick={() => setCh(p => ({ ...p, status: "registration_open" }))} style={{ fontFamily: f.b, fontSize: 10, fontWeight: 700, color: "#fff", background: c.green, padding: "6px 12px", borderRadius: 8, cursor: "pointer" }}>🚀 Go Live</div> : null
|
||||
} />
|
||||
<div style={{ padding: "4px 16px 20px" }}>
|
||||
<Tabs tabs={tabDefs} active={tab} onChange={setTab} accent={ch.accent} />
|
||||
|
||||
{/* ═══ OVERVIEW ═══ */}
|
||||
{tab === "Overview" && <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
||||
{/* Setup progress */}
|
||||
{ch.status === "draft" && <Cd style={{ background: `${c.yellow}06`, border: `1px solid ${c.yellow}20` }}>
|
||||
<ST right={<span style={{ fontFamily: f.m, fontSize: 10, color: c.yellow }}>{Object.values(ch.configured).filter(Boolean).length}/{Object.keys(ch.configured).length}</span>}>⚙️ Setup Progress</ST>
|
||||
{Object.entries(ch.configured).map(([section, done]) => {
|
||||
const tabMap = { info: "Overview", categories: "Categories", fees: "Fees", rules: "Rules", judging: "Judges" };
|
||||
return <div key={section} onClick={() => !done && setTab(tabMap[section] || section)} style={{ display: "flex", alignItems: "center", gap: 10, padding: "8px 0", borderBottom: `1px solid ${c.brd}`, cursor: done ? "default" : "pointer" }}>
|
||||
<div style={{ width: 22, height: 22, borderRadius: 6, border: `2px solid ${done ? c.green : c.yellow}`, background: done ? c.greenS : "transparent", display: "flex", alignItems: "center", justifyContent: "center", fontFamily: f.m, fontSize: 10, fontWeight: 700, color: done ? c.green : c.yellow }}>{done ? "✓" : ""}</div>
|
||||
<span style={{ fontFamily: f.b, fontSize: 12, color: done ? c.dim : c.text, textTransform: "capitalize", textDecoration: done ? "line-through" : "none", flex: 1 }}>{section === "judging" ? "judges" : section}</span>
|
||||
{!done && <span style={{ fontFamily: f.b, fontSize: 10, color: ch.accent }}>Configure ›</span>}
|
||||
</div>;
|
||||
})}
|
||||
{allDone && <div onClick={() => setCh(p => ({ ...p, status: "registration_open" }))} style={{ marginTop: 10, padding: "12px", borderRadius: 10, background: c.green, textAlign: "center", cursor: "pointer" }}>
|
||||
<span style={{ fontFamily: f.b, fontSize: 13, fontWeight: 700, color: "#fff" }}>🚀 Open Registration</span>
|
||||
</div>}
|
||||
</Cd>}
|
||||
|
||||
{/* Info (always editable) */}
|
||||
<Cd>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 10 }}>
|
||||
<h3 style={{ fontFamily: f.d, fontSize: 14, fontWeight: 700, color: c.mid, margin: 0 }}>Event Info</h3>
|
||||
<div onClick={() => setEditing(editing === "info" ? null : "info")} style={{ fontFamily: f.b, fontSize: 10, fontWeight: 600, color: editing === "info" ? c.dim : "#fff", background: editing === "info" ? "transparent" : ch.accent, border: `1px solid ${editing === "info" ? c.brd : ch.accent}`, padding: "3px 10px", borderRadius: 6, cursor: "pointer" }}>{editing === "info" ? "✕ Close" : "✎ Edit"}</div>
|
||||
</div>
|
||||
{editing === "info" ? <>
|
||||
<Input label="Name" value={ch.name} onChange={v => upd("name", v)} placeholder="Championship name" />
|
||||
<Input label="Subtitle" value={ch.subtitle} onChange={v => upd("subtitle", v)} placeholder="Subtitle" />
|
||||
<Input label="Event Date" value={ch.eventDate} onChange={v => upd("eventDate", v)} placeholder="e.g. May 30, 2026" />
|
||||
<Input label="Location" value={ch.location} onChange={v => upd("location", v)} placeholder="City, Country" />
|
||||
<Input label="Venue" value={ch.venue} onChange={v => upd("venue", v)} placeholder="Venue name" />
|
||||
<div style={{ height: 1, background: c.brd, margin: "4px 0 8px" }} />
|
||||
<p style={{ fontFamily: f.m, fontSize: 9, color: c.dim, margin: "0 0 6px", letterSpacing: 0.5 }}>REGISTRATION PERIOD</p>
|
||||
<div style={{ display: "flex", gap: 8 }}>
|
||||
<div style={{ flex: 1 }}><Input label="Opens" value={ch.regStart} onChange={v => upd("regStart", v)} placeholder="e.g. Feb 1, 2026" /></div>
|
||||
<div style={{ flex: 1 }}><Input label="Closes" value={ch.regEnd} onChange={v => upd("regEnd", v)} placeholder="e.g. Apr 22, 2026" /></div>
|
||||
</div>
|
||||
<p style={{ fontFamily: f.m, fontSize: 9, color: c.yellow, margin: "-6px 0 8px" }}>⚠️ Registration close date must be before event date</p>
|
||||
<div onClick={() => { markDone("info"); setEditing(null); }} style={{ padding: "10px", borderRadius: 8, background: c.green, textAlign: "center", cursor: "pointer" }}><span style={{ fontFamily: f.b, fontSize: 12, fontWeight: 700, color: "#fff" }}>✓ Save</span></div>
|
||||
</> : <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
||||
<div style={{ display: "flex", flexWrap: "wrap", gap: "6px 16px" }}>
|
||||
<span style={{ fontFamily: f.b, fontSize: 11, color: c.mid }}>📅 {ch.eventDate || "—"}</span>
|
||||
<span style={{ fontFamily: f.b, fontSize: 11, color: c.mid }}>📍 {ch.venue ? `${ch.venue}, ` : ""}{ch.location || "—"}</span>
|
||||
</div>
|
||||
{(ch.regStart || ch.regEnd) && <div style={{ display: "flex", alignItems: "center", gap: 6, padding: "6px 10px", borderRadius: 8, background: `${c.green}08`, border: `1px solid ${c.green}15` }}>
|
||||
<span style={{ fontFamily: f.m, fontSize: 10, color: c.green }}>📋 Registration:</span>
|
||||
<span style={{ fontFamily: f.b, fontSize: 11, color: c.text }}>{ch.regStart || "?"} → {ch.regEnd || "?"}</span>
|
||||
</div>}
|
||||
</div>}
|
||||
</Cd>
|
||||
|
||||
{/* Stats (only for live) */}
|
||||
{ch.status === "registration_open" && <>
|
||||
<div style={{ display: "flex", gap: 6 }}>
|
||||
{[{ n: stats.total, l: "Members", co: c.mid }, { n: stats.passed, l: "Passed", co: c.green }, { n: stats.failed, l: "Failed", co: c.red }, { n: stats.pending, 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: "10px 6px", textAlign: "center" }}>
|
||||
<p style={{ fontFamily: f.d, fontSize: 20, fontWeight: 700, color: s.co, margin: 0 }}>{s.n}</p>
|
||||
<p style={{ fontFamily: f.m, fontSize: 7, color: c.dim, margin: "2px 0 0", textTransform: "uppercase" }}>{s.l}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Cd>
|
||||
<ST right={<span style={{ fontFamily: f.m, fontSize: 10, color: c.dim }}>tap to view</span>}>⚡ Needs Action</ST>
|
||||
{[
|
||||
{ l: "Receipts to review", n: stats.receipts, icon: "📸", co: c.yellow, go: "Members" },
|
||||
{ l: "Videos to review", n: stats.pending, icon: "🎬", co: c.blue, go: "Results" },
|
||||
].map(a => <div key={a.l} onClick={() => { if (a.go === "Members") setMemFilter("receipts"); setTab(a.go); }} style={{ display: "flex", alignItems: "center", gap: 10, padding: "10px 0", borderBottom: `1px solid ${c.brd}`, cursor: "pointer" }}>
|
||||
<span style={{ fontSize: 16 }}>{a.icon}</span>
|
||||
<span style={{ fontFamily: f.b, fontSize: 13, color: c.text, flex: 1 }}>{a.l}</span>
|
||||
<span style={{ fontFamily: f.m, fontSize: 14, fontWeight: 700, color: a.co }}>{a.n}</span>
|
||||
<span style={{ color: c.dim }}>›</span>
|
||||
</div>)}
|
||||
</Cd>
|
||||
</>}
|
||||
</div>}
|
||||
|
||||
{/* ═══ CATEGORIES ═══ */}
|
||||
{tab === "Categories" && <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
||||
<Cd>
|
||||
<ST right={ch.configured.categories ? <Bg label="✓ DONE" color={c.green} bg={c.greenS} /> : null}>Levels</ST>
|
||||
<TagEditor items={ch.disciplines.flatMap(d => d.levels).filter((v, i, a) => a.indexOf(v) === i)} color={ch.accent} placeholder="Add level (e.g. Amateur)"
|
||||
onAdd={v => { const d = ch.disciplines.length ? [...ch.disciplines] : [{ name: "Exotic Pole Dance", levels: [] }]; d[0] = { ...d[0], levels: [...d[0].levels, v] }; upd("disciplines", d); }}
|
||||
onRemove={i => { const all = ch.disciplines.flatMap(d => d.levels).filter((v, idx, a) => a.indexOf(v) === idx); const rm = all[i]; const d = ch.disciplines.map(d2 => ({ ...d2, levels: d2.levels.filter(l => l !== rm) })); upd("disciplines", d); }} />
|
||||
</Cd>
|
||||
<Cd>
|
||||
<ST>Styles</ST>
|
||||
<TagEditor items={ch.styles} color={c.purple} placeholder="Add style (e.g. Classic)"
|
||||
onAdd={v => upd("styles", [...ch.styles, v])} onRemove={i => upd("styles", ch.styles.filter((_, j) => j !== i))} />
|
||||
</Cd>
|
||||
{!ch.configured.categories && (ch.disciplines.some(d => d.levels.length > 0) && ch.styles.length > 0) && <div onClick={() => markDone("categories")} style={{ padding: "12px", borderRadius: 10, background: c.green, textAlign: "center", cursor: "pointer" }}>
|
||||
<span style={{ fontFamily: f.b, fontSize: 13, fontWeight: 700, color: "#fff" }}>✓ Mark Categories as Done</span>
|
||||
</div>}
|
||||
</div>}
|
||||
|
||||
{/* ═══ FEES ═══ */}
|
||||
{tab === "Fees" && <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
||||
<Cd>
|
||||
<ST right={ch.configured.fees ? <Bg label="✓ DONE" color={c.green} bg={c.greenS} /> : null}>Video Selection Fee</ST>
|
||||
<Input label="Fee amount" value={ch.fees?.videoSelection || ""} onChange={v => upd("fees", { ...ch.fees, videoSelection: v })} placeholder="e.g. 50 BYN / 1,500 RUB" />
|
||||
</Cd>
|
||||
<Cd>
|
||||
<ST>Championship Fees</ST>
|
||||
<Input label="Solo" value={ch.fees?.solo || ""} onChange={v => upd("fees", { ...ch.fees, solo: v })} placeholder="e.g. 280 BYN" />
|
||||
<Input label="Duet (per person)" value={ch.fees?.duet || ""} onChange={v => upd("fees", { ...ch.fees, duet: v })} placeholder="e.g. 210 BYN" />
|
||||
<Input label="Group (per person)" value={ch.fees?.group || ""} onChange={v => upd("fees", { ...ch.fees, group: v })} placeholder="e.g. 190 BYN" />
|
||||
</Cd>
|
||||
{!ch.configured.fees && ch.fees?.videoSelection && <div onClick={() => markDone("fees")} style={{ padding: "12px", borderRadius: 10, background: c.green, textAlign: "center", cursor: "pointer" }}>
|
||||
<span style={{ fontFamily: f.b, fontSize: 13, fontWeight: 700, color: "#fff" }}>✓ Mark Fees as Done</span>
|
||||
</div>}
|
||||
</div>}
|
||||
|
||||
{/* ═══ RULES ═══ */}
|
||||
{tab === "Rules" && <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
||||
<Cd>
|
||||
<ST right={ch.configured.rules ? <Bg label="✓ DONE" color={c.green} bg={c.greenS} /> : null}>General Rules</ST>
|
||||
<TagEditor items={ch.rules} color={c.blue} placeholder="Add rule" onAdd={v => upd("rules", [...ch.rules, v])} onRemove={i => upd("rules", ch.rules.filter((_, j) => j !== i))} />
|
||||
</Cd>
|
||||
<Cd>
|
||||
<ST>Costume Rules</ST>
|
||||
<TagEditor items={ch.costumeRules} color={c.yellow} placeholder="Add costume rule" onAdd={v => upd("costumeRules", [...ch.costumeRules, v])} onRemove={i => upd("costumeRules", ch.costumeRules.filter((_, j) => j !== i))} />
|
||||
</Cd>
|
||||
<Cd>
|
||||
<ST>Scoring Criteria (0–10)</ST>
|
||||
{ch.judging.map((j, i) => <div key={i} style={{ display: "flex", alignItems: "center", gap: 8, padding: "6px 0", borderBottom: `1px solid ${c.brd}` }}>
|
||||
<span style={{ fontFamily: f.b, fontSize: 12, color: c.text, flex: 1 }}>{j.name}</span>
|
||||
<span style={{ fontFamily: f.m, fontSize: 11, color: c.purple }}>0–{j.max}</span>
|
||||
<span onClick={() => upd("judging", ch.judging.filter((_, k) => k !== i))} style={{ fontSize: 10, color: c.dim, cursor: "pointer" }}>×</span>
|
||||
</div>)}
|
||||
<TagEditor items={[]} color={c.purple} placeholder="Add criterion (e.g. Artistry)"
|
||||
onAdd={v => upd("judging", [...ch.judging, { name: v, max: 10 }])} onRemove={() => {}} />
|
||||
</Cd>
|
||||
<Cd>
|
||||
<ST>Penalties</ST>
|
||||
{ch.penalties.map((p, i) => <div key={i} style={{ display: "flex", alignItems: "center", gap: 8, padding: "6px 0", borderBottom: `1px solid ${c.brd}` }}>
|
||||
<span style={{ fontFamily: f.b, fontSize: 12, color: c.text, flex: 1 }}>{p.name}</span>
|
||||
<span style={{ fontFamily: f.m, fontSize: 10, fontWeight: 700, color: p.val === "DQ" ? c.red : c.yellow, background: p.val === "DQ" ? c.redS : c.yellowS, padding: "2px 8px", borderRadius: 4 }}>{p.val}</span>
|
||||
<span onClick={() => upd("penalties", ch.penalties.filter((_, k) => k !== i))} style={{ fontSize: 10, color: c.dim, cursor: "pointer" }}>×</span>
|
||||
</div>)}
|
||||
<TagEditor items={[]} color={c.red} placeholder="Add penalty (e.g. Fall: -2)"
|
||||
onAdd={v => { const [name, val] = v.includes(":") ? v.split(":").map(s => s.trim()) : [v, "-2"]; upd("penalties", [...ch.penalties, { name, val }]); }} onRemove={() => {}} />
|
||||
</Cd>
|
||||
{!ch.configured.rules && ch.rules.length > 0 && <div onClick={() => markDone("rules")} style={{ padding: "12px", borderRadius: 10, background: c.green, textAlign: "center", cursor: "pointer" }}>
|
||||
<span style={{ fontFamily: f.b, fontSize: 13, fontWeight: 700, color: "#fff" }}>✓ Mark Rules as Done</span>
|
||||
</div>}
|
||||
</div>}
|
||||
|
||||
{/* ═══ JUDGES ═══ */}
|
||||
{tab === "Judges" && <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
||||
<ST right={ch.configured.judging ? <Bg label="✓ DONE" color={c.green} bg={c.greenS} /> : <span style={{ fontFamily: f.m, fontSize: 10, color: c.dim }}>{ch.judges.length} judges</span>}>Jury Panel</ST>
|
||||
{ch.judges.map((j, i) => <Cd key={j.id || i} style={{ padding: 14 }}>
|
||||
<div style={{ display: "flex", gap: 12, alignItems: "flex-start" }}>
|
||||
<div style={{ width: 44, height: 44, borderRadius: 12, background: `linear-gradient(135deg,${c.purple}20,${c.purple}40)`, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 20, flexShrink: 0 }}>👩⚖️</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<p style={{ fontFamily: f.b, fontSize: 14, fontWeight: 600, color: c.text, margin: 0 }}>{j.name}</p>
|
||||
<span onClick={() => upd("judges", ch.judges.filter((_, k) => k !== i))} style={{ fontSize: 10, color: c.dim, cursor: "pointer", padding: "4px" }}>×</span>
|
||||
</div>
|
||||
<p style={{ fontFamily: f.m, fontSize: 11, color: c.purple, margin: "2px 0 4px" }}>{j.instagram}</p>
|
||||
<p style={{ fontFamily: f.b, fontSize: 11, color: c.mid, margin: 0, lineHeight: 1.4 }}>{j.bio}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Cd>)}
|
||||
|
||||
{/* Add judge form */}
|
||||
<Cd style={{ background: `${c.purple}06`, border: `1px solid ${c.purple}20` }}>
|
||||
<ST>Add Judge</ST>
|
||||
<Input label="Name" value={newJudge.name} onChange={v => setNewJudge(p => ({ ...p, name: v }))} placeholder="e.g. Anastasia Skukhtorova" />
|
||||
<Input label="Instagram" value={newJudge.instagram} onChange={v => setNewJudge(p => ({ ...p, instagram: v }))} placeholder="e.g. @skukhtorova" />
|
||||
<Input label="Bio / Description" value={newJudge.bio} onChange={v => setNewJudge(p => ({ ...p, bio: v }))} placeholder="Experience, titles, achievements..." />
|
||||
<div onClick={() => { if (newJudge.name) { upd("judges", [...ch.judges, { ...newJudge, id: `j${Date.now()}` }]); setNewJudge({ name: "", instagram: "", bio: "" }); } }} style={{ padding: "10px", borderRadius: 8, background: newJudge.name ? c.purple : c.brd, textAlign: "center", cursor: newJudge.name ? "pointer" : "default", opacity: newJudge.name ? 1 : 0.5 }}>
|
||||
<span style={{ fontFamily: f.b, fontSize: 12, fontWeight: 700, color: "#fff" }}>+ Add Judge</span>
|
||||
</div>
|
||||
</Cd>
|
||||
|
||||
{!ch.configured.judging && ch.judges.length > 0 && <div onClick={() => markDone("judging")} style={{ padding: "12px", borderRadius: 10, background: c.green, textAlign: "center", cursor: "pointer" }}>
|
||||
<span style={{ fontFamily: f.b, fontSize: 13, fontWeight: 700, color: "#fff" }}>✓ Mark Judges as Done</span>
|
||||
</div>}
|
||||
</div>}
|
||||
|
||||
{/* ═══ MEMBERS ═══ */}
|
||||
{tab === "Members" && <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
||||
<div style={{ background: c.card, border: `1px solid ${c.brd}`, borderRadius: 12, padding: "10px 14px", display: "flex", alignItems: "center", gap: 10 }}>
|
||||
<span style={{ fontSize: 14, opacity: 0.4 }}>🔍</span>
|
||||
<input type="text" placeholder="Search..." value={memSearch} onChange={e => setMemSearch(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: 4, overflowX: "auto", scrollbarWidth: "none" }}>
|
||||
{memFilters.map(fi => <div key={fi.id} onClick={() => setMemFilter(fi.id)} style={{ fontFamily: f.m, fontSize: 9, fontWeight: 600, whiteSpace: "nowrap", color: memFilter === fi.id ? ch.accent : c.dim, background: memFilter === fi.id ? `${ch.accent}15` : "transparent", border: `1px solid ${memFilter === fi.id ? `${ch.accent}30` : "transparent"}`, padding: "5px 10px", borderRadius: 16, cursor: "pointer" }}>{fi.l} ({fi.n})</div>)}
|
||||
</div>
|
||||
{filteredMem.map(m => <div key={m.id} onClick={() => onMemberTap(m, ch)} style={{ background: c.card, border: `1px solid ${c.brd}`, borderRadius: 14, padding: 12, cursor: "pointer" }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", marginBottom: 4 }}>
|
||||
<div><p style={{ fontFamily: f.b, fontSize: 13, fontWeight: 600, color: c.text, margin: 0 }}>{m.name}</p><p style={{ fontFamily: f.m, fontSize: 10, color: ch.accent, margin: "1px 0 0" }}>{m.instagram}</p></div>
|
||||
<Bg label={m.passed === true ? "PASSED" : m.passed === false ? "FAILED" : "PENDING"} color={m.passed === true ? c.green : m.passed === false ? c.red : c.yellow} bg={m.passed === true ? c.greenS : m.passed === false ? c.redS : c.yellowS} />
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: 5, flexWrap: "wrap" }}>{[m.level, m.style, m.city].map(t => <span key={t} style={{ fontFamily: f.b, fontSize: 9, color: c.mid, background: `${c.mid}10`, padding: "2px 7px", borderRadius: 10 }}>{t}</span>)}</div>
|
||||
</div>)}
|
||||
{filteredMem.length === 0 && <div style={{ textAlign: "center", padding: 30 }}><span style={{ fontSize: 28 }}>🤷</span><p style={{ fontFamily: f.b, fontSize: 13, color: c.dim, marginTop: 8 }}>No members match</p></div>}
|
||||
</div>}
|
||||
|
||||
{/* ═══ RESULTS ═══ */}
|
||||
{tab === "Results" && <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
||||
<div style={{ display: "flex", gap: 6 }}>
|
||||
{[{ n: stats.pending, l: "Pending", co: c.yellow }, { n: stats.passed, l: "Passed", co: c.green }, { n: stats.failed, l: "Failed", co: c.red }].map(s =>
|
||||
<div key={s.l} style={{ flex: 1, background: c.card, border: `1px solid ${c.brd}`, borderRadius: 12, padding: "10px 6px", textAlign: "center" }}>
|
||||
<p style={{ fontFamily: f.d, fontSize: 20, fontWeight: 700, color: s.co, margin: 0 }}>{s.n}</p>
|
||||
<p style={{ fontFamily: f.m, fontSize: 7, color: c.dim, margin: "2px 0 0", textTransform: "uppercase" }}>{s.l}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{members.filter(m => m.videoUrl && m.passed === null).map(m => <Cd key={m.id} style={{ padding: 12 }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 8 }}>
|
||||
<div><p style={{ fontFamily: f.b, fontSize: 13, fontWeight: 600, color: c.text, margin: 0 }}>{m.name}</p><p style={{ fontFamily: f.m, fontSize: 10, color: c.dim, margin: "2px 0 0" }}>{m.level} · {m.style}</p></div>
|
||||
<span style={{ fontFamily: f.b, fontSize: 10, color: c.blue, background: c.blueS, padding: "3px 8px", borderRadius: 8, cursor: "pointer" }}>🎥 View</span>
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: 8 }}>
|
||||
<div onClick={() => decide(m.id, true)} style={{ flex: 1, padding: "10px", borderRadius: 10, background: `${c.green}15`, border: `1px solid ${c.green}30`, cursor: "pointer", textAlign: "center" }}><span style={{ fontFamily: f.b, fontSize: 12, fontWeight: 700, color: c.green }}>✅ Pass</span></div>
|
||||
<div onClick={() => decide(m.id, false)} style={{ flex: 1, padding: "10px", borderRadius: 10, background: `${c.red}15`, border: `1px solid ${c.red}30`, cursor: "pointer", textAlign: "center" }}><span style={{ fontFamily: f.b, fontSize: 12, fontWeight: 700, color: c.red }}>❌ Fail</span></div>
|
||||
</div>
|
||||
</Cd>)}
|
||||
{members.filter(m => m.passed !== null).length > 0 && <>
|
||||
<ST>Decided</ST>
|
||||
{members.filter(m => m.passed !== null).map(m => <div key={m.id} style={{ display: "flex", alignItems: "center", gap: 10, padding: "10px 12px", background: c.card, border: `1px solid ${c.brd}`, borderRadius: 12 }}>
|
||||
<div style={{ flex: 1 }}><p style={{ fontFamily: f.b, fontSize: 13, color: c.text, margin: 0 }}>{m.name}</p><p style={{ fontFamily: f.m, fontSize: 10, color: c.dim, margin: "2px 0 0" }}>{m.level}</p></div>
|
||||
<Bg label={m.passed ? "PASSED" : "FAILED"} color={m.passed ? c.green : c.red} bg={m.passed ? c.greenS : c.redS} />
|
||||
</div>)}
|
||||
</>}
|
||||
<div style={{ padding: "14px", borderRadius: 12, background: ch.accent, textAlign: "center", cursor: "pointer" }}><span style={{ fontFamily: f.b, fontSize: 14, fontWeight: 700, color: "#fff" }}>📢 Publish Results</span></div>
|
||||
</div>}
|
||||
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
/* ── Member Detail ── */
|
||||
function MemberDetail({ member, champ, onBack }) {
|
||||
const [m, setM] = useState(member);
|
||||
const [showLvl, setShowLvl] = useState(false);
|
||||
const [showSty, setShowSty] = useState(false);
|
||||
const levels = champ.disciplines.flatMap(d => d.levels).filter((v, i, a) => a.indexOf(v) === i);
|
||||
|
||||
return <div style={{ flex: 1, overflow: "auto" }}>
|
||||
<Hdr title={m.name} subtitle={`${champ.name} · ${m.instagram}`} onBack={onBack} />
|
||||
<div style={{ padding: "6px 16px 20px", display: "flex", flexDirection: "column", gap: 12 }}>
|
||||
<Cd style={{ display: "flex", alignItems: "center", gap: 14, padding: "14px 16px" }}>
|
||||
<div style={{ width: 50, height: 50, borderRadius: 14, background: `linear-gradient(135deg,${champ.accent}20,${champ.accent}40)`, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 22, flexShrink: 0 }}>👤</div>
|
||||
<div style={{ flex: 1 }}><p style={{ fontFamily: f.b, fontSize: 16, fontWeight: 600, color: c.text, margin: 0 }}>{m.name}</p><p style={{ fontFamily: f.m, fontSize: 11, color: champ.accent, margin: "2px 0 0" }}>{m.instagram}</p><p style={{ fontFamily: f.b, fontSize: 11, color: c.dim, margin: "2px 0 0" }}>📍 {m.city}</p></div>
|
||||
<Bg label={m.passed === true ? "PASSED" : m.passed === false ? "FAILED" : "PENDING"} color={m.passed === true ? c.green : m.passed === false ? c.red : c.yellow} bg={m.passed === true ? c.greenS : m.passed === false ? c.redS : c.yellowS} />
|
||||
</Cd>
|
||||
|
||||
<Cd>
|
||||
<ST>Registration</ST>
|
||||
{[{ l: "Discipline", v: m.discipline }, { l: "Type", v: m.type }].map(r => <div key={r.l} style={{ display: "flex", justifyContent: "space-between", padding: "7px 0", borderBottom: `1px solid ${c.brd}` }}><span style={{ fontFamily: f.m, fontSize: 10, color: c.dim, textTransform: "uppercase" }}>{r.l}</span><span style={{ fontFamily: f.b, fontSize: 12, color: c.text }}>{r.v}</span></div>)}
|
||||
|
||||
{/* Level */}
|
||||
<div style={{ padding: "7px 0", borderBottom: `1px solid ${c.brd}` }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<span style={{ fontFamily: f.m, fontSize: 10, color: c.dim, textTransform: "uppercase" }}>Level</span>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<span style={{ fontFamily: f.b, fontSize: 12, fontWeight: 600, color: c.text }}>{m.level}</span>
|
||||
<div onClick={() => { setShowLvl(!showLvl); setShowSty(false); }} style={{ fontFamily: f.b, fontSize: 10, fontWeight: 600, color: showLvl ? c.dim : "#fff", background: showLvl ? "transparent" : champ.accent, border: `1px solid ${showLvl ? c.brd : champ.accent}`, padding: "3px 10px", borderRadius: 6, cursor: "pointer" }}>{showLvl ? "✕" : "✎ Edit"}</div>
|
||||
</div>
|
||||
</div>
|
||||
{showLvl && <div style={{ marginTop: 8, padding: 8, background: c.bg, borderRadius: 8 }}>
|
||||
<p style={{ fontFamily: f.m, fontSize: 9, color: c.yellow, margin: "0 0 6px" }}>⚠️ Member will be notified</p>
|
||||
{levels.map(l => <div key={l} onClick={() => { setM(p => ({ ...p, level: l })); setShowLvl(false); }} style={{ padding: "8px 10px", borderRadius: 6, cursor: "pointer", marginBottom: 3, background: l === m.level ? `${champ.accent}15` : "transparent", border: `1px solid ${l === m.level ? `${champ.accent}30` : c.brd}` }}>
|
||||
<span style={{ fontFamily: f.b, fontSize: 12, color: l === m.level ? champ.accent : c.text }}>{l}</span>
|
||||
</div>)}
|
||||
</div>}
|
||||
</div>
|
||||
|
||||
{/* Style */}
|
||||
<div style={{ padding: "7px 0" }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<span style={{ fontFamily: f.m, fontSize: 10, color: c.dim, textTransform: "uppercase" }}>Style</span>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<span style={{ fontFamily: f.b, fontSize: 12, fontWeight: 600, color: c.text }}>{m.style}</span>
|
||||
<div onClick={() => { setShowSty(!showSty); setShowLvl(false); }} style={{ fontFamily: f.b, fontSize: 10, fontWeight: 600, color: showSty ? c.dim : "#fff", background: showSty ? "transparent" : c.purple, border: `1px solid ${showSty ? c.brd : c.purple}`, padding: "3px 10px", borderRadius: 6, cursor: "pointer" }}>{showSty ? "✕" : "✎ Edit"}</div>
|
||||
</div>
|
||||
</div>
|
||||
{showSty && <div style={{ marginTop: 8, padding: 8, background: c.bg, borderRadius: 8 }}>
|
||||
<p style={{ fontFamily: f.m, fontSize: 9, color: c.yellow, margin: "0 0 6px" }}>⚠️ Member will be notified</p>
|
||||
{champ.styles.map(s => <div key={s} onClick={() => { setM(p => ({ ...p, style: s })); setShowSty(false); }} style={{ padding: "8px 10px", borderRadius: 6, cursor: "pointer", marginBottom: 3, background: s === m.style ? `${c.purple}15` : "transparent", border: `1px solid ${s === m.style ? `${c.purple}30` : c.brd}` }}>
|
||||
<span style={{ fontFamily: f.b, fontSize: 12, color: s === m.style ? c.purple : c.text }}>{s}</span>
|
||||
</div>)}
|
||||
</div>}
|
||||
</div>
|
||||
</Cd>
|
||||
|
||||
{/* Video */}
|
||||
<Cd>
|
||||
<ST>🎬 Video</ST>
|
||||
{m.videoUrl ? <>
|
||||
<div style={{ background: c.bg, borderRadius: 8, padding: 10, marginBottom: 8, display: "flex", alignItems: "center", gap: 8 }}><span style={{ fontSize: 18 }}>🎥</span><p style={{ fontFamily: f.m, fontSize: 10, color: c.blue, margin: 0, flex: 1, overflow: "hidden", textOverflow: "ellipsis" }}>{m.videoUrl}</p></div>
|
||||
{m.passed === null ? <div style={{ display: "flex", gap: 8 }}>
|
||||
<div onClick={() => setM(p => ({ ...p, passed: true }))} style={{ flex: 1, padding: "10px", borderRadius: 10, background: c.green, cursor: "pointer", textAlign: "center" }}><span style={{ fontFamily: f.b, fontSize: 12, fontWeight: 700, color: "#fff" }}>✅ Pass</span></div>
|
||||
<div onClick={() => setM(p => ({ ...p, passed: false }))} style={{ flex: 1, padding: "10px", borderRadius: 10, background: c.red, cursor: "pointer", textAlign: "center" }}><span style={{ fontFamily: f.b, fontSize: 12, fontWeight: 700, color: "#fff" }}>❌ Fail</span></div>
|
||||
</div> : <Bg label={m.passed ? "PASSED" : "FAILED"} color={m.passed ? c.green : c.red} bg={m.passed ? c.greenS : c.redS} />}
|
||||
</> : <p style={{ fontFamily: f.b, fontSize: 12, color: c.dim, margin: 0 }}>No video yet</p>}
|
||||
</Cd>
|
||||
|
||||
{/* Payment */}
|
||||
<Cd>
|
||||
<ST>💳 Payment</ST>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<div><p style={{ fontFamily: f.b, fontSize: 12, color: c.text, margin: 0 }}>Video fee</p><p style={{ fontFamily: f.m, fontSize: 10, color: c.dim, margin: "2px 0 0" }}>{champ.fees?.videoSelection || "—"}</p></div>
|
||||
{m.receiptUploaded && !m.feePaid ? <div onClick={() => setM(p => ({ ...p, feePaid: true }))} style={{ padding: "6px 12px", borderRadius: 8, background: `${c.green}15`, border: `1px solid ${c.green}30`, cursor: "pointer" }}><span style={{ fontFamily: f.b, fontSize: 11, fontWeight: 600, color: c.green }}>📸 Confirm</span></div>
|
||||
: <Bg label={m.feePaid ? "CONFIRMED" : "PENDING"} color={m.feePaid ? c.green : c.yellow} bg={m.feePaid ? c.greenS : c.yellowS} />}
|
||||
</div>
|
||||
</Cd>
|
||||
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: 6, padding: "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 }}>Send Notification</span></div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
/* ── Quick Create ── */
|
||||
function QuickCreate({ onBack, onDone }) {
|
||||
const [name, setName] = useState("");
|
||||
const [eventDate, setEventDate] = useState("");
|
||||
const [location, setLocation] = useState("");
|
||||
|
||||
return <div style={{ flex: 1, overflow: "auto" }}>
|
||||
<Hdr title="New Championship" subtitle="Quick create — configure details later" onBack={onBack} />
|
||||
<div style={{ padding: "6px 16px 20px", display: "flex", flexDirection: "column", gap: 12 }}>
|
||||
<Cd>
|
||||
<Input label="Championship Name" value={name} onChange={setName} placeholder="e.g. Zero Gravity" />
|
||||
<Input label="Event Date" value={eventDate} onChange={setEventDate} placeholder="e.g. May 30, 2026" />
|
||||
<Input label="Location" value={location} onChange={setLocation} placeholder="e.g. Minsk, Belarus" />
|
||||
</Cd>
|
||||
|
||||
<Cd style={{ background: `${c.blue}06`, border: `1px solid ${c.blue}20` }}>
|
||||
<p style={{ fontFamily: f.b, fontSize: 12, color: c.blue, margin: "0 0 6px" }}>💡 What happens next?</p>
|
||||
<p style={{ fontFamily: f.b, fontSize: 11, color: c.mid, margin: 0, lineHeight: 1.6 }}>Your championship will be created as a draft. Configure categories, fees, rules, and judging at your own pace. Once everything is set, hit "Go Live" to open registration.</p>
|
||||
</Cd>
|
||||
|
||||
<div onClick={() => name && onDone(makeCh({ id: `ch${Date.now()}`, name, eventDate, location, status: "draft", configured: { info: !!eventDate && !!location, categories: false, fees: false, rules: false, judging: false } }))} style={{ padding: "14px", borderRadius: 12, background: name ? c.accent : c.brd, textAlign: "center", cursor: name ? "pointer" : "default", opacity: name ? 1 : 0.5 }}>
|
||||
<span style={{ fontFamily: f.b, fontSize: 14, fontWeight: 700, color: "#fff" }}>✨ Create Draft</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
/* ── Org Settings ── */
|
||||
function OrgSettings({ org, onUpdateOrg }) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [name, setName] = useState(org.name);
|
||||
const [instagram, setInstagram] = useState(org.instagram);
|
||||
const [subScreen, setSubScreen] = useState(null);
|
||||
|
||||
if (subScreen === "notifications") return <div>
|
||||
<Hdr title="Notifications" subtitle="Notification preferences" onBack={() => setSubScreen(null)} />
|
||||
<div style={{ padding: "6px 16px 20px", display: "flex", flexDirection: "column", gap: 10 }}>
|
||||
{[{ l: "Push notifications", d: "Get notified on new registrations", on: true },
|
||||
{ l: "Email notifications", d: "Receive email for payments & uploads", on: true },
|
||||
{ l: "Registration alerts", d: "When a new member registers", on: true },
|
||||
{ l: "Payment alerts", d: "When a receipt is uploaded", on: true },
|
||||
{ l: "Deadline reminders", d: "Auto-remind members before deadlines", on: false },
|
||||
].map(n => <ToggleRow key={n.l} label={n.l} desc={n.d} defaultOn={n.on} />)}
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
if (subScreen === "accounts") return <div>
|
||||
<Hdr title="Connected Accounts" subtitle="Integrations" onBack={() => setSubScreen(null)} />
|
||||
<div style={{ padding: "6px 16px 20px", display: "flex", flexDirection: "column", gap: 10 }}>
|
||||
{[{ name: "Instagram", handle: org.instagram, icon: "📸", connected: true, color: c.purple },
|
||||
{ name: "Gmail", handle: "zerogravity@gmail.com", icon: "📧", connected: true, color: c.red },
|
||||
{ name: "Telegram", handle: "@zerogravity_bot", icon: "💬", connected: false, color: c.blue },
|
||||
].map(a => <Cd key={a.name} style={{ display: "flex", alignItems: "center", gap: 12, padding: 14 }}>
|
||||
<div style={{ width: 38, height: 38, borderRadius: 10, background: `${a.color}15`, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 17 }}>{a.icon}</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<p style={{ fontFamily: f.b, fontSize: 13, fontWeight: 600, color: c.text, margin: 0 }}>{a.name}</p>
|
||||
<p style={{ fontFamily: f.m, fontSize: 10, color: a.connected ? a.color : c.dim, margin: "2px 0 0" }}>{a.connected ? a.handle : "Not connected"}</p>
|
||||
</div>
|
||||
<Bg label={a.connected ? "CONNECTED" : "CONNECT"} color={a.connected ? c.green : c.accent} bg={a.connected ? c.greenS : c.accentS} />
|
||||
</Cd>)}
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
return <div>
|
||||
<Hdr title="Settings" subtitle="Organization profile" />
|
||||
<div style={{ padding: "6px 20px 20px" }}>
|
||||
{/* Profile header */}
|
||||
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", marginBottom: 20 }}>
|
||||
<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` }}>{org.logo}</div>
|
||||
{editing ? <div style={{ width: "100%", display: "flex", flexDirection: "column", gap: 8, marginTop: 4 }}>
|
||||
<Input label="Organization Name" value={name} onChange={setName} placeholder="Your org name" />
|
||||
<Input label="Instagram" value={instagram} onChange={setInstagram} placeholder="@handle" />
|
||||
<div style={{ display: "flex", gap: 8 }}>
|
||||
<div onClick={() => setEditing(false)} style={{ flex: 1, padding: "10px", borderRadius: 8, background: c.card, border: `1px solid ${c.brd}`, textAlign: "center", cursor: "pointer" }}><span style={{ fontFamily: f.b, fontSize: 12, color: c.dim }}>Cancel</span></div>
|
||||
<div onClick={() => { onUpdateOrg({ name, instagram }); setEditing(false); }} style={{ flex: 1, padding: "10px", borderRadius: 8, background: c.green, textAlign: "center", cursor: "pointer" }}><span style={{ fontFamily: f.b, fontSize: 12, fontWeight: 700, color: "#fff" }}>✓ Save</span></div>
|
||||
</div>
|
||||
</div> : <>
|
||||
<h2 style={{ fontFamily: f.d, fontSize: 19, fontWeight: 700, color: c.text, margin: "0 0 2px" }}>{org.name}</h2>
|
||||
<p style={{ fontFamily: f.m, fontSize: 11, color: c.accent, margin: 0 }}>{org.instagram}</p>
|
||||
</>}
|
||||
</div>
|
||||
|
||||
{/* Menu items — hide when editing */}
|
||||
{!editing && <div style={{ background: c.card, border: `1px solid ${c.brd}`, borderRadius: 12, overflow: "hidden" }}>
|
||||
{[
|
||||
{ label: "Edit Organization Profile", action: () => setEditing(true), icon: "✎" },
|
||||
{ label: "Notification Preferences", action: () => setSubScreen("notifications"), icon: "🔔" },
|
||||
{ label: "Connected Accounts", action: () => setSubScreen("accounts"), icon: "🔗" },
|
||||
{ label: "Help & Support", action: () => {}, icon: "❓" },
|
||||
{ label: "Log Out", action: () => {}, icon: "🚪", danger: true },
|
||||
].map((x, i, a) =>
|
||||
<div key={x.label} onClick={x.action} style={{ padding: "13px 16px", fontFamily: f.b, fontSize: 13, color: x.danger ? c.red : c.text, borderBottom: i < a.length - 1 ? `1px solid ${c.brd}` : "none", cursor: "pointer", display: "flex", alignItems: "center", gap: 10 }}>
|
||||
<span style={{ fontSize: 14, opacity: 0.6 }}>{x.icon}</span>
|
||||
<span style={{ flex: 1 }}>{x.label}</span>
|
||||
<span style={{ color: c.dim, fontSize: 12 }}>›</span>
|
||||
</div>
|
||||
)}
|
||||
</div>}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
/* Toggle row helper */
|
||||
function ToggleRow({ label, desc, defaultOn }) {
|
||||
const [on, setOn] = useState(defaultOn);
|
||||
return <Cd style={{ display: "flex", alignItems: "center", gap: 12, padding: 14 }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<p style={{ fontFamily: f.b, fontSize: 13, color: c.text, margin: 0 }}>{label}</p>
|
||||
{desc && <p style={{ fontFamily: f.b, fontSize: 10, color: c.dim, margin: "2px 0 0" }}>{desc}</p>}
|
||||
</div>
|
||||
<div onClick={() => setOn(!on)} style={{ width: 42, height: 24, borderRadius: 12, background: on ? c.green : c.brd, padding: 2, cursor: "pointer", transition: "background 0.2s" }}>
|
||||
<div style={{ width: 20, height: 20, borderRadius: 10, background: "#fff", transform: on ? "translateX(18px)" : "translateX(0)", transition: "transform 0.2s" }} />
|
||||
</div>
|
||||
</Cd>;
|
||||
}
|
||||
|
||||
/* ── App Shell ── */
|
||||
export default function OrgApp() {
|
||||
const [champs, setChamps] = useState(INITIAL_CHAMPS);
|
||||
const [org, setOrg] = useState(ORG);
|
||||
const [scr, setScr] = useState("dash");
|
||||
const [selChamp, setSelChamp] = useState(null);
|
||||
const [selMember, setSelMember] = useState(null);
|
||||
|
||||
const addChamp = ch => { setChamps(p => [...p, ch]); setSelChamp(ch); setScr("champ"); };
|
||||
const updateOrg = updates => setOrg(p => ({ ...p, ...updates }));
|
||||
|
||||
const render = () => {
|
||||
if (scr === "create") return <QuickCreate onBack={() => setScr("dash")} onDone={addChamp} />;
|
||||
if (scr === "champ" && selChamp) return <ChampDetail ch={selChamp} onBack={() => { setScr("dash"); setSelChamp(null); }} onMemberTap={(m, ch) => { setSelMember({ m, ch }); setScr("member"); }} />;
|
||||
if (scr === "member" && selMember) return <MemberDetail member={selMember.m} champ={selMember.ch} onBack={() => setScr("champ")} />;
|
||||
if (scr === "orgSettings") return <OrgSettings org={org} onUpdateOrg={updateOrg} />;
|
||||
return <Dashboard champs={champs} org={org} onChampTap={ch => { setSelChamp(ch); setScr("champ"); }} onCreateChamp={() => setScr("create")} />;
|
||||
};
|
||||
|
||||
const showNav = scr === "dash" || scr === "orgSettings";
|
||||
|
||||
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); setSelChamp(null); setSelMember(null); }} />}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
Reference in New Issue
Block a user