diff --git a/i18n/locales/ar_SA.json b/i18n/locales/ar_SA.json
index 3b5ae94e..f3e7fb22 100644
--- a/i18n/locales/ar_SA.json
+++ b/i18n/locales/ar_SA.json
@@ -9467,10 +9467,6 @@
"link_invalid": "أدخل URL صالحًا يبدأ بـ http(s)",
"link_added": "تمت إضافة الرابط"
},
- "highlights": {
- "none": "لا توجد أبرز لقطات من مباريات هذا الحدث بعد.",
- "none_for_player": "لا توجد أبرز لقطات لهذا اللاعب في هذا الحدث."
- },
"phase": {
"upcoming": "قادم",
"live": "مباشر",
diff --git a/i18n/locales/da_DK.json b/i18n/locales/da_DK.json
index 95d5a018..fdaaf880 100644
--- a/i18n/locales/da_DK.json
+++ b/i18n/locales/da_DK.json
@@ -9467,10 +9467,6 @@
"link_invalid": "Indtast en gyldig http(s)-URL",
"link_added": "Link tilføjet"
},
- "highlights": {
- "none": "Ingen highlights fra dette events kampe endnu.",
- "none_for_player": "Ingen highlights med denne spiller ved dette event."
- },
"phase": {
"upcoming": "Kommende",
"live": "Live",
diff --git a/i18n/locales/de_DE.json b/i18n/locales/de_DE.json
index 5c998b67..13a380e5 100644
--- a/i18n/locales/de_DE.json
+++ b/i18n/locales/de_DE.json
@@ -9467,10 +9467,6 @@
"link_invalid": "Gib eine gültige http(s)-URL ein",
"link_added": "Link hinzugefügt"
},
- "highlights": {
- "none": "Noch keine Highlights aus den Matches dieses Events.",
- "none_for_player": "Keine Highlights dieses Spielers bei diesem Event."
- },
"phase": {
"upcoming": "Anstehend",
"live": "Live",
diff --git a/i18n/locales/en.json b/i18n/locales/en.json
index 1c3cc250..faed271d 100644
--- a/i18n/locales/en.json
+++ b/i18n/locales/en.json
@@ -9467,10 +9467,6 @@
"link_invalid": "Enter a valid http(s) URL",
"link_added": "Link added"
},
- "highlights": {
- "none": "No highlights from this event's matches yet.",
- "none_for_player": "No highlights of this player at this event."
- },
"phase": {
"upcoming": "Upcoming",
"live": "Live",
diff --git a/i18n/locales/es_ES.json b/i18n/locales/es_ES.json
index b6f5b94a..80a759fc 100644
--- a/i18n/locales/es_ES.json
+++ b/i18n/locales/es_ES.json
@@ -9467,10 +9467,6 @@
"link_invalid": "Introduce una URL http(s) válida",
"link_added": "Enlace añadido"
},
- "highlights": {
- "none": "Todavía no hay highlights de las partidas de este evento.",
- "none_for_player": "No hay highlights de este jugador en este evento."
- },
"phase": {
"upcoming": "Próximo",
"live": "En directo",
diff --git a/i18n/locales/fr_FR.json b/i18n/locales/fr_FR.json
index d607c067..dfb6fde6 100644
--- a/i18n/locales/fr_FR.json
+++ b/i18n/locales/fr_FR.json
@@ -9467,10 +9467,6 @@
"link_invalid": "Saisis une URL http(s) valide",
"link_added": "Lien ajouté"
},
- "highlights": {
- "none": "Aucun highlight pour les matchs de cet événement pour l'instant.",
- "none_for_player": "Aucun highlight de ce joueur pour cet événement."
- },
"phase": {
"upcoming": "À venir",
"live": "En direct",
diff --git a/i18n/locales/it_IT.json b/i18n/locales/it_IT.json
index b712f116..c67b7f72 100644
--- a/i18n/locales/it_IT.json
+++ b/i18n/locales/it_IT.json
@@ -9467,10 +9467,6 @@
"link_invalid": "Inserisci un URL http(s) valido",
"link_added": "Link aggiunto"
},
- "highlights": {
- "none": "Ancora nessun highlight dalle partite di questo evento.",
- "none_for_player": "Nessun highlight di questo giocatore in questo evento."
- },
"phase": {
"upcoming": "In arrivo",
"live": "Live",
diff --git a/i18n/locales/ja_JP.json b/i18n/locales/ja_JP.json
index 2feef057..3316b122 100644
--- a/i18n/locales/ja_JP.json
+++ b/i18n/locales/ja_JP.json
@@ -9467,10 +9467,6 @@
"link_invalid": "有効な http(s) の URL を入力してください",
"link_added": "リンクを追加しました"
},
- "highlights": {
- "none": "このイベントの試合のハイライトはまだありません。",
- "none_for_player": "このイベントでのこのプレイヤーのハイライトはありません。"
- },
"phase": {
"upcoming": "開催予定",
"live": "開催中",
diff --git a/i18n/locales/ko_KR.json b/i18n/locales/ko_KR.json
index 7667b7e2..71826f87 100644
--- a/i18n/locales/ko_KR.json
+++ b/i18n/locales/ko_KR.json
@@ -9467,10 +9467,6 @@
"link_invalid": "유효한 http(s) URL을 입력하세요",
"link_added": "링크가 추가되었습니다"
},
- "highlights": {
- "none": "아직 이 이벤트 경기의 하이라이트가 없습니다.",
- "none_for_player": "이 이벤트에서 이 플레이어의 하이라이트가 없습니다."
- },
"phase": {
"upcoming": "예정",
"live": "진행 중",
diff --git a/i18n/locales/pl_PL.json b/i18n/locales/pl_PL.json
index e4d2f3a7..38ded8f0 100644
--- a/i18n/locales/pl_PL.json
+++ b/i18n/locales/pl_PL.json
@@ -9467,10 +9467,6 @@
"link_invalid": "Podaj prawidłowy adres URL http(s)",
"link_added": "Link dodany"
},
- "highlights": {
- "none": "Brak jeszcze highlightów z meczów tego wydarzenia",
- "none_for_player": "Brak highlightów tego gracza w tym wydarzeniu"
- },
"phase": {
"upcoming": "Nadchodzące",
"live": "Na żywo",
diff --git a/i18n/locales/pt_BR.json b/i18n/locales/pt_BR.json
index bdbd8df4..9d23c1c1 100644
--- a/i18n/locales/pt_BR.json
+++ b/i18n/locales/pt_BR.json
@@ -9467,10 +9467,6 @@
"link_invalid": "Insira uma URL http(s) válida",
"link_added": "Link adicionado"
},
- "highlights": {
- "none": "Ainda não há highlights das partidas deste evento.",
- "none_for_player": "Nenhum highlight deste jogador neste evento."
- },
"phase": {
"upcoming": "Em breve",
"live": "Ao Vivo",
diff --git a/i18n/locales/ru_RU.json b/i18n/locales/ru_RU.json
index 5cbe4d75..718c8969 100644
--- a/i18n/locales/ru_RU.json
+++ b/i18n/locales/ru_RU.json
@@ -9467,10 +9467,6 @@
"link_invalid": "Введи корректный http(s) URL",
"link_added": "Ссылка добавлена"
},
- "highlights": {
- "none": "Пока нет хайлайтов с матчей этого события.",
- "none_for_player": "Нет хайлайтов этого игрока на этом событии."
- },
"phase": {
"upcoming": "Скоро",
"live": "В эфире",
diff --git a/i18n/locales/sv_SE.json b/i18n/locales/sv_SE.json
index 9439b1ee..a5738176 100644
--- a/i18n/locales/sv_SE.json
+++ b/i18n/locales/sv_SE.json
@@ -9467,10 +9467,6 @@
"link_invalid": "Ange en giltig http(s)-URL",
"link_added": "Länk tillagd"
},
- "highlights": {
- "none": "Inga höjdpunkter från detta events matcher än.",
- "none_for_player": "Inga höjdpunkter med denna spelare från detta event."
- },
"phase": {
"upcoming": "Kommande",
"live": "Live",
diff --git a/i18n/locales/tr_TR.json b/i18n/locales/tr_TR.json
index 1f290844..084b0918 100644
--- a/i18n/locales/tr_TR.json
+++ b/i18n/locales/tr_TR.json
@@ -9467,10 +9467,6 @@
"link_invalid": "Geçerli bir http(s) URL'si gir",
"link_added": "Bağlantı eklendi"
},
- "highlights": {
- "none": "Bu etkinliğin maçlarından henüz öne çıkan an yok.",
- "none_for_player": "Bu oyuncunun bu etkinlikte öne çıkan anı yok."
- },
"phase": {
"upcoming": "Yaklaşan",
"live": "Canlı",
diff --git a/i18n/locales/uk_UA.json b/i18n/locales/uk_UA.json
index 9ed5148d..4bb220b9 100644
--- a/i18n/locales/uk_UA.json
+++ b/i18n/locales/uk_UA.json
@@ -9467,10 +9467,6 @@
"link_invalid": "Введи дійсний http(s) URL",
"link_added": "Посилання додано"
},
- "highlights": {
- "none": "Поки немає хайлайтів з матчів цієї події.",
- "none_for_player": "Немає хайлайтів цього гравця на цій події."
- },
"phase": {
"upcoming": "Майбутня",
"live": "Наживо",
diff --git a/i18n/locales/zh_Hans.json b/i18n/locales/zh_Hans.json
index 7c589680..1dc599f9 100644
--- a/i18n/locales/zh_Hans.json
+++ b/i18n/locales/zh_Hans.json
@@ -9467,10 +9467,6 @@
"link_invalid": "请输入有效的 http(s) URL",
"link_added": "链接已添加"
},
- "highlights": {
- "none": "此活动的比赛尚无高光时刻。",
- "none_for_player": "此玩家在此活动中暂无高光时刻。"
- },
"phase": {
"upcoming": "即将开始",
"live": "进行中",
diff --git a/i18n/locales/zh_Hant.json b/i18n/locales/zh_Hant.json
index 14139676..34a0f226 100644
--- a/i18n/locales/zh_Hant.json
+++ b/i18n/locales/zh_Hant.json
@@ -9467,10 +9467,6 @@
"link_invalid": "請輸入有效的 http(s) URL",
"link_added": "連結已新增"
},
- "highlights": {
- "none": "此活動的比賽尚無精華時刻。",
- "none_for_player": "此玩家在此活動中尚無精華時刻。"
- },
"phase": {
"upcoming": "即將開始",
"live": "進行中",
diff --git a/pages/events/[eventId]/index.vue b/pages/events/[eventId]/index.vue
index c9e355e6..bfbefbac 100644
--- a/pages/events/[eventId]/index.vue
+++ b/pages/events/[eventId]/index.vue
@@ -28,7 +28,7 @@ import EventStandings from "~/components/events/EventStandings.vue";
import EventMembershipPanel from "~/components/events/EventMembershipPanel.vue";
import EventForm from "~/components/events/EventForm.vue";
import EventMediaPanel from "~/components/events/EventMediaPanel.vue";
-import EventHighlights from "~/components/events/EventHighlights.vue";
+import HighlightsBrowser from "~/components/clips/HighlightsBrowser.vue";
import EventTeamsPanel from "~/components/events/EventTeamsPanel.vue";
import EventOverview from "~/components/events/EventOverview.vue";
import EventBannerUpload from "~/components/events/EventBannerUpload.vue";
@@ -50,6 +50,10 @@ import {
} from "~/utilities/eventDisplay";
import { useEventContext } from "~/composables/useEventContext";
+definePageMeta({
+ persistQueryKeys: ["player", "since", "kills", "view", "sort"],
+});
+
// Reflect the event name in the browser tab / in-app title (nuxt.config's
// titleTemplate appends " | 5Stack"). The context is populated by the
// subscription in the Options block below.
@@ -436,7 +440,7 @@ const {
-
+
diff --git a/pages/highlights/index.vue b/pages/highlights/index.vue
index 87c0ed9b..02095371 100644
--- a/pages/highlights/index.vue
+++ b/pages/highlights/index.vue
@@ -1,45 +1,11 @@
@@ -663,320 +140,5 @@ const viewModeOptions = computed<
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ $t("pages.highlights.filter_labels.visibility") }}
-
-
-
-
-
-
-
- {{ $t("pages.highlights.filter_labels.date") }}
-
-
-
-
-
-
-
- {{ $t("pages.highlights.filter_labels.sort") }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{
- playerFilter || sinceFilter !== "all" || killsFilter !== "any"
- ? $t("pages.highlights.empty.no_match_filters")
- : $t("pages.highlights.empty.no_clips_yet")
- }}
-
-
-
- {{ $t("pages.highlights.empty.try_widening") }}
-
-
- {{ $t("pages.highlights.empty.try_different_admin") }}
-
-
- {{ $t("pages.highlights.empty.check_back_soon") }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- (page = p)"
- />
+
-
-
diff --git a/tests/components/clips/HighlightsBrowser.spec.ts b/tests/components/clips/HighlightsBrowser.spec.ts
new file mode 100644
index 00000000..ffeb7cb4
--- /dev/null
+++ b/tests/components/clips/HighlightsBrowser.spec.ts
@@ -0,0 +1,88 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { flushPromises } from "@vue/test-utils";
+import { mountSuspended } from "@nuxt/test-utils/runtime";
+import { print } from "graphql";
+import HighlightsBrowser from "~/components/clips/HighlightsBrowser.vue";
+import EventPlayerPicker from "~/components/events/EventPlayerPicker.vue";
+import PlayerSearch from "~/components/PlayerSearch.vue";
+
+const { query } = vi.hoisted(() => ({ query: vi.fn() }));
+
+vi.mock("~/graphql/getGraphqlClient", () => ({
+ default: () => ({ query }),
+}));
+
+vi.mock("@vue/apollo-composable", async (importOriginal) => ({
+ ...(await importOriginal
()),
+ useApolloClient: () => ({ client: { query } }),
+}));
+
+const EVENT_SCOPE = 'event_links:{event_id:{_eq:"event-1"}}';
+
+function sentQueries(root: string) {
+ return query.mock.calls
+ .map(([options]) => print(options.query).replace(/\s+/g, ""))
+ .filter((text) => text.includes(`${root}(`));
+}
+
+async function mountBrowser(route: string, eventId?: string) {
+ query.mockResolvedValue({ data: {} });
+ const wrapper = await mountSuspended(HighlightsBrowser, {
+ route,
+ props: eventId ? { eventId } : {},
+ });
+ await flushPromises();
+ return wrapper;
+}
+
+beforeEach(() => {
+ query.mockReset();
+});
+
+describe("HighlightsBrowser", () => {
+ it("scopes grouped match clips and their count to the event's matches", async () => {
+ await mountBrowser("/events/event-1?tab=highlights", "event-1");
+
+ const groups = sentQueries("match_maps");
+ const counts = sentQueries("match_maps_aggregate");
+ expect(groups.length).toBeGreaterThan(0);
+ expect(counts.length).toBeGreaterThan(0);
+ for (const text of [...groups, ...counts]) {
+ expect(text).toContain(`match:{${EVENT_SCOPE}}`);
+ }
+ });
+
+ it("scopes single clips and their count to the event's matches", async () => {
+ await mountBrowser("/events/event-1?tab=highlights&sort=views", "event-1");
+
+ const clips = sentQueries("match_clips");
+ const counts = sentQueries("match_clips_aggregate");
+ expect(clips.length).toBeGreaterThan(0);
+ expect(counts.length).toBeGreaterThan(0);
+ for (const text of [...clips, ...counts]) {
+ expect(text).toContain(`match_map:{match:{${EVENT_SCOPE}}}`);
+ }
+ });
+
+ it("lists the event's participants in the player filter", async () => {
+ const wrapper = await mountBrowser(
+ "/events/event-1?tab=highlights",
+ "event-1",
+ );
+
+ expect(wrapper.findComponent(EventPlayerPicker).exists()).toBe(true);
+ expect(wrapper.findComponent(PlayerSearch).exists()).toBe(false);
+ });
+
+ it("stays unscoped with the global player search on the highlights page", async () => {
+ const wrapper = await mountBrowser("/highlights");
+
+ const sent = query.mock.calls.map(([options]) => print(options.query));
+ expect(sent.length).toBeGreaterThan(0);
+ for (const text of sent) {
+ expect(text).not.toContain("event_links");
+ }
+ expect(wrapper.findComponent(PlayerSearch).exists()).toBe(true);
+ expect(wrapper.findComponent(EventPlayerPicker).exists()).toBe(false);
+ });
+});
diff --git a/tests/components/events/EventHighlights.spec.ts b/tests/components/events/EventHighlights.spec.ts
deleted file mode 100644
index 79fdd64c..00000000
--- a/tests/components/events/EventHighlights.spec.ts
+++ /dev/null
@@ -1,118 +0,0 @@
-import { beforeEach, describe, expect, it, vi } from "vitest";
-import { flushPromises } from "@vue/test-utils";
-import { mountSuspended } from "@nuxt/test-utils/runtime";
-import EventHighlights from "~/components/events/EventHighlights.vue";
-import EventPlayerFilter from "~/components/events/EventPlayerFilter.vue";
-
-const { subscribe, emitters } = vi.hoisted(() => {
- const emitters: Array<(value: unknown) => void> = [];
- const subscribe = vi.fn((_options: any) => ({
- subscribe: ({ next }: { next: (value: unknown) => void }) => {
- emitters.push(next);
- return { unsubscribe: () => {} };
- },
- }));
- return { subscribe, emitters };
-});
-
-vi.mock("@vue/apollo-composable", async (importOriginal) => ({
- ...(await importOriginal()),
- useApolloClient: () => ({ client: { subscribe } }),
-}));
-
-function lastVariables() {
- return subscribe.mock.calls.at(-1)![0].variables;
-}
-
-async function emitClips(clips: Array<{ id: string }>) {
- emitters.at(-1)!({ data: { match_clips: clips } });
- await flushPromises();
-}
-
-async function mountHighlights() {
- const wrapper = await mountSuspended(EventHighlights, {
- props: { eventId: "event-1" },
- global: {
- stubs: {
- HighlightCard: {
- props: ["clip"],
- template: '{{ clip.id }}
',
- },
- },
- },
- });
- await flushPromises();
- return wrapper;
-}
-
-beforeEach(() => {
- subscribe.mockClear();
- emitters.length = 0;
-});
-
-describe("EventHighlights", () => {
- it("subscribes to public clips from every match linked to the event, most viewed first", async () => {
- await mountHighlights();
-
- expect(lastVariables().where).toEqual({
- _and: [
- { visibility: { _eq: "public" } },
- {
- match_map: {
- match: { event_links: { event_id: { _eq: "event-1" } } },
- },
- },
- ],
- });
- expect(lastVariables().orderBy[0]).toEqual({
- views_count: "desc_nulls_last",
- });
- });
-
- it("shows only the empty state when the event has no highlights", async () => {
- const wrapper = await mountHighlights();
- await emitClips([]);
-
- expect(wrapper.text()).toContain(
- "No highlights from this event's matches yet.",
- );
- expect(wrapper.findComponent(EventPlayerFilter).exists()).toBe(false);
- });
-
- it("narrows to one player and keeps the filter when that player has none", async () => {
- const wrapper = await mountHighlights();
- await emitClips([{ id: "clip-1" }]);
- expect(wrapper.findAll(".highlight-card")).toHaveLength(1);
-
- wrapper.findComponent(EventPlayerFilter).vm.$emit("update:modelValue", {
- steam_id: "76561198000000001",
- name: "Roster Player",
- });
- await flushPromises();
-
- expect(lastVariables().where._and).toContainEqual({
- target_steam_id: { _eq: "76561198000000001" },
- });
-
- await emitClips([]);
-
- expect(wrapper.text()).toContain(
- "No highlights of this player at this event.",
- );
- expect(wrapper.findComponent(EventPlayerFilter).exists()).toBe(true);
- });
-
- it("resubscribes newest first when switched to most recent", async () => {
- const wrapper = await mountHighlights();
- await emitClips([{ id: "clip-1" }]);
-
- const recent = wrapper
- .findAll("button")
- .find((button) => button.text() === "Most recent");
- expect(recent).toBeTruthy();
- await recent!.trigger("click");
- await flushPromises();
-
- expect(lastVariables().orderBy).toEqual([{ created_at: "desc" }]);
- });
-});