Skip to content
Open
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 @@ -44,7 +44,7 @@ const TabsWrapper: React.FC<{ boardDetails: BoardDetails }> = ({ boardDetails })
children: (
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<div style={{ flex: 1, overflow: 'auto' }}>
<BasicSearchForm />
<BasicSearchForm boardDetails={boardDetails} />
</div>
<SearchResultsFooter />
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,25 @@ export default async function DynamicResultsPage(props: {
getBoardDetails(parsedParams),
]);

if (!fetchedResults || fetchedResults.climbs.length === 0) {
// Only show 404 if there are no climbs at all AND no filters are applied
// If filters are active and return 0 results, that's a valid empty state
const hasActiveFilters =
searchParamsObject.name ||
searchParamsObject.minGrade ||
searchParamsObject.maxGrade ||
searchParamsObject.minAscents ||
searchParamsObject.minRating ||
searchParamsObject.onlyClassics ||
searchParamsObject.tallClimbsOnly ||
searchParamsObject.gradeAccuracy ||
searchParamsObject.settername.length > 0 ||
Object.keys(searchParamsObject.holdsFilter).length > 0 ||
searchParamsObject.hideAttempted ||
searchParamsObject.hideCompleted ||
searchParamsObject.showOnlyAttempted ||
searchParamsObject.showOnlyCompleted;

if (!fetchedResults || (fetchedResults.climbs.length === 0 && !hasActiveFilters)) {
notFound();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ const mockSearchParams: SearchRequestPagination = {
hideAttempted: false,
hideCompleted: false,
showOnlyAttempted: false,
showOnlyCompleted: false
showOnlyCompleted: false,
tallClimbsOnly: false
};

const mockParsedParams: ParsedBoardRouteParameters = {
Expand Down
6 changes: 4 additions & 2 deletions app/components/queue-control/__tests__/reducer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ const mockSearchParams: SearchRequestPagination = {
hideAttempted: false,
hideCompleted: false,
showOnlyAttempted: false,
showOnlyCompleted: false
showOnlyCompleted: false,
tallClimbsOnly: false
};

const initialState: QueueState = {
Expand Down Expand Up @@ -292,7 +293,8 @@ describe('queueReducer', () => {
hideAttempted: false,
hideCompleted: false,
showOnlyAttempted: false,
showOnlyCompleted: false
showOnlyCompleted: false,
tallClimbsOnly: false
};

const action: QueueAction = {
Expand Down
1 change: 1 addition & 0 deletions app/components/queue-control/ui-searchparams-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.tallClimbsOnly) activeFilters.push('tallClimbsOnly');

if (activeFilters.length > 0) {
track('Climb Search Performed', {
Expand Down
20 changes: 18 additions & 2 deletions app/components/search-drawer/basic-search-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,21 @@ import { useUISearchParams } from '@/app/components/queue-control/ui-searchparam
import { useBoardProvider } from '@/app/components/board-provider/board-provider-context';
import SearchClimbNameInput from './search-climb-name-input';
import SetterNameSelect from './setter-name-select';
import { BoardDetails } from '@/app/lib/types';

const { Title } = Typography;

const BasicSearchForm: React.FC = () => {
interface BasicSearchFormProps {
boardDetails: BoardDetails;
}

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

const isLoggedIn = token && user_id;
const isLargestSize = boardDetails.isLargestSize ?? false;

const handleGradeChange = (type: 'min' | 'max', value: number | undefined) => {
if (type === 'min') {
Expand Down Expand Up @@ -178,6 +184,16 @@ const BasicSearchForm: React.FC = () => {
/>
</Form.Item>

{isLargestSize && (
<Form.Item label="Tall Climbs Only" valuePropName="checked">
<Switch
style={{ float: 'right' }}
checked={uiSearchParams.tallClimbsOnly}
onChange={(checked) => updateFilters({ tallClimbsOnly: checked })}
/>
</Form.Item>
)}

<Form.Item label="Grade Accuracy">
<Select
value={uiSearchParams.gradeAccuracy}
Expand Down
2 changes: 1 addition & 1 deletion app/components/search-drawer/search-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const SearchForm: React.FC<SearchFormProps> = ({ boardDetails }) => {
{
key: 'filters',
label: 'Search',
children: <BasicSearchForm />,
children: <BasicSearchForm boardDetails={boardDetails} />,
},
{
key: 'holds',
Expand Down
24 changes: 23 additions & 1 deletion app/lib/data/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export const getBoardDetails = async ({
})),
);

// Fetch names for slug-based URLs
// Fetch names for slug-based URLs and all sizes to determine if current is largest
const [layouts, sizes, sets] = await Promise.all([
getLayouts(board_name),
getSizes(board_name, layout_id),
Expand All @@ -115,6 +115,27 @@ export const getBoardDetails = async ({
const size = sizes.find((s) => s.id === size_id);
const selectedSets = sets.filter((s) => set_ids.includes(s.id));

// Determine if current size is the largest by checking all size dimensions
// Get all sizes for this layout to compare
const allSizesForLayoutResult = await sql`
SELECT edge_left, edge_right, edge_bottom, edge_top
FROM ${sql.unsafe(getTableName(board_name, 'product_sizes'))} ps
INNER JOIN ${sql.unsafe(getTableName(board_name, 'layouts'))} layouts ON ps.product_id = layouts.product_id
WHERE layouts.id = ${layout_id}
`;

const allSizesForLayout = allSizesForLayoutResult as ProductSizeRow[];
const currentEdges = sizeDimensions[0];
const isLargestSize = !allSizesForLayout.some((otherSize) => {
// Check if any other size has larger boundaries
return (
otherSize.edge_top > currentEdges.edge_top ||
otherSize.edge_bottom < currentEdges.edge_bottom ||
otherSize.edge_left < currentEdges.edge_left ||
otherSize.edge_right > currentEdges.edge_right
);
});

return {
images_to_holds: imagesToHolds,
holdsData,
Expand All @@ -130,6 +151,7 @@ export const getBoardDetails = async ({
set_ids,
ledPlacements: Object.fromEntries(ledPlacements.map(({ id, position }) => [id, position])),
supportsMirroring: board_name === 'tension' && layout_id !== 11,
isLargestSize,
// Added for slug-based URLs
layout_name: layout?.name,
size_name: size?.name,
Expand Down
44 changes: 43 additions & 1 deletion app/lib/db/queries/climbs/create-climb-filters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,47 @@ export const createClimbFilters = (
}
}

// Tall climbs filter condition
// A climb is "tall" if it uses holds beyond the boundaries of ALL other sizes
const tallClimbsConditions: SQL[] = [];
if (searchParams.tallClimbsOnly) {
// Find climbs that extend beyond the boundaries of ALL other sizes for this layout
// This means the climb uses at least 1 hold only available on the current (largest) size
// COALESCE fallbacks ensure no matches if there's only one size (which is correct - no "tall-only" climbs exist)
tallClimbsConditions.push(
sql`(
${tables.climbs.edgeTop} > (
SELECT COALESCE(MAX(ps2.edge_top), 999)
FROM ${tables.productSizes} ps2
INNER JOIN ${tables.layouts} layouts2 ON ps2.product_id = layouts2.product_id
WHERE layouts2.id = ${params.layout_id}
AND ps2.id != ${params.size_id}
)
OR ${tables.climbs.edgeBottom} < (
SELECT COALESCE(MIN(ps2.edge_bottom), -999)
FROM ${tables.productSizes} ps2
INNER JOIN ${tables.layouts} layouts2 ON ps2.product_id = layouts2.product_id
WHERE layouts2.id = ${params.layout_id}
AND ps2.id != ${params.size_id}
)
OR ${tables.climbs.edgeLeft} < (
SELECT COALESCE(MIN(ps2.edge_left), -999)
FROM ${tables.productSizes} ps2
INNER JOIN ${tables.layouts} layouts2 ON ps2.product_id = layouts2.product_id
WHERE layouts2.id = ${params.layout_id}
AND ps2.id != ${params.size_id}
)
OR ${tables.climbs.edgeRight} > (
SELECT COALESCE(MAX(ps2.edge_right), 999)
FROM ${tables.productSizes} ps2
INNER JOIN ${tables.layouts} layouts2 ON ps2.product_id = layouts2.product_id
WHERE layouts2.id = ${params.layout_id}
AND ps2.id != ${params.size_id}
)
)`
);
}

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

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

// Size-specific conditions
getSizeConditions: () => sizeConditions,
Expand Down Expand Up @@ -227,6 +268,7 @@ export const createClimbFilters = (
holdConditions,
sizeConditions,
personalProgressConditions,
tallClimbsConditions,
anyHolds,
notHolds,
};
Expand Down
2 changes: 2 additions & 0 deletions app/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export type SearchRequest = {
hideCompleted: boolean;
showOnlyAttempted: boolean;
showOnlyCompleted: boolean;
tallClimbsOnly: boolean;
[key: `hold_${number}`]: HoldFilterValue; // Allow dynamic hold keys directly in the search params
};

Expand Down Expand Up @@ -214,6 +215,7 @@ export type BoardDetails = {
set_ids: SetIdList;
ledPlacements: LedPlacements;
supportsMirroring?: boolean;
isLargestSize?: boolean;
// Added for slug-based URLs
layout_name?: string;
size_name?: string;
Expand Down
6 changes: 6 additions & 0 deletions app/lib/url-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export const searchParamsToUrlParams = ({
hideCompleted,
showOnlyAttempted,
showOnlyCompleted,
tallClimbsOnly,
page,
pageSize,
}: SearchRequestPagination): URLSearchParams => {
Expand Down Expand Up @@ -111,6 +112,9 @@ export const searchParamsToUrlParams = ({
if (showOnlyCompleted !== DEFAULT_SEARCH_PARAMS.showOnlyCompleted) {
params.showOnlyCompleted = showOnlyCompleted.toString();
}
if (tallClimbsOnly !== DEFAULT_SEARCH_PARAMS.tallClimbsOnly) {
params.tallClimbsOnly = tallClimbsOnly.toString();
}

// Add holds filter entries only if they exist
if (holdsFilter && Object.keys(holdsFilter).length > 0) {
Expand Down Expand Up @@ -138,6 +142,7 @@ export const DEFAULT_SEARCH_PARAMS: SearchRequestPagination = {
hideCompleted: false,
showOnlyAttempted: false,
showOnlyCompleted: false,
tallClimbsOnly: false,
page: 0,
pageSize: PAGE_LIMIT,
};
Expand Down Expand Up @@ -168,6 +173,7 @@ export const urlParamsToSearchParams = (urlParams: URLSearchParams): SearchReque
hideCompleted: urlParams.get('hideCompleted') === 'true',
showOnlyAttempted: urlParams.get('showOnlyAttempted') === 'true',
showOnlyCompleted: urlParams.get('showOnlyCompleted') === 'true',
tallClimbsOnly: urlParams.get('tallClimbsOnly') === 'true',
page: Number(urlParams.get('page') ?? DEFAULT_SEARCH_PARAMS.page),
pageSize: Number(urlParams.get('pageSize') ?? DEFAULT_SEARCH_PARAMS.pageSize),
};
Expand Down
Loading