Skip to content

Commit

Permalink
Merge pull request #115 from marcodejongh/add_board_renderer_edit_mode
Browse files Browse the repository at this point in the history
Implement search by hold
  • Loading branch information
marcodejongh authored Jan 11, 2025
2 parents 39ed3b1 + 3e30db5 commit d3645c3
Show file tree
Hide file tree
Showing 37 changed files with 5,859 additions and 3,784 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,29 @@ import SearchColumn from '@/app/components/search-drawer/search-drawer';
import Col from 'antd/es/col';
import { Content } from 'antd/es/layout/layout';
import Row from 'antd/es/row';
import { BoardRouteParametersWithUuid, ParsedBoardRouteParameters } from '@/app/lib/types';
import { parseBoardRouteParams } from '@/app/lib/url-utils';
import { fetchBoardDetails } from '@/app/components/rest-api/api';

interface LayoutProps {}
interface LayoutProps {
params: BoardRouteParametersWithUuid;
}

export default async function ListLayout({ children, params }: PropsWithChildren<LayoutProps>) {
const parsedParams: ParsedBoardRouteParameters = parseBoardRouteParams(params);

const { board_name, layout_id, set_ids, size_id } = parsedParams;

// Fetch the climbs and board details server-side
const [boardDetails] = await Promise.all([fetchBoardDetails(board_name, layout_id, size_id, set_ids)]);

export default function ListLayout({ children }: PropsWithChildren<LayoutProps>) {
return (
<Row gutter={16}>
<Col xs={24} md={16}>
<Content>{children}</Content>
</Col>
<Col xs={24} md={8} style={{ marginBottom: '16px' }}>
<SearchColumn />
<SearchColumn boardDetails={boardDetails} />
</Col>
</Row>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export async function GET(
const result = await getClimb(parsedParams);

// TODO: Multiframe support should remove the hardcoded [0]
const litUpHoldsMap = convertLitUpHoldsStringToMap(result.frames, parsedParams.board_name)[0];
const litUpHoldsMap = convertLitUpHoldsStringToMap(result.frames, parsedParams.board_name)[0];

if (!result) {
return NextResponse.json({ error: `Failed to find problem ${params.climb_uuid}` }, { status: 404 });
Expand Down
2 changes: 1 addition & 1 deletion app/components/board-page/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export default function BoardSeshHeader({ boardDetails }: BoardSeshHeaderProps)
<Col xs={14} sm={14} md={14} lg={14} xl={14}>
<Space>
{screens.md ? null : <SearchClimbNameInput />}
{isList ? <SearchButton /> : null}
{isList ? <SearchButton boardDetails={boardDetails} /> : null}
</Space>
</Col>

Expand Down
64 changes: 35 additions & 29 deletions app/components/board-renderer/board-litup-holds.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,55 @@
import React from 'react';
import { HoldRenderData } from './types';
import { Climb } from '@/app/lib/types';
import { HoldRenderData, LitUpHoldsMap } from './types';

interface BoardLitupHoldsProps {
holdsData: HoldRenderData[];
climb: Climb;
litUpHoldsMap: LitUpHoldsMap;
mirrored: boolean;
thumbnail?: boolean;
onHoldClick?: (holdId: number) => void;
}

const BoardLitupHolds: React.FC<BoardLitupHoldsProps> = ({
holdsData,
climb: { litUpHoldsMap, mirrored },
litUpHoldsMap,
mirrored,
thumbnail,
onHoldClick,
}) => {
if (!holdsData) return null;

return (
<>
{holdsData
.filter(({ id }) => litUpHoldsMap[id]?.state && litUpHoldsMap[id].state !== 'OFF') // Apply the lit-up state
.map((hold) => {
const color = litUpHoldsMap[hold.id].color;
if (mirrored) {
const mirroredHold = holdsData.find(({ id }) => id === hold.mirroredHoldId);
if (!mirroredHold) {
throw new Error("Couldn't find mirrored hold");
}
hold = mirroredHold;
{holdsData.map((hold) => {
const isLitUp = litUpHoldsMap[hold.id]?.state && litUpHoldsMap[hold.id].state !== 'OFF';
const color = isLitUp ? litUpHoldsMap[hold.id].color : 'transparent';

let renderHold = hold;
if (mirrored && hold.mirroredHoldId) {
const mirroredHold = holdsData.find(({ id }) => id === hold.mirroredHoldId);
if (!mirroredHold) {
throw new Error("Couldn't find mirrored hold");
}
renderHold = mirroredHold;
}

return (
<circle
key={hold.id}
id={`hold-${hold.id}`}
data-mirror-id={hold.mirroredHoldId || undefined}
cx={hold.cx}
cy={hold.cy}
r={hold.r}
stroke={color}
strokeWidth={thumbnail ? 8 : 6}
fillOpacity={thumbnail ? 1 : 0}
fill={thumbnail ? color : undefined}
/>
);
})}
return (
<circle
key={renderHold.id}
id={`hold-${renderHold.id}`}
data-mirror-id={renderHold.mirroredHoldId || undefined}
cx={renderHold.cx}
cy={renderHold.cy}
r={renderHold.r}
stroke={color}
strokeWidth={thumbnail ? 8 : 6}
fillOpacity={thumbnail ? 1 : 0}
fill={thumbnail ? color : undefined}
style={{ cursor: onHoldClick ? 'pointer' : 'default' }}
onClick={onHoldClick ? () => onHoldClick(renderHold.id) : undefined}
/>
);
})}
</>
);
};
Expand Down
18 changes: 14 additions & 4 deletions app/components/board-renderer/board-renderer.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import React from 'react';
import { getImageUrl } from './util';
import { BoardDetails, Climb } from '@/app/lib/types';
import { BoardDetails } from '@/app/lib/types';
import BoardLitupHolds from './board-litup-holds';
import { LitUpHoldsMap } from './types';

export type BoardProps = {
boardDetails: BoardDetails;
climb?: Climb;
litUpHoldsMap?: LitUpHoldsMap;
mirrored: boolean;
thumbnail?: boolean;
onHoldClick?: (holdId: number) => void;
};

const BoardRenderer = ({ boardDetails, thumbnail, climb }: BoardProps) => {
const BoardRenderer = ({ boardDetails, thumbnail, litUpHoldsMap, mirrored, onHoldClick }: BoardProps) => {
const { boardWidth, boardHeight, holdsData } = boardDetails;

return (
Expand All @@ -26,7 +29,14 @@ const BoardRenderer = ({ boardDetails, thumbnail, climb }: BoardProps) => {
{Object.keys(boardDetails.images_to_holds).map((imageUrl) => (
<image key={imageUrl} href={getImageUrl(imageUrl, boardDetails.board_name)} width="100%" height="100%" />
))}
{climb && climb.litUpHoldsMap && <BoardLitupHolds holdsData={holdsData} climb={climb} />}
{litUpHoldsMap && (
<BoardLitupHolds
onHoldClick={onHoldClick}
holdsData={holdsData}
litUpHoldsMap={litUpHoldsMap}
mirrored={mirrored}
/>
)}
</svg>
);
};
Expand Down
2 changes: 1 addition & 1 deletion app/components/climb-card/climb-card-cover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const ClimbCardCover = ({ climb, boardDetails, onClick }: ClimbCardCoverProps) =
cursor: 'pointer',
}}
>
<BoardRenderer boardDetails={boardDetails} climb={climb} />
<BoardRenderer boardDetails={boardDetails} litUpHoldsMap={climb?.litUpHoldsMap} mirrored={!!climb?.mirrored} />
<ClimbCardModal
isOpen={modalOpen}
onClose={() => setModalOpen(false)}
Expand Down
7 changes: 6 additions & 1 deletion app/components/climb-card/climb-thumbnail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ const ClimbThumbnail = ({ boardDetails, currentClimb }: ClimbThumbnailProps) =>
return (
<>
<a onClick={currentClimb ? () => setModalOpen(true) : undefined}>
<BoardRenderer climb={currentClimb || undefined} boardDetails={boardDetails} thumbnail />
<BoardRenderer
litUpHoldsMap={currentClimb?.litUpHoldsMap}
mirrored={!!currentClimb?.mirrored}
boardDetails={boardDetails}
thumbnail
/>
</a>
{currentClimb && (
<ClimbCardModal
Expand Down
146 changes: 146 additions & 0 deletions app/components/search-drawer/basic-search-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
'use client';

import React from 'react';
import { Form, InputNumber, Row, Col, Select, Input } from 'antd';
import { TENSION_KILTER_GRADES } from '@/app/lib/board-data';
import { useUISearchParams } from '@/app/components/queue-control/ui-searchparams-provider';
import SearchClimbNameInput from './search-climb-name-input';

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

const handleGradeChange = (type: 'min' | 'max', value: number | undefined) => {
if (type === 'min') {
updateFilters({ minGrade: value });
} else {
updateFilters({ maxGrade: value });
}
};

return (
<Form labelCol={{ span: 8 }} wrapperCol={{ span: 16 }}>
<Form.Item label="Climb Name">
<SearchClimbNameInput />
</Form.Item>

<Form.Item label="Grade Range">
<Row gutter={8}>
<Col span={12}>
<Form.Item label="Min" noStyle>
<Select
value={uiSearchParams.minGrade || 0}
defaultValue={0}
onChange={(value) => handleGradeChange('min', value)}
style={{ width: '100%' }}
>
<Select.Option value={0}>Any</Select.Option>
{grades.map((grade) => (
<Select.Option key={grade.difficulty_id} value={grade.difficulty_id}>
{grade.difficulty_name}
</Select.Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="Max" noStyle>
<Select
value={uiSearchParams.maxGrade || 0}
defaultValue={0}
onChange={(value) => handleGradeChange('max', value)}
style={{ width: '100%' }}
>
<Select.Option value={0}>Any</Select.Option>
{grades.map((grade) => (
<Select.Option key={grade.difficulty_id} value={grade.difficulty_id}>
{grade.difficulty_name}
</Select.Option>
))}
</Select>
</Form.Item>
</Col>
</Row>
</Form.Item>

<Form.Item label="Min Ascents">
<InputNumber
min={1}
value={uiSearchParams.minAscents}
onChange={(value) => updateFilters({ minAscents: value || undefined })}
style={{ width: '100%' }}
placeholder="Any"
/>
</Form.Item>

<Form.Item label="Sort By">
<Row gutter={8}>
<Col span={16}>
<Select
value={uiSearchParams.sortBy}
onChange={(value) => updateFilters({ sortBy: value })}
style={{ width: '100%' }}
>
<Select.Option value="ascents">Ascents</Select.Option>
<Select.Option value="difficulty">Difficulty</Select.Option>
<Select.Option value="name">Name</Select.Option>
<Select.Option value="quality">Quality</Select.Option>
</Select>
</Col>
<Col span={8}>
<Select
value={uiSearchParams.sortOrder}
onChange={(value) => updateFilters({ sortOrder: value })}
style={{ width: '100%' }}
>
<Select.Option value="desc">Descending</Select.Option>
<Select.Option value="asc">Ascending</Select.Option>
</Select>
</Col>
</Row>
</Form.Item>

<Form.Item label="Min Rating">
<InputNumber
min={1.0}
max={3.0}
step={0.1}
value={uiSearchParams.minRating}
onChange={(value) => updateFilters({ minRating: value || undefined })}
style={{ width: '100%' }}
placeholder="Any"
/>
</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>

<Form.Item label="Grade Accuracy">
<Select
value={uiSearchParams.gradeAccuracy}
onChange={(value) => updateFilters({ gradeAccuracy: value || undefined })}
style={{ width: '100%' }}
>
<Select.Option value={undefined}>Any</Select.Option>
<Select.Option value={0.2}>Somewhat Accurate (&lt;0.2)</Select.Option>
<Select.Option value={0.1}>Very Accurate (&lt;0.1)</Select.Option>
<Select.Option value={0.05}>Extremely Accurate (&lt;0.05)</Select.Option>
</Select>
</Form.Item>

<Form.Item label="Setter Name">
<Input value={uiSearchParams.settername} onChange={(e) => updateFilters({ settername: e.target.value })} />
</Form.Item>
</Form>
);
};

export default BasicSearchForm;
Loading

1 comment on commit d3645c3

@vercel
Copy link

@vercel vercel bot commented on d3645c3 Jan 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.