Skip to content

Commit

Permalink
improve area page loading skeleton
Browse files Browse the repository at this point in the history
use friendly page url in breadcrums and elsewhere
  • Loading branch information
viet nguyen committed Nov 27, 2023
1 parent cd4aee1 commit f897ba5
Show file tree
Hide file tree
Showing 11 changed files with 74 additions and 42 deletions.
8 changes: 8 additions & 0 deletions src/app/area/[...slug]/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { AreaPageContainer } from '@/app/components/ui/AreaPageContainer'

/**
* Loading skeleton for /area/<id> page.
*/
export default function Loading (): JSX.Element {
return (<AreaPageContainer />)
}
24 changes: 12 additions & 12 deletions src/app/area/[...slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { notFound, redirect } from 'next/navigation'
import { notFound, permanentRedirect } from 'next/navigation'
import { validate } from 'uuid'
import { MapPinLine } from '@phosphor-icons/react/dist/ssr'
import 'mapbox-gl/dist/mapbox-gl.css'
Expand All @@ -11,7 +11,7 @@ import { GluttenFreeCrumbs } from '@/components/ui/BreadCrumbs'
import { ArticleLastUpdate } from '@/components/edit/ArticleLastUpdate'
import { getMapHref, getFriendlySlug } from '@/js/utils'
import AreaMap from '@/components/area/areaMap'
import { PageContainer } from '@/app/components/ui/PageContainer'
import { AreaPageContainer } from '@/app/components/ui/AreaPageContainer'
import { AreaPageActions } from '../../components/AreaPageActions'
import { SubAreasSection } from './sections/SubAreasSection'
import { ClimbListSection } from './sections/ClimbListSection'
Expand Down Expand Up @@ -46,11 +46,17 @@ export default async function Page ({ params }: PageWithCatchAllUuidProps): Prom
const correctSlug = getFriendlySlug(areaName)

if (correctSlug !== userProvidedSlug) {
redirect(`/area/${uuid}/${correctSlug}`)
permanentRedirect(`/area/${uuid}/${correctSlug}`)
}

return (
<PageContainer
<AreaPageContainer
photoGallery={<PhotoMontage isHero photoList={photoList} />}
breadcrumbs={
<StickyHeaderContainer>
<GluttenFreeCrumbs pathTokens={pathTokens} ancestors={ancestors} />
</StickyHeaderContainer>
}
map={
<AreaMap
focused={null}
Expand All @@ -60,17 +66,11 @@ export default async function Page ({ params }: PageWithCatchAllUuidProps): Prom
/>
}
>
<PhotoMontage isHero photoList={photoList} />

<StickyHeaderContainer>
<GluttenFreeCrumbs pathTokens={pathTokens} ancestors={ancestors} />
</StickyHeaderContainer>

<div className='area-climb-page-summary'>
<div className='area-climb-page-summary-left'>
<h1>{areaName}</h1>

<div className='mt-6 flex flex-col text-xs text-base-300 border-t border-b divide-y'>
<div className='mt-6 flex flex-col text-xs text-secondary border-t border-b divide-y'>
<a
href={getMapHref({
lat,
Expand Down Expand Up @@ -102,7 +102,7 @@ export default async function Page ({ params }: PageWithCatchAllUuidProps): Prom

<SubAreasSection area={area} />
<ClimbListSection area={area} />
</PageContainer>
</AreaPageContainer>
)
}

Expand Down
28 changes: 28 additions & 0 deletions src/app/components/ui/AreaPageContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { GallerySkeleton } from '@/components/media/PhotoMontage'
import React from 'react'

/**
* Area page containter. Show loading skeleton if no params are provided.
*/
export const AreaPageContainer: React.FC<{
photoGallery?: React.ReactNode
breadcrumbs?: React.ReactNode
map?: React.ReactNode
children?: React.ReactNode
}> = ({ photoGallery, breadcrumbs, map, children }) => {
return (
<article>
<div className='p-4 mx-auto max-w-5xl xl:max-w-7xl'>
{photoGallery == null ? <GallerySkeleton /> : photoGallery}
{breadcrumbs == null ? <BreadCrumbsSkeleton /> : breadcrumbs}
{children == null ? <ContentSkeleton /> : children}
</div>
<div id='#map' className='w-full mt-16 relative h-[90vh] border-t'>
{map != null && map}
</div>
</article>
)
}

const BreadCrumbsSkeleton: React.FC = () => (<div className='w-full my-2 h-12 bg-base-200 rounded-box' />)
const ContentSkeleton: React.FC = () => (<div className='w-full mt-6 h-80 bg-base-200 rounded-box' />)
14 changes: 0 additions & 14 deletions src/app/components/ui/PageContainer.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion src/app/components/ui/StickyHeaderContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const StickyHeaderContainer: React.FC<{ children: ReactNode }> = ({ child
const atTop = intersection?.isIntersecting ?? false

return (
<div ref={intersectionRef} className={clx('sticky top-0 z-40 py-2 lg:min-h-[4rem] block lg:flex lg:items-center lg:justify-between bg-base-100 -mx-6 px-6', atTop ? 'border-b bottom-shadow backdrop-blur-sm bg-opacity-90' : '')}>
<div ref={intersectionRef} className={clx('sticky top-0 z-40 py-2 lg:min-h-[4rem] block lg:flex lg:items-center lg:justify-between bg-base-100 -mx-6 px-6', atTop ? 'border-b border-base-300/60 bottom-shadow backdrop-blur-sm bg-opacity-90' : '')}>
{children}
</div>
)
Expand Down
3 changes: 2 additions & 1 deletion src/app/editArea/[slug]/general/components/AreaItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Icon, IconProps } from '@phosphor-icons/react'

import { AreaType } from '@/js/types'
import { DeleteAreaTrigger, DeleteAreaTriggerButtonSm } from '@/components/edit/Triggers'
import { getFriendlySlug } from '@/js/utils'

export type EType = 'area' | 'crag' | 'boulder' | 'climb'

Expand Down Expand Up @@ -42,7 +43,7 @@ export const AreaItem: React.FC<{
// undefined array can mean we forget to include the field in GQL so let's make it not editable
const canDelete = (children?.length ?? 1) === 0 && (climbs?.length ?? 1) === 0

const url = editMode ? `/editArea/${uuid}` : `/area/${uuid}`
const url = editMode ? `/editArea/${uuid}` : `/area/${uuid}/${getFriendlySlug(areaName)}`
return (
<div className='break-inside-avoid-column break-inside-avoid pb-8'>
<Link href={url} className='block hover:outline hover:outline-1 hover:rounded-box'>
Expand Down
5 changes: 3 additions & 2 deletions src/app/editArea/[slug]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ArrowUUpLeft } from '@phosphor-icons/react/dist/ssr'
import { SidebarNav } from './SidebarNav'
import { getPageDataForEdit } from './general/page'
import { GluttenFreeCrumbs } from '@/components/ui/BreadCrumbs'
import { getAreaPageFriendlyUrl } from '@/js/utils'

// Opt out of caching for all data requests in the route segment
export const dynamic = 'force-dynamic'
Expand All @@ -15,15 +16,15 @@ export default async function RootLayout ({
children: React.ReactNode
params: { slug: string }
}): Promise<any> {
const { area: { uuid, pathTokens, ancestors } } = await getPageDataForEdit(params.slug)
const { area: { uuid, pathTokens, ancestors, areaName } } = await getPageDataForEdit(params.slug)
return (
<div>
<div className='px-12 pt-8 pb-4'>
<div className='text-3xl tracking-tight font-semibold'>Edit area</div>

<GluttenFreeCrumbs pathTokens={pathTokens} ancestors={ancestors} />
<div className='text-sm flex justify-end'>
<a href={`/area/${uuid}`} className='flex items-center gap-2 hover:underline'>
<a href={getAreaPageFriendlyUrl(uuid, areaName)} className='flex items-center gap-2 hover:underline'>
Return to public version <ArrowUUpLeft size={18} />
</a>
</div>
Expand Down
16 changes: 8 additions & 8 deletions src/components/media/PhotoMontage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const PhotoMontage = ({ photoList: initialList, isHero = false, showSkeleton = f
const [hover, setHover] = useState(false)

if (showSkeleton) {
return <Skeleton />
return <GallerySkeleton />
}

if (!showSkeleton && (shuffledList == null || shuffledList?.length === 0)) { return null }
Expand Down Expand Up @@ -147,13 +147,13 @@ const ResponsiveImage: React.FC<ResponsiveImageProps> = ({ mediaUrl, isHero = tr
alt=''
/>)

export const Skeleton: React.FC = () => (
<div className='grid grid-cols-4 grid-flow-row-dense gap-1 rounded-xl overflow-hidden h-80 bg-base-200/10 lg:bg-transparent'>
<div className='hidden lg:block relative col-start-1 col-span-2 row-span-2 col-end-3 bg-base-200/10 h-80' />
<div className='hidden lg:block w-full h-[158px] bg-base-200/10 ' />
<div className='hidden lg:block w-full h-[158px] bg-base-200/10' />
<div className='hidden lg:block w-full h-[158px] bg-base-200/10' />
<div className='hidden lg:block w-full h-[158px] bg-base-200/10' />
export const GallerySkeleton: React.FC = () => (
<div className='grid grid-cols-4 grid-flow-row-dense gap-1 rounded-xl overflow-hidden h-80 bg-base-200 lg:bg-transparent'>
<div className='hidden lg:block relative col-start-1 col-span-2 row-span-2 col-end-3 bg-base-200 h-80' />
<div className='hidden lg:block w-full h-[158px] bg-base-200 ' />
<div className='hidden lg:block w-full h-[158px] bg-base-200' />
<div className='hidden lg:block w-full h-[158px] bg-base-200' />
<div className='hidden lg:block w-full h-[158px] bg-base-200' />
</div>
)
export default memo(PhotoMontage)
6 changes: 3 additions & 3 deletions src/components/ui/BreadCrumbs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react'
import Link from 'next/link'
import clx from 'classnames'

import { sanitizeName } from '../../js/utils'
import { getFriendlySlug, sanitizeName } from '../../js/utils'
import { GlobeAltIcon } from '@heroicons/react/24/outline'
import { TypesenseAreaType } from '../../js/types'

Expand Down Expand Up @@ -131,12 +131,12 @@ export const GluttenFreeCrumbs: React.FC<{
editMode?: boolean
}> = ({ pathTokens, ancestors, editMode = false }) => {
return (
<div className='breadcrumbs text-sm'>
<div className='breadcrumbs text-sm font-medium text tracking-tight'>
<ul>
<li><a href='/' className='text-secondary'>Home</a></li>
{pathTokens.map((path, index) => {
const uuid = ancestors[index]
const url = `/${editMode ? 'editArea' : 'area'}/${uuid}`
const url = `/${editMode ? 'editArea' : 'area'}/${uuid}/${getFriendlySlug(path)}`
return <GFItem key={uuid} path={sanitizeName(path)} url={url} isLast={index === pathTokens.length - 1} />
})}
</ul>
Expand Down
8 changes: 8 additions & 0 deletions src/js/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,3 +269,11 @@ export const relayMediaConnectionToMediaArray = (mediaConnection: MediaConnectio
* @param name
*/
export const getFriendlySlug = (name: string): string => slugify(name, { lower: true, strict: true }).substring(0, 50)

/**
* Return the area page url with a trailing friendly area name
* @param uuid area uuid
* @param areaName area name
* @returns `/area/<area uuid>/<slugified area name>`
*/
export const getAreaPageFriendlyUrl = (uuid: string, areaName: string): string => `/area/${uuid}/${getFriendlySlug(areaName)}`
2 changes: 1 addition & 1 deletion src/pages/climbs/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { AreaType, ClimbDisciplineRecord, ClimbType, RulesType } from '../../js/
import SeoTags from '../../components/SeoTags'
import RouteGradeChip from '../../components/ui/RouteGradeChip'
import RouteTypeChips from '../../components/ui/RouteTypeChips'
import PhotoMontage, { Skeleton as PhotoMontageSkeleton } from '../../components/media/PhotoMontage'
import PhotoMontage, { GallerySkeleton as PhotoMontageSkeleton } from '../../components/media/PhotoMontage'
import { useClimbSeo } from '../../js/hooks/seo/useClimbSeo'
import TickButton from '../../components/users/TickButton'
import EditModeToggle from '../../components/editor/EditModeToggle'
Expand Down

0 comments on commit f897ba5

Please sign in to comment.