Implement account management features in API, including endpoints for listing accounts and updating account roles. Introduce moderator role with associated permissions, and refactor access control checks to accommodate staff roles. Update database schema to support new role constraints and enhance frontend navigation for staff access.
functionLoadingState({label='Loading match data…'}:{label?: string}){return<divclassName="state-card"role="status"><RefreshCwclassName="spin"/><h2>{label}</h2><p>Syncingwiththeserver.</p></div>}
functionErrorState({retry}:{retry?:()=>void}){return<divclassName="state-card"role="alert"><CircleHelp/><h2>Couldn’tloadthisview</h2><p>Your data issafe. Check your connection andtry again.</p>{retry&&<buttonclassName="button secondary"onClick={retry}>Tryagain</button>}</div>}
functionErrorState({retry,message}:{retry?:()=>void;message?: string}){return<divclassName="state-card"role="alert"><CircleHelp/><h2>Couldn’tloadthisview</h2><p>{message||'Yourdata is safe. Checkyourconnection and try again.'}</p>{retry&&<buttonclassName="button secondary"onClick={retry}>Tryagain</button>}</div>}
<PageHeadereyebrow="Scrim schedule"title="Your events"description="Join a mix, check the roster, and get match-ready."actions={<divclassName="segmented"><buttonclassName={filter==='upcoming'?'active':''}onClick={()=>setFilter('upcoming')}>Upcoming</button><buttonclassName={filter==='all'?'active':''}onClick={()=>setFilter('all')}>All</button></div>}/>
<sectionclassName="card event-hero"><divclassName="event-time"><CalendarDays/><div><span>Starts</span><strong>{newDate(event.startsAt).toLocaleString(undefined,{weekday:'long',month:'long',day:'numeric',hour:'2-digit',minute:'2-digit'})}</strong></div></div><divclassName="event-time"><Clock3/><div><span>Registrationcloses</span><strong>{newDate(event.registrationDeadline).toLocaleString(undefined,{weekday:'short',hour:'2-digit',minute:'2-digit'})}</strong></div></div><divclassName="divider"/><h2>Areyouplaying?</h2><p>{event.workflowState==='RegistrationOpen'?'Your latest response is sent to the organizer.':'Registration has been closed by the organizer.'}</p><divclassName="rsvp-control"role="group"aria-label="RSVP status">{(Object.keys(labels)asRsvpStatus[]).map((status)=><buttonkey={status}disabled={rsvpMutation.isPending||event.workflowState!=='RegistrationOpen'}className={rsvp===status?`active ${status}`:''}aria-pressed={rsvp===status}onClick={()=>rsvpMutation.mutate(status)}>{status==='going'?<Check/>:status==='maybe'?<CircleHelp/>:<X/>}{labels[status]}</button>)}</div>{rsvpMutation.isSuccess&&<divclassName="save-note"><Check/>Responsesaved·Serverconfirmedjustnow</div>}{rsvpMutation.isError&&<pclassName="error-note">{rsvpMutation.error.message}</p>}</section>
return<divclassName="page narrow"><PageHeadereyebrow="Player profile"title="Set your competitive ranks"description="Your role ranks help the server build fair, role-locked teams."/><sectionclassName="profile-banner card"><divclassName="large-avatar">{profile.data.displayName.slice(0,2).toUpperCase()}</div><div><h2>{profile.data.displayName}</h2><p>{profile.data.battleTag}</p><Badgetone="blue"><Gamepad2/>ConnectedviaDiscord</Badge></div></section><formclassName="card ratings-form"onSubmit={submit}><divclassName="section-title"><div><h2>Competitiveranks</h2><p>SelectyourcurrentOverwatchrankforeachrole.</p></div><span>Updated{newDate(profile.data.ratings.updatedAt).toLocaleDateString()}</span></div>{roles.map((role)=><labelclassName={`rating-row role-${role.key}`}key={role.key}><div><spanclassName="role-icon">{role.key==='tank'?<Shield/>:role.key==='damage'?<Zap/>:<Sparkles/>}</span><span><strong>{role.label}</strong><small>{role.hint}</small></span></div><selectid={`rating-${role.key}`}className="rank-select"value={ratings[role.key]}onChange={(e)=>setRatings({...ratings,[role.key]:toRankOrdinal(Number(e.target.value))})}>{options.map((option)=><optionvalue={option.value}key={option.value}>{option.label}</option>)}</select></label>)}<sectionclassName="preference-section"><div><h2>Preferredroles</h2><p>Chooseeveryroleyouenjoyplaying.</p></div><divclassName="preference-chips">{roles.map((role)=><buttontype="button"aria-pressed={preferredRoles.includes(role.key)}className={preferredRoles.includes(role.key)?'active':''}key={role.key}onClick={()=>setPreferredRoles((current)=>current.includes(role.key)?current.filter((item)=>item!==role.key):[...current,role.key])}>{role.label}</button>)}</div></section><sectionclassName="preference-section"><div><h2>Preferredteammates</h2><p>Chooseupto3players.Thebalancertreatstheseaspreferences,notguarantees.</p></div><divclassName="teammate-grid">{eligiblePlayers.filter((player)=>!avoidedPlayerIds.includes(player.id)).map((player)=>{constselected=preferredPlayerIds.includes(player.id);constdisabled=!selected&&preferredPlayerIds.length>=3;return<labelclassName={selected?'selected':''}key={player.id}><inputtype="checkbox"checked={selected}disabled={disabled}onChange={()=>setPreferredPlayerIds((current)=>selected?current.filter((id)=>id!==player.id):[...current,player.id])}/><spanclassName="mini-avatar">{player.displayName.slice(0,2).toUpperCase()}</span><strong>{player.displayName}</strong></label>})}</div></section><sectionclassName="preference-section avoid-section"><div><h2>Avoidedteammates</h2><p>Chooseupto3players.Thisisasoftpreference,sobalancecanstillplaceyoutogetherwhennecessary.</p></div><divclassName="teammate-grid">{eligiblePlayers.filter((player)=>!preferredPlayerIds.includes(player.id)).map((player)=>{constselected=avoidedPlayerIds.includes(player.id);constdisabled=!selected&&avoidedPlayerIds.length>=3;return<labelclassName={selected?'selected avoided':''}key={player.id}><inputtype="checkbox"checked={selected}disabled={disabled}onChange={()=>setAvoidedPlayerIds((current)=>selected?current.filter((id)=>id!==player.id):[...current,player.id])}/><spanclassName="mini-avatar">{player.displayName.slice(0,2).toUpperCase()}</span><strong>{player.displayName}</strong></label>})}</div></section><divclassName="form-footer"><p><Shield/>Onlyyoucanedityourranksandpreferences.{update.isError&&`${update.error.message}`}</p><buttonclassName="button primary"type="submit"disabled={update.isPending}>{update.isPending?'Saving…':update.isSuccess?<><Check/>Saved</>:'Save profile'}</button></div></form></div>
return<divclassName="page"><PageHeadereyebrow="Admin control room"title={createMode?'Create event':selected?.title??'No events yet'}description={selected?`Registration closes ${newDate(selected.registrationDeadline).toLocaleString()}.`:'Create the first community event.'}actions={<>{events.data&&events.data.length>0&&<selectaria-label="Selected event"value={selected?.id??''}onChange={(e)=>{setSelectedId(e.target.value);setCreateMode(false)}}>{events.data.map((event)=><optionvalue={event.id}key={event.id}>{event.title}</option>)}</select>}<buttonclassName="button secondary"onClick={()=>{setCreateMode(true);setTab('events')}}><Sparkles/>Newevent</button></>}/><divclassName="tabs"role="tablist">{(['events','participants','balance']asAdminTab[]).map((item)=><buttonkey={item}role="tab"aria-selected={tab===item}className={tab===item?'active':''}disabled={!selected&&item!=='events'}onClick={()=>setTab(item)}>{item==='events'?'Event setup':item==='participants'?'Participants':'Balance teams'}</button>)}</div>{tab==='events'&&<EventSetupevent={createMode?undefined:selected}onCreated={(event)=>{setCreateMode(false);setSelectedId(event.id)}}onDeleted={()=>{setSelectedId(undefined);setCreateMode(false)}}/>}{tab==='participants'&&selected&&<ParticipantseventId={selected.id}/>}{tab==='balance'&&selected&&<BalanceVieweventId={selected.id}/>}</div>
if(events.isLoading||session.isLoading)return<LoadingStatelabel="Loading admin control room…"/>
return<divclassName="page"><PageHeadereyebrow={session.data?.account.role==='moderator'?'Moderation':'Admin control room'}title={tab==='moderators'?'Moderator access':createMode?'Create event':selected?.title??'No events yet'}description={tab==='moderators'?'Grant moderation access to trusted Discord users.':selected?`Registration closes ${newDate(selected.registrationDeadline).toLocaleString()}.`:'Create the first community event.'}actions={tab!=='moderators'?<>{events.data&&events.data.length>0&&<selectaria-label="Selected event"value={selected?.id??''}onChange={(e)=>{setSelectedId(e.target.value);setCreateMode(false)}}>{events.data.map((event)=><optionvalue={event.id}key={event.id}>{event.title}</option>)}</select>}<buttonclassName="button secondary"onClick={()=>{setCreateMode(true);setTab('events')}}><Sparkles/>Newevent</button></>:undefined}/><divclassName="tabs"role="tablist">{tabs.map((item)=><buttonkey={item}role="tab"aria-selected={tab===item}className={tab===item?'active':''}disabled={!selected&&item!=='events'&&item!=='moderators'}onClick={()=>setTab(item)}>{item==='events'?'Event setup':item==='participants'?'Participants':item==='balance'?'Balance teams':'Moderators'}</button>)}</div>{tab==='events'&&<EventSetupevent={createMode?undefined:selected}onCreated={(event)=>{setCreateMode(false);setSelectedId(event.id)}}onDeleted={()=>{setSelectedId(undefined);setCreateMode(false)}}/>}{tab==='participants'&&selected&&<ParticipantseventId={selected.id}/>}{tab==='balance'&&selected&&<BalanceVieweventId={selected.id}/>}{tab==='moderators'&&session.data?.account.role==='admin'&&<ModeratorManagement/>}</div>
<divclassName="form-footer"><p>{save.isError?save.error.message : cancel.isError?cancel.error.message : remove.isError?remove.error.message:'Times are sent to the server in UTC.'}</p><buttonclassName="button primary"disabled={save.isPending}>{save.isPending?'Saving…':event?'Save changes':'Create event'}</button></div>
{state==='RegistrationOpen'&&<WorkflowActiontitle="Close registration"description="Freeze the Going participant list before balancing."button="Close registration"pending={close.isPending}onClick={()=>close.mutate()}/>}
{state==='Balancing'&&candidates.length===0&&<WorkflowActiontitle="Generate balance candidates"description="Generate again after a refresh to choose one of the server candidates."button="Generate candidates"pending={generate.isPending}onClick={()=>generate.mutate()}/>}
{state==='RostersDraft'&&roster.data&&<WorkflowActiontitle="Confirm rosters"description="Every team must have a valid 1/2/2 lineup and a captain."button="Confirm rosters"pending={confirm.isPending}onClick={()=>confirm.mutate()}/>}
{state==='RostersConfirmed'&&<WorkflowActiontitle="Start scrim"description="This atomically creates the Bo3 series or tournament bracket."button="Start scrim"pending={start.isPending}onClick={()=>start.mutate()}/>}
return<divclassName="page live-page"><PageHeadereyebrow={`${data.roundLabel} · ${data.status}`}title="Series control"description="Actions are validated and recorded by the server."actions={<LinkclassName="button secondary"to="/watch/$seriesId"params={{seriesId: data.id}}><Radio/>Spectatorview</Link>}/><Scoreboardseries={data}/><divclassName="live-layout"><sectionclassName={`turn-card team-${data.currentStep.activeTeam??'alpha'}`}><divclassName="turn-header"><div><spanclassName="eyebrow">Currentstep</span><h2>{data.currentStep.title}</h2><p>{data.currentStep.instruction}</p></div>{data.currentStep.activeTeam&&<divclassName="turn-team"><span>Actingteam</span><strong>{data.currentStep.activeTeam==='alpha'?data.teamAlpha.name : data.teamBeta.name}</strong></div>}</div>{data.currentStep.kind==='result'?<formclassName="result-form"onSubmit={(e)=>{e.preventDefault();result.mutate()}}><label>Outcome<selectvalue={outcome}onChange={(e)=>setOutcome(e.target.valueasMapOutcome)}><optionvalue="TeamAWin">{data.teamAlpha.name}wins</option><optionvalue="TeamBWin">{data.teamBeta.name}wins</option><optionvalue="Draw">Draw</option></select></label><buttonclassName="button team-action"disabled={result.isPending}>{result.isPending?'Recording…':'Record result'}</button>{result.isError&&<pclassName="error-note">{result.error.message}</p>}</form>:data.currentStep.kind==='complete'?<divclassName="turn-footer"><p><Check/>Seriescompletedandbracketupdated.</p></div>:<><divclassName="draft-options"role="radiogroup"aria-label="Available draft choices">{data.options.map((option)=><buttonrole="radio"aria-checked={selected===option.id}key={option.id}disabled={option.disabled}className={selected===option.id?'selected':''}onClick={()=>setSelected(option.id)}><spanclassName={`role-chip ${option.role}`}>{option.role?.slice(0,1).toUpperCase()}</span><strong>{option.name}</strong>{option.disabled?<small>{option.disabledReason}</small>:<Check/>}</button>)}</div><divclassName="turn-footer"><p><Shield/>Draftcommandsusededicatedbackendendpointsandserver-sidevalidation.</p><buttonclassName="button team-action"disabled={(data.currentStep.kind!=='coin_toss'&&!selected)||action.isPending}onClick={()=>action.mutate()}>{action.isPending?'Confirming…':data.currentStep.kind==='coin_toss'?'Toss coin':'Confirm action'}<ChevronRight/></button></div>{action.isError&&<pclassName="error-note">{action.error.message}</p>}</>}</section><asideclassName="match-sidebar"><MapTimelineseries={data}/><AuditLogseries={data}/></aside></div></div>
'Moderators can manage events, participants, rosters, and live scrims. Only administrators can change this list.':'Модераторы могут управлять событиями, участниками, составами и скримами. Изменять этот список могут только администраторы.',
'Only administrators can change this list.':'Только администраторы могут изменять этот список.',
'← Back':'← Назад',
'Going back…':'Возвращаемся…',
'Return to the previous setup stage':'Вернуться к предыдущему этапу настройки',
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.