From 62e326a1f19b68bbda02debcb0bf61f8e9441a4d Mon Sep 17 00:00:00 2001 From: Viet Nguyen <3805254+vnugent@users.noreply.github.com> Date: Thu, 31 Oct 2024 22:47:46 -0700 Subject: [PATCH] Migrate climb page to Next 13 structure (#1195) * migrate climb page to next13 structure * resolve to /climb page globally * add map link to main nav --- src/app/(default)/about/components/About.tsx | 2 +- .../(default)/area/[[...slug]]/loading.tsx | 4 +- src/app/(default)/area/[[...slug]]/page.tsx | 39 ++----- .../[[...slug]]/components/ClimbData.tsx | 66 +++++++++++ .../[[...slug]]/components/ContentBlock.tsx | 26 +++++ .../[[...slug]]/components/PageAlert.tsx | 12 ++ .../[[...slug]]/components/SiblingClimbs.tsx | 29 +++++ src/app/(default)/climb/[[...slug]]/page.tsx | 107 ++++++++++++++++++ .../components/AreaAndClimbPageActions.tsx | 47 ++++++++ .../(default)/components/AreaPageActions.tsx | 27 ----- .../(default)/components/DesktopHeader.tsx | 7 +- src/app/(default)/components/LandingHero.tsx | 12 -- ...aLinkButton.tsx => SharePageURLButton.tsx} | 10 +- ...Container.tsx => DefaultPageContainer.tsx} | 27 +++-- .../components/climb/ClimbListForm.tsx | 4 +- src/components/crag/ClimbListPreview.tsx | 2 +- src/components/crag/NeighboringRoute.tsx | 33 +++--- src/components/edit/RecentChangeHistory.tsx | 10 +- src/components/media/PhotoUploadButtons.tsx | 4 +- src/components/media/Tag.tsx | 2 +- src/components/media/__tests__/Tag.tsx | 2 +- src/components/search/ResultTemplates.tsx | 2 +- .../sources/TypesenseXSearchSources.tsx | 2 +- .../search/templates/ClimbResultXSearch.tsx | 2 +- src/js/graphql/api.ts | 6 +- src/js/graphql/gql/climbById.ts | 23 ++++ src/js/types/pages.ts | 7 ++ src/js/utils.ts | 33 +++++- src/pages/sitemap.xml.tsx | 42 +++---- src/pages/u2/[...slug].tsx | 2 +- 30 files changed, 441 insertions(+), 150 deletions(-) create mode 100644 src/app/(default)/climb/[[...slug]]/components/ClimbData.tsx create mode 100644 src/app/(default)/climb/[[...slug]]/components/ContentBlock.tsx create mode 100644 src/app/(default)/climb/[[...slug]]/components/PageAlert.tsx create mode 100644 src/app/(default)/climb/[[...slug]]/components/SiblingClimbs.tsx create mode 100644 src/app/(default)/climb/[[...slug]]/page.tsx create mode 100644 src/app/(default)/components/AreaAndClimbPageActions.tsx delete mode 100644 src/app/(default)/components/AreaPageActions.tsx rename src/app/(default)/components/{ShareAreaLinkButton.tsx => SharePageURLButton.tsx} (74%) rename src/app/(default)/components/ui/{AreaPageContainer.tsx => DefaultPageContainer.tsx} (59%) create mode 100644 src/js/types/pages.ts diff --git a/src/app/(default)/about/components/About.tsx b/src/app/(default)/about/components/About.tsx index 68593ef95..91954a985 100644 --- a/src/app/(default)/about/components/About.tsx +++ b/src/app/(default)/about/components/About.tsx @@ -62,7 +62,7 @@ export function About (): ReactNode { width={700} />
- Flyboy (Bishop, California) + Flyboy (Bishop, California) © Ray Phung
diff --git a/src/app/(default)/area/[[...slug]]/loading.tsx b/src/app/(default)/area/[[...slug]]/loading.tsx index 229609270..e9e59bf3e 100644 --- a/src/app/(default)/area/[[...slug]]/loading.tsx +++ b/src/app/(default)/area/[[...slug]]/loading.tsx @@ -1,8 +1,8 @@ -import { AreaPageContainer } from '@/app/(default)/components/ui/AreaPageContainer' +import { DefaultPageContainer } from '@/app/(default)/components/ui/DefaultPageContainer' /** * Loading skeleton for /area/ page. */ export default function Loading (): JSX.Element { - return () + return () } diff --git a/src/app/(default)/area/[[...slug]]/page.tsx b/src/app/(default)/area/[[...slug]]/page.tsx index 3a7c30586..cb505f0e9 100644 --- a/src/app/(default)/area/[[...slug]]/page.tsx +++ b/src/app/(default)/area/[[...slug]]/page.tsx @@ -1,7 +1,6 @@ import { notFound, permanentRedirect } from 'next/navigation' import Link from 'next/link' import { Metadata } from 'next' -import { validate } from 'uuid' import { MapPinLine, Lightbulb, ArrowRight } from '@phosphor-icons/react/dist/ssr' import Markdown from 'react-markdown' @@ -10,28 +9,22 @@ import { getArea } from '@/js/graphql/getArea' import { StickyHeaderContainer } from '@/app/(default)/components/ui/StickyHeaderContainer' import { AreaCrumbs } from '@/components/breadcrumbs/AreaCrumbs' import { ArticleLastUpdate } from '@/components/edit/ArticleLastUpdate' -import { getMapHref, getFriendlySlug, getAreaPageFriendlyUrl, sanitizeName } from '@/js/utils' +import { getMapHref, getFriendlySlug, getAreaPageFriendlyUrl, sanitizeName, parseUuidAsFirstParam } from '@/js/utils' import { LazyAreaMap } from '@/components/maps/AreaMap' -import { AreaPageContainer } from '@/app/(default)/components/ui/AreaPageContainer' -import { AreaPageActions } from '../../components/AreaPageActions' +import { DefaultPageContainer } from '@/app/(default)/components/ui/DefaultPageContainer' +import { AreaAndClimbPageActions } from '../../components/AreaAndClimbPageActions' import { SubAreasSection } from './sections/SubAreasSection' import { ClimbListSection } from './sections/ClimbListSection' import { CLIENT_CONFIG } from '@/js/configs/clientConfig' import { PageBanner as LCOBanner } from '@/components/lco/PageBanner' -import { AuthorMetadata, OrganizationType } from '@/js/types' +import { AuthorMetadata, OrganizationType, TagTargetType } from '@/js/types' +import { PageWithCatchAllUuidProps, PageSlugType } from '@/js/types/pages' /** * Page cache settings */ export const revalidate = 300 // 5 mins export const fetchCache = 'force-no-store' // opt out of Nextjs version of 'fetch' -interface PageSlugType { - slug: string [] -} -export interface PageWithCatchAllUuidProps { - params: PageSlugType -} - /** * Area/crag page */ @@ -58,13 +51,13 @@ export default async function Page ({ params }: PageWithCatchAllUuidProps): Prom } return ( - : } - pageActions={} + pageActions={} breadcrumbs={ @@ -92,26 +85,10 @@ export default async function Page ({ params }: PageWithCatchAllUuidProps): Prom - + ) } -/** - * Extract and validate uuid as the first param in a catch-all route - */ -const parseUuidAsFirstParam = ({ params }: PageWithCatchAllUuidProps): string => { - if (params.slug == null || params.slug?.length === 0) { - notFound() - } - - const uuid = params.slug[0] - if (!validate(uuid)) { - console.error('Invalid uuid', uuid) - notFound() - } - return uuid -} - const EditDescriptionCTA: React.FC<{ uuid: string }> = ({ uuid }) => (
diff --git a/src/app/(default)/climb/[[...slug]]/components/ClimbData.tsx b/src/app/(default)/climb/[[...slug]]/components/ClimbData.tsx new file mode 100644 index 000000000..67ff941e4 --- /dev/null +++ b/src/app/(default)/climb/[[...slug]]/components/ClimbData.tsx @@ -0,0 +1,66 @@ +import { ArrowsVertical } from '@phosphor-icons/react/dist/ssr' + +import RouteGradeChip from '@/components/ui/RouteGradeChip' +import RouteTypeChips from '@/components/ui/RouteTypeChips' +import { ArticleLastUpdate } from '@/components/edit/ArticleLastUpdate' +import { ClimbType, AreaType } from '@/js/types' +import Grade from '@/js/grades/Grade' +import { removeTypenameFromDisciplines } from '@/js/utils' + +export const ClimbData: React.FC & { isBoulder: boolean }> = (props) => { + const { name, type, safety, length, grades, fa: legacyFA, authorMetadata, gradeContext, isBoulder } = props + + const sanitizedDisciplines = removeTypenameFromDisciplines(type) + + const gradeStr = new Grade( + gradeContext, + grades, + sanitizedDisciplines, + isBoulder + ).toString() + return ( + <> +

+ {name} +

+
+
+ {gradeStr != null && ( + + )} + +
+ + {length !== -1 && ( +
+ + {length}m +
+ )} + {/* {editMode && } */} + +
+
{trimLegacyFA(legacyFA)}
+
+ + {(authorMetadata.createdAt != null || authorMetadata.updatedAt != null) && ( +
+ +
+ )} + + {/* {!editMode && ( +
+ +
+ )} */} +
+ + ) +} + +const trimLegacyFA = (s: string): string => { + if (s == null || s.trim() === '') return 'FA Unknown' + if (s.startsWith('FA')) return s + return 'FA ' + s +} diff --git a/src/app/(default)/climb/[[...slug]]/components/ContentBlock.tsx b/src/app/(default)/climb/[[...slug]]/components/ContentBlock.tsx new file mode 100644 index 000000000..39291056d --- /dev/null +++ b/src/app/(default)/climb/[[...slug]]/components/ContentBlock.tsx @@ -0,0 +1,26 @@ +import { Climb } from '@/js/types' + +export const ContentBlock: React.FC> = ({ content: { description, location, protection } }) => { + return ( + <> +
+

Description

+
+ {description} + + {(location?.trim() !== '') && ( + <> +

Location

+ {location} + + )} + + {(protection?.trim() !== '') && ( + <> +

Protection

+ {protection} + + )} + + ) +} diff --git a/src/app/(default)/climb/[[...slug]]/components/PageAlert.tsx b/src/app/(default)/climb/[[...slug]]/components/PageAlert.tsx new file mode 100644 index 000000000..31b0e7854 --- /dev/null +++ b/src/app/(default)/climb/[[...slug]]/components/PageAlert.tsx @@ -0,0 +1,12 @@ +import Link from 'next/link' +import { Bulldozer } from '@phosphor-icons/react/dist/ssr' + +export const PageAlert: React.FC<{ id: string }> = ({ id }) => ( +
+
+ + We're giving this page a facelift. + Visit the previous version + to make edits. +
+
) diff --git a/src/app/(default)/climb/[[...slug]]/components/SiblingClimbs.tsx b/src/app/(default)/climb/[[...slug]]/components/SiblingClimbs.tsx new file mode 100644 index 000000000..092e8bcae --- /dev/null +++ b/src/app/(default)/climb/[[...slug]]/components/SiblingClimbs.tsx @@ -0,0 +1,29 @@ +import { ClimbList } from '@/app/(default)/editArea/[slug]/general/components/climb/ClimbListForm' +import { AreaType } from '@/js/types' + +/** + * Show sibling climbs + */ +export const SiblingClimbs: React.FC<{ parentArea: AreaType, climbId: string }> = ({ + parentArea, + climbId +}) => { + return ( + <> +

+ Routes in{' '} + {parentArea.areaName.includes(', The') + ? 'The '.concat(parentArea.areaName.slice(0, -5)) + : parentArea.areaName} +

+
+ + + ) +} diff --git a/src/app/(default)/climb/[[...slug]]/page.tsx b/src/app/(default)/climb/[[...slug]]/page.tsx new file mode 100644 index 000000000..0242c18e6 --- /dev/null +++ b/src/app/(default)/climb/[[...slug]]/page.tsx @@ -0,0 +1,107 @@ +import { notFound, permanentRedirect } from 'next/navigation' + +import { AreaCrumbs } from '@/components/breadcrumbs/AreaCrumbs' +import { DefaultPageContainer } from '../../components/ui/DefaultPageContainer' +import PhotoMontage, { UploadPhotoCTA } from '@/components/media/PhotoMontage' +import { StickyHeaderContainer } from '../../components/ui/StickyHeaderContainer' +import { parseUuidAsFirstParam, climbLeftRightIndexComparator, getFriendlySlug, getClimbPageFriendlyUrl } from '@/js/utils' +import { PageWithCatchAllUuidProps } from '@/js/types/pages' +import { getClimbById } from '@/js/graphql/api' +import { ClimbData } from './components/ClimbData' +import { ContentBlock } from './components/ContentBlock' +import { Summary } from '../../components/ui/Summary' +import { SiblingClimbs } from './components/SiblingClimbs' +import { LazyAreaMap } from '@/components/maps/AreaMap' +import { ClimbType, TagTargetType } from '@/js/types' +import { NeighboringRoutesNav } from '@/components/crag/NeighboringRoute' +import { AreaAndClimbPageActions } from '../../components/AreaAndClimbPageActions' +import { PageAlert } from './components/PageAlert' +/** + * Page cache settings + */ +export const revalidate = 300 // 5 mins +export const fetchCache = 'force-no-store' // opt out of Nextjs version of 'fetch' + +/** + * Climb page + */ +export default async function Page ({ params }: PageWithCatchAllUuidProps): Promise { + const climbId = parseUuidAsFirstParam({ params }) + const climb = await getClimbById(climbId) + if (climb == null) { + notFound() + } + + const userProvidedSlug = getFriendlySlug(params.slug?.[1] ?? '') + + const photoList = climb.media + + const { + id, name, type, ancestors, pathTokens, parent + } = climb + + const correctSlug = getFriendlySlug(name) + + if (correctSlug !== userProvidedSlug) { + permanentRedirect(getClimbPageFriendlyUrl(id, name)) + } + + let leftClimb: ClimbType | null = null + let rightClimb: ClimbType | null = null + + const sortedClimbs = [...parent.climbs].sort(climbLeftRightIndexComparator) + + for (const [index, climb] of sortedClimbs.entries()) { + if (climb.id === id) { + leftClimb = (sortedClimbs[index - 1] != null) ? sortedClimbs[index - 1] : null + rightClimb = sortedClimbs[index + 1] != null ? sortedClimbs[index + 1] : null + } + } + + return ( + } + photoGallery={ + photoList.length === 0 + ? + : + } + pageActions={} + breadcrumbs={ + + + + } + leftRightNav={} + summary={{ + left: , + right: + }} + map={( + )} + mapContainerClass='block lg:hidden h-[90vh] w-full' + > +
+ , + right: ( +
+ +
) + }} + /> +
+ + ) +} diff --git a/src/app/(default)/components/AreaAndClimbPageActions.tsx b/src/app/(default)/components/AreaAndClimbPageActions.tsx new file mode 100644 index 000000000..e66acb89a --- /dev/null +++ b/src/app/(default)/components/AreaAndClimbPageActions.tsx @@ -0,0 +1,47 @@ +import Link from 'next/link' +import { PencilSimple, MapTrifold } from '@phosphor-icons/react/dist/ssr' +import clz from 'classnames' + +import { SharePageURLButton } from '@/app/(default)/components/SharePageURLButton' +import { UploadPhotoButton } from '@/components/media/PhotoUploadButtons' +import { TagTargetType } from '@/js/types' + +/** + * Main action bar for area & climb page + */ +export const AreaAndClimbPageActions: React.FC<{ uuid: string, name: string, targetType: TagTargetType }> = ({ uuid, name, targetType }) => { + let url: string + let sharePath: string + let enableEdit = true + let editLabel = 'Edit' + switch (targetType) { + case TagTargetType.area: + url = `/editArea/${uuid}` + sharePath = `/area/${uuid}` + break + case TagTargetType.climb: + url = `/editClimb/${uuid}` + sharePath = `/climb/${uuid}` + enableEdit = false + editLabel = 'Edit (TBD)' + } + return ( +
    + + {editLabel} + + + + + + Map + + +
+ ) +} + +/** + * Skeleton. Height = actual component's button height. + */ +export const AreaPageActionsSkeleton: React.FC = () => (
) diff --git a/src/app/(default)/components/AreaPageActions.tsx b/src/app/(default)/components/AreaPageActions.tsx deleted file mode 100644 index 2b9dd47e5..000000000 --- a/src/app/(default)/components/AreaPageActions.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import Link from 'next/link' -import { PencilSimple, MapTrifold } from '@phosphor-icons/react/dist/ssr' -import { ShareAreaLinkButton } from '@/app/(default)/components/ShareAreaLinkButton' -import { UploadPhotoButton } from '@/components/media/PhotoUploadButtons' - -/** - * Main action bar for area page - */ -export const AreaPageActions: React.FC<{ uuid: string, areaName: string } > = ({ uuid, areaName }) => ( -
    - - Edit - - - - - - Map - - -
-) - -/** - * Skeleton. Height = actual component's button height. - */ -export const AreaPageActionsSkeleton: React.FC = () => (
) diff --git a/src/app/(default)/components/DesktopHeader.tsx b/src/app/(default)/components/DesktopHeader.tsx index 3f5bd925a..01f7fcaed 100644 --- a/src/app/(default)/components/DesktopHeader.tsx +++ b/src/app/(default)/components/DesktopHeader.tsx @@ -1,11 +1,13 @@ 'use client' import { signIn, useSession } from 'next-auth/react' +import { MapTrifold } from '@phosphor-icons/react/dist/ssr' import { Logo } from '../header' import { XSearchMinimal } from '@/components/search/XSearch' import { NavMenuItem, NavMenuItemProps } from '@/components/ui/NavMenuButton' import GitHubStars from '@/components/GitHubStars' import AuthenticatedProfileNavButton from '../../../components/AuthenticatedProfileNavButton' +import Link from 'next/link' export const DesktopHeader: React.FC = () => { const { status } = useSession() @@ -71,8 +73,11 @@ export const DesktopHeader: React.FC = () => { return (
-
+
+ + | + Maps
{nav}
diff --git a/src/app/(default)/components/LandingHero.tsx b/src/app/(default)/components/LandingHero.tsx index 9db3b06c7..a2094e512 100644 --- a/src/app/(default)/components/LandingHero.tsx +++ b/src/app/(default)/components/LandingHero.tsx @@ -1,6 +1,3 @@ -import Link from 'next/link' -import { ArrowRight } from '@phosphor-icons/react/dist/ssr' - export const LandingHero: React.FC = () => { return (
@@ -8,15 +5,6 @@ export const LandingHero: React.FC = () => {
Join us to help improve this comprehensive climbing resource for the community.
-
- -
) } - -export const HeroAlert: React.FC = () => ( -
- NEW - Crag maps -
) diff --git a/src/app/(default)/components/ShareAreaLinkButton.tsx b/src/app/(default)/components/SharePageURLButton.tsx similarity index 74% rename from src/app/(default)/components/ShareAreaLinkButton.tsx rename to src/app/(default)/components/SharePageURLButton.tsx index 10cda5470..1233ef03d 100644 --- a/src/app/(default)/components/ShareAreaLinkButton.tsx +++ b/src/app/(default)/components/SharePageURLButton.tsx @@ -6,11 +6,11 @@ import { getFriendlySlug } from '@/js/utils' import { ControlledTooltip } from '@/components/ui/Tooltip' /** - * Copy area link to clipboard button + * Copy area/climb URL to clipboard button */ -export const ShareAreaLinkButton: React.FC<{ uuid: string, areaName: string }> = ({ uuid, areaName }) => { - const slug = getFriendlySlug(areaName) - const url = `https://openbeta.io/area/${uuid}/${slug}` +export const SharePageURLButton: React.FC<{ path: string, name: string }> = ({ path, name }) => { + const slug = getFriendlySlug(name) + const url = `https://openbeta.io/${path}/${slug}` const [clicked, setClicked] = useState(false) @@ -25,7 +25,7 @@ export const ShareAreaLinkButton: React.FC<{ uuid: string, areaName: string }> = return ( Copied
} open={clicked}>