Add team renaming functionality to API and frontend. Implement backend logic for renaming teams, including validation and synchronization with event rosters. Update frontend components to allow team name editing and ensure proper state management. Enhance translations and styles for new features.
This commit is contained in:
@@ -96,6 +96,7 @@ func New(service *application.Service, store application.Store, hub *realtime.Hu
|
|||||||
api.Post("/api/events/{eventID}/workflow/back", s.revertWorkflowStage)
|
api.Post("/api/events/{eventID}/workflow/back", s.revertWorkflowStage)
|
||||||
api.Post("/api/events/{eventID}/start", s.startScrim)
|
api.Post("/api/events/{eventID}/start", s.startScrim)
|
||||||
api.Put("/api/teams/{teamID}/captain", s.assignCaptain)
|
api.Put("/api/teams/{teamID}/captain", s.assignCaptain)
|
||||||
|
api.Put("/api/teams/{teamID}/name", s.renameTeam)
|
||||||
api.Post("/api/rulesets", s.saveRuleset)
|
api.Post("/api/rulesets", s.saveRuleset)
|
||||||
api.Get("/api/rulesets", s.listRulesets)
|
api.Get("/api/rulesets", s.listRulesets)
|
||||||
api.Get("/api/rulesets/{id}", s.getRuleset)
|
api.Get("/api/rulesets/{id}", s.getRuleset)
|
||||||
@@ -379,6 +380,18 @@ func (s *Server) assignCaptain(w http.ResponseWriter, r *http.Request) {
|
|||||||
respond(w, out, err, 200)
|
respond(w, out, err, 200)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) renameTeam(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var in struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
if !decode(w, r, &in) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id := who(r)
|
||||||
|
out, err := s.service.RenameTeam(r.Context(), id.account, id.player, chi.URLParam(r, "teamID"), in.Name)
|
||||||
|
respond(w, out, err, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) saveRuleset(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) saveRuleset(w http.ResponseWriter, r *http.Request) {
|
||||||
if err := requireStaff(r); err != nil {
|
if err := requireStaff(r); err != nil {
|
||||||
writeError(w, err)
|
writeError(w, err)
|
||||||
|
|||||||
@@ -476,6 +476,52 @@ func (s *Store) ListTeams(ctx context.Context, eventID string) ([]domain.Team, e
|
|||||||
return out, rows.Err()
|
return out, rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Store) GetTeam(ctx context.Context, teamID string) (domain.Team, error) {
|
||||||
|
var team domain.Team
|
||||||
|
var body []byte
|
||||||
|
err := s.pool.QueryRow(ctx, `SELECT id,event_id,name,COALESCE(captain_player_id,''),slots FROM teams WHERE id=$1`, teamID).
|
||||||
|
Scan(&team.ID, &team.EventID, &team.Name, &team.CaptainPlayerID, &body)
|
||||||
|
if err == nil {
|
||||||
|
err = json.Unmarshal(body, &team.Slots)
|
||||||
|
}
|
||||||
|
return team, mapError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) RenameTeam(ctx context.Context, teamID, name string) (domain.Team, error) {
|
||||||
|
tx, err := s.pool.Begin(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return domain.Team{}, err
|
||||||
|
}
|
||||||
|
defer tx.Rollback(ctx)
|
||||||
|
var eventID string
|
||||||
|
if err = tx.QueryRow(ctx, `UPDATE teams SET name=$2 WHERE id=$1 RETURNING event_id`, teamID, name).Scan(&eventID); err != nil {
|
||||||
|
return domain.Team{}, mapError(err)
|
||||||
|
}
|
||||||
|
var rosterBody []byte
|
||||||
|
err = tx.QueryRow(ctx, `SELECT body FROM event_rosters WHERE event_id=$1`, eventID).Scan(&rosterBody)
|
||||||
|
if err == nil {
|
||||||
|
var roster domain.RosterDraft
|
||||||
|
if err = json.Unmarshal(rosterBody, &roster); err != nil {
|
||||||
|
return domain.Team{}, err
|
||||||
|
}
|
||||||
|
for index := range roster.Teams {
|
||||||
|
if roster.Teams[index].ID == teamID {
|
||||||
|
roster.Teams[index].Name = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rosterBody, _ = json.Marshal(roster)
|
||||||
|
if _, err = tx.Exec(ctx, `UPDATE event_rosters SET body=$2,updated_at=now() WHERE event_id=$1`, eventID, rosterBody); err != nil {
|
||||||
|
return domain.Team{}, err
|
||||||
|
}
|
||||||
|
} else if !errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return domain.Team{}, err
|
||||||
|
}
|
||||||
|
if err = tx.Commit(ctx); err != nil {
|
||||||
|
return domain.Team{}, err
|
||||||
|
}
|
||||||
|
return s.GetTeam(ctx, teamID)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Store) AssignCaptain(ctx context.Context, teamID, playerID string) (domain.Team, error) {
|
func (s *Store) AssignCaptain(ctx context.Context, teamID, playerID string) (domain.Team, error) {
|
||||||
var eventID string
|
var eventID string
|
||||||
if err := s.pool.QueryRow(ctx, `SELECT event_id FROM teams WHERE id=$1 AND slots @> $2::jsonb`, teamID, fmt.Sprintf(`[{"playerId":%q}]`, playerID)).Scan(&eventID); err != nil {
|
if err := s.pool.QueryRow(ctx, `SELECT event_id FROM teams WHERE id=$1 AND slots @> $2::jsonb`, teamID, fmt.Sprintf(`[{"playerId":%q}]`, playerID)).Scan(&eventID); err != nil {
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ type Store interface {
|
|||||||
DeleteRSVP(context.Context, string, string) error
|
DeleteRSVP(context.Context, string, string) error
|
||||||
SaveTeams(context.Context, string, []domain.Team) error
|
SaveTeams(context.Context, string, []domain.Team) error
|
||||||
ListTeams(context.Context, string) ([]domain.Team, error)
|
ListTeams(context.Context, string) ([]domain.Team, error)
|
||||||
|
GetTeam(context.Context, string) (domain.Team, error)
|
||||||
|
RenameTeam(context.Context, string, string) (domain.Team, error)
|
||||||
AssignCaptain(context.Context, string, string) (domain.Team, error)
|
AssignCaptain(context.Context, string, string) (domain.Team, error)
|
||||||
SaveRuleset(context.Context, domain.Ruleset) (domain.Ruleset, error)
|
SaveRuleset(context.Context, domain.Ruleset) (domain.Ruleset, error)
|
||||||
ListRulesets(context.Context) ([]domain.Ruleset, error)
|
ListRulesets(context.Context) ([]domain.Ruleset, error)
|
||||||
@@ -339,6 +341,34 @@ func (s *Service) AssignCaptain(ctx context.Context, actor domain.Account, teamI
|
|||||||
return team, err
|
return team, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) RenameTeam(ctx context.Context, actor domain.Account, player domain.Player, teamID, name string) (domain.Team, error) {
|
||||||
|
team, err := s.Store.GetTeam(ctx, teamID)
|
||||||
|
if err != nil {
|
||||||
|
return team, err
|
||||||
|
}
|
||||||
|
event, err := s.Store.GetEvent(ctx, team.EventID)
|
||||||
|
if err != nil {
|
||||||
|
return team, err
|
||||||
|
}
|
||||||
|
if event.State != domain.Live {
|
||||||
|
return team, fmt.Errorf("%w: team names can only be changed during a live scrim", domain.ErrConflict)
|
||||||
|
}
|
||||||
|
if !CanActForTeam(actor, player, team) {
|
||||||
|
return team, domain.ErrForbidden
|
||||||
|
}
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
if len([]rune(name)) < 2 || len([]rune(name)) > 32 {
|
||||||
|
return team, fmt.Errorf("%w: team name must contain 2 to 32 characters", domain.ErrInvalid)
|
||||||
|
}
|
||||||
|
team, err = s.Store.RenameTeam(ctx, teamID, name)
|
||||||
|
if err == nil {
|
||||||
|
_ = s.Store.AppendAudit(ctx, actor.ID, "team.renamed", teamID, map[string]string{"name": name})
|
||||||
|
s.Bus.Publish("team:"+teamID, team)
|
||||||
|
s.Bus.Publish("event:"+team.EventID, team)
|
||||||
|
}
|
||||||
|
return team, err
|
||||||
|
}
|
||||||
|
|
||||||
func CanActForTeam(actor domain.Account, player domain.Player, team domain.Team) bool {
|
func CanActForTeam(actor domain.Account, player domain.Player, team domain.Team) bool {
|
||||||
return actor.IsStaff() || (team.CaptainPlayerID != "" && team.CaptainPlayerID == player.ID)
|
return actor.IsStaff() || (team.CaptainPlayerID != "" && team.CaptainPlayerID == player.ID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
import { QueryClient, QueryClientProvider, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
import { QueryClient, QueryClientProvider, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
import {
|
import {
|
||||||
Link, Outlet, RouterProvider, createRootRouteWithContext, createRoute,
|
Link, Outlet, RouterProvider, createRootRouteWithContext, createRoute,
|
||||||
createRouter, redirect,
|
createRouter, redirect, useRouterState,
|
||||||
} from '@tanstack/react-router'
|
} from '@tanstack/react-router'
|
||||||
import {
|
import {
|
||||||
api, subscribeToEvents, type EventInput, type MapOutcome, type MixEvent,
|
api, subscribeToEvents, type EventInput, type MapOutcome, type MixEvent,
|
||||||
@@ -81,6 +81,7 @@ function AppShell() {
|
|||||||
void client.invalidateQueries({ queryKey: ['tournament'] })
|
void client.invalidateQueries({ queryKey: ['tournament'] })
|
||||||
} else if (scope === 'team') {
|
} else if (scope === 'team') {
|
||||||
void client.invalidateQueries({ queryKey: ['teams'] })
|
void client.invalidateQueries({ queryKey: ['teams'] })
|
||||||
|
void client.invalidateQueries({ queryKey: ['series'] })
|
||||||
} else if (message.topic === 'events') {
|
} else if (message.topic === 'events') {
|
||||||
void client.invalidateQueries({ queryKey: ['events'] })
|
void client.invalidateQueries({ queryKey: ['events'] })
|
||||||
} else if (message.topic === 'players') {
|
} else if (message.topic === 'players') {
|
||||||
@@ -645,9 +646,31 @@ function LiveSeriesPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Scoreboard({ series }: { series: Series }) {
|
function Scoreboard({ series }: { series: Series }) {
|
||||||
|
const controlPage = useRouterState({ select: (state) => state.location.pathname.startsWith('/series/') })
|
||||||
|
const session = useQuery({ queryKey: ['session'], queryFn: api.session, enabled: controlPage, staleTime: Infinity })
|
||||||
const alphaCaptain = series.teamAlpha.members.find(({ player }) => player.id === series.teamAlpha.captainId)?.player.displayName
|
const alphaCaptain = series.teamAlpha.members.find(({ player }) => player.id === series.teamAlpha.captainId)?.player.displayName
|
||||||
const betaCaptain = series.teamBeta.members.find(({ player }) => player.id === series.teamBeta.captainId)?.player.displayName
|
const betaCaptain = series.teamBeta.members.find(({ player }) => player.id === series.teamBeta.captainId)?.player.displayName
|
||||||
return <section className="scoreboard" aria-label="Current series score"><div className="score-team alpha"><span>Team Alpha</span><h2>{series.teamAlpha.name}</h2><small>{alphaCaptain ? `Captain · ${alphaCaptain}` : 'Captain not assigned'}</small></div><div className="series-score"><span>Best of 3</span><strong>{series.score.alpha}<i>:</i>{series.score.beta}</strong><Badge tone="live"><Radio />{series.status}</Badge></div><div className="score-team beta"><span>Team Beta</span><h2>{series.teamBeta.name}</h2><small>{betaCaptain ? `Captain · ${betaCaptain}` : 'Captain not assigned'}</small></div></section>
|
const canRenameAlpha = controlPage && (isStaff(session.data?.account.role) || session.data?.player.id === series.teamAlpha.captainId)
|
||||||
|
const canRenameBeta = controlPage && (isStaff(session.data?.account.role) || session.data?.player.id === series.teamBeta.captainId)
|
||||||
|
return <><section className="scoreboard" aria-label="Current series score"><div className="score-team alpha"><span>Team Alpha</span><h2>{series.teamAlpha.name}</h2><small>{alphaCaptain ? `Captain · ${alphaCaptain}` : 'Captain not assigned'}</small></div><div className="series-score"><span>Best of 3</span><strong>{series.score.alpha}<i>:</i>{series.score.beta}</strong><Badge tone="live"><Radio />{series.status}</Badge></div><div className="score-team beta"><span>Team Beta</span><h2>{series.teamBeta.name}</h2><small>{betaCaptain ? `Captain · ${betaCaptain}` : 'Captain not assigned'}</small></div></section>{(canRenameAlpha || canRenameBeta) && <div className="team-name-controls">{canRenameAlpha && <TeamNameEditor team={series.teamAlpha} seriesId={series.id} />}{canRenameBeta && <TeamNameEditor team={series.teamBeta} seriesId={series.id} />}</div>}</>
|
||||||
|
}
|
||||||
|
|
||||||
|
function TeamNameEditor({ team, seriesId }: { team: Team; seriesId: string }) {
|
||||||
|
const client = useQueryClient()
|
||||||
|
const [name, setName] = useState(team.name)
|
||||||
|
useEffect(() => setName(team.name), [team.name])
|
||||||
|
const rename = useMutation({
|
||||||
|
mutationFn: () => api.renameTeam(team.id, name),
|
||||||
|
onSuccess: (updated) => {
|
||||||
|
client.setQueryData<Series>(['series', seriesId], (current) => current ? {
|
||||||
|
...current,
|
||||||
|
teamAlpha: current.teamAlpha.id === updated.id ? { ...current.teamAlpha, name: updated.name } : current.teamAlpha,
|
||||||
|
teamBeta: current.teamBeta.id === updated.id ? { ...current.teamBeta, name: updated.name } : current.teamBeta,
|
||||||
|
} : current)
|
||||||
|
void client.invalidateQueries({ queryKey: ['teams'] })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return <form className="team-name-editor" onSubmit={(event) => { event.preventDefault(); rename.mutate() }}><label><span>Team name</span><input minLength={2} maxLength={32} required value={name} onChange={(event) => setName(event.target.value)} /></label><button className="button secondary" disabled={rename.isPending || name.trim() === team.name}>{rename.isPending ? 'Saving…' : 'Rename'}</button>{rename.isError && <small className="error-note">{rename.error.message}</small>}</form>
|
||||||
}
|
}
|
||||||
function MapTimeline({ series }: { series: Series }) { return <section className="card map-timeline"><div className="section-title"><div><h2>Map timeline</h2><p>First to 2 wins</p></div></div>{series.maps.length === 0 ? <p className="empty-note">No results recorded.</p> : series.maps.map((map) => <div className={`map-row ${map.status}`} key={map.number}><span>0{map.number}</span><div><strong>{map.name}</strong><small>{map.mode} · {map.status}</small></div>{map.winner && <Badge tone={map.winner === 'alpha' ? 'warning' : 'blue'}>{map.winner === 'alpha' ? 'A' : map.winner === 'beta' ? 'B' : 'Draw'}</Badge>}</div>)}</section> }
|
function MapTimeline({ series }: { series: Series }) { return <section className="card map-timeline"><div className="section-title"><div><h2>Map timeline</h2><p>First to 2 wins</p></div></div>{series.maps.length === 0 ? <p className="empty-note">No results recorded.</p> : series.maps.map((map) => <div className={`map-row ${map.status}`} key={map.number}><span>0{map.number}</span><div><strong>{map.name}</strong><small>{map.mode} · {map.status}</small></div>{map.winner && <Badge tone={map.winner === 'alpha' ? 'warning' : 'blue'}>{map.winner === 'alpha' ? 'A' : map.winner === 'beta' ? 'B' : 'Draw'}</Badge>}</div>)}</section> }
|
||||||
function AuditLog({ series }: { series: Series }) { return <section className="card audit-log"><div className="section-title"><div><h2>Match history</h2><p>Auditable server actions</p></div></div>{series.audit.length === 0 ? <p className="empty-note">No actions yet.</p> : series.audit.slice(-4).reverse().map((a) => <div key={a.id}><span className="history-dot" /><p><strong>{a.summary}</strong><small>{a.actorName} · {new Date(a.createdAt).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })}</small></p></div>)}</section> }
|
function AuditLog({ series }: { series: Series }) { return <section className="card audit-log"><div className="section-title"><div><h2>Match history</h2><p>Auditable server actions</p></div></div>{series.audit.length === 0 ? <p className="empty-note">No actions yet.</p> : series.audit.slice(-4).reverse().map((a) => <div key={a.id}><span className="history-dot" /><p><strong>{a.summary}</strong><small>{a.actorName} · {new Date(a.createdAt).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })}</small></p></div>)}</section> }
|
||||||
|
|||||||
@@ -120,4 +120,17 @@ describe('actual backend routes', () => {
|
|||||||
body: JSON.stringify({ moderator: true }),
|
body: JSON.stringify({ moderator: true }),
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('renames a live team through its team route', async () => {
|
||||||
|
const team = { id: 'team-a', eventId: 'event-1', name: 'Ember Wolves', captainPlayerId: 'player-1', slots: [] }
|
||||||
|
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(JSON.stringify(team), {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
}))
|
||||||
|
await api.renameTeam(team.id, team.name)
|
||||||
|
expect(fetchMock).toHaveBeenCalledWith('/api/teams/team-a/name', expect.objectContaining({
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify({ name: 'Ember Wolves' }),
|
||||||
|
}))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -615,6 +615,8 @@ export const api = {
|
|||||||
},
|
},
|
||||||
assignCaptain: async (teamId: string, playerId: string) =>
|
assignCaptain: async (teamId: string, playerId: string) =>
|
||||||
adaptTeam(await request<RawTeam>(`/teams/${teamId}/captain`, { method: 'PUT', body: JSON.stringify({ playerId }) }), new Map()),
|
adaptTeam(await request<RawTeam>(`/teams/${teamId}/captain`, { method: 'PUT', body: JSON.stringify({ playerId }) }), new Map()),
|
||||||
|
renameTeam: (teamId: string, name: string) =>
|
||||||
|
request<RawTeam>(`/teams/${teamId}/name`, { method: 'PUT', body: JSON.stringify({ name }) }),
|
||||||
series: async (seriesId: string) => {
|
series: async (seriesId: string) => {
|
||||||
const series = await request<RawSeries>(`/series/${seriesId}`)
|
const series = await request<RawSeries>(`/series/${seriesId}`)
|
||||||
return adaptSeriesWithTeams(series)
|
return adaptSeriesWithTeams(series)
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ const translations: Record<string, string> = {
|
|||||||
'All': 'Все',
|
'All': 'Все',
|
||||||
'Live now': 'Сейчас в эфире',
|
'Live now': 'Сейчас в эфире',
|
||||||
'The scrim has started': 'Скрим начался',
|
'The scrim has started': 'Скрим начался',
|
||||||
|
'Team name': 'Название команды',
|
||||||
|
'Rename': 'Переименовать',
|
||||||
'Watch live': 'Смотреть',
|
'Watch live': 'Смотреть',
|
||||||
'Coming up': 'Скоро',
|
'Coming up': 'Скоро',
|
||||||
'Filter events': 'Фильтровать события',
|
'Filter events': 'Фильтровать события',
|
||||||
|
|||||||
@@ -254,6 +254,7 @@ textarea { min-height: 85px; resize: vertical; }
|
|||||||
.scoreboard { min-height: 150px; display: grid; grid-template-columns: 1fr 210px 1fr; align-items: center; border: 1px solid var(--border); background: linear-gradient(90deg,rgba(74,37,13,.35),rgba(16,19,24,.9) 42% 58%,rgba(10,45,70,.35)); border-radius: 12px; overflow: hidden; }
|
.scoreboard { min-height: 150px; display: grid; grid-template-columns: 1fr 210px 1fr; align-items: center; border: 1px solid var(--border); background: linear-gradient(90deg,rgba(74,37,13,.35),rgba(16,19,24,.9) 42% 58%,rgba(10,45,70,.35)); border-radius: 12px; overflow: hidden; }
|
||||||
.score-team { padding: 25px 30px; }.score-team > span { color: var(--orange); font-size: 9px; text-transform: uppercase; letter-spacing: .18em; font-weight: 900; }.score-team h2 { margin: 5px 0; font-size: clamp(18px,2vw,28px); }.score-team small { color: var(--muted); }.score-team.beta { text-align: right; }.score-team.beta > span { color: var(--blue); }
|
.score-team { padding: 25px 30px; }.score-team > span { color: var(--orange); font-size: 9px; text-transform: uppercase; letter-spacing: .18em; font-weight: 900; }.score-team h2 { margin: 5px 0; font-size: clamp(18px,2vw,28px); }.score-team small { color: var(--muted); }.score-team.beta { text-align: right; }.score-team.beta > span { color: var(--blue); }
|
||||||
.series-score { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; border-left: 1px solid var(--border); border-right: 1px solid var(--border); }.series-score > span { color: var(--muted); text-transform: uppercase; letter-spacing: .16em; font-size: 9px; }.series-score strong { font-size: 48px; line-height: 1.25; }
|
.series-score { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; border-left: 1px solid var(--border); border-right: 1px solid var(--border); }.series-score > span { color: var(--muted); text-transform: uppercase; letter-spacing: .16em; font-size: 9px; }.series-score strong { font-size: 48px; line-height: 1.25; }
|
||||||
|
.team-name-controls { display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 12px; margin-top: 12px; }.team-name-editor { display: flex; align-items: end; gap: 8px; padding: 12px; border: 1px solid var(--border); border-radius: 9px; background: var(--surface); }.team-name-editor label { flex: 1; display: grid; gap: 5px; }.team-name-editor label span { color: var(--muted); font-size: 9px; text-transform: uppercase; letter-spacing: .08em; }.team-name-editor input { width: 100%; }.team-name-editor .error-note { align-self: center; }
|
||||||
.live-layout { display: grid; grid-template-columns: 1fr 330px; gap: 16px; margin-top: 16px; align-items: start; }
|
.live-layout { display: grid; grid-template-columns: 1fr 330px; gap: 16px; margin-top: 16px; align-items: start; }
|
||||||
.turn-card { border: 1px solid color-mix(in srgb,var(--team) 55%,#2b3038); border-top: 3px solid var(--team); border-radius: 12px; background: linear-gradient(155deg,var(--team-bg),var(--surface) 42%); overflow: hidden; }
|
.turn-card { border: 1px solid color-mix(in srgb,var(--team) 55%,#2b3038); border-top: 3px solid var(--team); border-radius: 12px; background: linear-gradient(155deg,var(--team-bg),var(--surface) 42%); overflow: hidden; }
|
||||||
.turn-header { padding: 26px; display: flex; align-items: start; justify-content: space-between; gap: 20px; border-bottom: 1px solid var(--border); }.turn-header h2 { font-size: 27px; margin: 7px 0; }.turn-header p { color: var(--muted); font-size: 12px; }
|
.turn-header { padding: 26px; display: flex; align-items: start; justify-content: space-between; gap: 20px; border-bottom: 1px solid var(--border); }.turn-header h2 { font-size: 27px; margin: 7px 0; }.turn-header p { color: var(--muted); font-size: 12px; }
|
||||||
@@ -311,6 +312,7 @@ textarea { min-height: 85px; resize: vertical; }
|
|||||||
.candidate-list { grid-template-columns: 1fr; }.teams-grid { grid-template-columns: 1fr; }.balance-score { grid-template-columns: 1.4fr 1fr 1fr; padding: 15px; gap: 10px; }.reserve-bar,.sticky-action { align-items: stretch; flex-direction: column; }.sticky-action .button { width: 100%; }
|
.candidate-list { grid-template-columns: 1fr; }.teams-grid { grid-template-columns: 1fr; }.balance-score { grid-template-columns: 1.4fr 1fr 1fr; padding: 15px; gap: 10px; }.reserve-bar,.sticky-action { align-items: stretch; flex-direction: column; }.sticky-action .button { width: 100%; }
|
||||||
.workflow-action { align-items: stretch; flex-direction: column; }.workflow-action .button { width: 100%; }.roster-editor-body { grid-template-columns: 1fr; }.roster-slot { grid-template-columns: 24px 1fr auto 30px; }
|
.workflow-action { align-items: stretch; flex-direction: column; }.workflow-action .button { width: 100%; }.roster-editor-body { grid-template-columns: 1fr; }.roster-slot { grid-template-columns: 24px 1fr auto 30px; }
|
||||||
.scoreboard { min-height: 120px; grid-template-columns: 1fr 90px 1fr; }.score-team { padding: 15px 10px; }.score-team h2 { font-size: 13px; }.score-team small { display: none; }.series-score strong { font-size: 30px; }.series-score .badge { display: none; }
|
.scoreboard { min-height: 120px; grid-template-columns: 1fr 90px 1fr; }.score-team { padding: 15px 10px; }.score-team h2 { font-size: 13px; }.score-team small { display: none; }.series-score strong { font-size: 30px; }.series-score .badge { display: none; }
|
||||||
|
.team-name-controls { grid-template-columns: 1fr; }.team-name-editor { align-items: stretch; flex-direction: column; }
|
||||||
.turn-header { padding: 20px; }.turn-header h2 { font-size: 23px; }.draft-options { grid-template-columns: repeat(2,1fr); padding: 14px; }.turn-footer { align-items: stretch; flex-direction: column; }.turn-footer .button { width: 100%; }
|
.turn-header { padding: 20px; }.turn-header h2 { font-size: 23px; }.draft-options { grid-template-columns: repeat(2,1fr); padding: 14px; }.turn-footer { align-items: stretch; flex-direction: column; }.turn-footer .button { width: 100%; }
|
||||||
.result-form { grid-template-columns: 1fr; }
|
.result-form { grid-template-columns: 1fr; }
|
||||||
.ban-display { grid-template-columns: repeat(2,1fr); }.spectator-focus { padding: 24px 14px; }.spectator-grid { grid-template-columns: 1fr; }
|
.ban-display { grid-template-columns: repeat(2,1fr); }.spectator-focus { padding: 24px 14px; }.spectator-grid { grid-template-columns: 1fr; }
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
|
|
||||||
### Team
|
### Team
|
||||||
|
|
||||||
Состав игроков, назначенный Admin капитан, цвет/название и рассчитанные показатели силы. Капитан должен входить в текущий состав команды; переназначение фиксируется в аудите.
|
Состав игроков, назначенный staff-пользователем капитан, цвет/название и рассчитанные показатели силы. Капитан должен входить в текущий состав команды; переназначение фиксируется в аудите. После запуска скрима капитан может задать своей команде отображаемое название; staff может переименовать любую команду, изменение синхронизируется с live- и spectator-экранами.
|
||||||
|
|
||||||
- общий средний рейтинг;
|
- общий средний рейтинг;
|
||||||
- средний рейтинг по каждой роли;
|
- средний рейтинг по каждой роли;
|
||||||
|
|||||||
@@ -559,6 +559,25 @@ paths:
|
|||||||
$ref: "#/components/schemas/Team"
|
$ref: "#/components/schemas/Team"
|
||||||
"422":
|
"422":
|
||||||
$ref: "#/components/responses/ValidationError"
|
$ref: "#/components/responses/ValidationError"
|
||||||
|
/api/teams/{teamId}/name:
|
||||||
|
put:
|
||||||
|
tags: [Teams]
|
||||||
|
operationId: renameLiveTeam
|
||||||
|
parameters:
|
||||||
|
- $ref: "#/components/parameters/TeamId"
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required: [name]
|
||||||
|
properties:
|
||||||
|
name: { type: string, minLength: 2, maxLength: 32 }
|
||||||
|
responses:
|
||||||
|
"200": { description: Team renamed, content: { application/json: { schema: { $ref: "#/components/schemas/Team" } } } }
|
||||||
|
"403": { $ref: "#/components/responses/Forbidden" }
|
||||||
|
"409": { $ref: "#/components/responses/Conflict" }
|
||||||
/api/coin-toss:
|
/api/coin-toss:
|
||||||
post:
|
post:
|
||||||
tags: [Series]
|
tags: [Series]
|
||||||
|
|||||||
Reference in New Issue
Block a user