Skip to content

Commit

Permalink
wip: support area CRUD
Browse files Browse the repository at this point in the history
  • Loading branch information
viet nguyen committed Nov 19, 2023
1 parent 6c1fa73 commit 3ca5c48
Show file tree
Hide file tree
Showing 32 changed files with 473 additions and 217 deletions.
33 changes: 0 additions & 33 deletions src/app/area/[...slug]/SingleEntryForm.tsx

This file was deleted.

26 changes: 21 additions & 5 deletions src/app/editArea/[slug]/SidebarNav.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,42 @@
'use client'
import Link from 'next/link'
import { MapPinLine, TreeStructure, Article } from '@phosphor-icons/react/dist/ssr'
import { usePathname } from 'next/navigation'
import { MapPinLine, Graph, Article, FolderSimplePlus } from '@phosphor-icons/react/dist/ssr'

/**
* Sidebar navigation for area edit
*/
export const SidebarNav: React.FC<{ slug: string }> = ({ slug }) => {
const activePath = usePathname()
/**
* Disable menu item's hover/click when own page is showing
*/
const classForActivePage = (myPath: string): string => activePath.endsWith(myPath) ? 'bg-base-300/60 pointer-events-none' : ''
return (
<nav className='px-6'>
<ul className='menu w-56'>
<li>
<Link href={`/editArea/${slug}`}>
<Link href={`/editArea/${slug}/general`} className={classForActivePage('general')}>
<Article size={24} /> General
</Link>
</li>
<li>
<Link href={`/editArea/${slug}/attributes`}>
<Link href={`/editArea/${slug}/location`} className={classForActivePage('location')}>
<MapPinLine size={24} /> Location
</Link>
</li>
<li>
<Link href={`/editArea/${slug}/attributes`}>
<TreeStructure size={24} className='rotate-90' /> Child areas
<Link href={`/editArea/${slug}/manage`} className={classForActivePage('manage')}>
<Graph size={24} weight='duotone' /> Manage areas
</Link>
</li>
</ul>

<div className='p-2 w-56'>
<Link href={`/editArea/${slug}/attributes`}>
<button className='btn btn-primary btn-block justify-start'> <FolderSimplePlus size={24} /> Add area</button>
</Link>
</div>
</nav>
)
}
75 changes: 75 additions & 0 deletions src/app/editArea/[slug]/components/SingleEntryForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
'use client'
import { ReactNode } from 'react'
import { FieldValues, FormProvider, useForm, DefaultValues, ValidationMode } from 'react-hook-form'
import { SpinnerGap } from '@phosphor-icons/react/dist/ssr'
import clx from 'classnames'

export interface SingleEntryFormProps<T> {
children: ReactNode
initialValues: DefaultValues<T>
validationMode?: keyof ValidationMode
submitHandler: (formData: T) => Promise<void> | void
title: string
helperText?: string
keepValuesAfterReset?: boolean
className?: string
}

export function SingleEntryForm<T extends FieldValues> ({
children,
initialValues,
submitHandler,
validationMode = 'onBlur',
helperText,
title,
keepValuesAfterReset = true,
className = ''
}: SingleEntryFormProps<T>): ReactNode {
const form = useForm<T>({
mode: validationMode,
defaultValues: { ...initialValues }
})

const { handleSubmit, reset, formState: { isValid, isSubmitting, isDirty } } = form

return (
<FormProvider {...form}>
{/* eslint-disable-next-line */}
<form onSubmit={handleSubmit(async data => {
await submitHandler(data)
if (keepValuesAfterReset) {
reset({ ...data })
} else {
reset()
}
}
)}
>
<div className={clx('card card-bordered border-base-300/40 overflow-hidden w-full bg-base-100', className)}>
<div className='card-body'>
<h2 className='font-semibold text-2xl'>{title}</h2>
<div className='pt-2 flex flex-col gap-y-4'>
{children}
</div>
</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} />
</div>
</div>
</form>
</FormProvider>
)
}

export const SubmitButton: React.FC<{ isValid: boolean, isSubmitting: boolean, isDirty: boolean }> = ({
isValid, isSubmitting, isDirty
}) => (
<button
className='btn btn-primary btn-solid w-full lg:w-fit'
disabled={!isValid || isSubmitting || !isDirty}
type='submit'
>
{isSubmitting && <SpinnerGap size={24} className='animate-spin' />} Save
</button>
)
8 changes: 4 additions & 4 deletions src/app/editArea/[slug]/general/AreaDescriptionForm.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client'
import { useSession } from 'next-auth/react'

import { SingleEntryForm } from 'app/area/[...slug]/SingleEntryForm'
import { SingleEntryForm } from 'app/editArea/[slug]/components/SingleEntryForm'
import { AREA_DESCRIPTION_FORM_VALIDATION_RULES } from '@/components/edit/EditAreaForm'
import useUpdateAreasCmd from '@/js/hooks/useUpdateAreasCmd'
import { MDTextArea } from '@/components/ui/form/MDTextArea'
Expand All @@ -22,6 +22,8 @@ export const AreaDescriptionForm: React.FC<{ initialValue: string, uuid: string

return (
<SingleEntryForm<{ description: string }>
title='Description'
helperText='You can use markdown syntax: **bold** *italic* [link](https://example.com].'
initialValues={{ description: initialValue }}
submitHandler={async ({ description }) => {
await updateOneAreaCmd({ description })
Expand All @@ -30,9 +32,7 @@ export const AreaDescriptionForm: React.FC<{ initialValue: string, uuid: string
<MDTextArea
initialValue={initialValue}
name='description'
label='Description'
description='Describe this area to the best of your knowledge.'
helper='Do not copy description from guidebooks.'
label='Describe this area to the best of your knowledge. Do not copy descriptions from guidebooks.'
rules={AREA_DESCRIPTION_FORM_VALIDATION_RULES}
/>
</SingleEntryForm>
Expand Down
8 changes: 4 additions & 4 deletions src/app/editArea/[slug]/general/AreaLatLngForm.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client'
import { useSession } from 'next-auth/react'

import { SingleEntryForm } from 'app/area/[...slug]/SingleEntryForm'
import { SingleEntryForm } from 'app/editArea/[slug]/components/SingleEntryForm'
import { AREA_LATLNG_FORM_VALIDATION_RULES } from '@/components/edit/EditAreaForm'
import { DashboardInput } from '@/components/ui/form/Input'
import useUpdateAreasCmd from '@/js/hooks/useUpdateAreasCmd'
Expand All @@ -18,6 +18,8 @@ export const AreaLatLngForm: React.FC<{ initLat: number, initLng: number, uuid:
return (
<SingleEntryForm<{ latlngStr: string }>
initialValues={{ latlngStr }}
title='Coordinates'
helperText='The location may be where the trail meets the wall or the midpoint of the wall.'
submitHandler={({ latlngStr }) => {
const latlng = parseLatLng(latlngStr)
if (latlng != null) {
Expand All @@ -29,9 +31,7 @@ export const AreaLatLngForm: React.FC<{ initLat: number, initLng: number, uuid:
>
<DashboardInput
name='latlngStr'
label='Coordinates'
description='Specify the approximate latitude and longitude. The location may be where the trail meets the wall or in the middle of a long wall.'
helper='Please use <latitude>, <longitude>'
label='Coordinates in latitude, longitude format.'
className='w-80'
registerOptions={AREA_LATLNG_FORM_VALIDATION_RULES}
/>
Expand Down
16 changes: 11 additions & 5 deletions src/app/editArea/[slug]/general/AreaNameForm.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
'use client'
import { useSession } from 'next-auth/react'
import { ValidationValueMessage } from 'react-hook-form'

import { SingleEntryForm } from 'app/area/[...slug]/SingleEntryForm'
import { SingleEntryForm } from 'app/editArea/[slug]/components/SingleEntryForm'
import { DashboardInput } from '@/components/ui/form/Input'
import useUpdateAreasCmd from '@/js/hooks/useUpdateAreasCmd'
import { AREA_NAME_FORM_VALIDATION_RULES } from '@/components/edit/EditAreaForm'

export const AreaNameForm: React.FC<{ initialValue: string, uuid: string }> = ({ uuid, initialValue }) => {
const session = useSession({ required: true })
Expand All @@ -12,19 +14,23 @@ export const AreaNameForm: React.FC<{ initialValue: string, uuid: string }> = ({
accessToken: session?.data?.accessToken as string
}
)

const maxLengthValidation = AREA_NAME_FORM_VALIDATION_RULES.maxLength as ValidationValueMessage

return (
<SingleEntryForm<{ areaName: string }>
title='Area name'
initialValues={{ areaName: initialValue }}
submitHandler={async ({ areaName }) => {
await updateOneAreaCmd({ areaName })
}}
helperText={`Please use ${maxLengthValidation.value.toString()} characters at maximum.`}
>
<DashboardInput
name='areaName'
label='Area name'
description='This is the name of the climbing area.'
helper='Please use 100 characters at maximum.'
className='w-80'
label='This is the name of the climbing area.'
registerOptions={AREA_NAME_FORM_VALIDATION_RULES}
className='w-full'
/>
</SingleEntryForm>
)
Expand Down
46 changes: 46 additions & 0 deletions src/app/editArea/[slug]/general/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { notFound } from 'next/navigation'
import { validate } from 'uuid'
import { ReactNode } from 'react'

import { AreaPageDataProps, getArea } from '@/js/graphql/getArea'
import { AreaNameForm } from './AreaNameForm'
import { AreaDescriptionForm } from './AreaDescriptionForm'

// Opt out of caching for all data requests in the route segment
export const dynamic = 'force-dynamic'

export interface DashboardPageProps {
params: {
slug: string
}
}

export default async function AreaEditPage ({ params }: DashboardPageProps): Promise<any> {
const { area: { areaName, uuid, content: { description } } } = await getPageDataForEdit(params.slug)
return (
<PageContainer>
<AreaNameForm initialValue={areaName} uuid={uuid} />
<AreaDescriptionForm initialValue={description} uuid={uuid} />
</PageContainer>
)
}

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

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

if (!validate(pageSlug)) {
notFound()
}

const pageData = await getArea(pageSlug)
if (pageData == null) {
notFound()
}
return pageData
}
4 changes: 2 additions & 2 deletions src/app/editArea/[slug]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ export default function RootLayout ({
<div>
<h1 className='px-12 text-4xl tracking-tight py-12 block'>Edit area</h1>
<hr className='border-1' />
<div className='pt-12 flex bg-base-200'>
<div className='pt-12 flex bg-base-200 flex-col lg:flex-row'>
<SidebarNav slug={params.slug} />
<main className='w-full px-16'>
<main className='w-full px-2 lg:px-16'>
{children}
</main>
</div>
Expand Down
8 changes: 4 additions & 4 deletions src/app/editArea/[slug]/loading.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { PageContainer } from './page'
import { PageContainer } from './general/page'

export default function Loading (): JSX.Element {
return (
<PageContainer>
<div className='card card-compact card-bordered w-full h-56 bg-base-300/60' />
<div className='card card-compact card-bordered w-full h-56 bg-base-300/60' />
<div className='card card-compact card-bordered w-full h-56 bg-base-300/60' />
<div className='card card-compact card-bordered w-full h-56 bg-base-300/20' />
<div className='card card-compact card-bordered w-full h-56 bg-base-300/20' />
<div className='card card-compact card-bordered w-full h-56 bg-base-300/20' />
</PageContainer>
)
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { AreaLatLngForm } from '../general/AreaLatLngForm'
import { DashboardPageProps, getPageDataForEdit, PageContainer } from '../page'
import { DashboardPageProps, getPageDataForEdit, PageContainer } from '../general/page'

export default async function AreaEditPage ({ params }: DashboardPageProps): Promise<any> {
const { area: { uuid, metadata: { lat, lng } } } = await getPageDataForEdit(params.slug)

console.log('#', lat, lng)
return (
<PageContainer>
<AreaLatLngForm initLat={lat} initLng={lng} uuid={uuid} />
Expand Down
46 changes: 46 additions & 0 deletions src/app/editArea/[slug]/manage/AddAreaForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
'use client'
import { useSession } from 'next-auth/react'
import { useRouter } from 'next/navigation'

import { SingleEntryForm } from 'app/editArea/[slug]/components/SingleEntryForm'
import { DashboardInput } from '@/components/ui/form/Input'
import useUpdateAreasCmd from '@/js/hooks/useUpdateAreasCmd'
import { AreaType } from '@/js/types'
import { AreaDesignationRadioGroup, areaDesignationToDb, AreaTypeFormProp } from '@/components/edit/form/AreaDesignationRadioGroup'
import { AREA_NAME_FORM_VALIDATION_RULES } from '@/components/edit/EditAreaForm'

/**
*
*/
export const AddAreaForm: React.FC<{ area: AreaType }> = ({ area }) => {
const { uuid } = area
const session = useSession({ required: true })
const router = useRouter()
const { addOneAreaCmd } = useUpdateAreasCmd({
areaId: uuid,
accessToken: session?.data?.accessToken as string
}
)
return (
<SingleEntryForm<{ areaName: string, areaType: AreaTypeFormProp }>
initialValues={{ areaName: '' }}
keepValuesAfterReset={false}
title='Add new area'
helperText='Do not copy description from guidebooks.'
submitHandler={async ({ areaName, areaType }) => {
const { isBoulder, isLeaf } = areaDesignationToDb(areaType)
await addOneAreaCmd({ name: areaName, parentUuid: uuid, isBoulder, isLeaf })
router.refresh() // Ask Next to refresh props from the server
}}
className='border-primary border-2'
>
<DashboardInput
name='areaName'
label='Enter the area name.'
className='w-full'
registerOptions={AREA_NAME_FORM_VALIDATION_RULES}
/>
<AreaDesignationRadioGroup disabled={false} />
</SingleEntryForm>
)
}
Loading

0 comments on commit 3ca5c48

Please sign in to comment.