Skip to content

Commit

Permalink
fix: new add climbs page in dashboard (#1037)
Browse files Browse the repository at this point in the history
* new add climbs page in area edit dashboard
  • Loading branch information
vnugent authored Dec 23, 2023
1 parent 83f5d27 commit eb83f0b
Show file tree
Hide file tree
Showing 19 changed files with 656 additions and 91 deletions.
7 changes: 5 additions & 2 deletions src/app/area/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,11 @@ export default async function Page ({ params }: PageWithCatchAllUuidProps): Prom

</div>

<SubAreasSection area={area} />
<ClimbListSection area={area} />
{/* An area can only have either subareas or climbs, but not both. */}
<div className='mt-6'>
<SubAreasSection area={area} />
<ClimbListSection area={area} />
</div>
</AreaPageContainer>
)
}
Expand Down
24 changes: 12 additions & 12 deletions src/app/area/[[...slug]]/sections/ClimbListSection.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +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
* Climb list section
*/
export const ClimbListSection: React.FC<{ area: AreaType }> = ({ area }) => {
export const ClimbListSection: React.FC<{ area: AreaType, editMode?: boolean }> = ({ area, editMode = false }) => {
const { uuid, gradeContext, climbs, metadata } = area
if (!metadata.leaf) return null
return (
<section className='w-full mt-16'>
<section className='w-full'>
<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>
{/* Already in the edit dashboard. Don't show the button */}
{!editMode &&
<div className='flex items-center gap-2'>
<a href={`/editArea/${uuid}/manageClimbs`} className='btn btn-sm btn-accent btn-outline'>
<Plus size={18} weight='bold' /> New Climbs
</a>
</div>}
</div>

<hr className='my-6 border-2 border-base-content' />
<hr className='mt-2 mb-6 border-2 border-base-content' />

<ClimbList gradeContext={gradeContext} areaMetadata={metadata} climbs={climbs} />
<ClimbList gradeContext={gradeContext} areaMetadata={metadata} climbs={climbs} editMode={editMode} />
</section>
)
}
4 changes: 2 additions & 2 deletions src/app/area/[[...slug]]/sections/SubAreasSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ 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'>
<section className='w-full'>
<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>
Expand All @@ -21,7 +21,7 @@ export const SubAreasSection: React.FC<{ area: AreaType } > = ({ area }) => {
</Link>
</div>

<hr className='my-6 border-2 border-base-content' />
<hr className='mt-2 mb-6 border-2 border-base-content' />

<AreaList parentUuid={uuid} areas={children} />
</section>
Expand Down
42 changes: 28 additions & 14 deletions src/app/editArea/[slug]/SidebarNav.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
'use client'
import Link from 'next/link'
import clx from 'classnames'
import { usePathname } from 'next/navigation'
import { Article, Plus } from '@phosphor-icons/react/dist/ssr'
import { Article, Plus, LineSegments } from '@phosphor-icons/react/dist/ssr'

/**
* Sidebar navigation for area edit
Expand All @@ -12,32 +11,47 @@ export const SidebarNav: React.FC<{ slug: string, canAddAreas: boolean, canAddCl
/**
* Disable menu item's hover/click when own page is showing
*/
const classForActivePage = (myPath: string): string => activePath.endsWith(myPath) ? 'font-semibold pointer-events-none' : ''
const classForActivePage = (myPath: string): string => activePath.endsWith(myPath) ? 'font-bold pointer-events-none' : 'font-base'
return (
<nav className='px-6'>
<div className='sticky top-16'>
<ul className='menu w-56 px-0'>
<li>
<Link href={`/editArea/${slug}/general`} className={classForActivePage('general')}>
<a href={`/editArea/${slug}/general`} className={classForActivePage('general')}>
<Article size={24} /> General
</Link>
</a>
</li>
<li>
<a
href={`/editArea/${slug}/manageClimbs`}
className={clx(
classForActivePage('manageClimbs')
)}
>
<LineSegments size={24} /> Manage climbs
</a>
</li>
</ul>

<div className='w-56 mt-4'>
<hr className='border-t my-2' />
<Link href={`/editArea/${slug}/general#addArea`} className={clx(canAddAreas ? '' : 'cursor-not-allowed pointer-events-none', 'block py-2')}>
<button disabled={!canAddAreas} className='btn btn-accent btn-block justify-start'>
<Plus size={20} weight='bold' /> Add area
<a
href={`/editArea/${slug}/general#addArea`}
className={clx(canAddAreas ? '' : 'cursor-not-allowed pointer-events-none', 'block py-2')}
>
<button disabled={!canAddAreas} className='btn btn-accent btn-outline btn-block justify-start'>
<Plus size={20} weight='bold' /> Add areas
</button>
</Link>
</a>

<div className={clx(canAddAreas ? '' : 'cursor-not-allowed pointer-events-none', 'block py-1')}>
<div className='text-sm italic text-secondary'>Coming soon:</div>
<button disabled={!canAddClimbs} className='btn btn-accent btn-block justify-start'>
<Plus size={20} weight='bold' /> Add climb
<a
href={`/editArea/${slug}/manageClimbs`}
className={clx(canAddClimbs ? '' : 'cursor-not-allowed pointer-events-none', 'block py-1')}
>
<button disabled={!canAddClimbs} className='btn btn-accent btn-outline btn-block justify-start'>
<Plus size={20} weight='bold' /> Add climbs
</button>
</div>
</a>

<hr className='border-t my-2' />

Expand Down
21 changes: 21 additions & 0 deletions src/app/editArea/[slug]/components/EditAreaContainers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ReactNode } from 'react'

/**
* Reusable section container
*/
export const SectionContainer: React.FC<{ children: ReactNode, id: string } > = ({ id, children }) => (
<div id={id}>
<section className='mt-2 w-full flex flex-col gap-y-8'>
{children}
</section>
</div>
)

/**
* Reusable page container
*/
export const PageContainer: React.FC<{ children: ReactNode } > = ({ children }) => (
<div className='grid grid-cols-1 gap-y-8'>
{children}
</div>
)
11 changes: 10 additions & 1 deletion src/app/editArea/[slug]/components/SingleEntryForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,24 @@ export interface SingleEntryFormProps<T> {
children: ReactNode
initialValues: DefaultValues<T>
validationMode?: keyof ValidationMode
ignoreIsValid?: boolean
submitHandler: (formData: T) => Promise<void> | void
title: string
helperText?: string
keepValuesAfterReset?: boolean
className?: string
}

/**
* A form container for abstracting away the react-hook-form boilerplate.
* @param ignoreIsValid If true, the submit button will always be enabled.
*/
export function SingleEntryForm<T extends FieldValues> ({
children,
initialValues,
submitHandler,
validationMode = 'onBlur',
ignoreIsValid = false,
helperText,
title,
keepValuesAfterReset = true,
Expand Down Expand Up @@ -54,7 +60,10 @@ export function SingleEntryForm<T extends FieldValues> ({
</div>
<div className='px-8 py-2 w-full flex flex-col lg:flex-row items-start lg:items-center justify-between gap-4 bg-base-200 border-t'>
<span className='text-base-content/50'>{helperText}</span>
<SubmitButton isDirty={isDirty} isSubmitting={isSubmitting} isValid={isValid} />
<SubmitButton
isDirty={isDirty} isSubmitting={isSubmitting}
isValid={ignoreIsValid ? true : isValid}
/>
</div>
</div>
</form>
Expand Down
68 changes: 42 additions & 26 deletions src/app/editArea/[slug]/general/components/climb/ClimbListForm.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,42 @@
import Link from 'next/link'
import clx from 'classnames'
import { AreaMetadataType, ClimbDisciplineRecord, ClimbType } from '@/js/types'
import { disciplineTypeToDisplay } from '@/js/grades/util'
import { removeTypenameFromDisciplines } from '@/js/utils'
import Grade, { GradeContexts } from '@/js/grades/Grade'
import { ClimbListMiniToolbar } from '../../../manageClimbs/components/ClimbListMiniToolbar'

export const ClimbList: React.FC<{ gradeContext: GradeContexts, climbs: ClimbType[], areaMetadata: AreaMetadataType }> = ({ gradeContext, climbs, areaMetadata }) => {
const leftRightIndexComparator = (a: ClimbType, b: ClimbType): number => {
const aIndex = a.metadata.leftRightIndex
const bIndex = b.metadata.leftRightIndex
if (aIndex < bIndex) return -1
else if (aIndex > bIndex) return 1
return 0
}

export const ClimbList: React.FC<{ gradeContext: GradeContexts, climbs: ClimbType[], areaMetadata: AreaMetadataType, editMode: boolean }> = ({ gradeContext, climbs, areaMetadata, editMode }) => {
const sortedClimbs = climbs.sort(leftRightIndexComparator)
return (
// it looks better
<ol className={clx(climbs.length < 5 ? 'block max-w-sm' : 'three-column-table', 'divide-y divide-base-200')}>
{climbs.map((climb, index) => {
return (
<ClimbRow
key={climb.id}
gradeContext={gradeContext}
areaMetadata={areaMetadata}
{...climb}
index={index + 1}
/>
)
})}
</ol>
<div>
{climbs.length === 0 && <div className='alert alert-info text-sm'>No climbs found. Use the form below to add new climbs.</div>}
<ol className={clx(climbs.length < 5 ? 'block max-w-sm' : 'three-column-table', 'divide-y divide-base-200')}>
{sortedClimbs.map((climb, index) => {
return (
<ClimbRow
key={climb.id}
gradeContext={gradeContext}
areaMetadata={areaMetadata}
{...climb}
index={index + 1}
editMode={editMode}
/>
)
})}
</ol>
</div>
)
}

const ClimbRow: React.FC<ClimbType & { index: number, gradeContext: GradeContexts, areaMetadata: AreaMetadataType }> = ({ id, name, type: disciplines, index, gradeContext, grades, areaMetadata }) => {
const ClimbRow: React.FC<ClimbType & { index: number, gradeContext: GradeContexts, areaMetadata: AreaMetadataType, editMode?: boolean }> = ({ id, name, type: disciplines, index, gradeContext, grades, areaMetadata, editMode = false }) => {
const sanitizedDisciplines = removeTypenameFromDisciplines(disciplines)
const gradeStr = new Grade(
gradeContext,
Expand All @@ -35,16 +47,20 @@ const ClimbRow: React.FC<ClimbType & { index: number, gradeContext: GradeContext
const url = `/climbs/${id}`
return (
<li className='py-2 break-inside-avoid-column break-inside-avoid'>
<Link href={url} className='flex gap-x-4 flex-nowrap'>
<ListBullet index={index} disciplines={disciplines} />
<div className='w-full'>
<div className='flex justify-between'>
<div className='text-base font-semibold uppercase tracking-tight hover:underline'>{name}</div>
<div>{gradeStr}</div>
<div className={clx('w-full', editMode ? 'card card-compact p-2 card-bordered bg-base-100 shadow' : '')}>
<a href={url} className='flex gap-x-4 flex-nowrap w-full'>
<ListBullet index={index} disciplines={disciplines} />
<div className='w-full'>
<div className='flex justify-between'>
<div className='text-base font-semibold uppercase tracking-tight hover:underline'>{name}</div>
<div>{gradeStr}</div>
</div>
<div><DisciplinesInfo disciplines={disciplines} /></div>
</div>
<div><DisciplinesInfo disciplines={disciplines} /></div>
</div>
</Link>
</a>
</div>
{editMode &&
<ClimbListMiniToolbar climbId={id} parentAreaId={areaMetadata.areaId} climbName={name} />}
</li>
)
}
Expand Down
58 changes: 34 additions & 24 deletions src/app/editArea/[slug]/general/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { notFound } from 'next/navigation'
import { validate } from 'uuid'
import { ReactNode } from 'react'
import { Metadata } from 'next'
import { FetchPolicy } from '@apollo/client'
import { ArrowCircleRight } from '@phosphor-icons/react/dist/ssr'

import { AreaPageDataProps, getArea } from '@/js/graphql/getArea'
import { AreaNameForm } from './components/AreaNameForm'
Expand All @@ -10,7 +11,7 @@ import { AreaLatLngForm } from './components/AreaLatLngForm'
import { AddAreaForm } from './components/AddAreaForm'
import { AreaListForm } from './components/AreaList'
import { AreaTypeForm } from './components/AreaTypeForm'
import { FetchPolicy } from '@apollo/client'
import { PageContainer, SectionContainer } from '../components/EditAreaContainers'

// Opt out of caching for all data requests in the route segment
export const dynamic = 'force-dynamic'
Expand Down Expand Up @@ -38,50 +39,59 @@ export default async function AreaEditPage ({ params }: DashboardPageProps): Pro
metadata: { lat, lng, leaf }
} = area

const canAddClimbs = leaf && children.length === 0

return (
<div className='grid grid-cols-1 gap-y-8'>
<PageContainer id='general'>
<PageContainer>
<SectionContainer id='general'>
<AreaNameForm initialValue={areaName} uuid={uuid} />
</PageContainer>
</SectionContainer>

<PageContainer id='description'>
<SectionContainer id='description'>
<AreaDescriptionForm initialValue={description} uuid={uuid} />
</PageContainer>
</SectionContainer>

<PageContainer id='location'>
<SectionContainer id='location'>
<AreaLatLngForm initLat={lat} initLng={lng} uuid={uuid} />
</PageContainer>
</SectionContainer>

<PageContainer id='areaType'>
<SectionContainer id='areaType'>
<AreaTypeForm area={area} />
</PageContainer>
</SectionContainer>

<PageContainer id='addArea'>
<SectionContainer id='addArea'>
<AddAreaForm area={area} />
</PageContainer>
</SectionContainer>

{!leaf &&
<PageContainer id='children'>
<SectionContainer id='children'>
<AreaListForm
areaName={areaName}
uuid={uuid}
ancestors={ancestors}
pathTokens={pathTokens}
areas={children}
/>
</PageContainer>}
</div>
</SectionContainer>}

<SectionContainer id='manageClimbs'>
<div className='card card-bordered border-base-300 /40 overflow-hidden w-full bg-base-100'>
<div className='card-body'>
<h4 className='font-semibold text-2xl'>Manage Climbs</h4>
{canAddClimbs
? (
<div className='alert'>
<a href={`/editArea/${uuid}/manageClimbs`} className='btn btn-link'>Manage climbs page <ArrowCircleRight size={20} /></a>
</div>
)
: <div className='alert'>This area contains subareas. Please add climbs to areas designated as crags or boulders. </div>}
</div>
</div>
</SectionContainer>
</PageContainer>
)
}

export const PageContainer: React.FC<{ children: ReactNode, id: string }> = ({ id, children }) => (
<div id={id}>
<section className='mt-2 w-full flex flex-col gap-y-8'>
{children}
</section>
</div>
)

export const getPageDataForEdit = async (pageSlug: string, fetchPolicy?: FetchPolicy): Promise<AreaPageDataProps> => {
if (pageSlug == null) notFound()

Expand Down
Loading

1 comment on commit eb83f0b

@vercel
Copy link

@vercel vercel bot commented on eb83f0b Dec 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.