Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,26 @@ export async function GET(
const parsedParams = await parseBoardRouteParamsWithSlugs(params);
const searchParams: SearchRequestPagination = urlParamsToSearchParams(query);

// Extract user authentication from headers for personal progress filters
let userId: number | undefined;
const personalProgressFiltersEnabled =
searchParams.hideAttempted ||
searchParams.hideCompleted ||
searchParams.showOnlyAttempted ||
searchParams.showOnlyCompleted;

if (personalProgressFiltersEnabled) {
const userIdHeader = req.headers.get('x-user-id');
const tokenHeader = req.headers.get('x-auth-token');

// Only use userId if both user ID and token are provided (basic auth check)
if (userIdHeader && tokenHeader && userIdHeader !== 'null') {
userId = parseInt(userIdHeader, 10);
}
}

// Call the separate function to perform the search
const result = await searchClimbs(parsedParams, searchParams);
const result = await searchClimbs(parsedParams, searchParams, userId);

// Return response
return NextResponse.json({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,11 @@ const mockSearchParams: SearchRequestPagination = {
onlyClassics: false,
settername: '',
setternameSuggestion: '',
holdsFilter: {}
holdsFilter: {},
hideAttempted: false,
hideCompleted: false,
showOnlyAttempted: false,
showOnlyCompleted: false
};

const mockParsedParams: ParsedBoardRouteParameters = {
Expand Down
12 changes: 10 additions & 2 deletions app/components/queue-control/__tests__/reducer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@ const mockSearchParams: SearchRequestPagination = {
onlyClassics: false,
settername: '',
setternameSuggestion: '',
holdsFilter: {}
holdsFilter: {},
hideAttempted: false,
hideCompleted: false,
showOnlyAttempted: false,
showOnlyCompleted: false
};

const initialState: QueueState = {
Expand Down Expand Up @@ -284,7 +288,11 @@ describe('queueReducer', () => {
onlyClassics: false,
settername: '',
setternameSuggestion: '',
holdsFilter: {}
holdsFilter: {},
hideAttempted: false,
hideCompleted: false,
showOnlyAttempted: false,
showOnlyCompleted: false
};

const action: QueueAction = {
Expand Down
16 changes: 14 additions & 2 deletions app/components/queue-control/hooks/use-queue-data-fetching.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,18 @@ import { Climb, ParsedBoardRouteParameters, SearchRequestPagination } from '@/ap
import { useEffect, useMemo } from 'react';
import { useBoardProvider } from '../../board-provider/board-provider-context';

const fetcher = (url: string) => fetch(url).then((res) => res.json());
const createFetcher = (authState: { token: string | null; user_id: number | null }) =>
(url: string) => {
const headers: Record<string, string> = {};

// Add authentication headers if available
if (authState.token && authState.user_id) {
headers['x-auth-token'] = authState.token;
headers['x-user-id'] = authState.user_id.toString();
}

return fetch(url, { headers }).then((res) => res.json());
};

interface UseQueueDataFetchingProps {
searchParams: SearchRequestPagination;
Expand All @@ -24,8 +35,9 @@ export const useQueueDataFetching = ({
hasDoneFirstFetch,
setHasDoneFirstFetch,
}: UseQueueDataFetchingProps) => {
const { getLogbook } = useBoardProvider();
const { getLogbook, token, user_id } = useBoardProvider();
const fetchedUuidsRef = useRef<string>('');
const fetcher = useMemo(() => createFetcher({ token, user_id }), [token, user_id]);

const getKey = (pageIndex: number, previousPageData: { climbs: Climb[] }) => {
if (previousPageData && previousPageData.climbs.length === 0) return null;
Expand Down
4 changes: 4 additions & 0 deletions app/components/queue-control/ui-searchparams-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ export const UISearchParamsProvider: React.FC<{ children: React.ReactNode }> = (
if (uiSearchParams.settername) activeFilters.push('setter');
if (uiSearchParams.holdsFilter && Object.entries(uiSearchParams.holdsFilter).length > 0)
activeFilters.push('holds');
if (uiSearchParams.hideAttempted) activeFilters.push('hideAttempted');
if (uiSearchParams.hideCompleted) activeFilters.push('hideCompleted');
if (uiSearchParams.showOnlyAttempted) activeFilters.push('showOnlyAttempted');
if (uiSearchParams.showOnlyCompleted) activeFilters.push('showOnlyCompleted');

if (activeFilters.length > 0) {
track('Climb Search Performed', {
Expand Down
82 changes: 71 additions & 11 deletions app/components/search-drawer/basic-search-form.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
'use client';

import React from 'react';
import { Form, InputNumber, Row, Col, Select, Input } from 'antd';
import { Form, InputNumber, Row, Col, Select, Input, Switch, Alert, Typography } from 'antd';
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 SearchClimbNameInput from './search-climb-name-input';

const { Title } = Typography;

const BasicSearchForm: React.FC = () => {
const { uiSearchParams, updateFilters } = useUISearchParams();
const { token, user_id } = useBoardProvider();
const grades = TENSION_KILTER_GRADES;

const isLoggedIn = token && user_id;

const handleGradeChange = (type: 'min' | 'max', value: number | undefined) => {
if (type === 'min') {
Expand All @@ -18,8 +24,59 @@ const BasicSearchForm: React.FC = () => {
}
};

const renderLogbookSection = () => {
if (!isLoggedIn) {
return (
<Form.Item>
<Alert
message="Sign in to access personal progress filters"
description="Login to your account to filter climbs based on your attempt and completion history."
type="info"
showIcon
/>
</Form.Item>
);
}

return (
<>
<Form.Item label="Hide Attempted" valuePropName="checked">
<Switch
style={{ float: 'right' }}
checked={uiSearchParams.hideAttempted}
onChange={(checked) => updateFilters({ hideAttempted: checked })}
/>
</Form.Item>

<Form.Item label="Hide Completed" valuePropName="checked">
<Switch
style={{ float: 'right' }}
checked={uiSearchParams.hideCompleted}
onChange={(checked) => updateFilters({ hideCompleted: checked })}
/>
</Form.Item>

<Form.Item label="Only Attempted" valuePropName="checked">
<Switch
style={{ float: 'right' }}
checked={uiSearchParams.showOnlyAttempted}
onChange={(checked) => updateFilters({ showOnlyAttempted: checked })}
/>
</Form.Item>

<Form.Item label="Only Completed" valuePropName="checked">
<Switch
style={{ float: 'right' }}
checked={uiSearchParams.showOnlyCompleted}
onChange={(checked) => updateFilters({ showOnlyCompleted: checked })}
/>
</Form.Item>
</>
);
};

return (
<Form labelCol={{ span: 8 }} wrapperCol={{ span: 16 }}>
<Form layout="horizontal" labelAlign="left" labelCol={{ span: 14 }} wrapperCol={{ span: 10 }}>
<Form.Item label="Climb Name">
<SearchClimbNameInput />
</Form.Item>
Expand Down Expand Up @@ -112,15 +169,12 @@ const BasicSearchForm: React.FC = () => {
/>
</Form.Item>

<Form.Item label="Classics Only">
<Select
value={uiSearchParams.onlyClassics}
onChange={(value) => updateFilters({ onlyClassics: value })}
style={{ width: '100%' }}
>
<Select.Option value="0">No</Select.Option>
<Select.Option value="1">Yes</Select.Option>
</Select>
<Form.Item label="Classics Only" valuePropName="checked">
<Switch
style={{ float: 'right' }}
checked={uiSearchParams.onlyClassics}
onChange={(checked) => updateFilters({ onlyClassics: checked })}
/>
</Form.Item>

<Form.Item label="Grade Accuracy">
Expand All @@ -139,6 +193,12 @@ const BasicSearchForm: React.FC = () => {
<Form.Item label="Setter Name">
<Input value={uiSearchParams.settername} onChange={(e) => updateFilters({ settername: e.target.value })} />
</Form.Item>

<Form.Item>
<Title level={5}>Personal Progress</Title>
</Form.Item>

{renderLogbookSection()}
</Form>
);
};
Expand Down
54 changes: 53 additions & 1 deletion app/lib/db/queries/climbs/create-climb-filters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,57 @@ export const createClimbFilters = (
...notHolds.map((holdId) => notLike(tables.climbs.frames, `%${holdId}r%`)),
];

// Personal progress filter conditions (only apply if userId is provided)
const personalProgressConditions: SQL[] = [];
if (userId) {
const ascentsTable = getTableName(params.board_name, 'ascents');
const bidsTable = getTableName(params.board_name, 'bids');

if (searchParams.hideAttempted) {
personalProgressConditions.push(
sql`NOT EXISTS (
SELECT 1 FROM ${sql.identifier(bidsTable)}
WHERE climb_uuid = ${tables.climbs.uuid}
AND user_id = ${userId}
AND angle = ${params.angle}
)`
);
}

if (searchParams.hideCompleted) {
personalProgressConditions.push(
sql`NOT EXISTS (
SELECT 1 FROM ${sql.identifier(ascentsTable)}
WHERE climb_uuid = ${tables.climbs.uuid}
AND user_id = ${userId}
AND angle = ${params.angle}
)`
);
}

if (searchParams.showOnlyAttempted) {
personalProgressConditions.push(
sql`EXISTS (
SELECT 1 FROM ${sql.identifier(bidsTable)}
WHERE climb_uuid = ${tables.climbs.uuid}
AND user_id = ${userId}
AND angle = ${params.angle}
)`
);
}

if (searchParams.showOnlyCompleted) {
personalProgressConditions.push(
sql`EXISTS (
SELECT 1 FROM ${sql.identifier(ascentsTable)}
WHERE climb_uuid = ${tables.climbs.uuid}
AND user_id = ${userId}
AND angle = ${params.angle}
)`
);
}
}

// User-specific logbook data selectors
const getUserLogbookSelects = () => {
const ascentsTable = getTableName(params.board_name, 'ascents');
Expand Down Expand Up @@ -137,7 +188,7 @@ export const createClimbFilters = (

return {
// Helper function to get all climb filtering conditions
getClimbWhereConditions: () => [...baseConditions, ...nameCondition, ...holdConditions],
getClimbWhereConditions: () => [...baseConditions, ...nameCondition, ...holdConditions, ...personalProgressConditions],

// Size-specific conditions
getSizeConditions: () => sizeConditions,
Expand Down Expand Up @@ -169,6 +220,7 @@ export const createClimbFilters = (
nameCondition,
holdConditions,
sizeConditions,
personalProgressConditions,
anyHolds,
notHolds,
};
Expand Down
4 changes: 4 additions & 0 deletions app/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ export type SearchRequest = {
settername: string;
setternameSuggestion: string;
holdsFilter: LitUpHoldsMap;
hideAttempted: boolean;
hideCompleted: boolean;
showOnlyAttempted: boolean;
showOnlyCompleted: boolean;
[key: `hold_${number}`]: HoldFilterValue; // Allow dynamic hold keys directly in the search params
};

Expand Down
24 changes: 24 additions & 0 deletions app/lib/url-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ export const searchParamsToUrlParams = ({
settername,
setternameSuggestion,
holdsFilter,
hideAttempted,
hideCompleted,
showOnlyAttempted,
showOnlyCompleted,
page,
pageSize,
}: SearchRequestPagination): URLSearchParams => {
Expand Down Expand Up @@ -95,6 +99,18 @@ export const searchParamsToUrlParams = ({
if (pageSize !== DEFAULT_SEARCH_PARAMS.pageSize) {
params.pageSize = pageSize.toString();
}
if (hideAttempted !== DEFAULT_SEARCH_PARAMS.hideAttempted) {
params.hideAttempted = hideAttempted.toString();
}
if (hideCompleted !== DEFAULT_SEARCH_PARAMS.hideCompleted) {
params.hideCompleted = hideCompleted.toString();
}
if (showOnlyAttempted !== DEFAULT_SEARCH_PARAMS.showOnlyAttempted) {
params.showOnlyAttempted = showOnlyAttempted.toString();
}
if (showOnlyCompleted !== DEFAULT_SEARCH_PARAMS.showOnlyCompleted) {
params.showOnlyCompleted = showOnlyCompleted.toString();
}

// Add holds filter entries only if they exist
if (holdsFilter && Object.keys(holdsFilter).length > 0) {
Expand All @@ -118,6 +134,10 @@ export const DEFAULT_SEARCH_PARAMS: SearchRequestPagination = {
settername: '',
setternameSuggestion: '',
holdsFilter: {},
hideAttempted: false,
hideCompleted: false,
showOnlyAttempted: false,
showOnlyCompleted: false,
page: 0,
pageSize: PAGE_LIMIT,
};
Expand All @@ -144,6 +164,10 @@ export const urlParamsToSearchParams = (urlParams: URLSearchParams): SearchReque
setternameSuggestion: urlParams.get('setternameSuggestion') ?? DEFAULT_SEARCH_PARAMS.setternameSuggestion,
//@ts-expect-error fix later
holdsFilter: holdsFilter ?? DEFAULT_SEARCH_PARAMS.holdsFilter,
hideAttempted: urlParams.get('hideAttempted') === 'true',
hideCompleted: urlParams.get('hideCompleted') === 'true',
showOnlyAttempted: urlParams.get('showOnlyAttempted') === 'true',
showOnlyCompleted: urlParams.get('showOnlyCompleted') === 'true',
page: Number(urlParams.get('page') ?? DEFAULT_SEARCH_PARAMS.page),
pageSize: Number(urlParams.get('pageSize') ?? DEFAULT_SEARCH_PARAMS.pageSize),
};
Expand Down