Skip to content

Commit

Permalink
Migrate climb page to Next 13 structure (#1195)
Browse files Browse the repository at this point in the history
* migrate climb page to next13 structure
* resolve to /climb page globally
* add map link to main nav
  • Loading branch information
vnugent authored Nov 1, 2024
1 parent e06b801 commit 62e326a
Show file tree
Hide file tree
Showing 30 changed files with 441 additions and 150 deletions.
2 changes: 1 addition & 1 deletion src/app/(default)/about/components/About.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export function About (): ReactNode {
width={700}
/>
<div className='absolute left-0 bottom-0 bg-base-200/80 p-2 text-xs'>
<Link href='/climbs/197b6958-c871-5c81-b463-d493d7515656' className='block'>Flyboy (Bishop, California)</Link>
<Link href='/climb/197b6958-c871-5c81-b463-d493d7515656' className='block'>Flyboy (Bishop, California)</Link>
<a href='https://www.instagram.com/rayphungphoto/' target='_blank' rel='noreferrer' className='font-semibold'>&copy; Ray Phung</a>
</div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/app/(default)/area/[[...slug]]/loading.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { AreaPageContainer } from '@/app/(default)/components/ui/AreaPageContainer'
import { DefaultPageContainer } from '@/app/(default)/components/ui/DefaultPageContainer'

/**
* Loading skeleton for /area/<id> page.
*/
export default function Loading (): JSX.Element {
return (<AreaPageContainer />)
return (<DefaultPageContainer />)
}
39 changes: 8 additions & 31 deletions src/app/(default)/area/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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
*/
Expand All @@ -58,13 +51,13 @@ export default async function Page ({ params }: PageWithCatchAllUuidProps): Prom
}

return (
<AreaPageContainer
<DefaultPageContainer
photoGallery={
photoList.length === 0
? <UploadPhotoCTA />
: <PhotoMontage photoList={photoList} />
}
pageActions={<AreaPageActions areaName={areaName} uuid={uuid} />}
pageActions={<AreaAndClimbPageActions name={areaName} uuid={uuid} targetType={TagTargetType.area} />}
breadcrumbs={
<StickyHeaderContainer>
<AreaCrumbs pathTokens={pathTokens} ancestors={ancestors} />
Expand Down Expand Up @@ -92,26 +85,10 @@ export default async function Page ({ params }: PageWithCatchAllUuidProps): Prom
<SubAreasSection area={area} />
<ClimbListSection area={area} />
</div>
</AreaPageContainer>
</DefaultPageContainer>
)
}

/**
* 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 }) => (
<div role='alert' className='alert'>
<Lightbulb size={24} />
Expand Down
66 changes: 66 additions & 0 deletions src/app/(default)/climb/[[...slug]]/components/ClimbData.tsx
Original file line number Diff line number Diff line change
@@ -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<ClimbType & Pick<AreaType, 'gradeContext'> & { 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 (
<>
<h1 className='text-4xl md:text-5xl mr-10'>
{name}
</h1>
<div className='mt-6'>
<div className='flex items-center space-x-2 w-full'>
{gradeStr != null && (
<RouteGradeChip gradeStr={gradeStr} safety={safety} />
)}
<RouteTypeChips type={type} />
</div>

{length !== -1 && (
<div className='mt-6 inline-flex items-center justify-left border-2 border-neutral/80 rounded'>
<ArrowsVertical className='h-5 w-5' />
<span className='bg-neutral/80 text-base-100 px-2 text-sm'>{length}m</span>
</div>
)}
{/* {editMode && <TotalLengthInput />} */}

<div className='mt-6'>
<div className='text-sm font-medium text-base-content'>{trimLegacyFA(legacyFA)}</div>
</div>

{(authorMetadata.createdAt != null || authorMetadata.updatedAt != null) && (
<div className='mt-8 border-t border-b'>
<ArticleLastUpdate {...authorMetadata} />
</div>
)}

{/* {!editMode && (
<div className='mt-8'>
<TickButton climbId={climbId} name={name} grade={yds} />
</div>
)} */}
</div>
</>
)
}

const trimLegacyFA = (s: string): string => {
if (s == null || s.trim() === '') return 'FA Unknown'
if (s.startsWith('FA')) return s
return 'FA ' + s
}
26 changes: 26 additions & 0 deletions src/app/(default)/climb/[[...slug]]/components/ContentBlock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Climb } from '@/js/types'

export const ContentBlock: React.FC<Pick<Climb, 'content'>> = ({ content: { description, location, protection } }) => {
return (
<>
<div className='mb-3 flex justify-between items-center'>
<h3>Description</h3>
</div>
{description}

{(location?.trim() !== '') && (
<>
<h3 className='mb-3 mt-6'>Location</h3>
{location}
</>
)}

{(protection?.trim() !== '') && (
<>
<h3 className='mb-3 mt-6'>Protection</h3>
{protection}
</>
)}
</>
)
}
12 changes: 12 additions & 0 deletions src/app/(default)/climb/[[...slug]]/components/PageAlert.tsx
Original file line number Diff line number Diff line change
@@ -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 }) => (
<div className='alert alert-warning text-md flex justify-center'>
<div className='flex gap-1'>
<Bulldozer size={24} className='mr-2' />
We're giving this page a facelift.
<Link href={`/climbs/${id}`} className='underline font-semibold'>Visit the previous version</Link>
to make edits.
</div>
</div>)
29 changes: 29 additions & 0 deletions src/app/(default)/climb/[[...slug]]/components/SiblingClimbs.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<h4>
Routes in{' '}
{parentArea.areaName.includes(', The')
? 'The '.concat(parentArea.areaName.slice(0, -5))
: parentArea.areaName}
</h4>
<hr className='mt-2 mb-2 border-1 border-base-content' />
<ClimbList
gradeContext={parentArea.gradeContext}
climbs={parentArea.climbs}
areaMetadata={parentArea.metadata}
routePageId={climbId}
editMode={false}
/>
</>
)
}
107 changes: 107 additions & 0 deletions src/app/(default)/climb/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
@@ -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<any> {
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 (
<DefaultPageContainer
heroAlert={<PageAlert id={id} />}
photoGallery={
photoList.length === 0
? <UploadPhotoCTA />
: <PhotoMontage photoList={photoList} />
}
pageActions={<AreaAndClimbPageActions name={name} uuid={id} targetType={TagTargetType.climb} />}
breadcrumbs={
<StickyHeaderContainer>
<AreaCrumbs pathTokens={pathTokens} ancestors={ancestors} />
</StickyHeaderContainer>
}
leftRightNav={<NeighboringRoutesNav climbs={[leftClimb, rightClimb]} parentArea={parent} />}
summary={{
left: <ClimbData {...climb} isBoulder={type.bouldering} gradeContext={parent.gradeContext} />,
right: <ContentBlock content={climb.content} />
}}
map={(
<LazyAreaMap
focused={null}
selected={climb.parent.id}
subAreas={[]}
area={climb.parent}
/>)}
mapContainerClass='block lg:hidden h-[90vh] w-full'
>
<hr className='border-1 my-8' />
<Summary
columns={{
left: <SiblingClimbs parentArea={climb.parent} climbId={id} />,
right: (
<div id='map' className='hidden lg:min-h-[500px] lg:h-full lg:block lg:relative'>
<LazyAreaMap
focused={null}
selected={climb.parent.id}
subAreas={[]}
area={climb.parent}
/>
</div>)
}}
/>
<div className='mt-16' />
</DefaultPageContainer>
)
}
47 changes: 47 additions & 0 deletions src/app/(default)/components/AreaAndClimbPageActions.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<ul className='flex items-center justify-between gap-2'>
<Link href={url} target='_new' className={clz('btn no-animation shadow-md', enableEdit ? 'btn-solid btn-accent' : 'btn-disabled')}>
<PencilSimple size={20} weight='duotone' /> {editLabel}
</Link>

<UploadPhotoButton />

<Link href='#map' className='btn no-animation'>
<MapTrifold size={20} className='hidden md:inline' /> Map
</Link>
<SharePageURLButton path={sharePath} name={name} />
</ul>
)
}

/**
* Skeleton. Height = actual component's button height.
*/
export const AreaPageActionsSkeleton: React.FC = () => (<div className='w-80 bg-base-200 h-9 rounded-btn' />)
Loading

0 comments on commit 62e326a

Please sign in to comment.