From c59761fb0f986f9c81b2a4e76aba1fa42798aabb Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Feb 2026 09:58:36 +0000 Subject: [PATCH] Enable MoonBoard climb details/view page - Remove viewDetails exclusion from climb-card for MoonBoard climbs - Fix getClimb to skip sizeEdges validation for MoonBoard (uses fixed grid) - Add MoonBoard-specific redirect logic using static config instead of Aurora generated product-sizes data - Exclude openInApp action for MoonBoard (no Aurora app URL) - Make auroraAppUrl optional in ClimbViewActions Closes #617 https://claude.ai/code/session_014W5j5Jk4pMWrzuWCTQK6aB --- .../[angle]/view/[climb_uuid]/page.tsx | 91 +++++++++++++------ .../app/components/climb-card/climb-card.tsx | 7 +- .../climb-view/climb-view-actions.tsx | 17 +++- packages/web/app/lib/data/queries.ts | 11 ++- 4 files changed, 85 insertions(+), 41 deletions(-) diff --git a/packages/web/app/[board_name]/[layout_id]/[size_id]/[set_ids]/[angle]/view/[climb_uuid]/page.tsx b/packages/web/app/[board_name]/[layout_id]/[size_id]/[set_ids]/[angle]/view/[climb_uuid]/page.tsx index f2903659..0458eb2c 100644 --- a/packages/web/app/[board_name]/[layout_id]/[size_id]/[set_ids]/[angle]/view/[climb_uuid]/page.tsx +++ b/packages/web/app/[board_name]/[layout_id]/[size_id]/[set_ids]/[angle]/view/[climb_uuid]/page.tsx @@ -16,6 +16,12 @@ import { parseBoardRouteParams, } from '@/app/lib/url-utils'; import { parseBoardRouteParamsWithSlugs } from '@/app/lib/url-utils.server'; +import { + getLayoutById, + MOONBOARD_SETS, + MOONBOARD_SIZE, + MoonBoardLayoutKey, +} from '@/app/lib/moonboard-config'; import { convertLitUpHoldsStringToMap } from '@/app/components/board-renderer/util'; import ClimbViewActions from '@/app/components/climb-view/climb-view-actions'; import { Metadata } from 'next'; @@ -105,31 +111,55 @@ export default async function DynamicResultsPage(props: { params: Promise m.getLayouts(parsedParams.board_name)); - const sizes = await import('@/app/lib/data/queries').then((m) => - m.getSizes(parsedParams.board_name, parsedParams.layout_id), - ); - const sets = await import('@/app/lib/data/queries').then((m) => - m.getSets(parsedParams.board_name, parsedParams.layout_id, parsedParams.size_id), - ); - - const layout = layouts.find((l) => l.id === parsedParams.layout_id); - const size = sizes.find((s) => s.id === parsedParams.size_id); - const selectedSets = sets.filter((s) => parsedParams.set_ids.includes(s.id)); - - if (layout && size && selectedSets.length > 0) { - const newUrl = constructClimbViewUrlWithSlugs( - parsedParams.board_name, - layout.name, - size.name, - size.description, - selectedSets.map((s) => s.name), - parsedParams.angle, - parsedParams.climb_uuid, - currentClimb.name, + if (parsedParams.board_name === 'moonboard') { + // MoonBoard uses static config instead of generated product-sizes data + const layoutEntry = getLayoutById(parsedParams.layout_id); + if (layoutEntry) { + const [layoutKey, layoutData] = layoutEntry; + const sets = MOONBOARD_SETS[layoutKey as MoonBoardLayoutKey] || []; + const selectedSets = sets.filter((s) => parsedParams.set_ids.includes(s.id)); + + if (selectedSets.length > 0) { + const newUrl = constructClimbViewUrlWithSlugs( + parsedParams.board_name, + layoutData.name, + MOONBOARD_SIZE.name, + MOONBOARD_SIZE.description, + selectedSets.map((s) => s.name), + parsedParams.angle, + parsedParams.climb_uuid, + currentClimb.name, + ); + permanentRedirect(newUrl); + } + } + } else { + // Aurora boards: get names from generated data for slug generation + const layouts = await import('@/app/lib/data/queries').then((m) => m.getLayouts(parsedParams.board_name)); + const sizes = await import('@/app/lib/data/queries').then((m) => + m.getSizes(parsedParams.board_name, parsedParams.layout_id), + ); + const sets = await import('@/app/lib/data/queries').then((m) => + m.getSets(parsedParams.board_name, parsedParams.layout_id, parsedParams.size_id), ); - permanentRedirect(newUrl); + + const layout = layouts.find((l) => l.id === parsedParams.layout_id); + const size = sizes.find((s) => s.id === parsedParams.size_id); + const selectedSets = sets.filter((s) => parsedParams.set_ids.includes(s.id)); + + if (layout && size && selectedSets.length > 0) { + const newUrl = constructClimbViewUrlWithSlugs( + parsedParams.board_name, + layout.name, + size.name, + size.description, + selectedSets.map((s) => s.name), + parsedParams.angle, + parsedParams.climb_uuid, + currentClimb.name, + ); + permanentRedirect(newUrl); + } } } // Fetch beta links server-side @@ -179,11 +209,14 @@ export default async function DynamicResultsPage(props: { params: Promise diff --git a/packages/web/app/components/climb-card/climb-card.tsx b/packages/web/app/components/climb-card/climb-card.tsx index d626416e..03113dd1 100644 --- a/packages/web/app/components/climb-card/climb-card.tsx +++ b/packages/web/app/components/climb-card/climb-card.tsx @@ -64,17 +64,14 @@ function ClimbCardWithActions({ const cover = ; const cardTitle = ; - // Build exclude list - MoonBoard doesn't have a view details page yet - const excludeActions: ('tick' | 'openInApp' | 'mirror' | 'share' | 'addToList' | 'viewDetails')[] = [ + // Build exclude list + const excludeActions: ('tick' | 'openInApp' | 'mirror' | 'share' | 'addToList')[] = [ 'tick', 'openInApp', 'mirror', 'share', 'addToList', ]; - if (boardDetails.board_name === 'moonboard') { - excludeActions.push('viewDetails'); - } return (
diff --git a/packages/web/app/components/climb-view/climb-view-actions.tsx b/packages/web/app/components/climb-view/climb-view-actions.tsx index bf05c362..d9a851db 100644 --- a/packages/web/app/components/climb-view/climb-view-actions.tsx +++ b/packages/web/app/components/climb-view/climb-view-actions.tsx @@ -10,11 +10,13 @@ import { ClimbActions } from '../climb-actions'; type ClimbViewActionsProps = { climb: Climb; boardDetails: BoardDetails; - auroraAppUrl: string; + auroraAppUrl?: string; angle: number; }; const ClimbViewActions = ({ climb, boardDetails, auroraAppUrl, angle }: ClimbViewActionsProps) => { + const isMoonBoard = boardDetails.board_name === 'moonboard'; + const getBackToListUrl = () => { const { board_name, layout_name, size_name, size_description, set_names } = boardDetails; @@ -27,6 +29,15 @@ const ClimbViewActions = ({ climb, boardDetails, auroraAppUrl, angle }: ClimbVie return `/${board_name}/${boardDetails.layout_id}/${boardDetails.size_id}/${boardDetails.set_ids.join(',')}/${angle}/list`; }; + // MoonBoard doesn't have an Aurora app, so exclude 'openInApp' + const mobileDropdownActions: ('tick' | 'share' | 'addToList' | 'openInApp')[] = isMoonBoard + ? ['tick', 'share', 'addToList'] + : ['tick', 'share', 'addToList', 'openInApp']; + + const desktopActions: ('favorite' | 'tick' | 'queue' | 'share' | 'addToList' | 'openInApp')[] = isMoonBoard + ? ['favorite', 'tick', 'queue', 'share', 'addToList'] + : ['favorite', 'tick', 'queue', 'share', 'addToList', 'openInApp']; + return (
{/* Mobile view: Show back button + key actions + overflow menu */} @@ -50,7 +61,7 @@ const ClimbViewActions = ({ climb, boardDetails, auroraAppUrl, angle }: ClimbVie boardDetails={boardDetails} angle={angle} viewMode="dropdown" - include={['tick', 'share', 'addToList', 'openInApp']} + include={mobileDropdownActions} auroraAppUrl={auroraAppUrl} />
@@ -66,7 +77,7 @@ const ClimbViewActions = ({ climb, boardDetails, auroraAppUrl, angle }: ClimbVie boardDetails={boardDetails} angle={angle} viewMode="button" - include={['favorite', 'tick', 'queue', 'share', 'addToList', 'openInApp']} + include={desktopActions} auroraAppUrl={auroraAppUrl} />
diff --git a/packages/web/app/lib/data/queries.ts b/packages/web/app/lib/data/queries.ts index 312869aa..512b8974 100644 --- a/packages/web/app/lib/data/queries.ts +++ b/packages/web/app/lib/data/queries.ts @@ -16,10 +16,13 @@ import { } from '@/app/lib/__generated__/product-sizes-data'; export const getClimb = async (params: ParsedBoardRouteParametersWithUuid): Promise => { - // Get hardcoded size edges (eliminates database query) - const sizeEdges = getSizeEdges(params.board_name, params.size_id); - if (!sizeEdges) { - throw new Error(`Invalid size_id ${params.size_id} for board ${params.board_name}`); + // MoonBoard uses grid-based rendering with a fixed size, so it has no entries in PRODUCT_SIZES. + // Skip the size edges validation for MoonBoard. + if (params.board_name !== 'moonboard') { + const sizeEdges = getSizeEdges(params.board_name, params.size_id); + if (!sizeEdges) { + throw new Error(`Invalid size_id ${params.size_id} for board ${params.board_name}`); + } } const result = await sql`