Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate climb page to Next 13 structure #1195

Merged
merged 8 commits into from
Nov 1, 2024
Merged
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
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
Loading