Repository: leaderboardsgg/leaderboard-admin
Pull request #43: Use Search and Status Filters
Author: login: TheTedder
Created: 2025-05-27T23:44:55Z | Merged: 2025-05-29T00:34:42Z

This PR makes it so that the LB list view actually uses server-side searching and filtering. Closes #42.

Linked issue #42: Use Status Filters
Change any queries to the backend that use the `includeDeleted` query param to use the `status` query param instead.
Linked issue comments: user: login: TheTedder
created_at: 2025-05-14T20:14:07Z
body: Blocked by https://github.com/leaderboardsgg/leaderboard-backend/issues/306.
author_association: COLLABORATOR

Diff:
diff --git a/src/components/leaderboards/CategoryList.vue b/src/components/leaderboards/CategoryList.vue
index a5e0f17..342c269 100644
--- a/src/components/leaderboards/CategoryList.vue
+++ b/src/components/leaderboards/CategoryList.vue
@@ -24,8 +24,8 @@ const {
 	execute
 } = useAsyncState(async () => {
 	const resp = await categories.getCategoriesForLeaderboard({ id: props.id })
-	return resp.data
-}, null)
+	return resp.data.data
+}, [])
 </script>
 
 <template>
diff --git a/src/components/leaderboards/List.vue b/src/components/leaderboards/List.vue
index d15ce28..7d3e3b8 100644
--- a/src/components/leaderboards/List.vue
+++ b/src/components/leaderboards/List.vue
@@ -1,52 +1,58 @@
 <script setup lang="ts">
 import { useAsyncState } from '@vueuse/core'
-import { computed, ref } from 'vue'
+import { ref } from 'vue'
 import { Leaderboards } from '../../lib/api/Leaderboards'
+import { StatusFilter } from '../../lib/api/data-contracts'
 
-enum IncludeDeleted {
-	NotDeleted,
-	Deleted,
-	All
-}
-
-const includeDeleted = ref(IncludeDeleted.NotDeleted)
-const search = ref<string>('')
+const limit = 50;
+const status = ref<StatusFilter>('Published')
+const query = ref('')
+const searchedQuery = ref('')
+const searchedStatus = ref<StatusFilter>()
 
 const leaderboardClient = new Leaderboards({
 	baseUrl: import.meta.env.VITE_BACKEND_URL
 })
 
 const {
-	state: allBoards,
+	state: boards,
 	error,
 	isLoading,
 	execute
-} = useAsyncState(async () => {
-	const resp = await leaderboardClient.listLeaderboards({
-		includeDeleted: true
+} = useAsyncState(async (q: string) => {
+	const resp = q ? await leaderboardClient.searchLeaderboards({
+		q: q,
+		status: status.value,
+		limit: limit
+	}) : await leaderboardClient.listLeaderboards({
+		status: status.value,
+		limit: limit
 	})
 
 	return resp.data
-}, [])
-
-const boardsFiltered = computed(() =>
-	allBoards.value.filter(
-		(board) =>
-			includeDeleted.value === IncludeDeleted.All ||
-			(includeDeleted.value === IncludeDeleted.NotDeleted) ===
-				(board.deletedAt === null)
-	)
-)
-
-const boardsSearched = computed(() =>
-	boardsFiltered.value.filter(
-		(board) =>
-			!search.value ||
-			board.name
-				.toLocaleLowerCase()
-				.includes(search.value.trim().toLocaleLowerCase())
-	)
-)
+}, {
+	data: [],
+	limitDefault: 0,
+	limitMax: 0,
+	total: 0
+})
+
+function search(q: string) {
+	execute(0, q)
+	searchedQuery.value = q
+	searchedStatus.value = status.value
+}
+
+function clear()
+{
+	execute(0, '')
+	searchedQuery.value = ''
+}
+
+function filterChanged() {
+	execute(0, searchedQuery.value)
+	searchedStatus.value = status.value
+}
 </script>
 
 <template>
@@ -56,24 +62,31 @@ const boardsSearched = computed(() =>
 			<RouterLink :to="{ name: 'leaderboardCreate' }">
 				<button class="button create-new">Create New</button>
 			</RouterLink>
-			<input v-model="search" placeholder="Search" class="input" />
-			<select v-model="includeDeleted" class="input">
-				<option value="" disabled>Please select one</option>
-				<option :value="IncludeDeleted.NotDeleted">Not Deleted</option>
-				<option :value="IncludeDeleted.Deleted">Deleted</option>
-				<option :value="IncludeDeleted.All">All</option>
-			</select>
+			<form @submit.prevent="search(query)" class="form">
+				<input v-model="query" placeholder="Search" class="input" type="search"/>
+				<select v-model="status" class="input" @change="filterChanged">
+					<option value="" disabled>Please select one</option>
+					<option value="Published">Published</option>
+					<option value="Deleted">Deleted</option>
+					<option value="Any">All</option>
+				</select>
+			</form>
+		</div>
+
+		<div v-if="searchedQuery" class="results-text" >
+			Displaying {{ boards.data.length }} of {{ boards.total }} results for "{{ searchedQuery }}" ({{ searchedStatus }}).&nbsp;
+			<a href="" @click.prevent="clear">clear</a>
 		</div>
 
 		<div v-if="isLoading">Loading...</div>
 
 		<div v-else-if="error" class="error-container">
 			<p>An error occurred.</p>
-			<button @click="execute()" class="retry-button">Retry</button>
+			<button @click="search(query)" class="retry-button">Retry</button>
 		</div>
 
 		<ul v-else>
-			<li v-for="board in boardsSearched" :key="board.id">
+			<li v-for="board in boards.data" :key="board.id">
 				<RouterLink
 					:to="{ name: 'leaderboardView', params: { id: board.id } }"
 					:class="{ dull: board.deletedAt !== null }"
@@ -95,12 +108,22 @@ const boardsSearched = computed(() =>
 	gap: 0.5rem;
 }
 
+.results-text {
+	align-self: flex-start;
+	display: flex;
+}
+
 .input-container {
 	align-self: flex-end;
 	display: flex;
 	gap: 1rem;
 }
 
+.form {
+	display: flex;
+	gap: 1rem;
+}
+
 .input {
 	padding: 0.5rem;
 }
diff --git a/src/lib/api/Account.ts b/src/lib/api/Account.ts
index 01cf88d..bdd12bd 100644
--- a/src/lib/api/Account.ts
+++ b/src/lib/api/Account.ts
@@ -10,12 +10,12 @@
  */
 
 import {
-	ChangePasswordRequest,
-	LoginRequest,
+	ChangePasswordPayload,
+	LoginPayload,
 	LoginResponse,
 	ProblemDetails,
-	RecoverAccountRequest,
-	RegisterRequest,
+	RegisterPayload,
+	SendRecoveryEmailPayload,
 	UserViewModel,
 	ValidationProblemDetails
 } from './data-contracts'
@@ -38,7 +38,7 @@ export class Account<
 	 * @response `422` `ValidationProblemDetails` The request contains errors. Validation error codes by property: - **Username**: - **UsernameFormat**: Invalid username format - **Password**: - **PasswordFormat**: Invalid password format - **Email**: - **EmailValidator**: Invalid email format
 	 * @response `500` `void` Internal Server Error
 	 */
-	register = (data: RegisterRequest, params: RequestParams = {}) =>
+	register = (data: RegisterPayload, params: RequestParams = {}) =>
 		this.request<
 			UserViewModel,
 			ProblemDetails | ValidationProblemDetails | void
@@ -67,7 +67,7 @@ export class Account<
 	 * @response `422` `ValidationProblemDetails` The request contains errors. Validation error codes by property: - **Password**: - **NotEmptyValidator**: No password was passed - **PasswordFormat**: Invalid password format - **Email**: - **NotEmptyValidator**: No email was passed - **EmailValidator**: Invalid email format
 	 * @response `500` `void` Internal Server Error
 	 */
-	login = (data: LoginRequest, params: RequestParams = {}) =>
+	login = (data: LoginPayload, params: RequestParams = {}) =>
 		this.request<
 			LoginResponse,
 			ProblemDetails | void | ValidationProblemDetails
@@ -114,7 +114,7 @@ export class Account<
 	 * @response `500` `void` Internal Server Error
 	 */
 	sendRecoveryEmail = (
-		data: RecoverAccountRequest,
+		data: SendRecoveryEmailPayload,
 		params: RequestParams = {}
 	) =>
 		this.request<void, ProblemDetails | void>({
@@ -184,7 +184,7 @@ export class Account<
 	 */
 	changePassword = (
 		id: string,
-		data: ChangePasswordRequest,
+		data: ChangePasswordPayload,
 		params: RequestParams = {}
 	) =>
 		this.request<void, ProblemDetails | void | ValidationProblemDetails>({
diff --git a/src/lib/api/AccountRoute.ts b/src/lib/api/AccountRoute.ts
index c23e1b0..9f52f00 100644
--- a/src/lib/api/AccountRoute.ts
+++ b/src/lib/api/AccountRoute.ts
@@ -10,11 +10,11 @@
  */
 
 import {
-	ChangePasswordRequest,
-	LoginRequest,
+	ChangePasswordPayload,
+	LoginPayload,
 	LoginResponse,
-	RecoverAccountRequest,
-	RegisterRequest,
+	RegisterPayload,
+	SendRecoveryEmailPayload,
 	UserViewModel
 } from './data-contracts'
 
@@ -35,7 +35,7 @@ export namespace Account {
 	export namespace Register {
 		export type RequestParams = {}
 		export type RequestQuery = {}
-		export type RequestBody = RegisterRequest
+		export type RequestBody = RegisterPayload
 		export type RequestHeaders = {}
 		export type ResponseBody = UserViewModel
 	}
@@ -58,7 +58,7 @@ export namespace Account {
 	export namespace Login {
 		export type RequestParams = {}
 		export type RequestQuery = {}
-		export type RequestBody = LoginRequest
+		export type RequestBody = LoginPayload
 		export type RequestHeaders = {}
 		export type ResponseBody = LoginResponse
 	}
@@ -98,7 +98,7 @@ export namespace Account {
 	export namespace SendRecoveryEmail {
 		export type RequestParams = {}
 		export type RequestQuery = {}
-		export type RequestBody = RecoverAccountRequest
+		export type RequestBody = SendRecoveryEmailPayload
 		export type RequestHeaders = {}
 		export type ResponseBody = void
 	}
@@ -180,7 +180,7 @@ export namespace Account {
 			id: string
 		}
 		export type RequestQuery = {}
-		export type RequestBody = ChangePasswordRequest
+		export type RequestBody = ChangePasswordPayload
 		export type RequestHeaders = {}
 		export type ResponseBody = void
 	}
diff --git a/src/lib/api/Categories.ts b/src/lib/api/Categories.ts
index 48531cb..94179be 100644
--- a/src/lib/api/Categories.ts
+++ b/src/lib/api/Categories.ts
@@ -12,11 +12,12 @@
 import {
 	CategoryViewModel,
 	CategoryViewModelConflictDetails,
-	CreateCategoryRequest,
+	CategoryViewModelListView,
+	CreateCategoryPayload,
 	GetCategoriesForLeaderboardParams,
 	GetCategoryBySlugParams,
 	ProblemDetails,
-	UpdateCategoryRequest,
+	UpdateCategoryPayload,
 	ValidationProblemDetails
 } from './data-contracts'
 import { ContentType, HttpClient, RequestParams } from './http-client'
@@ -78,16 +79,20 @@ export class Categories<
 	 * @summary Gets all Categories of Leaderboard `id`.
 	 * @request GET:/api/leaderboard/{id}/categories
 	 * @secure
-	 * @response `200` `(CategoryViewModel)[]` OK
+	 * @response `200` `CategoryViewModelListView` OK
 	 * @response `400` `ProblemDetails` Bad Request
 	 * @response `404` `ProblemDetails` The Leaderboard with ID `id` could not be found.
+	 * @response `422` `ValidationProblemDetails` Unprocessable Content
 	 * @response `500` `void` Internal Server Error
 	 */
 	getCategoriesForLeaderboard = (
 		{ id, ...query }: GetCategoriesForLeaderboardParams,
 		params: RequestParams = {}
 	) =>
-		this.request<CategoryViewModel[], ProblemDetails | void>({
+		this.request<
+			CategoryViewModelListView,
+			ProblemDetails | ValidationProblemDetails | void
+		>({
 			path: `/api/leaderboard/${id}/categories`,
 			method: 'GET',
 			query: query,
@@ -114,7 +119,7 @@ export class Categories<
 	 */
 	createCategory = (
 		id: number,
-		data: CreateCategoryRequest,
+		data: CreateCategoryPayload,
 		params: RequestParams = {}
 	) =>
 		this.request<
@@ -151,7 +156,7 @@ export class Categories<
 	 */
 	updateCategory = (
 		id: number,
-		data: UpdateCategoryRequest,
+		data: UpdateCategoryPayload,
 		params: RequestParams = {}
 	) =>
 		this.request<
diff --git a/src/lib/api/CategoriesRoute.ts b/src/lib/api/CategoriesRoute.ts
index 9c48929..beec733 100644
--- a/src/lib/api/CategoriesRoute.ts
+++ b/src/lib/api/CategoriesRoute.ts
@@ -11,8 +11,10 @@
 
 import {
 	CategoryViewModel,
-	CreateCategoryRequest,
-	UpdateCategoryRequest
+	CategoryViewModelListView,
+	CreateCategoryPayload,
+	StatusFilter,
+	UpdateCategoryPayload
 } from './data-contracts'
 
 export namespace Categories {
@@ -71,9 +73,10 @@ export namespace Categories {
 	 * @summary Gets all Categories of Leaderboard `id`.
 	 * @request GET:/api/leaderboard/{id}/categories
 	 * @secure
-	 * @response `200` `(CategoryViewModel)[]` OK
+	 * @response `200` `CategoryViewModelListView` OK
 	 * @response `400` `ProblemDetails` Bad Request
 	 * @response `404` `ProblemDetails` The Leaderboard with ID `id` could not be found.
+	 * @response `422` `ValidationProblemDetails` Unprocessable Content
 	 * @response `500` `void` Internal Server Error
 	 */
 	export namespace GetCategoriesForLeaderboard {
@@ -83,14 +86,22 @@ export namespace Categories {
 		}
 		export type RequestQuery = {
 			/**
-			 * Whether to include deleted Categories. Defaults to `false`.
-			 * @default false
+			 * The maximum number of records to return. Fewer records may be returned.
+			 * @format int32
+			 */
+			limit?: number
+			/**
+			 * The zero-based index at which to begin selecting records to return.
+			 * @format int32
+			 * @default 0
 			 */
-			includeDeleted?: boolean
+			offset?: number
+			/** @default "Published" */
+			status?: StatusFilter
 		}
 		export type RequestBody = never
 		export type RequestHeaders = {}
-		export type ResponseBody = CategoryViewModel[]
+		export type ResponseBody = CategoryViewModelListView
 	}
 
 	/**
@@ -115,7 +126,7 @@ export namespace Categories {
 			id: number
 		}
 		export type RequestQuery = {}
-		export type RequestBody = CreateCategoryRequest
+		export type RequestBody = CreateCategoryPayload
 		export type RequestHeaders = {}
 		export type ResponseBody = CategoryViewModel
 	}
@@ -142,7 +153,7 @@ export namespace Categories {
 			id: number
 		}
 		export type RequestQuery = {}
-		export type RequestBody = UpdateCategoryRequest
+		export type RequestBody = UpdateCategoryPayload
 		export type RequestHeaders = {}
 		export type ResponseBody = void
 	}
diff --git a/src/lib/api/Leaderboards.ts b/src/lib/api/Leaderboards.ts
index 02a9fac..06c8c7d 100644
--- a/src/lib/api/Leaderboards.ts
+++ b/src/lib/api/Leaderboards.ts
@@ -10,13 +10,15 @@
  */
 
 import {
-	CreateLeaderboardRequest,
+	CreateLeaderboardPayload,
 	GetLeaderboardBySlugParams,
 	LeaderboardViewModel,
 	LeaderboardViewModelConflictDetails,
+	LeaderboardViewModelListView,
 	ListLeaderboardsParams,
 	ProblemDetails,
-	UpdateLeaderboardRequest,
+	SearchLeaderboardsParams,
+	UpdateLeaderboardPayload,
 	ValidationProblemDetails
 } from './data-contracts'
 import { ContentType, HttpClient, RequestParams } from './http-client'
@@ -78,15 +80,19 @@ export class Leaderboards<
 	 * @summary Gets all leaderboards.
 	 * @request GET:/api/leaderboards
 	 * @secure
-	 * @response `200` `(LeaderboardViewModel)[]` OK
+	 * @response `200` `LeaderboardViewModelListView` OK
 	 * @response `400` `ProblemDetails` Bad Request
+	 * @response `422` `ValidationProblemDetails` Unprocessable Content
 	 * @response `500` `void` Internal Server Error
 	 */
 	listLeaderboards = (
 		query: ListLeaderboardsParams,
 		params: RequestParams = {}
 	) =>
-		this.request<LeaderboardViewModel[], ProblemDetails | void>({
+		this.request<
+			LeaderboardViewModelListView,
+			ProblemDetails | ValidationProblemDetails | void
+		>({
 			path: `/api/leaderboards`,
 			method: 'GET',
 			query: query,
@@ -94,6 +100,31 @@ export class Leaderboards<
 			format: 'json',
 			...params
 		})
+	/**
+	 * No description
+	 *
+	 * @tags Leaderboards
+	 * @name SearchLeaderboards
+	 * @summary Search leaderboards by name.
+	 * @request GET:/api/leaderboards/search
+	 * @secure
+	 * @response `200` `LeaderboardViewModelListView` OK
+	 * @response `400` `ProblemDetails` Bad Request
+	 * @response `422` `ProblemDetails` Unprocessable Content
+	 * @response `500` `void` Internal Server Error
+	 */
+	searchLeaderboards = (
+		query: SearchLeaderboardsParams,
+		params: RequestParams = {}
+	) =>
+		this.request<LeaderboardViewModelListView, ProblemDetails | void>({
+			path: `/api/leaderboards/search`,
+			method: 'GET',
+			query: query,
+			secure: true,
+			format: 'json',
+			...params
+		})
 	/**
 	 * No description
 	 *
@@ -111,7 +142,7 @@ export class Leaderboards<
 	 * @response `500` `void` Internal Server Error
 	 */
 	createLeaderboard = (
-		data: CreateLeaderboardRequest,
+		data: CreateLeaderboardPayload,
 		params: RequestParams = {}
 	) =>
 		this.request<
@@ -197,7 +228,7 @@ export class Leaderboards<
 	 */
 	updateLeaderboard = (
 		id: number,
-		data: UpdateLeaderboardRequest,
+		data: UpdateLeaderboardPayload,
 		params: RequestParams = {}
 	) =>
 		this.request<
diff --git a/src/lib/api/LeaderboardsRoute.ts b/src/lib/api/LeaderboardsRoute.ts
index e51a876..8c046ca 100644
--- a/src/lib/api/LeaderboardsRoute.ts
+++ b/src/lib/api/LeaderboardsRoute.ts
@@ -10,9 +10,11 @@
  */
 
 import {
-	CreateLeaderboardRequest,
+	CreateLeaderboardPayload,
 	LeaderboardViewModel,
-	UpdateLeaderboardRequest
+	LeaderboardViewModelListView,
+	StatusFilter,
+	UpdateLeaderboardPayload
 } from './data-contracts'
 
 export namespace Leaderboards {
@@ -68,19 +70,67 @@ export namespace Leaderboards {
 	 * @summary Gets all leaderboards.
 	 * @request GET:/api/leaderboards
 	 * @secure
-	 * @response `200` `(LeaderboardViewModel)[]` OK
+	 * @response `200` `LeaderboardViewModelListView` OK
 	 * @response `400` `ProblemDetails` Bad Request
+	 * @response `422` `ValidationProblemDetails` Unprocessable Content
 	 * @response `500` `void` Internal Server Error
 	 */
 	export namespace ListLeaderboards {
 		export type RequestParams = {}
 		export type RequestQuery = {
-			/** @default false */
-			includeDeleted?: boolean
+			/**
+			 * The maximum number of records to return. Fewer records may be returned.
+			 * @format int32
+			 */
+			limit?: number
+			/**
+			 * The zero-based index at which to begin selecting records to return.
+			 * @format int32
+			 * @default 0
+			 */
+			offset?: number
+			/** @default "Published" */
+			status?: StatusFilter
+		}
+		export type RequestBody = never
+		export type RequestHeaders = {}
+		export type ResponseBody = LeaderboardViewModelListView
+	}
+
+	/**
+	 * No description
+	 * @tags Leaderboards
+	 * @name SearchLeaderboards
+	 * @summary Search leaderboards by name.
+	 * @request GET:/api/leaderboards/search
+	 * @secure
+	 * @response `200` `LeaderboardViewModelListView` OK
+	 * @response `400` `ProblemDetails` Bad Request
+	 * @response `422` `ProblemDetails` Unprocessable Content
+	 * @response `500` `void` Internal Server Error
+	 */
+	export namespace SearchLeaderboards {
+		export type RequestParams = {}
+		export type RequestQuery = {
+			/** The query string. Must not be empty. */
+			q: string
+			/**
+			 * The maximum number of records to return. Fewer records may be returned.
+			 * @format int32
+			 */
+			limit?: number
+			/**
+			 * The zero-based index at which to begin selecting records to return.
+			 * @format int32
+			 * @default 0
+			 */
+			offset?: number
+			/** @default "Published" */
+			status?: StatusFilter
 		}
 		export type RequestBody = never
 		export type RequestHeaders = {}
-		export type ResponseBody = LeaderboardViewModel[]
+		export type ResponseBody = LeaderboardViewModelListView
 	}
 
 	/**
@@ -101,7 +151,7 @@ export namespace Leaderboards {
 	export namespace CreateLeaderboard {
 		export type RequestParams = {}
 		export type RequestQuery = {}
-		export type RequestBody = CreateLeaderboardRequest
+		export type RequestBody = CreateLeaderboardPayload
 		export type RequestHeaders = {}
 		export type ResponseBody = LeaderboardViewModel
 	}
@@ -179,7 +229,7 @@ export namespace Leaderboards {
 			id: number
 		}
 		export type RequestQuery = {}
-		export type RequestBody = UpdateLeaderboardRequest
+		export type RequestBody = UpdateLeaderboardPayload
 		export type RequestHeaders = {}
 		export type ResponseBody = void
 	}
diff --git a/src/lib/api/Runs.ts b/src/lib/api/Runs.ts
index 458e198..af1dec5 100644
--- a/src/lib/api/Runs.ts
+++ b/src/lib/api/Runs.ts
@@ -11,11 +11,13 @@
 
 import {
 	CategoryViewModel,
-	CreateRunRequest,
+	CreateRunPayload,
+	GetRunsForCategoryParams,
 	ProblemDetails,
-	RunViewModel,
+	RunViewModelListView,
 	ScoredRunViewModel,
 	TimedRunViewModel,
+	UpdateRunPayload,
 	ValidationProblemDetails
 } from './data-contracts'
 import { ContentType, HttpClient, RequestParams } from './http-client'
@@ -31,19 +33,51 @@ export class Runs<
 	 * @summary Gets a Run by its ID.
 	 * @request GET:/api/run/{id}
 	 * @secure
-	 * @response `200` `(RunViewModel | TimedRunViewModel | ScoredRunViewModel)` OK
+	 * @response `200` `(TimedRunViewModel | ScoredRunViewModel)` OK
 	 * @response `400` `ProblemDetails` Bad Request
-	 * @response `404` `void` Not Found
+	 * @response `404` `ProblemDetails` The Run with ID `id` could not be found.
 	 * @response `500` `void` Internal Server Error
 	 */
 	getRun = (id: string, params: RequestParams = {}) =>
+		this.request<TimedRunViewModel | ScoredRunViewModel, ProblemDetails | void>(
+			{
+				path: `/api/run/${id}`,
+				method: 'GET',
+				secure: true,
+				format: 'json',
+				...params
+			}
+		)
+	/**
+	 * No description
+	 *
+	 * @tags Runs
+	 * @name CreateRun
+	 * @summary Creates a new Run for a Category with ID `id`. This request is restricted to confirmed Users and Administrators.
+	 * @request POST:/category/{id}/runs/create
+	 * @secure
+	 * @response `201` `(TimedRunViewModel | ScoredRunViewModel)` Created
+	 * @response `400` `ValidationProblemDetails` Bad Request
+	 * @response `401` `ProblemDetails` The client is not logged in.
+	 * @response `403` `ProblemDetails` The requesting User is unauthorized to create Runs.
+	 * @response `404` `ProblemDetails` The Category with ID `id` could not be found, or has been deleted. Read `title` for more information.
+	 * @response `422` `ProblemDetails` Unprocessable Content
+	 * @response `500` `void` Internal Server Error
+	 */
+	createRun = (
+		id: number,
+		data: CreateRunPayload,
+		params: RequestParams = {}
+	) =>
 		this.request<
-			RunViewModel | TimedRunViewModel | ScoredRunViewModel,
-			ProblemDetails | void
+			TimedRunViewModel | ScoredRunViewModel,
+			ValidationProblemDetails | ProblemDetails | void
 		>({
-			path: `/api/run/${id}`,
-			method: 'GET',
+			path: `/category/${id}/runs/create`,
+			method: 'POST',
+			body: data,
 			secure: true,
+			type: ContentType.Json,
 			format: 'json',
 			...params
 		})
@@ -51,24 +85,29 @@ export class Runs<
 	 * No description
 	 *
 	 * @tags Runs
-	 * @name CreateRun
-	 * @summary Creates a new Run.
-	 * @request POST:/runs/create
+	 * @name GetRunsForCategory
+	 * @summary Gets the Runs for a Category.
+	 * @request GET:/api/category/{id}/runs
 	 * @secure
-	 * @response `201` `void` Created
+	 * @response `200` `RunViewModelListView` OK
 	 * @response `400` `ProblemDetails` Bad Request
-	 * @response `401` `void` Unauthorized
-	 * @response `403` `void` Forbidden
+	 * @response `404` `ProblemDetails` The Category with ID `id` could not be found, or has been deleted. Read `title` for more information.
 	 * @response `422` `ValidationProblemDetails` Unprocessable Content
 	 * @response `500` `void` Internal Server Error
 	 */
-	createRun = (data: CreateRunRequest, params: RequestParams = {}) =>
-		this.request<void, ProblemDetails | void | ValidationProblemDetails>({
-			path: `/runs/create`,
-			method: 'POST',
-			body: data,
+	getRunsForCategory = (
+		{ id, ...query }: GetRunsForCategoryParams,
+		params: RequestParams = {}
+	) =>
+		this.request<
+			RunViewModelListView,
+			ProblemDetails | ValidationProblemDetails | void
+		>({
+			path: `/api/category/${id}/runs`,
+			method: 'GET',
+			query: query,
 			secure: true,
-			type: ContentType.Json,
+			format: 'json',
 			...params
 		})
 	/**
@@ -92,4 +131,55 @@ export class Runs<
 			format: 'json',
 			...params
 		})
+	/**
+	 * No description
+	 *
+	 * @tags Runs
+	 * @name UpdateRun
+	 * @summary Updates a run with the specified new fields. This request is restricted to administrators or users updating their own runs. Note: `runType` cannot be updated. This operation is atomic; if an error occurs, the run will not be updated. All fields of the request body are optional but you must specify at least one.
+	 * @request PATCH:/run/{id}
+	 * @secure
+	 * @response `204` `void` No Content
+	 * @response `400` `ProblemDetails` Bad Request
+	 * @response `401` `void` Unauthorized
+	 * @response `403` `ProblemDetails` The user attempted to update another user's run, or the user is banned or not yet confirmed.
+	 * @response `404` `ProblemDetails` The Run with ID `id` could not be found, or has been deleted. Read `title` for more information.
+	 * @response `422` `ProblemDetails` Response can be a `ProblemDetails` for a request that doesn't match the run type of a category, or a `ValidationProblemDetails` otherwise.
+	 * @response `500` `void` Internal Server Error
+	 */
+	updateRun = (
+		id: string,
+		data: UpdateRunPayload,
+		params: RequestParams = {}
+	) =>
+		this.request<void, ProblemDetails | void>({
+			path: `/run/${id}`,
+			method: 'PATCH',
+			body: data,
+			secure: true,
+			type: ContentType.Json,
+			...params
+		})
+	/**
+	 * No description
+	 *
+	 * @tags Runs
+	 * @name DeleteRun
+	 * @summary Deletes a Run.
+	 * @request DELETE:/run/{id}
+	 * @secure
+	 * @response `204` `void` No Content
+	 * @response `400` `ProblemDetails` Bad Request
+	 * @response `401` `void` Unauthorized
+	 * @response `403` `void` Forbidden
+	 * @response `404` `ProblemDetails` The run does not exist (Not Found) or was already deleted (Already Deleted). Use the `title` field of the response to differentiate between the two cases if necessary.
+	 * @response `500` `void` Internal Server Error
+	 */
+	deleteRun = (id: string, params: RequestParams = {}) =>
+		this.request<void, ProblemDetails | void>({
+			path: `/run/${id}`,
+			method: 'DELETE',
+			secure: true,
+			...params
+		})
 }
diff --git a/src/lib/api/RunsRoute.ts b/src/lib/api/RunsRoute.ts
index 0666d35..24608d9 100644
--- a/src/lib/api/RunsRoute.ts
+++ b/src/lib/api/RunsRoute.ts
@@ -11,10 +11,12 @@
 
 import {
 	CategoryViewModel,
-	CreateRunRequest,
-	RunViewModel,
+	CreateRunPayload,
+	RunViewModelListView,
 	ScoredRunViewModel,
-	TimedRunViewModel
+	StatusFilter,
+	TimedRunViewModel,
+	UpdateRunPayload
 } from './data-contracts'
 
 export namespace Runs {
@@ -25,9 +27,9 @@ export namespace Runs {
 	 * @summary Gets a Run by its ID.
 	 * @request GET:/api/run/{id}
 	 * @secure
-	 * @response `200` `(RunViewModel | TimedRunViewModel | ScoredRunViewModel)` OK
+	 * @response `200` `(TimedRunViewModel | ScoredRunViewModel)` OK
 	 * @response `400` `ProblemDetails` Bad Request
-	 * @response `404` `void` Not Found
+	 * @response `404` `ProblemDetails` The Run with ID `id` could not be found.
 	 * @response `500` `void` Internal Server Error
 	 */
 	export namespace GetRun {
@@ -38,32 +40,71 @@ export namespace Runs {
 		export type RequestQuery = {}
 		export type RequestBody = never
 		export type RequestHeaders = {}
-		export type ResponseBody =
-			| RunViewModel
-			| TimedRunViewModel
-			| ScoredRunViewModel
+		export type ResponseBody = TimedRunViewModel | ScoredRunViewModel
 	}
 
 	/**
 	 * No description
 	 * @tags Runs
 	 * @name CreateRun
-	 * @summary Creates a new Run.
-	 * @request POST:/runs/create
+	 * @summary Creates a new Run for a Category with ID `id`. This request is restricted to confirmed Users and Administrators.
+	 * @request POST:/category/{id}/runs/create
 	 * @secure
-	 * @response `201` `void` Created
-	 * @response `400` `ProblemDetails` Bad Request
-	 * @response `401` `void` Unauthorized
-	 * @response `403` `void` Forbidden
-	 * @response `422` `ValidationProblemDetails` Unprocessable Content
+	 * @response `201` `(TimedRunViewModel | ScoredRunViewModel)` Created
+	 * @response `400` `ValidationProblemDetails` Bad Request
+	 * @response `401` `ProblemDetails` The client is not logged in.
+	 * @response `403` `ProblemDetails` The requesting User is unauthorized to create Runs.
+	 * @response `404` `ProblemDetails` The Category with ID `id` could not be found, or has been deleted. Read `title` for more information.
+	 * @response `422` `ProblemDetails` Unprocessable Content
 	 * @response `500` `void` Internal Server Error
 	 */
 	export namespace CreateRun {
-		export type RequestParams = {}
+		export type RequestParams = {
+			/** @format int64 */
+			id: number
+		}
 		export type RequestQuery = {}
-		export type RequestBody = CreateRunRequest
+		export type RequestBody = CreateRunPayload
 		export type RequestHeaders = {}
-		export type ResponseBody = void
+		export type ResponseBody = TimedRunViewModel | ScoredRunViewModel
+	}
+
+	/**
+	 * No description
+	 * @tags Runs
+	 * @name GetRunsForCategory
+	 * @summary Gets the Runs for a Category.
+	 * @request GET:/api/category/{id}/runs
+	 * @secure
+	 * @response `200` `RunViewModelListView` OK
+	 * @response `400` `ProblemDetails` Bad Request
+	 * @response `404` `ProblemDetails` The Category with ID `id` could not be found, or has been deleted. Read `title` for more information.
+	 * @response `422` `ValidationProblemDetails` Unprocessable Content
+	 * @response `500` `void` Internal Server Error
+	 */
+	export namespace GetRunsForCategory {
+		export type RequestParams = {
+			/** @format int64 */
+			id: number
+		}
+		export type RequestQuery = {
+			/**
+			 * The maximum number of records to return. Fewer records may be returned.
+			 * @format int32
+			 */
+			limit?: number
+			/**
+			 * The zero-based index at which to begin selecting records to return.
+			 * @format int32
+			 * @default 0
+			 */
+			offset?: number
+			/** @default "Published" */
+			status?: StatusFilter
+		}
+		export type RequestBody = never
+		export type RequestHeaders = {}
+		export type ResponseBody = RunViewModelListView
 	}
 
 	/**
@@ -88,4 +129,55 @@ export namespace Runs {
 		export type RequestHeaders = {}
 		export type ResponseBody = CategoryViewModel
 	}
+
+	/**
+	 * No description
+	 * @tags Runs
+	 * @name UpdateRun
+	 * @summary Updates a run with the specified new fields. This request is restricted to administrators or users updating their own runs. Note: `runType` cannot be updated. This operation is atomic; if an error occurs, the run will not be updated. All fields of the request body are optional but you must specify at least one.
+	 * @request PATCH:/run/{id}
+	 * @secure
+	 * @response `204` `void` No Content
+	 * @response `400` `ProblemDetails` Bad Request
+	 * @response `401` `void` Unauthorized
+	 * @response `403` `ProblemDetails` The user attempted to update another user's run, or the user is banned or not yet confirmed.
+	 * @response `404` `ProblemDetails` The Run with ID `id` could not be found, or has been deleted. Read `title` for more information.
+	 * @response `422` `ProblemDetails` Response can be a `ProblemDetails` for a request that doesn't match the run type of a category, or a `ValidationProblemDetails` otherwise.
+	 * @response `500` `void` Internal Server Error
+	 */
+	export namespace UpdateRun {
+		export type RequestParams = {
+			/** @pattern ^[a-zA-Z0-9-_]{22}$ */
+			id: string
+		}
+		export type RequestQuery = {}
+		export type RequestBody = UpdateRunPayload
+		export type RequestHeaders = {}
+		export type ResponseBody = void
+	}
+
+	/**
+	 * No description
+	 * @tags Runs
+	 * @name DeleteRun
+	 * @summary Deletes a Run.
+	 * @request DELETE:/run/{id}
+	 * @secure
+	 * @response `204` `void` No Content
+	 * @response `400` `ProblemDetails` Bad Request
+	 * @response `401` `void` Unauthorized
+	 * @response `403` `void` Forbidden
+	 * @response `404` `ProblemDetails` The run does not exist (Not Found) or was already deleted (Already Deleted). Use the `title` field of the response to differentiate between the two cases if necessary.
+	 * @response `500` `void` Internal Server Error
+	 */
+	export namespace DeleteRun {
+		export type RequestParams = {
+			/** @pattern ^[a-zA-Z0-9-_]{22}$ */
+			id: string
+		}
+		export type RequestQuery = {}
+		export type RequestBody = never
+		export type RequestHeaders = {}
+		export type ResponseBody = void
+	}
 }
diff --git a/src/lib/api/data-contracts.ts b/src/lib/api/data-contracts.ts
index 1f0cc6b..2f1a17c 100644
--- a/src/lib/api/data-contracts.ts
+++ b/src/lib/api/data-contracts.ts
@@ -50,6 +50,7 @@ export interface CategoryViewModel {
 	 * @example "1984-01-01T00:00:00Z"
 	 */
 	deletedAt: string | null
+	status: Status
 }
 
 /** A fake ProblemDetails subclass used for deserialization and documentation. Do not instantiate! */
@@ -61,10 +62,33 @@ export interface CategoryViewModelConflictDetails {
 	detail?: string | null
 	instance?: string | null
 	/** Represents a `Category` tied to a `Leaderboard`. */
-	conflicting?: CategoryViewModel
+	conflicting?: CategoryViewModel | null
 	[key: string]: any
 }
 
+export interface CategoryViewModelListView {
+	data: CategoryViewModel[]
+	/**
+	 * The total number of records matching the given criteria that
+	 * exist in the database, NOT the total number of records returned.
+	 * @format int64
+	 */
+	total: number
+	/**
+	 * The default limit that will be applied for this resource type
+	 * if the client does not specify one in the query string.
+	 * @format int32
+	 */
+	limitDefault: number
+	/**
+	 * The maximum value the client is allowed to specify as a limt for
+	 * endpoints return a paginated list of resources of this type.
+	 * Exceeding this value will result in an error.
+	 * @format int32
+	 */
+	limitMax: number
+}
+
 export interface ChangePasswordRequest {
 	/** @minLength 1 */
 	password: string
@@ -114,22 +138,33 @@ export interface CreateLeaderboardRequest {
 	info?: string
 }
 
-/** This request object is sent when creating a `Run`. */
-export interface CreateRunRequest {
-	info: string | null
+/**
+ * Request sent when creating a Run. Set `runType` to `"Time"` for a timed
+ * request, and `"Score"` for a scored one. `runType` *must* be at the top
+ * of the request object.
+ */
+export type CreateRunRequest = BaseCreateRunRequest &
+	(
+		| BaseCreateRunRequestRunTypeMapping<'Time', CreateTimedRunRequest>
+		| BaseCreateRunRequestRunTypeMapping<'Score', CreateScoredRunRequest>
+	)
+
+/** `runType: "Score"` */
+export type CreateScoredRunRequest = BaseCreateRunRequest & {
 	/**
-	 * The date the `Run` was played on.
-	 * @format date
-	 * @example "2000-01-01"
+	 * The score achieved during the run.
+	 * @format int64
 	 */
-	playedOn: string
+	score: number
+}
+
+/** `runType: "Time"` */
+export type CreateTimedRunRequest = BaseCreateRunRequest & {
 	/**
-	 * The ID of the `Category` for the `Run`.
-	 * @format int64
+	 * The duration of the run. Must obey the format 'HH:mm:ss.sss', with leading zeroes.
+	 * @example "12:34:56.999"
 	 */
-	categoryId: number
-	/** @format int64 */
-	timeOrScore: number
+	time: string
 }
 
 /** Represents a collection of `Leaderboard` entities. */
@@ -176,6 +211,7 @@ export interface LeaderboardViewModel {
 	 * @example "1984-01-01T00:00:00Z"
 	 */
 	deletedAt: string | null
+	status: Status
 }
 
 /** A fake ProblemDetails subclass used for deserialization and documentation. Do not instantiate! */
@@ -187,10 +223,33 @@ export interface LeaderboardViewModelConflictDetails {
 	detail?: string | null
 	instance?: string | null
 	/** Represents a collection of `Leaderboard` entities. */
-	conflicting?: LeaderboardViewModel
+	conflicting?: LeaderboardViewModel | null
 	[key: string]: any
 }
 
+export interface LeaderboardViewModelListView {
+	data: LeaderboardViewModel[]
+	/**
+	 * The total number of records matching the given criteria that
+	 * exist in the database, NOT the total number of records returned.
+	 * @format int64
+	 */
+	total: number
+	/**
+	 * The default limit that will be applied for this resource type
+	 * if the client does not specify one in the query string.
+	 * @format int32
+	 */
+	limitDefault: number
+	/**
+	 * The maximum value the client is allowed to specify as a limt for
+	 * endpoints return a paginated list of resources of this type.
+	 * Exceeding this value will result in an error.
+	 * @format int32
+	 */
+	limitMax: number
+}
+
 /** This request object is sent when a `User` is attempting to log in. */
 export interface LoginRequest {
 	/**
@@ -273,10 +332,33 @@ export type RunType = 'Time' | 'Score'
 
 export type RunViewModel = BaseRunViewModel &
 	(
-		| BaseRunViewModelTypeMapping<'Time', TimedRunViewModel>
-		| BaseRunViewModelTypeMapping<'Score', ScoredRunViewModel>
+		| BaseRunViewModelRunTypeMapping<'Time', TimedRunViewModel>
+		| BaseRunViewModelRunTypeMapping<'Score', ScoredRunViewModel>
 	)
 
+export interface RunViewModelListView {
+	data: (TimedRunViewModel | ScoredRunViewModel)[]
+	/**
+	 * The total number of records matching the given criteria that
+	 * exist in the database, NOT the total number of records returned.
+	 * @format int64
+	 */
+	total: number
+	/**
+	 * The default limit that will be applied for this resource type
+	 * if the client does not specify one in the query string.
+	 * @format int32
+	 */
+	limitDefault: number
+	/**
+	 * The maximum value the client is allowed to specify as a limt for
+	 * endpoints return a paginated list of resources of this type.
+	 * Exceeding this value will result in an error.
+	 * @format int32
+	 */
+	limitMax: number
+}
+
 export type ScoredRunViewModel = BaseRunViewModel & {
 	/**
 	 * The score achieved during the run.
@@ -287,6 +369,10 @@ export type ScoredRunViewModel = BaseRunViewModel & {
 
 export type SortDirection = 'Ascending' | 'Descending'
 
+export type Status = 'Published' | 'Deleted'
+
+export type StatusFilter = 'Published' | 'Deleted' | 'Any'
+
 export type TimedRunViewModel = BaseRunViewModel & {
 	/**
 	 * The duration of the run.
@@ -299,7 +385,7 @@ export interface UpdateCategoryRequest {
 	name?: string
 	slug?: string
 	info?: string
-	sortDirection?: SortDirection
+	sortDirection?: SortDirection | null
 }
 
 export interface UpdateLeaderboardRequest {
@@ -308,6 +394,26 @@ export interface UpdateLeaderboardRequest {
 	info?: string
 }
 
+/**
+ * Request sent when updating a run.
+ * All fields are optional but you must specify at least one.
+ */
+export type UpdateRunRequest = BaseUpdateRunRequest &
+	(
+		| BaseUpdateRunRequestRunTypeMapping<'Time', UpdateTimedRunRequest>
+		| BaseUpdateRunRequestRunTypeMapping<'Score', UpdateScoredRunRequest>
+	)
+
+export type UpdateScoredRunRequest = BaseUpdateRunRequest & {
+	/** @format int64 */
+	score?: number | null
+}
+
+export type UpdateTimedRunRequest = BaseUpdateRunRequest & {
+	/** @example "25:01:01.001" */
+	time?: string | null
+}
+
 export type UserRole = 'Registered' | 'Confirmed' | 'Administrator' | 'Banned'
 
 export interface UserViewModel {
@@ -346,8 +452,28 @@ export interface ValidationProblemDetails {
 	[key: string]: any
 }
 
+/**
+ * Request sent when creating a Run. Set `runType` to `"Time"` for a timed
+ * request, and `"Score"` for a scored one. `runType` *must* be at the top
+ * of the request object.
+ */
+interface BaseCreateRunRequest {
+	runType: RunType
+	info?: string
+	/**
+	 * The date the `Run` was played on. Must obey the format 'YYYY-MM-DD', with leading zeroes.
+	 * @format date
+	 * @example "2000-01-01"
+	 */
+	playedOn: string
+}
+
+type BaseCreateRunRequestRunTypeMapping<Key, Type> = {
+	runType: Key
+} & Type
+
 interface BaseRunViewModel {
-	$type: string
+	runType: RunType
 	/**
 	 * The unique identifier of the `Run`.
 	 *
@@ -358,7 +484,13 @@ interface BaseRunViewModel {
 	/** User-provided details about the run. */
 	info: string | null
 	/**
-	 * The time the run was created.
+	 * The date the run was done, *not* when it was submitted.
+	 * @format date
+	 * @example "2000-01-01"
+	 */
+	playedOn: string
+	/**
+	 * The time the run was submitted to the DB.
 	 * @format date-time
 	 * @example "1984-01-01T00:00:00Z"
 	 */
@@ -385,12 +517,41 @@ interface BaseRunViewModel {
 	 * @pattern ^[a-zA-Z0-9-_]{22}$
 	 */
 	userId: string
+	status: Status
 }
 
-type BaseRunViewModelTypeMapping<Key, Type> = {
-	$type: Key
+type BaseRunViewModelRunTypeMapping<Key, Type> = {
+	runType: Key
 } & Type
 
+/**
+ * Request sent when updating a run.
+ * All fields are optional but you must specify at least one.
+ */
+interface BaseUpdateRunRequest {
+	runType: RunType
+	info?: string
+	/**
+	 * @format date
+	 * @example "2000-01-01"
+	 */
+	playedOn?: string | null
+}
+
+type BaseUpdateRunRequestRunTypeMapping<Key, Type> = {
+	runType: Key
+} & Type
+
+/** This request object is sent when a `User` is attempting to register. */
+export type RegisterPayload = RegisterRequest
+
+/** This request object is sent when a `User` is attempting to log in. */
+export type LoginPayload = LoginRequest
+
+export type SendRecoveryEmailPayload = RecoverAccountRequest
+
+export type ChangePasswordPayload = ChangePasswordRequest
+
 export interface GetCategoryBySlugParams {
 	slug: string
 	/** @format int64 */
@@ -399,19 +560,97 @@ export interface GetCategoryBySlugParams {
 
 export interface GetCategoriesForLeaderboardParams {
 	/**
-	 * Whether to include deleted Categories. Defaults to `false`.
-	 * @default false
+	 * The maximum number of records to return. Fewer records may be returned.
+	 * @format int32
+	 */
+	limit?: number
+	/**
+	 * The zero-based index at which to begin selecting records to return.
+	 * @format int32
+	 * @default 0
 	 */
-	includeDeleted?: boolean
+	offset?: number
+	/** @default "Published" */
+	status?: StatusFilter
 	/** @format int64 */
 	id: number
 }
 
+/** This request object is sent when creating a `Category`. */
+export type CreateCategoryPayload = CreateCategoryRequest
+
+export type UpdateCategoryPayload = UpdateCategoryRequest
+
 export interface GetLeaderboardBySlugParams {
 	slug: string
 }
 
 export interface ListLeaderboardsParams {
-	/** @default false */
-	includeDeleted?: boolean
+	/**
+	 * The maximum number of records to return. Fewer records may be returned.
+	 * @format int32
+	 */
+	limit?: number
+	/**
+	 * The zero-based index at which to begin selecting records to return.
+	 * @format int32
+	 * @default 0
+	 */
+	offset?: number
+	/** @default "Published" */
+	status?: StatusFilter
 }
+
+export interface SearchLeaderboardsParams {
+	/** The query string. Must not be empty. */
+	q: string
+	/**
+	 * The maximum number of records to return. Fewer records may be returned.
+	 * @format int32
+	 */
+	limit?: number
+	/**
+	 * The zero-based index at which to begin selecting records to return.
+	 * @format int32
+	 * @default 0
+	 */
+	offset?: number
+	/** @default "Published" */
+	status?: StatusFilter
+}
+
+/** This request object is sent when creating a `Leaderboard`. */
+export type CreateLeaderboardPayload = CreateLeaderboardRequest
+
+export type UpdateLeaderboardPayload = UpdateLeaderboardRequest
+
+/**
+ * Request sent when creating a Run. Set `runType` to `"Time"` for a timed
+ * request, and `"Score"` for a scored one. `runType` *must* be at the top
+ * of the request object.
+ */
+export type CreateRunPayload = CreateTimedRunRequest | CreateScoredRunRequest
+
+export interface GetRunsForCategoryParams {
+	/**
+	 * The maximum number of records to return. Fewer records may be returned.
+	 * @format int32
+	 */
+	limit?: number
+	/**
+	 * The zero-based index at which to begin selecting records to return.
+	 * @format int32
+	 * @default 0
+	 */
+	offset?: number
+	/** @default "Published" */
+	status?: StatusFilter
+	/** @format int64 */
+	id: number
+}
+
+/**
+ * Request sent when updating a run.
+ * All fields are optional but you must specify at least one.
+ */
+export type UpdateRunPayload = UpdateTimedRunRequest | UpdateScoredRunRequest


Reviews:
user: login: buffet-time
body: Only some Nits, otherwise looks good.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-28T15:59:04Z
user: login: zysim
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T17:13:51Z
user: login: TheTedder
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-28T19:39:57Z
user: login: zysim
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-29T00:25:50Z
user: login: zysim
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-29T00:27:52Z

Review comments:
diff_hunk: @@ -1,52 +1,54 @@
 <script setup lang="ts">
 import { useAsyncState } from '@vueuse/core'
-import { computed, ref } from 'vue'
+import { ref } from 'vue'
 import { Leaderboards } from '../../lib/api/Leaderboards'
+import { StatusFilter } from '../../lib/api/data-contracts'
 
-enum IncludeDeleted {
-	NotDeleted,
-	Deleted,
-	All
-}
-
-const includeDeleted = ref(IncludeDeleted.NotDeleted)
-const search = ref<string>('')
+const limit = 256;
+const status = ref<StatusFilter>('Published')
+const query = ref<string>('')
path: src/components/leaderboards/List.vue
user: login: buffet-time
body: the `<string>` here is unnecessary as giving the `ref` function an initial value of an empty string TS wise we know the value is `string`.
created_at: 2025-05-28T15:58:09Z
author_association: MEMBER
diff_hunk: @@ -1,52 +1,54 @@
 <script setup lang="ts">
 import { useAsyncState } from '@vueuse/core'
-import { computed, ref } from 'vue'
+import { ref } from 'vue'
 import { Leaderboards } from '../../lib/api/Leaderboards'
+import { StatusFilter } from '../../lib/api/data-contracts'
 
-enum IncludeDeleted {
-	NotDeleted,
-	Deleted,
-	All
-}
-
-const includeDeleted = ref(IncludeDeleted.NotDeleted)
-const search = ref<string>('')
+const limit = 256;
+const status = ref<StatusFilter>('Published')
+const query = ref<string>('')
+const searchedQuery = ref<string>('')
path: src/components/leaderboards/List.vue
user: login: buffet-time
body: same as above
created_at: 2025-05-28T15:58:17Z
author_association: MEMBER
diff_hunk: @@ -1,52 +1,54 @@
 <script setup lang="ts">
 import { useAsyncState } from '@vueuse/core'
-import { computed, ref } from 'vue'
+import { ref } from 'vue'
 import { Leaderboards } from '../../lib/api/Leaderboards'
+import { StatusFilter } from '../../lib/api/data-contracts'
 
-enum IncludeDeleted {
-	NotDeleted,
-	Deleted,
-	All
-}
-
-const includeDeleted = ref(IncludeDeleted.NotDeleted)
-const search = ref<string>('')
+const limit = 256;
+const status = ref<StatusFilter>('Published')
+const query = ref<string>('')
+const searchedQuery = ref<string>('')
+const searchedStatus = ref<StatusFilter>()
 
 const leaderboardClient = new Leaderboards({
 	baseUrl: import.meta.env.VITE_BACKEND_URL
 })
 
 const {
-	state: allBoards,
+	state: boards,
 	error,
 	isLoading,
 	execute
 } = useAsyncState(async () => {
-	const resp = await leaderboardClient.listLeaderboards({
-		includeDeleted: true
+	const resp = query.value ? await leaderboardClient.searchLeaderboards({
+		q: query.value,
+		status: status.value,
+		limit: limit
+	}) : await leaderboardClient.listLeaderboards({
+		status: status.value,
+		limit: limit
 	})
 
-	return resp.data
+	return resp.data.data
 }, [])
 
-const boardsFiltered = computed(() =>
-	allBoards.value.filter(
-		(board) =>
-			includeDeleted.value === IncludeDeleted.All ||
-			(includeDeleted.value === IncludeDeleted.NotDeleted) ===
-				(board.deletedAt === null)
-	)
-)
-
-const boardsSearched = computed(() =>
-	boardsFiltered.value.filter(
-		(board) =>
-			!search.value ||
-			board.name
-				.toLocaleLowerCase()
-				.includes(search.value.trim().toLocaleLowerCase())
-	)
-)
+function search() {
+	execute()
+	searchedQuery.value = query.value
+	searchedStatus.value = status.value
+}
+
+function clear() {
+	query.value = ''
+	searchedQuery.value = ''
+	execute()
+}
+
+function filterChanged() {
+	if (!query.value){
+		search()
+	}
+}
path: src/components/leaderboards/List.vue
user: login: zysim
body: How do we get the boards of the same query and of the other status with that `if` check?
created_at: 2025-05-28T17:09:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,52 +1,54 @@
 <script setup lang="ts">
 import { useAsyncState } from '@vueuse/core'
-import { computed, ref } from 'vue'
+import { ref } from 'vue'
 import { Leaderboards } from '../../lib/api/Leaderboards'
+import { StatusFilter } from '../../lib/api/data-contracts'
 
-enum IncludeDeleted {
-	NotDeleted,
-	Deleted,
-	All
-}
-
-const includeDeleted = ref(IncludeDeleted.NotDeleted)
-const search = ref<string>('')
+const limit = 256;
+const status = ref<StatusFilter>('Published')
+const query = ref<string>('')
+const searchedQuery = ref<string>('')
+const searchedStatus = ref<StatusFilter>()
 
 const leaderboardClient = new Leaderboards({
 	baseUrl: import.meta.env.VITE_BACKEND_URL
 })
 
 const {
-	state: allBoards,
+	state: boards,
 	error,
 	isLoading,
 	execute
 } = useAsyncState(async () => {
-	const resp = await leaderboardClient.listLeaderboards({
-		includeDeleted: true
+	const resp = query.value ? await leaderboardClient.searchLeaderboards({
+		q: query.value,
+		status: status.value,
+		limit: limit
+	}) : await leaderboardClient.listLeaderboards({
+		status: status.value,
+		limit: limit
 	})
 
-	return resp.data
+	return resp.data.data
 }, [])
 
-const boardsFiltered = computed(() =>
-	allBoards.value.filter(
-		(board) =>
-			includeDeleted.value === IncludeDeleted.All ||
-			(includeDeleted.value === IncludeDeleted.NotDeleted) ===
-				(board.deletedAt === null)
-	)
-)
-
-const boardsSearched = computed(() =>
-	boardsFiltered.value.filter(
-		(board) =>
-			!search.value ||
-			board.name
-				.toLocaleLowerCase()
-				.includes(search.value.trim().toLocaleLowerCase())
-	)
-)
+function search() {
+	execute()
+	searchedQuery.value = query.value
+	searchedStatus.value = status.value
+}
+
+function clear() {
+	query.value = ''
+	searchedQuery.value = ''
+	execute()
+}
+
+function filterChanged() {
+	if (!query.value){
+		search()
+	}
+}
path: src/components/leaderboards/List.vue
user: login: TheTedder
body: You hit enter again. I could also make it just redo the query if you change the filter.
created_at: 2025-05-28T19:39:57Z
author_association: COLLABORATOR
diff_hunk: @@ -1,52 +1,54 @@
 <script setup lang="ts">
 import { useAsyncState } from '@vueuse/core'
-import { computed, ref } from 'vue'
+import { ref } from 'vue'
 import { Leaderboards } from '../../lib/api/Leaderboards'
+import { StatusFilter } from '../../lib/api/data-contracts'
 
-enum IncludeDeleted {
-	NotDeleted,
-	Deleted,
-	All
-}
-
-const includeDeleted = ref(IncludeDeleted.NotDeleted)
-const search = ref<string>('')
+const limit = 256;
+const status = ref<StatusFilter>('Published')
+const query = ref<string>('')
+const searchedQuery = ref<string>('')
+const searchedStatus = ref<StatusFilter>()
 
 const leaderboardClient = new Leaderboards({
 	baseUrl: import.meta.env.VITE_BACKEND_URL
 })
 
 const {
-	state: allBoards,
+	state: boards,
 	error,
 	isLoading,
 	execute
 } = useAsyncState(async () => {
-	const resp = await leaderboardClient.listLeaderboards({
-		includeDeleted: true
+	const resp = query.value ? await leaderboardClient.searchLeaderboards({
+		q: query.value,
+		status: status.value,
+		limit: limit
+	}) : await leaderboardClient.listLeaderboards({
+		status: status.value,
+		limit: limit
 	})
 
-	return resp.data
+	return resp.data.data
 }, [])
 
-const boardsFiltered = computed(() =>
-	allBoards.value.filter(
-		(board) =>
-			includeDeleted.value === IncludeDeleted.All ||
-			(includeDeleted.value === IncludeDeleted.NotDeleted) ===
-				(board.deletedAt === null)
-	)
-)
-
-const boardsSearched = computed(() =>
-	boardsFiltered.value.filter(
-		(board) =>
-			!search.value ||
-			board.name
-				.toLocaleLowerCase()
-				.includes(search.value.trim().toLocaleLowerCase())
-	)
-)
+function search() {
+	execute()
+	searchedQuery.value = query.value
+	searchedStatus.value = status.value
+}
+
+function clear() {
+	query.value = ''
+	searchedQuery.value = ''
+	execute()
+}
+
+function filterChanged() {
+	if (!query.value){
+		search()
+	}
+}
path: src/components/leaderboards/List.vue
user: login: zysim
body: I think it'll be better that way
created_at: 2025-05-29T00:25:50Z
author_association: CONTRIBUTOR

PR comments:
user: login: TheTedder
created_at: 2025-05-28T20:09:06Z
body: If I had more time I would have the query and the filter be stored in the URL.
author_association: COLLABORATOR
user: login: zysim
created_at: 2025-05-29T00:29:57Z
body: Do that when we have the time to, yeah
author_association: CONTRIBUTOR