import { useEffect, useMemo, useState, type FormEvent, type ReactNode } from 'react' import { CalendarDays, Check, ChevronRight, CircleHelp, Clock3, Gamepad2, ListFilter, LogOut, Menu, Radio, RefreshCw, Shield, Sparkles, Swords, Trophy, UserRound, UsersRound, X, Zap, } from 'lucide-react' import { QueryClient, QueryClientProvider, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { Link, Outlet, RouterProvider, createRootRouteWithContext, createRoute, createRouter, redirect, } from '@tanstack/react-router' import { api, subscribeToEvents, type EventInput, type MapOutcome, type MixEvent, type PlayerRole, type Registration, type RsvpStatus, type Series, type Session, type Team, } from './api/client' import { demoBalanceCandidates, demoBracket, demoEvents, demoProfile, demoMode, demoRegistrations, demoSeries, demoSession, } from './api/demo' import { LanguageSelector } from './i18n' import { useLanguage } from './i18n-context' import { rankLabel, rankOptions, toRankOrdinal, type RankOrdinal } from './ranks' type RouterContext = { session: Session | null } const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 30_000, retry: 1, refetchOnWindowFocus: false }, mutations: { retry: 0 } } }) const rootRoute = createRootRouteWithContext()({ component: Outlet }) const loginRoute = createRoute({ getParentRoute: () => rootRoute, path: '/login', component: LoginPage }) const protectedRoute = createRoute({ getParentRoute: () => rootRoute, id: '_protected', component: AppShell, beforeLoad: ({ context }) => { if (!context.session) throw redirect({ to: '/login' }) }, }) const indexRoute = createRoute({ getParentRoute: () => protectedRoute, path: '/', beforeLoad: () => { throw redirect({ to: '/events' }) } }) const profileRoute = createRoute({ getParentRoute: () => protectedRoute, path: '/profile', component: ProfilePage }) const eventsRoute = createRoute({ getParentRoute: () => protectedRoute, path: '/events', component: EventsPage }) const eventRoute = createRoute({ getParentRoute: () => protectedRoute, path: '/events/$eventId', component: EventPage }) const liveRoute = createRoute({ getParentRoute: () => protectedRoute, path: '/series/$seriesId', component: LiveSeriesPage }) const spectatorRoute = createRoute({ getParentRoute: () => protectedRoute, path: '/watch/$seriesId', component: SpectatorPage }) const bracketRoute = createRoute({ getParentRoute: () => protectedRoute, path: '/bracket/$eventId', component: BracketPage }) const adminRoute = createRoute({ getParentRoute: () => protectedRoute, path: '/admin', component: AdminPage, beforeLoad: ({ context }) => { if (context.session?.account.role !== 'admin') throw redirect({ to: '/events' }) }, }) const routeTree = rootRoute.addChildren([loginRoute, protectedRoute.addChildren([ indexRoute, profileRoute, eventsRoute, eventRoute, liveRoute, spectatorRoute, bracketRoute, adminRoute, ])]) const router = createRouter({ routeTree, context: { session: null } }) declare module '@tanstack/react-router' { interface Register { router: typeof router } } const mainNavItems = [ { to: '/events', label: 'Events', icon: CalendarDays }, ...(demoMode ? [ { to: '/series/$seriesId' as const, params: { seriesId: 'series-1' }, label: 'Live', icon: Radio }, { to: '/bracket/$eventId' as const, params: { eventId: 'bracket-1' }, label: 'Bracket', icon: Trophy }, ] : []), ] function Brand() { return MIXMAKER } function AppShell() { const [open, setOpen] = useState(false) const session = useQuery({ queryKey: ['session'], queryFn: api.session, staleTime: Infinity }) const logout = useMutation({ mutationFn: api.logout, onSuccess: () => { queryClient.clear() void router.navigate({ to: '/login' }) }, }) const initials = session.data?.account.displayName.slice(0, 2).toUpperCase() ?? 'MM' const navItems = session.data?.account.role === 'admin' ? [...mainNavItems, { to: '/admin' as const, label: 'Admin', icon: Shield }] : mainNavItems return
{initials}
{open && }
} function LoginPage() { return
Community scrims, organized

Ready up.
Play together.

Balance teams, run drafts, and follow every map from one esports control room.

Continue with DiscordSigning in creates a secure session. We never receive your Discord password.
5v5 role-locked teamsLIVE synchronized draftsBO3 tournament series
} function PageHeader({ eyebrow, title, description, actions }: { eyebrow: string; title: string; description?: string; actions?: ReactNode }) { return
{eyebrow}

{title}

{description &&

{description}

}
{actions &&
{actions}
}
} function Badge({ children, tone = 'neutral' }: { children: ReactNode; tone?: 'success' | 'warning' | 'live' | 'neutral' | 'blue' }) { return {children} } function LoadingState({ label = 'Loading match data…' }: { label?: string }) { return

{label}

Syncing with the server.

} function ErrorState({ retry }: { retry?: () => void }) { return

Couldn’t load this view

Your data is safe. Check your connection and try again.

{retry && }
} function EventsPage() { const [filter, setFilter] = useState<'upcoming' | 'all'>('upcoming') const events = useQuery({ queryKey: ['events'], queryFn: api.events, initialData: demoMode ? demoEvents : undefined }) if (events.isLoading) return if (events.isError) return void events.refetch()} /> if (!events.data) return const visible = filter === 'upcoming' ? events.data.filter((e) => e.status !== 'completed') : events.data return
} /> {demoMode &&
Live now

{demoEvents[2].title}

Semifinal 1 · Map 2 draft in progress

EMBER1:0AZURE
Watch live
}

Coming up

{visible.length} scheduled events

{visible.length === 0 ?

No events yet

New community nights will appear here.

:
{visible.filter((e) => e.status !== 'live').map((event) => )}
}
} function EventCard({ event }: { event: MixEvent }) { const date = new Date(event.startsAt) return
{date.toLocaleDateString(undefined, { day: '2-digit' })}{date.toLocaleDateString(undefined, { month: 'short' }).toUpperCase()}
{event.myRsvp === 'going' ? : }{event.myRsvp === 'going' ? 'Going' : 'Maybe'}{date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })}

{event.title}

{event.description}

{event.counts.going} goingView event
} function EventPage() { const { eventId } = eventRoute.useParams() const client = useQueryClient() const demoEvent = demoEvents.find((event) => event.id === eventId) const eventQuery = useQuery({ queryKey: ['event', eventId], queryFn: () => api.event(eventId), initialData: demoMode ? demoEvent : undefined }) const registrations = useQuery({ queryKey: ['registrations', eventId], queryFn: () => api.registrations(eventId), initialData: demoMode ? demoRegistrations : undefined }) const session = useQuery({ queryKey: ['session'], queryFn: api.session, initialData: demoMode ? demoSession : undefined }) const rsvpMutation = useMutation({ mutationFn: (status: RsvpStatus) => api.setRsvp(eventId, session.data!.player.id, status), onSuccess: () => { void client.invalidateQueries({ queryKey: ['event', eventId] }) void client.invalidateQueries({ queryKey: ['events'] }) void client.invalidateQueries({ queryKey: ['registrations', eventId] }) }, }) useEffect(() => subscribeToEvents(`event:${eventId}`, () => { void client.invalidateQueries({ queryKey: ['event', eventId] }) void client.invalidateQueries({ queryKey: ['registrations', eventId] }) }), [client, eventId]) const labels: Record = { going: 'Going', maybe: 'Maybe', not_going: 'Can’t go' } if (eventQuery.isLoading || registrations.isLoading || session.isLoading) return if (eventQuery.isError || registrations.isError || session.isError || !eventQuery.data || !session.data) return { void eventQuery.refetch(); void registrations.refetch(); void session.refetch() }} /> const event = eventQuery.data const rsvp = registrations.data?.find((registration) => registration.player.id === session.data.player.id)?.status ?? event.myRsvp const counts = registrations.data?.reduce((total, registration) => ({ ...total, [registration.status]: total[registration.status] + 1 }), { going: 0, maybe: 0, not_going: 0 }) ?? event.counts const statusOrder: Record = { going: 0, maybe: 1, not_going: 2 } const attendees = [...(registrations.data ?? [])].sort((a, b) => statusOrder[a.status] - statusOrder[b.status] || a.player.displayName.localeCompare(b.player.displayName)) return
Registration open} />
Starts{new Date(event.startsAt).toLocaleString(undefined, { weekday: 'long', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit' })}
Registration closes{new Date(event.registrationDeadline).toLocaleString(undefined, { weekday: 'short', hour: '2-digit', minute: '2-digit' })}

Are you playing?

Your latest response is sent to the organizer.

{(Object.keys(labels) as RsvpStatus[]).map((status) => )}
{rsvpMutation.isSuccess &&
Response saved · Server confirmed just now
}{rsvpMutation.isError &&

{rsvpMutation.error.message}

}

Registered players

Everyone can see who responded and their current status.

{attendees.length === 0 ?

No responses yet.

:
{attendees.map((registration) =>
{registration.player.displayName.slice(0, 2).toUpperCase()}{registration.player.displayName}{labels[registration.status]}
)}
}
} function ProfilePage() { const client = useQueryClient() const language = useLanguage() const options = useMemo(() => rankOptions(language), [language]) const profile = useQuery({ queryKey: ['profile'], queryFn: api.profile, initialData: demoMode ? demoProfile : undefined }) const players = useQuery({ queryKey: ['players'], queryFn: api.players, initialData: demoMode ? demoRegistrations.map((registration) => registration.player) : undefined }) const [ratings, setRatings] = useState>({ tank: 20, damage: 20, support: 20 }) const [preferredRoles, setPreferredRoles] = useState([]) const [preferredPlayerIds, setPreferredPlayerIds] = useState([]) useEffect(() => { if (profile.data) { setRatings({ tank: profile.data.ratings.tank, damage: profile.data.ratings.damage, support: profile.data.ratings.support }) setPreferredRoles(profile.data.preferredRoles) setPreferredPlayerIds(profile.data.preferredPlayerIds) } }, [profile.data]) const update = useMutation({ mutationFn: () => api.updateRatings(profile.data!.displayName, ratings, preferredRoles, preferredPlayerIds), onSuccess: (player) => { client.setQueryData(['profile'], player) void client.invalidateQueries({ queryKey: ['session'] }) }, }) if (profile.isLoading || players.isLoading) return if (profile.isError || players.isError) return { void profile.refetch(); void players.refetch() }} /> if (!profile.data || !players.data) return const roles: Array<{ key: PlayerRole; label: string; hint: string }> = [{ key: 'tank', label: 'Tank', hint: 'Space, pressure, and frontline' }, { key: 'damage', label: 'Damage', hint: 'Eliminations and map control' }, { key: 'support', label: 'Support', hint: 'Sustain, utility, and tempo' }] const submit = (e: FormEvent) => { e.preventDefault(); update.mutate() } return
{profile.data.displayName.slice(0, 2).toUpperCase()}

{profile.data.displayName}

{profile.data.battleTag}

Connected via Discord

Competitive ranks

Select your current Overwatch rank for each role.

Updated {new Date(profile.data.ratings.updatedAt).toLocaleDateString()}
{roles.map((role) => )}

Preferred roles

Choose every role you enjoy playing.

{roles.map((role) => )}

Preferred teammates

Choose up to 3 players. The balancer treats these as preferences, not guarantees.

{players.data.filter((player) => player.id !== profile.data.id).map((player) => { const selected = preferredPlayerIds.includes(player.id); const disabled = !selected && preferredPlayerIds.length >= 3; return })}

Only you can edit your ranks and preferences.{update.isError && ` ${update.error.message}`}

} type AdminTab = 'events' | 'participants' | 'balance' function AdminPage() { const [tab, setTab] = useState('events') const [createMode, setCreateMode] = useState(false) const events = useQuery({ queryKey: ['events'], queryFn: api.events, initialData: demoMode ? demoEvents : undefined }) const [selectedId, setSelectedId] = useState() const selected = events.data?.find((event) => event.id === selectedId) ?? events.data?.[0] useEffect(() => { if (!selectedId && events.data?.[0]) setSelectedId(events.data[0].id) }, [events.data, selectedId]) if (events.isLoading) return if (events.isError) return void events.refetch()} /> return
{events.data && events.data.length > 0 && }} />
{(['events', 'participants', 'balance'] as AdminTab[]).map((item) => )}
{tab === 'events' && { setCreateMode(false); setSelectedId(event.id) }} />}{tab === 'participants' && selected && }{tab === 'balance' && selected && }
} function toLocalInput(value: string) { const date = new Date(value) return Number.isNaN(date.getTime()) ? '' : new Date(date.getTime() - date.getTimezoneOffset() * 60_000).toISOString().slice(0, 16) } function eventInput(event?: MixEvent): EventInput { const start = new Date(Date.now() + 86_400_000) const end = new Date(start.getTime() + 4 * 3_600_000) const deadline = new Date(start.getTime() - 3_600_000) return { name: event?.title ?? '', description: event?.description ?? '', startsAt: toLocalInput(event?.startsAt ?? start.toISOString()), endsAt: toLocalInput(event?.endsAt ?? end.toISOString()), registrationDeadline: toLocalInput(event?.registrationDeadline ?? deadline.toISOString()), } } function EventSetup({ event, onCreated }: { event?: MixEvent; onCreated: (event: MixEvent) => void }) { const client = useQueryClient() const initialForm = useMemo(() => eventInput(event), [event]) const [form, setForm] = useState(initialForm) useEffect(() => setForm(initialForm), [initialForm]) const save = useMutation({ mutationFn: () => { const payload = { ...form, startsAt: new Date(form.startsAt).toISOString(), endsAt: new Date(form.endsAt).toISOString(), registrationDeadline: new Date(form.registrationDeadline).toISOString() } return event ? api.updateEvent(event.id, payload) : api.createEvent(payload) }, onSuccess: (saved) => { void client.invalidateQueries({ queryKey: ['events'] }) onCreated(saved) }, }) const set = (key: keyof EventInput, value: string) => setForm((current) => ({ ...current, [key]: value })) return
{ e.preventDefault(); save.mutate() }}>

{event ? 'Event settings' : 'New event'}

Scheduling and registration

{event && Published}