diff --git a/app/[board_name]/[layout_id]/[size_id]/[set_ids]/[angle]/list/layout-client.tsx b/app/[board_name]/[layout_id]/[size_id]/[set_ids]/[angle]/list/layout-client.tsx index 8afbe60..06291a1 100644 --- a/app/[board_name]/[layout_id]/[size_id]/[set_ids]/[angle]/list/layout-client.tsx +++ b/app/[board_name]/[layout_id]/[size_id]/[set_ids]/[angle]/list/layout-client.tsx @@ -69,14 +69,14 @@ const TabsWrapper: React.FC<{ boardDetails: BoardDetails }> = ({ boardDetails }) const ListLayoutClient: React.FC> = ({ boardDetails, children }) => { return ( - - {children} - - + + + {children} + - - - + + + ); }; diff --git a/app/api/internal/[board]/circuits/[uuid]/climbs/route.ts b/app/api/internal/[board]/circuits/[uuid]/climbs/route.ts new file mode 100644 index 0000000..b5cd17d --- /dev/null +++ b/app/api/internal/[board]/circuits/[uuid]/climbs/route.ts @@ -0,0 +1,28 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { BoardName } from '@/app/lib/types'; +import { getClimbsByCircuit } from '@/app/lib/db/queries/circuits/get-circuits'; + +type Params = Promise<{ + board: BoardName; + uuid: string; +}>; + +export async function GET( + request: NextRequest, + segmentData: { params: Params } +): Promise { + const params = await segmentData.params; + const { board, uuid } = params; + + try { + const climbUuids = await getClimbsByCircuit(board, uuid); + + return NextResponse.json({ climbUuids }); + } catch (error) { + console.error('Error fetching circuit climbs:', error); + return NextResponse.json( + { error: 'Failed to fetch circuit climbs' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/internal/[board]/circuits/route.ts b/app/api/internal/[board]/circuits/route.ts new file mode 100644 index 0000000..7cefbbb --- /dev/null +++ b/app/api/internal/[board]/circuits/route.ts @@ -0,0 +1,45 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { BoardName } from '@/app/lib/types'; +import { getUserAndPublicCircuits, getCircuitsByUser } from '@/app/lib/db/queries/circuits/get-circuits'; +import { getSession } from '@/app/lib/session'; +import { cookies } from 'next/headers'; + +type Params = Promise<{ + board: BoardName; +}>; + +export async function GET( + request: NextRequest, + segmentData: { params: Params } +): Promise { + const params = await segmentData.params; + const { board } = params; + + try { + const cookieStore = await cookies(); + const session = await getSession(cookieStore, board); + + let userId: number | undefined; + if (session) { + userId = session.userId; + } + + const searchParams = request.nextUrl.searchParams; + const userOnly = searchParams.get('userOnly') === 'true'; + + let circuits; + if (userOnly && userId) { + circuits = await getCircuitsByUser(board, userId); + } else { + circuits = await getUserAndPublicCircuits(board, userId); + } + + return NextResponse.json({ circuits }); + } catch (error) { + console.error('Error fetching circuits:', error); + return NextResponse.json( + { error: 'Failed to fetch circuits' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/internal/[board]/climbs/[uuid]/circuits/route.ts b/app/api/internal/[board]/climbs/[uuid]/circuits/route.ts new file mode 100644 index 0000000..f9b676f --- /dev/null +++ b/app/api/internal/[board]/climbs/[uuid]/circuits/route.ts @@ -0,0 +1,28 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { BoardName } from '@/app/lib/types'; +import { getCircuitsForClimb } from '@/app/lib/db/queries/circuits/get-circuits'; + +type Params = Promise<{ + board: BoardName; + uuid: string; +}>; + +export async function GET( + request: NextRequest, + segmentData: { params: Params } +): Promise { + const params = await segmentData.params; + const { board, uuid } = params; + + try { + const circuits = await getCircuitsForClimb(board, uuid); + + return NextResponse.json({ circuits }); + } catch (error) { + console.error('Error fetching climb circuits:', error); + return NextResponse.json( + { error: 'Failed to fetch climb circuits' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/components/board-page/climbs-list.tsx b/app/components/board-page/climbs-list.tsx index c07a590..1f63c34 100644 --- a/app/components/board-page/climbs-list.tsx +++ b/app/components/board-page/climbs-list.tsx @@ -6,6 +6,7 @@ import InfiniteScroll from 'react-infinite-scroll-component'; import { track } from '@vercel/analytics'; import { Climb, ParsedBoardRouteParameters, BoardDetails } from '@/app/lib/types'; import { useQueueContext } from '../queue-control/queue-context'; +import { useUISearchParams } from '../queue-control/ui-searchparams-provider'; import ClimbCard from '../climb-card/climb-card'; import { useEffect, useRef } from 'react'; import { PlusCircleOutlined, FireOutlined } from '@ant-design/icons'; @@ -44,6 +45,8 @@ const ClimbsList = ({ boardDetails, initialClimbs }: ClimbsListProps) => { hasDoneFirstFetch, isFetchingClimbs, } = useQueueContext(); + + const { updateFilters } = useUISearchParams(); const searchParams = useSearchParams(); const page = searchParams.get('page'); @@ -161,6 +164,13 @@ const ClimbsList = ({ boardDetails, initialClimbs }: ClimbsListProps) => { climbUuid: climb.uuid, }); }} + onCircuitClick={(circuitUuid) => { + updateFilters({ circuitUuids: [circuitUuid] }); + track('Circuit Tag Clicked', { + circuitUuid, + climbUuid: climb.uuid, + }); + }} /> diff --git a/app/components/climb-card/climb-card.tsx b/app/components/climb-card/climb-card.tsx index b3f495f..c5a9af0 100644 --- a/app/components/climb-card/climb-card.tsx +++ b/app/components/climb-card/climb-card.tsx @@ -2,11 +2,14 @@ import React from 'react'; import Card from 'antd/es/card'; +import { Tag } from 'antd'; import { CopyrightOutlined } from '@ant-design/icons'; import ClimbCardCover from './climb-card-cover'; import { Climb, BoardDetails } from '@/app/lib/types'; import ClimbCardActions from './climb-card-actions'; +import { useClimbCircuits } from '@/app/hooks/use-climb-circuits'; +import { useBoardProvider } from '@/app/components/board-provider/board-provider-context'; type ClimbCardProps = { climb?: Climb; @@ -15,9 +18,13 @@ type ClimbCardProps = { onCoverClick?: () => void; selected?: boolean; actions?: React.JSX.Element[]; + onCircuitClick?: (circuitUuid: string) => void; }; -const ClimbCard = ({ climb, boardDetails, onCoverClick, selected, actions }: ClimbCardProps) => { +const ClimbCard = ({ climb, boardDetails, onCoverClick, selected, actions, onCircuitClick }: ClimbCardProps) => { + const { boardName, isAuthenticated } = useBoardProvider(); + const { circuits } = useClimbCircuits(boardName, climb?.uuid || null, Boolean(isAuthenticated && climb?.uuid)); + const cover = ; const cardTitle = climb ? ( @@ -49,7 +56,37 @@ const ClimbCard = ({ climb, boardDetails, onCoverClick, selected, actions }: Cli actions={actions || ClimbCardActions({ climb, boardDetails })} > {/* TODO: Make a link to the list with the setter_name filter */} - {climb ? `By ${climb.setter_username} - ${climb.ascensionist_count} ascents` : null} + {climb && ( +
+
0 ? '8px' : '0' }}> + By {climb.setter_username} - {climb.ascensionist_count} ascents +
+ {circuits.length > 0 && ( +
+ {circuits.map((circuit) => ( + { + e.stopPropagation(); + onCircuitClick(circuit.uuid); + } : undefined} + title={onCircuitClick ? `Filter by circuit: ${circuit.name}` : circuit.name || 'Unnamed Circuit'} + > + {circuit.name || 'Unnamed Circuit'} + + ))} +
+ )} +
+ )} {cover} ); diff --git a/app/components/queue-control/__tests__/hooks/use-queue-data-fetching.test.tsx b/app/components/queue-control/__tests__/hooks/use-queue-data-fetching.test.tsx index 115538d..2a2780c 100644 --- a/app/components/queue-control/__tests__/hooks/use-queue-data-fetching.test.tsx +++ b/app/components/queue-control/__tests__/hooks/use-queue-data-fetching.test.tsx @@ -81,7 +81,8 @@ const mockSearchParams: SearchRequestPagination = { hideAttempted: false, hideCompleted: false, showOnlyAttempted: false, - showOnlyCompleted: false + showOnlyCompleted: false, + circuitUuids: [] }; const mockParsedParams: ParsedBoardRouteParameters = { diff --git a/app/components/queue-control/__tests__/reducer.test.ts b/app/components/queue-control/__tests__/reducer.test.ts index 03ad4b9..9eaff6a 100644 --- a/app/components/queue-control/__tests__/reducer.test.ts +++ b/app/components/queue-control/__tests__/reducer.test.ts @@ -47,7 +47,8 @@ const mockSearchParams: SearchRequestPagination = { hideAttempted: false, hideCompleted: false, showOnlyAttempted: false, - showOnlyCompleted: false + showOnlyCompleted: false, + circuitUuids: [] }; const initialState: QueueState = { @@ -292,7 +293,8 @@ describe('queueReducer', () => { hideAttempted: false, hideCompleted: false, showOnlyAttempted: false, - showOnlyCompleted: false + showOnlyCompleted: false, + circuitUuids: [] }; const action: QueueAction = { diff --git a/app/components/queue-control/ui-searchparams-provider.tsx b/app/components/queue-control/ui-searchparams-provider.tsx index 42c2503..5eb4c58 100644 --- a/app/components/queue-control/ui-searchparams-provider.tsx +++ b/app/components/queue-control/ui-searchparams-provider.tsx @@ -41,6 +41,7 @@ export const UISearchParamsProvider: React.FC<{ children: React.ReactNode }> = ( if (uiSearchParams.hideCompleted) activeFilters.push('hideCompleted'); if (uiSearchParams.showOnlyAttempted) activeFilters.push('showOnlyAttempted'); if (uiSearchParams.showOnlyCompleted) activeFilters.push('showOnlyCompleted'); + if (uiSearchParams.circuitUuids && uiSearchParams.circuitUuids.length > 0) activeFilters.push('circuits'); if (activeFilters.length > 0) { track('Climb Search Performed', { diff --git a/app/components/search-drawer/basic-search-form.tsx b/app/components/search-drawer/basic-search-form.tsx index c802b79..5054511 100644 --- a/app/components/search-drawer/basic-search-form.tsx +++ b/app/components/search-drawer/basic-search-form.tsx @@ -5,13 +5,15 @@ import { Form, InputNumber, Row, Col, Select, Input, Switch, Alert, Typography } import { TENSION_KILTER_GRADES } from '@/app/lib/board-data'; import { useUISearchParams } from '@/app/components/queue-control/ui-searchparams-provider'; import { useBoardProvider } from '@/app/components/board-provider/board-provider-context'; +import { useCircuits } from '@/app/hooks/use-circuits'; import SearchClimbNameInput from './search-climb-name-input'; const { Title } = Typography; const BasicSearchForm: React.FC = () => { const { uiSearchParams, updateFilters } = useUISearchParams(); - const { token, user_id } = useBoardProvider(); + const { token, user_id, boardName } = useBoardProvider(); + const { circuits, isLoading: circuitsLoading } = useCircuits(boardName, Boolean(token && user_id)); const grades = TENSION_KILTER_GRADES; const isLoggedIn = token && user_id; @@ -194,6 +196,40 @@ const BasicSearchForm: React.FC = () => { updateFilters({ settername: e.target.value })} /> + {isLoggedIn && ( + + + + )} + Personal Progress diff --git a/app/hooks/use-circuits.ts b/app/hooks/use-circuits.ts new file mode 100644 index 0000000..1373015 --- /dev/null +++ b/app/hooks/use-circuits.ts @@ -0,0 +1,18 @@ +import useSWR from 'swr'; +import { BoardName } from '@/app/lib/types'; +import { Circuit } from '@/app/lib/db/queries/circuits/get-circuits'; + +const fetcher = (url: string) => fetch(url).then((res) => res.json()); + +export function useCircuits(boardName: BoardName | null, enabled: boolean = true) { + const { data, error, isLoading } = useSWR( + enabled && boardName ? `/api/internal/${boardName}/circuits` : null, + fetcher + ); + + return { + circuits: (data?.circuits || []) as Circuit[], + isLoading, + error, + }; +} \ No newline at end of file diff --git a/app/hooks/use-climb-circuits.ts b/app/hooks/use-climb-circuits.ts new file mode 100644 index 0000000..7feb19f --- /dev/null +++ b/app/hooks/use-climb-circuits.ts @@ -0,0 +1,18 @@ +import useSWR from 'swr'; +import { BoardName, ClimbUuid } from '@/app/lib/types'; +import { Circuit } from '@/app/lib/db/queries/circuits/get-circuits'; + +const fetcher = (url: string) => fetch(url).then((res) => res.json()); + +export function useClimbCircuits(boardName: BoardName | null, climbUuid: ClimbUuid | null, enabled: boolean = true) { + const { data, error, isLoading } = useSWR( + enabled && boardName && climbUuid ? `/api/internal/${boardName}/climbs/${climbUuid}/circuits` : null, + fetcher + ); + + return { + circuits: (data?.circuits || []) as Circuit[], + isLoading, + error, + }; +} \ No newline at end of file diff --git a/app/lib/api-wrappers/aurora/types.ts b/app/lib/api-wrappers/aurora/types.ts index 712080e..c3c3f42 100644 --- a/app/lib/api-wrappers/aurora/types.ts +++ b/app/lib/api-wrappers/aurora/types.ts @@ -201,6 +201,7 @@ export const USER_TABLES = [ 'bids', 'tags', 'circuits', + 'circuits_climbs', ]; export const SHARED_SYNC_TABLES = [ 'products', diff --git a/app/lib/data-sync/aurora/user-sync.ts b/app/lib/data-sync/aurora/user-sync.ts index a4a3966..42ee074 100644 --- a/app/lib/data-sync/aurora/user-sync.ts +++ b/app/lib/data-sync/aurora/user-sync.ts @@ -252,6 +252,26 @@ async function upsertTableData( break; } + case 'circuits_climbs': { + const circuitsClimbsSchema = getTable('circuitsClimbs', boardName); + for (const item of data) { + await db + .insert(circuitsClimbsSchema) + .values({ + circuitUuid: item.circuit_uuid, + climbUuid: item.climb_uuid, + position: Number(item.position || 0), + }) + .onConflictDoUpdate({ + target: [circuitsClimbsSchema.circuitUuid, circuitsClimbsSchema.climbUuid], + set: { + position: Number(item.position || 0), + }, + }); + } + break; + } + default: console.warn(`No specific upsert logic for table: ${tableName}`); break; diff --git a/app/lib/db/queries/circuits/get-circuits.ts b/app/lib/db/queries/circuits/get-circuits.ts new file mode 100644 index 0000000..a5ccd0e --- /dev/null +++ b/app/lib/db/queries/circuits/get-circuits.ts @@ -0,0 +1,176 @@ +import { getPool } from '@/app/lib/db/db'; +import { BoardName } from '@/app/lib/types'; +import { getTable } from '../util/table-select'; +import { drizzle } from 'drizzle-orm/neon-serverless'; +import { eq, or, desc } from 'drizzle-orm'; + +export interface Circuit { + uuid: string; + name: string | null; + description: string | null; + color: string | null; + userId: number | null; + isPublic: boolean | null; + createdAt: string | null; + updatedAt: string | null; +} + +export interface CircuitWithClimbs extends Circuit { + climbUuids: string[]; +} + +export async function getCircuitsByUser(boardName: BoardName, userId: number): Promise { + const pool = getPool(); + const client = await pool.connect(); + + try { + const db = drizzle(client); + const circuitsTable = getTable('circuits', boardName); + + const circuits = await db + .select() + .from(circuitsTable) + .where(eq(circuitsTable.userId, userId)) + .orderBy(desc(circuitsTable.updatedAt)); + + return circuits as Circuit[]; + } finally { + client.release(); + } +} + +export async function getPublicCircuits(boardName: BoardName, limit = 50): Promise { + const pool = getPool(); + const client = await pool.connect(); + + try { + const db = drizzle(client); + const circuitsTable = getTable('circuits', boardName); + + const circuits = await db + .select() + .from(circuitsTable) + .where(eq(circuitsTable.isPublic, true)) + .orderBy(desc(circuitsTable.updatedAt)) + .limit(limit); + + return circuits as Circuit[]; + } finally { + client.release(); + } +} + +export async function getUserAndPublicCircuits(boardName: BoardName, userId?: number): Promise { + const pool = getPool(); + const client = await pool.connect(); + + try { + const db = drizzle(client); + const circuitsTable = getTable('circuits', boardName); + + let whereCondition; + if (userId) { + whereCondition = or( + eq(circuitsTable.userId, userId), + eq(circuitsTable.isPublic, true) + ); + } else { + whereCondition = eq(circuitsTable.isPublic, true); + } + + const circuits = await db + .select() + .from(circuitsTable) + .where(whereCondition) + .orderBy(desc(circuitsTable.updatedAt)); + + return circuits as Circuit[]; + } finally { + client.release(); + } +} + +export async function getClimbsByCircuit(boardName: BoardName, circuitUuid: string): Promise { + const pool = getPool(); + const client = await pool.connect(); + + try { + const db = drizzle(client); + const circuitsClimbsTable = getTable('circuitsClimbs', boardName); + + const climbs = await db + .select({ + climbUuid: circuitsClimbsTable.climbUuid, + position: circuitsClimbsTable.position, + }) + .from(circuitsClimbsTable) + .where(eq(circuitsClimbsTable.circuitUuid, circuitUuid)) + .orderBy(circuitsClimbsTable.position || circuitsClimbsTable.climbUuid); // fallback to uuid if position is null + + return climbs.map((c) => c.climbUuid); + } finally { + client.release(); + } +} + +export async function getCircuitsForClimb(boardName: BoardName, climbUuid: string): Promise { + const pool = getPool(); + const client = await pool.connect(); + + try { + const db = drizzle(client); + const circuitsTable = getTable('circuits', boardName); + const circuitsClimbsTable = getTable('circuitsClimbs', boardName); + + const circuits = await db + .select({ + uuid: circuitsTable.uuid, + name: circuitsTable.name, + description: circuitsTable.description, + color: circuitsTable.color, + userId: circuitsTable.userId, + isPublic: circuitsTable.isPublic, + createdAt: circuitsTable.createdAt, + updatedAt: circuitsTable.updatedAt, + }) + .from(circuitsTable) + .innerJoin( + circuitsClimbsTable, + eq(circuitsTable.uuid, circuitsClimbsTable.circuitUuid) + ) + .where(eq(circuitsClimbsTable.climbUuid, climbUuid)); + + return circuits as Circuit[]; + } finally { + client.release(); + } +} + +export async function getCircuitWithClimbs(boardName: BoardName, circuitUuid: string): Promise { + const pool = getPool(); + const client = await pool.connect(); + + try { + const db = drizzle(client); + const circuitsTable = getTable('circuits', boardName); + + const [circuit] = await db + .select() + .from(circuitsTable) + .where(eq(circuitsTable.uuid, circuitUuid)) + .limit(1); + + if (!circuit) { + return null; + } + + const climbUuids = await getClimbsByCircuit(boardName, circuitUuid); + + return { + ...(circuit as Circuit), + climbUuids, + }; + } finally { + client.release(); + } +} \ No newline at end of file diff --git a/app/lib/db/queries/climbs/create-climb-filters.ts b/app/lib/db/queries/climbs/create-climb-filters.ts index 87ce9c4..13a5037 100644 --- a/app/lib/db/queries/climbs/create-climb-filters.ts +++ b/app/lib/db/queries/climbs/create-climb-filters.ts @@ -140,6 +140,19 @@ export const createClimbFilters = ( } } + // Circuit filter conditions + const circuitConditions: SQL[] = []; + if (searchParams.circuitUuids && searchParams.circuitUuids.length > 0) { + const circuitsClimbsTable = getTableName(params.board_name, 'circuitsClimbs'); + circuitConditions.push( + sql`EXISTS ( + SELECT 1 FROM ${sql.identifier(circuitsClimbsTable)} + WHERE climb_uuid = ${tables.climbs.uuid} + AND circuit_uuid = ANY(${searchParams.circuitUuids}) + )` + ); + } + // User-specific logbook data selectors const getUserLogbookSelects = () => { const ascentsTable = getTableName(params.board_name, 'ascents'); @@ -188,7 +201,7 @@ export const createClimbFilters = ( return { // Helper function to get all climb filtering conditions - getClimbWhereConditions: () => [...baseConditions, ...nameCondition, ...holdConditions, ...personalProgressConditions], + getClimbWhereConditions: () => [...baseConditions, ...nameCondition, ...holdConditions, ...personalProgressConditions, ...circuitConditions], // Size-specific conditions getSizeConditions: () => sizeConditions, @@ -221,6 +234,7 @@ export const createClimbFilters = ( holdConditions, sizeConditions, personalProgressConditions, + circuitConditions, anyHolds, notHolds, }; diff --git a/app/lib/db/queries/util/table-select.ts b/app/lib/db/queries/util/table-select.ts index 4ba5e7c..5a7af76 100644 --- a/app/lib/db/queries/util/table-select.ts +++ b/app/lib/db/queries/util/table-select.ts @@ -7,6 +7,7 @@ import { kilterLayouts, kilterUsers, kilterCircuits, + kilterCircuitsClimbs, kilterAscents, kilterBids, kilterClimbStatsHistory, @@ -17,6 +18,7 @@ import { tensionLayouts, tensionUsers, tensionCircuits, + tensionCircuitsClimbs, tensionAscents, tensionBids, tensionClimbStatsHistory, @@ -49,6 +51,7 @@ export type TableSet = { layouts: typeof kilterLayouts | typeof tensionLayouts; users: typeof kilterUsers | typeof tensionUsers; circuits: typeof kilterCircuits | typeof tensionCircuits; + circuitsClimbs: typeof kilterCircuitsClimbs | typeof tensionCircuitsClimbs; ascents: typeof kilterAscents | typeof tensionAscents; bids: typeof kilterBids | typeof tensionBids; climbStatsHistory: typeof kilterClimbStatsHistory | typeof tensionClimbStatsHistory; @@ -72,6 +75,7 @@ const BOARD_TABLES: Record = { layouts: kilterLayouts, users: kilterUsers, circuits: kilterCircuits, + circuitsClimbs: kilterCircuitsClimbs, ascents: kilterAscents, bids: kilterBids, climbStatsHistory: kilterClimbStatsHistory, @@ -92,6 +96,7 @@ const BOARD_TABLES: Record = { layouts: tensionLayouts, users: tensionUsers, circuits: tensionCircuits, + circuitsClimbs: tensionCircuitsClimbs, ascents: tensionAscents, bids: tensionBids, climbStatsHistory: tensionClimbStatsHistory, diff --git a/app/lib/types.ts b/app/lib/types.ts index f3671ff..afa3909 100644 --- a/app/lib/types.ts +++ b/app/lib/types.ts @@ -93,6 +93,7 @@ export type SearchRequest = { hideCompleted: boolean; showOnlyAttempted: boolean; showOnlyCompleted: boolean; + circuitUuids: string[]; [key: `hold_${number}`]: HoldFilterValue; // Allow dynamic hold keys directly in the search params }; diff --git a/app/lib/url-utils.ts b/app/lib/url-utils.ts index ab457f0..6258c65 100644 --- a/app/lib/url-utils.ts +++ b/app/lib/url-utils.ts @@ -54,6 +54,7 @@ export const searchParamsToUrlParams = ({ hideCompleted, showOnlyAttempted, showOnlyCompleted, + circuitUuids, page, pageSize, }: SearchRequestPagination): URLSearchParams => { @@ -119,6 +120,11 @@ export const searchParamsToUrlParams = ({ }); } + // Add circuit UUIDs if they exist + if (circuitUuids && circuitUuids.length > 0) { + params.circuitUuids = circuitUuids.join(','); + } + return new URLSearchParams(params); }; export const DEFAULT_SEARCH_PARAMS: SearchRequestPagination = { @@ -138,6 +144,7 @@ export const DEFAULT_SEARCH_PARAMS: SearchRequestPagination = { hideCompleted: false, showOnlyAttempted: false, showOnlyCompleted: false, + circuitUuids: [], page: 0, pageSize: PAGE_LIMIT, }; @@ -149,6 +156,8 @@ export const urlParamsToSearchParams = (urlParams: URLSearchParams): SearchReque .map(([key, value]) => [key.replace('hold_', ''), value]), ); + const circuitUuids = urlParams.get('circuitUuids')?.split(',').filter(Boolean) ?? DEFAULT_SEARCH_PARAMS.circuitUuids; + return { ...DEFAULT_SEARCH_PARAMS, gradeAccuracy: Number(urlParams.get('gradeAccuracy') ?? DEFAULT_SEARCH_PARAMS.gradeAccuracy), @@ -168,6 +177,7 @@ export const urlParamsToSearchParams = (urlParams: URLSearchParams): SearchReque hideCompleted: urlParams.get('hideCompleted') === 'true', showOnlyAttempted: urlParams.get('showOnlyAttempted') === 'true', showOnlyCompleted: urlParams.get('showOnlyCompleted') === 'true', + circuitUuids, page: Number(urlParams.get('page') ?? DEFAULT_SEARCH_PARAMS.page), pageSize: Number(urlParams.get('pageSize') ?? DEFAULT_SEARCH_PARAMS.pageSize), };