generated from taylorbryant/gatsby-starter-tailwind
-
-
Notifications
You must be signed in to change notification settings - Fork 130
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Create a separate edit area dashboard - Separate edit vs view for ease of development - Migrate area page to Next13 server components (retiring the old crag page)
- Loading branch information
Showing
103 changed files
with
3,404 additions
and
806 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { NextRequest, NextResponse } from 'next/server' | ||
import { revalidatePath } from 'next/cache' | ||
import { validate } from 'uuid' | ||
|
||
export const dynamic = 'force-dynamic' | ||
export const fetchCache = 'force-no-store' | ||
|
||
/** | ||
* Endpoint: /api/updateAreaPage | ||
*/ | ||
export async function GET (request: NextRequest): Promise<any> { | ||
const uuid = request.nextUrl.searchParams.get('uuid') as string | ||
if (uuid == null || !validate(uuid)) { | ||
return NextResponse.json({ message: 'Missing uuid in query string' }) | ||
} else { | ||
revalidatePath(`/area/${uuid}`, 'page') | ||
revalidatePath(`/editArea/${uuid}`, 'layout') | ||
return NextResponse.json({ message: 'OK' }, { status: 200 }) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 />) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
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 'mapbox-gl/dist/mapbox-gl.css' | ||
import Markdown from 'react-markdown' | ||
|
||
import PhotoMontage, { UploadPhotoCTA } from '@/components/media/PhotoMontage' | ||
import { getArea } from '@/js/graphql/getArea' | ||
import { StickyHeaderContainer } from '@/app/components/ui/StickyHeaderContainer' | ||
import { GluttenFreeCrumbs } from '@/components/ui/BreadCrumbs' | ||
import { ArticleLastUpdate } from '@/components/edit/ArticleLastUpdate' | ||
import { getMapHref, getFriendlySlug, getAreaPageFriendlyUrl, sanitizeName } from '@/js/utils' | ||
import { LazyAreaMap } from '@/components/area/areaMap' | ||
import { AreaPageContainer } from '@/app/components/ui/AreaPageContainer' | ||
import { AreaPageActions } from '../../components/AreaPageActions' | ||
import { SubAreasSection } from './sections/SubAreasSection' | ||
import { ClimbListSection } from './sections/ClimbListSection' | ||
import { CLIENT_CONFIG } from '@/js/configs/clientConfig' | ||
/** | ||
* Page cache settings | ||
*/ | ||
export const revalidate = 86400 // 24 hours | ||
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 | ||
*/ | ||
export default async function Page ({ params }: PageWithCatchAllUuidProps): Promise<any> { | ||
const areaUuid = parseUuidAsFirstParam({ params }) | ||
const pageData = await getArea(areaUuid) | ||
if (pageData == null) { | ||
notFound() | ||
} | ||
|
||
const userProvidedSlug = getFriendlySlug(params.slug?.[1] ?? '') | ||
|
||
const { area } = pageData | ||
|
||
const photoList = area?.media ?? [] | ||
const { uuid, pathTokens, ancestors, areaName, content, authorMetadata, metadata } = area | ||
const { description } = content | ||
const { lat, lng } = metadata | ||
|
||
const correctSlug = getFriendlySlug(areaName) | ||
|
||
if (correctSlug !== userProvidedSlug) { | ||
permanentRedirect(getAreaPageFriendlyUrl(uuid, areaName)) | ||
} | ||
|
||
return ( | ||
<AreaPageContainer | ||
photoGallery={ | ||
photoList.length === 0 | ||
? <UploadPhotoCTA /> | ||
: <PhotoMontage photoList={photoList} /> | ||
} | ||
pageActions={<AreaPageActions areaName={areaName} uuid={uuid} />} | ||
breadcrumbs={ | ||
<StickyHeaderContainer> | ||
<GluttenFreeCrumbs pathTokens={pathTokens} ancestors={ancestors} /> | ||
</StickyHeaderContainer> | ||
} | ||
map={ | ||
<LazyAreaMap | ||
focused={null} | ||
selected={area.id} | ||
subAreas={area.children} | ||
area={area} | ||
/> | ||
} | ||
> | ||
<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-secondary border-t border-b divide-y'> | ||
<a | ||
href={getMapHref({ | ||
lat, | ||
lng | ||
})} | ||
target='blank' | ||
className='flex items-center gap-2 py-3' | ||
> | ||
<MapPinLine size={20} /> | ||
<span className='mt-0.5'> | ||
<b>LAT,LNG</b> | ||
<span className='link-dotted'> | ||
{lat.toFixed(5)}, {lng.toFixed(5)} | ||
</span> | ||
</span> | ||
</a> | ||
<ArticleLastUpdate {...authorMetadata} /> | ||
</div> | ||
</div> | ||
|
||
<div className='area-climb-page-summary-right'> | ||
<div className='flex items-center gap-2'> | ||
<h3>Description</h3> | ||
<span className='text-xs inline-block align-baseline'> | ||
[ | ||
<Link | ||
href={`/editArea/${uuid}/general#description`} | ||
target='_new' | ||
className='hover:underline' | ||
> | ||
Edit | ||
</Link>] | ||
</span> | ||
</div> | ||
{(description == null || description.trim() === '') && <EditDescriptionCTA uuid={uuid} />} | ||
<Markdown>{description}</Markdown> | ||
</div> | ||
|
||
</div> | ||
|
||
<SubAreasSection area={area} /> | ||
<ClimbListSection area={area} /> | ||
</AreaPageContainer> | ||
) | ||
} | ||
|
||
/** | ||
* Extract and validate uuid as the first param in a catch-all route | ||
*/ | ||
const parseUuidAsFirstParam = ({ params }: PageWithCatchAllUuidProps): string => { | ||
if (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} /> | ||
<div className='text-sm'>No information available. Be the first to | ||
<Link href={`/editArea/${uuid}/general#description`} target='_new' className='link-dotted inline-flex items-center gap-1'> | ||
add a description <ArrowRight size={16} /> | ||
</Link> | ||
</div> | ||
</div> | ||
) | ||
|
||
/** | ||
* List of area pages to prebuild | ||
*/ | ||
export function generateStaticParams (): PageSlugType[] { | ||
return [ | ||
{ slug: ['bea6bf11-de53-5046-a5b4-b89217b7e9bc'] }, // Red Rock | ||
{ slug: ['78da26bc-cd94-5ac8-8e1c-815f7f30a28b'] }, // Red River Gorge | ||
{ slug: ['1db1e8ba-a40e-587c-88a4-64f5ea814b8e'] }, // USA | ||
{ slug: ['ab48aed5-2e8d-54bb-b099-6140fe1f098f'] }, // Colorado | ||
{ slug: ['decc1251-4a67-52b9-b23f-3243e10e93d0'] }, // Boulder | ||
{ slug: ['f166e672-4a52-56d3-94f1-14c876feb670'] }, // Indian Creek | ||
{ slug: ['5f0ed4d8-ebb0-5e78-ae15-ba7f1b3b5c51'] }, // Wasatch range | ||
{ slug: ['b1166235-3328-5537-b5ed-92f406ea8495'] }, // Lander | ||
{ slug: ['9abad566-2113-587e-95a5-b3abcfaa28ac'] } // Ten Sleep | ||
] | ||
} | ||
|
||
// Page metadata | ||
export async function generateMetadata ({ params }: PageWithCatchAllUuidProps): Promise<Metadata> { | ||
const areaUuid = parseUuidAsFirstParam({ params }) | ||
|
||
const { area: { areaName, pathTokens, media } } = await getArea(areaUuid, 'cache-first') | ||
|
||
let wall = '' | ||
if (pathTokens.length >= 2) { | ||
// Get the ancestor area's name | ||
wall = sanitizeName(pathTokens[pathTokens.length - 2]) + ' • ' | ||
} | ||
|
||
const name = sanitizeName(areaName) | ||
|
||
const previewImage = media.length > 0 ? `${CLIENT_CONFIG.CDN_BASE_URL}/${media[0].mediaUrl}?w=1200q=75` : null | ||
|
||
const description = `Community knowledge • ${wall}${name}` | ||
|
||
return { | ||
title: `${name} climbing area`, | ||
description, | ||
openGraph: { | ||
description, | ||
...previewImage != null && { images: previewImage } | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import Link from 'next/link' | ||
import { Plus } from '@phosphor-icons/react/dist/ssr' | ||
import { ClimbList } from '@/app/editArea/[slug]/general/components/climb/ClimbListForm' | ||
import { AreaType } from '@/js/types' | ||
/** | ||
* Sub-areas section | ||
*/ | ||
export const ClimbListSection: React.FC<{ area: AreaType }> = ({ area }) => { | ||
const { uuid, gradeContext, climbs, metadata } = area | ||
if (!metadata.leaf) return null | ||
return ( | ||
<section className='w-full mt-16'> | ||
<div className='flex items-center justify-between'> | ||
<div className='flex items-center gap-3'> | ||
<h3 className='flex items-center gap-4'>{climbs.length} Climbs</h3> | ||
</div> | ||
<div className='flex items-center gap-2'> | ||
<span className='text-sm italic'>Coming soon:</span> | ||
<Link href={`/editArea/${uuid}/general#addArea`} className='btn-disabled btn btn-sm'> | ||
<Plus size={18} weight='bold' /> New Climbs | ||
</Link> | ||
</div> | ||
</div> | ||
|
||
<hr className='my-6 border-2 border-base-content' /> | ||
|
||
<ClimbList gradeContext={gradeContext} areaMetadata={metadata} climbs={climbs} /> | ||
</section> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import Link from 'next/link' | ||
import { PlusCircle } from '@phosphor-icons/react/dist/ssr' | ||
import { AreaList } from 'app/editArea/[slug]/general/components/AreaList' | ||
import { AreaEntityBullet } from '@/components/cues/Entities' | ||
import { AreaType } from '@/js/types' | ||
|
||
/** | ||
* Sub-areas section | ||
*/ | ||
export const SubAreasSection: React.FC<{ area: AreaType } > = ({ area }) => { | ||
const { uuid, children, metadata: { leaf } } = area | ||
if (leaf) return null | ||
return ( | ||
<section className='w-full mt-16'> | ||
<div className='flex items-center justify-between'> | ||
<div className='flex items-center gap-3'> | ||
<h3 className='flex items-center gap-4'><AreaEntityBullet />{children.length} Areas</h3> | ||
</div> | ||
<Link href={`/editArea/${uuid}/general#addArea`} target='_new' className='btn btn-sm btn-accent'> | ||
<PlusCircle size={16} /> New Areas | ||
</Link> | ||
</div> | ||
|
||
<hr className='my-6 border-2 border-base-content' /> | ||
|
||
<AreaList parentUuid={uuid} areas={children} /> | ||
</section> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import Link from 'next/link' | ||
import { PencilSimple, ArrowElbowLeftDown } from '@phosphor-icons/react/dist/ssr' | ||
import { ShareAreaLinkButton } from '@/app/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 }) => ( | ||
<ul className='max-w-sm md:max-w-md flex items-center justify-between gap-2 w-full'> | ||
<Link href={`/editArea/${uuid}`} target='_new' className='btn btn-solid btn-accent shadow-md'> | ||
<PencilSimple size={20} weight='duotone' /> Edit | ||
</Link> | ||
|
||
<UploadPhotoButton /> | ||
|
||
<Link href='#map' className='btn'> | ||
<ArrowElbowLeftDown size={20} className='hidden md:inline' /> Map | ||
</Link> | ||
<ShareAreaLinkButton uuid={uuid} areaName={areaName} /> | ||
</ul> | ||
) | ||
|
||
/** | ||
* Skeleton. Height = actual component's button height. | ||
*/ | ||
export const AreaPageActionsSkeleton: React.FC = () => (<div className='w-80 bg-base-200 h-9 rounded-btn' />) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
48acddd
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs:
open-tacos – ./
openclimbmap.com
open-tacos-git-develop-openbeta-dev.vercel.app
open-tacos-openbeta-dev.vercel.app
openbeta.io
tacos.openbeta.io
www.openbeta.io