Skip to content

Commit 45a580e

Browse files
authored
bug: the standings and the compact tournament card order a finished final by its placements, like the podium (#836)
1 parent 8c99ec1 commit 45a580e

10 files changed

Lines changed: 573 additions & 31 deletions

‎components/events/EventOverview.vue‎

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import type { Clip } from "~/types/clip";
1919
import { $ } from "~/generated/zeus";
2020
import { typedGql } from "~/generated/zeus/typedDocumentNode";
2121
import { matchClipFields, topPlayOrderBy } from "~/graphql/matchClip";
22+
import { tournamentPodiumQuery } from "~/graphql/tournamentPodium";
2223
import {
2324
daysUntil,
2425
formatEventRange,
@@ -213,10 +214,49 @@ const liveTournaments = computed(() =>
213214
(tournament: any) => tournamentStatusVariant(tournament.status) === "live",
214215
),
215216
);
217+
// Fetched once per finished tournament rather than selected in the event
218+
// subscription, which Hasura re-runs every second for every viewer.
219+
const podiums = ref<Record<string, unknown>>({});
220+
watch(
221+
() =>
222+
tournaments.value
223+
.filter(
224+
(tournament: any) =>
225+
tournament.status === "Finished" && !(tournament.id in podiums.value),
226+
)
227+
.map((tournament: any) => tournament.id as string),
228+
(ids) => {
229+
for (const id of ids) {
230+
podiums.value = { ...podiums.value, [id]: null };
231+
apolloClient
232+
.query({
233+
query: tournamentPodiumQuery,
234+
variables: { tournamentId: id },
235+
context: { optional: true },
236+
})
237+
.then(({ data }) => {
238+
podiums.value = {
239+
...podiums.value,
240+
[id]: (data as any)?.tournaments_by_pk?.podium ?? null,
241+
};
242+
})
243+
.catch(() => {});
244+
}
245+
},
246+
{ immediate: true },
247+
);
248+
216249
const otherTournaments = computed(() =>
217-
tournaments.value.filter(
218-
(tournament: any) => tournamentStatusVariant(tournament.status) !== "live",
219-
),
250+
tournaments.value
251+
.filter(
252+
(tournament: any) =>
253+
tournamentStatusVariant(tournament.status) !== "live",
254+
)
255+
.map((tournament: any) =>
256+
podiums.value[tournament.id]
257+
? { ...tournament, podium: podiums.value[tournament.id] }
258+
: tournament,
259+
),
220260
);
221261
222262
// "Sign up" only while something is open; "Champions" only once every one

‎components/tournament/StageStandings.vue‎

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,11 @@ export default {
270270
type: Array,
271271
default: () => [],
272272
},
273+
// The finished tournament's placements, for its final stage only.
274+
placements: {
275+
type: Array,
276+
default: () => [],
277+
},
273278
},
274279
data() {
275280
return {
@@ -284,6 +289,15 @@ export default {
284289
if (this.onlyGroup != null) return false;
285290
return this.forceGroupHeading || this.totalGroups > 1;
286291
},
292+
placementByTeam() {
293+
const map = new Map<string, number>();
294+
for (const step of (this.placements as any[]) || []) {
295+
if (step?.tournament_team_id && Number(step.placement) > 0) {
296+
map.set(String(step.tournament_team_id), Number(step.placement));
297+
}
298+
}
299+
return map;
300+
},
287301
statsByPlayer() {
288302
const map = new Map<string, any>();
289303
for (const s of (this.playerStats as any[]) || []) {
@@ -318,11 +332,21 @@ export default {
318332
for (const gn of numbers) {
319333
if (this.onlyGroup != null && this.onlyGroup !== gn) continue;
320334
const rows = grouped.get(gn) || [];
321-
const sorted = rows
322-
.slice()
323-
.sort(
324-
(a: any, b: any) => (Number(a.rank) || 0) - (Number(b.rank) || 0),
335+
const placementOf = (row: any) =>
336+
this.placementByTeam.get(
337+
String(row.tournament_team_id || row.team?.id),
325338
);
339+
// A placed team ranks by its placement, which is what the podium
340+
// shows; the tiebreakers only order the teams after them.
341+
const sorted = rows.slice().sort((a: any, b: any) => {
342+
const placedA = placementOf(a);
343+
const placedB = placementOf(b);
344+
if (placedA !== placedB) {
345+
return (placedA ?? Infinity) - (placedB ?? Infinity);
346+
}
347+
return (Number(a.rank) || 0) - (Number(b.rank) || 0);
348+
});
349+
const placed = sorted.filter((row: any) => placementOf(row)).length;
326350
327351
// Tied iff multiple teams share the same `placement` (RANK with ties)
328352
// from the API. `rank` itself is a deterministic ROW_NUMBER and never
@@ -336,20 +360,26 @@ export default {
336360
placementCounts.set(p, (placementCounts.get(p) || 0) + 1);
337361
}
338362
339-
const entries = sorted.map((row: any) => ({
340-
rank: Number(row.rank) || 0,
341-
teamId: row.tournament_team_id || row.team?.id,
342-
teamLinkId: row.team?.team?.id || null,
343-
teamName: this.displayTeamName(
344-
row.team,
345-
row.tournament_team_id || row.team?.id,
346-
),
347-
wins: Number(row.wins) || 0,
348-
losses: Number(row.losses) || 0,
349-
matchesPlayed: Number(row.matches_played) || 0,
350-
tied: (placementCounts.get(Number(row.placement) || 0) || 0) > 1,
351-
roster: (row.team?.roster || []).filter((m: any) => m?.player),
352-
}));
363+
const entries = sorted.map((row: any, index: number) => {
364+
const placement = placementOf(row);
365+
return {
366+
rank: placement ?? (placed ? index + 1 : Number(row.rank) || 0),
367+
teamId: row.tournament_team_id || row.team?.id,
368+
teamLinkId: row.team?.team?.id || null,
369+
teamName: this.displayTeamName(
370+
row.team,
371+
row.tournament_team_id || row.team?.id,
372+
),
373+
wins: Number(row.wins) || 0,
374+
losses: Number(row.losses) || 0,
375+
matchesPlayed: Number(row.matches_played) || 0,
376+
tied: placement
377+
? sorted.filter((other: any) => placementOf(other) === placement)
378+
.length > 1
379+
: (placementCounts.get(Number(row.placement) || 0) || 0) > 1,
380+
roster: (row.team?.roster || []).filter((m: any) => m?.player),
381+
};
382+
});
353383
out.push({
354384
number: gn,
355385
display: this.groupDisplay(gn),

‎components/tournament/TournamentCompactCard.vue‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,21 @@ function teamNameForResult(row: any): string | null {
102102
);
103103
}
104104
105+
// The tournament's placements first, the ones its podium and medals read; then
106+
// a placement award they don't name.
105107
const podium = computed(() => {
106108
const awards = (props.tournament?.awards || []) as any[];
107109
const byPlacement: Record<number, string | null> = {};
110+
for (const step of (props.tournament?.podium || []) as any[]) {
111+
const placement = Number(step.placement);
112+
if (placement >= 1 && placement <= 3 && step.name) {
113+
byPlacement[placement] = byPlacement[placement]
114+
? `${byPlacement[placement]} · ${step.name}`
115+
: step.name;
116+
}
117+
}
108118
for (const t of awards) {
109-
if (t.placement >= 1 && t.placement <= 3) {
119+
if (t.placement >= 1 && t.placement <= 3 && !byPlacement[t.placement]) {
110120
byPlacement[t.placement] = teamNameForAward(t);
111121
}
112122
}

‎components/tournament/TournamentQuickLook.vue‎

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import {
1111
} from "lucide-vue-next";
1212
import { Button } from "~/components/ui/button";
1313
import { typedGql } from "~/generated/zeus/typedDocumentNode";
14-
import { $, order_by } from "~/generated/zeus";
14+
import { $, e_tournament_status_enum, order_by } from "~/generated/zeus";
15+
import { tournamentPodiumQuery } from "~/graphql/tournamentPodium";
1516
import { useAuthStore } from "~/stores/AuthStore";
1617
import { useApplicationSettingsStore } from "~/stores/ApplicationSettings";
1718
import { loginLinks } from "~/utilities/loginLinks";
@@ -72,6 +73,15 @@ const { result } = useQuery(
7273
() => ({ id: props.tournament.id }),
7374
);
7475
76+
const { result: podiumResult } = useQuery(
77+
tournamentPodiumQuery,
78+
() => ({ tournamentId: props.tournament.id }),
79+
() => ({
80+
enabled: props.tournament.status === e_tournament_status_enum.Finished,
81+
context: { optional: true },
82+
}),
83+
);
84+
7585
const details = computed(() => (result.value as any)?.tournaments_by_pk);
7686
const event = computed(() =>
7787
useApplicationSettingsStore().eventsEnabled
@@ -133,8 +143,24 @@ const requirements = computed(() => {
133143
return list;
134144
});
135145
136-
// Final stage's standings, best first.
146+
const podiumSteps = computed<any[]>(
147+
() => (podiumResult.value as any)?.tournaments_by_pk?.podium ?? [],
148+
);
149+
150+
// The tournament's placements, which the podium reads; the final stage's
151+
// standings when the api has none.
137152
const podium = computed(() => {
153+
const placed = podiumSteps.value
154+
.filter((step) => step.name && step.placement >= 1 && step.placement <= 3)
155+
.map((step) => ({
156+
key: `${step.placement}-${step.tournament_team_id}`,
157+
rank: Number(step.placement),
158+
name: step.name,
159+
}))
160+
.sort((a, b) => a.rank - b.rank);
161+
if (placed.length) {
162+
return placed;
163+
}
138164
const stages = [...(props.tournament.stages || [])].sort(
139165
(a: any, b: any) => (b.order ?? 0) - (a.order ?? 0),
140166
);
@@ -143,6 +169,7 @@ const podium = computed(() => {
143169
.sort((a: any, b: any) => a.rank - b.rank)
144170
.slice(0, 3)
145171
.map((row: any) => ({
172+
key: String(row.rank),
146173
rank: row.rank,
147174
name: row.team?.team?.name || row.team?.name,
148175
}));
@@ -346,7 +373,7 @@ const section = "grid gap-2.5 border-t border-border/65 pt-4";
346373
<ol class="m-0 grid list-none gap-1 p-0">
347374
<li
348375
v-for="row in podium"
349-
:key="row.rank"
376+
:key="row.key"
350377
class="grid grid-cols-[2.25rem_minmax(0,1fr)] items-center gap-2 py-1 text-sm"
351378
>
352379
<span

‎components/tournament/TournamentResults.vue‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,7 @@ function playerAvatarSrc(player: {
569569
<StageStandings
570570
:stage="stage"
571571
:player-stats="tournamentPlayerStats"
572+
:placements="stage.id === finalStageId ? podiumSteps || [] : []"
572573
/>
573574
</CardContent>
574575
</Card>
@@ -586,6 +587,7 @@ function playerAvatarSrc(player: {
586587
import { $, e_tournament_status_enum } from "~/generated/zeus";
587588
import { typedGql } from "~/generated/zeus/typedDocumentNode";
588589
import { TIER_PALETTES, resolveAwardTier } from "~/utilities/awardSeed";
590+
import { tournamentPodiumQuery } from "~/graphql/tournamentPodium";
589591
590592
export default {
591593
props: {
@@ -609,12 +611,7 @@ export default {
609611
// Its own optional query, so an api without the podium field only loses
610612
// this and falls back to the podium built from the awards.
611613
podiumSteps: {
612-
query: typedGql("query")({
613-
tournaments_by_pk: [
614-
{ id: $("tournamentId", "uuid!") },
615-
{ id: true, podium: true } as any,
616-
],
617-
}),
614+
query: tournamentPodiumQuery,
618615
variables: function () {
619616
return { tournamentId: (this as any).tournament?.id };
620617
},
@@ -822,6 +819,10 @@ export default {
822819
const stages = (this.tournament as any)?.stages || [];
823820
return stages.length ? stages[stages.length - 1]?.type || null : null;
824821
},
822+
finalStageId() {
823+
const stages = (this.tournament as any)?.stages || [];
824+
return stages.length ? stages[stages.length - 1]?.id || null : null;
825+
},
825826
// The tournament's placements, plus any placement award granted to a team
826827
// they don't name (an organizer's own 3rd); the awards alone when the api
827828
// has no placements to give.

‎graphql/tournamentPodium.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { $ } from "~/generated/zeus";
2+
import { typedGql } from "~/generated/zeus/typedDocumentNode";
3+
4+
// The finished tournament's placements (api#588). Run with
5+
// `context: { optional: true }`, so an api without the field only loses them.
6+
export const tournamentPodiumQuery = typedGql("query")({
7+
tournaments_by_pk: [
8+
{ id: $("tournamentId", "uuid!") },
9+
{ id: true, podium: true } as any,
10+
],
11+
});

0 commit comments

Comments
 (0)