From 36c34b8fcb7307c9ad8420f2df9fb8656192b998 Mon Sep 17 00:00:00 2001 From: tommy Date: Sat, 18 Apr 2026 15:10:53 -0400 Subject: [PATCH 1/3] web: migrate times lists to unified /api/times endpoint with filter bar Adds sort/best/has_replay/invalidated filter controls to map and player pages via a shared TimesFilterBar component. Uses the new { data, total } envelope for accurate "Page X of Y" pagination. Moderator-gated invalidated control with permission stripping on URL parse. --- src/api/offstylesApi.ts | 124 ++++++++------------ src/components/CheckboxInput.vue | 8 +- src/components/CustomDropdown.vue | 10 +- src/components/MapDetails.vue | 63 +++++++---- src/components/PlayerDetails.vue | 90 ++++++++------- src/components/RecentTimes.vue | 9 +- src/components/TimesFilterBar.vue | 149 +++++++++++++++++++++++++ src/components/TimesListPagination.vue | 24 +++- src/types/Time.ts | 9 +- src/types/TimesFilter.ts | 15 +++ src/utils/timesFilterFromQuery.ts | 38 +++++++ src/utils/urlParams.ts | 15 ++- src/views/IndividualRecordView.vue | 4 +- src/views/MapsView.vue | 44 ++++---- src/views/PlayersView.vue | 67 +++++------ src/views/RecentRecordsView.vue | 44 +++++--- 16 files changed, 482 insertions(+), 231 deletions(-) create mode 100644 src/components/TimesFilterBar.vue create mode 100644 src/types/TimesFilter.ts create mode 100644 src/utils/timesFilterFromQuery.ts diff --git a/src/api/offstylesApi.ts b/src/api/offstylesApi.ts index e1c9c9e..8789b3a 100644 --- a/src/api/offstylesApi.ts +++ b/src/api/offstylesApi.ts @@ -1,18 +1,10 @@ import { Style } from '@/types/Style'; import Api from './api'; -import type { Time } from '@/types/Time'; +import type { Time, TimesPage } from '@/types/Time'; +import type { TimesFilter } from '@/types/TimesFilter'; import type { User } from '@/types/User'; import type { RecentModAction, ModerationTargetFilter, ModerationAction } from '@/types/moderation'; -// Add new interfaces based on the API spec -export interface RankAwareRecord extends Time { - rank: number; -} - -export interface WRAwareRecord extends Time { - wr_time: number; -} - export interface ReturnStyle { name: string; s_id: number; @@ -89,74 +81,36 @@ export interface ModerationLogResponse { class OffstylesApi extends Api { static offstylesApiUrl = "/api"; - // Fixed method signature to require style parameter - static async getTimesByMap( - mapName: string, - style: number = Style.normal, - steamid?: string, - limit: number = 50, - page: number = 1, - ): Promise { - const params = new URLSearchParams({ - map: mapName, - style: style.toString(), - limit: limit.toString(), - page: page.toString(), - }); - - if (steamid) { - params.append("steamid", steamid); - } - - this.url = `${this.offstylesApiUrl}/map?${params.toString()}`; - return await this.fetchFromUrl(); - } - - static async getTimesByPlayer( - steamID: string, - map?: string, - style: number = Style.all, - limit: number = 50, - page: number = 1, - best: boolean = false, - ): Promise { - const params = new URLSearchParams({ - steamid: steamID, - limit: limit.toString(), - page: page.toString(), - best: best.toString(), - }); + static async getTimes(filter: TimesFilter): Promise { + const params = new URLSearchParams(); - if (map) { - params.append("map", map); + if (filter.map) params.append("map", filter.map); + if (filter.steamid) params.append("steamid", filter.steamid); + if (filter.style !== undefined && filter.style !== Style.all) { + params.append("style", filter.style.toString()); } - - if (style !== undefined && style !== Style.all) { - params.append("style", style.toString()); + if (filter.sort) params.append("sort", filter.sort); + if (filter.best !== undefined) params.append("best", filter.best.toString()); + if (filter.has_replay) params.append("has_replay", "true"); + if (filter.invalidated !== undefined) { + params.append("invalidated", filter.invalidated.toString()); } + if (filter.wr !== undefined) params.append("wr", filter.wr.toString()); + if (filter.recent) params.append("recent", "true"); + params.append("page", filter.page.toString()); + params.append("limit", filter.limit.toString()); - this.url = `${this.offstylesApiUrl}/times?${params.toString()}`; - return await this.fetchFromUrl(); - } - - static async getRecentTimes( - style: number = Style.all, - limit: number = 15, - page: number = 1, - wr: boolean = true, - ): Promise { - const params = new URLSearchParams({ - limit: limit.toString(), - page: page.toString(), - wr: wr.toString(), - }); - - if (style !== undefined && style !== Style.all) { - params.append("style", style.toString()); + const response = await fetch(`${this.offstylesApiUrl}/times?${params.toString()}`); + if (!response.ok) { + const errorText = await response.text(); + try { + const error: JsonError = JSON.parse(errorText); + throw new Error(`${error.code}: ${error.reason}`); + } catch { + throw new Error(`${response.status}: ${response.statusText}`); + } } - - this.url = `${this.offstylesApiUrl}/recent?${params.toString()}`; - return await this.fetchFromUrl(); + return await response.json(); } // New methods based on the API spec @@ -178,13 +132,23 @@ class OffstylesApi extends Api { return await this.fetchFromUrl(); } - static async getSingleTime(id: string): Promise { - const params = new URLSearchParams({ - id: id, - }); - - this.url = `${this.offstylesApiUrl}/time?${params.toString()}`; - return await this.fetchFromUrl(); + static async getSingleTime(id: string): Promise