Skip to content

Commit

Permalink
wip: add climbs form
Browse files Browse the repository at this point in the history
  • Loading branch information
viet nguyen committed Dec 20, 2023
1 parent d79432b commit ba9af1d
Show file tree
Hide file tree
Showing 11 changed files with 377 additions and 66 deletions.
14 changes: 6 additions & 8 deletions src/app/editArea/[slug]/SidebarNav.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client'
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 @@ -25,11 +25,10 @@ export const SidebarNav: React.FC<{ slug: string, canAddAreas: boolean, canAddCl
<a
href={`/editArea/${slug}/addClimbs`}
className={clx(
classForActivePage('addClimbs'),
canAddClimbs ? '' : 'italic'
classForActivePage('addClimbs')
)}
>
<Plus size={24} /> Add climbs
<LineSegments size={24} /> Manage climbs
</a>
</li>
</ul>
Expand All @@ -38,14 +37,13 @@ export const SidebarNav: React.FC<{ slug: string, canAddAreas: boolean, canAddCl
<hr className='border-t my-2' />
<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-block justify-start'>
<Plus size={20} weight='bold' /> Add area
<Plus size={20} weight='bold' /> Add areas
</button>
</a>

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

Expand Down
51 changes: 35 additions & 16 deletions src/app/editArea/[slug]/addClimbs/components/AddClimbsForm.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
'use client'
import { useSession } from 'next-auth/react'

import { WarningOctagon } from '@phosphor-icons/react/dist/ssr'
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 { MarkdownTextArea } from '@/components/ui/form/MarkdownTextArea'
import useUpdateClimbsCmd from '@/js/hooks/useUpdateClimbsCmd'
import { DashboardInput } from '@/components/ui/form/Input'
import { DynamicClimbInputList } from './DynamicClimbInputList'
import { GradeContexts } from '@/js/grades/Grade'
import { defaultDisciplines } from '@/js/grades/util'
import { IndividualClimbChangeInput } from '@/js/graphql/gql/contribs'

export type QuickAddNewClimbProps =
Partial<IndividualClimbChangeInput> &
Required<Pick<IndividualClimbChangeInput, 'name' | 'disciplines' | 'grade'>>

export interface AddClimbsFormData {
climbList: QuickAddNewClimbProps[]
}
/**
* Area description edit form
* @param param0
* @returns
* Add new climbs to an area form
*/
export const AddClimbsForm: React.FC<{ parentAreaUuid: string }> = ({ parentAreaUuid }) => {
export const AddClimbsForm: React.FC<{ parentAreaName: string, parentAreaUuid: string, gradeContext: GradeContexts, canAddClimbs: boolean }> = ({ parentAreaName, parentAreaUuid, gradeContext, canAddClimbs }) => {
const session = useSession({ required: true })
const { updateClimbCmd } = useUpdateClimbsCmd(
{
Expand All @@ -23,16 +28,30 @@ export const AddClimbsForm: React.FC<{ parentAreaUuid: string }> = ({ parentArea
)

return (
<SingleEntryForm<{ climbList: any[] }>
title='Add climbs'
initialValues={{ climbList: [{ climbName: '' }] }}
// initialValues={{ description: initialValue }}
<SingleEntryForm<AddClimbsFormData>
title={`Add climbs to ${parentAreaName} area`}
initialValues={{ climbList: [{ name: '', disciplines: defaultDisciplines() }] }}
validationMode='onSubmit'
ignoreIsValid
keepValuesAfterReset={false}
submitHandler={async (data) => {
console.log(data)
// await updateClimbCmd({ description })
const { climbList } = data
const changes = climbList.filter(el => el.name.trim() !== '')
await updateClimbCmd({ parentId: parentAreaUuid, changes })
}}
>
<DynamicClimbInputList parentAreaUuid={parentAreaUuid} name='climbList' />
{canAddClimbs
? <DynamicClimbInputList
parentAreaUuid={parentAreaUuid}
gradeContext={gradeContext}
/>
: (
<div role='alert' className='alert alert-info'>
<WarningOctagon size={24} />
<span>This area is either a crag or a boulder. Adding a new child area is not allowed.</span>
</div>
)}

</SingleEntryForm>
)
}
236 changes: 236 additions & 0 deletions src/app/editArea/[slug]/addClimbs/components/DisciplinesSelection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
'use client'
import { useState, useEffect } from 'react'
import { UseFormReturn, useWatch } from 'react-hook-form'
import clx from 'classnames'
import { getScale } from '@openbeta/sandbag'

import { BaseInput } from '@/components/ui/form/Input'
import { ClimbDisciplineRecord, RulesType } from '@/js/types'
import { CLIMB_ARRAY_FIELD_NAME } from './DynamicClimbInputList'
import { defaultDisciplines } from '@/js/grades/util'
import { GradeContexts, gradeContextToGradeScales } from '@/js/grades/Grade'
import { AddClimbsFormData } from './AddClimbsForm'

interface FieldArrayInputProps {
index: number
formContext: UseFormReturn<AddClimbsFormData>
gradeContext: GradeContexts
}

/**
* Disciplines selection and grade input.
* When the climb name is not empty, at least one discipline must be selected.
*/
export const DisciplinesSelection: React.FC<FieldArrayInputProps> = ({ formContext, index, gradeContext }) => {
const { setValue, setError, clearErrors, formState: { errors } } = formContext
const fieldName = `${CLIMB_ARRAY_FIELD_NAME}[${index}].disciplines`

const disciplines: Partial<ClimbDisciplineRecord> = useWatch({ name: fieldName })
const climbName: string = useWatch({ name: `${CLIMB_ARRAY_FIELD_NAME}[${index}].name` })

const numberOfCheckedDisciplines = Object.values(disciplines).filter(el => el).length

useEffect(() => {
if (disciplines != null) {
const hasDiscipline = Object.values(disciplines).some(el => el)
if (hasDiscipline) {
// @ts-expect-error
clearErrors(fieldName)
} else if (climbName != null && climbName.trim() !== '') {
// @ts-expect-error
setError(fieldName, { type: 'custom', message: 'Please select at least one discipline.' })
}
}
}, [disciplines, climbName])

const clearAll = (): void => {
// @ts-expect-error
setValue(fieldName, defaultDisciplines())
}

const hasError = (errors?.climbList?.[index]?.disciplines ?? null) != null && (climbName?.trim() ?? '') !== ''

return (
<>
<fieldset className='border rounded-box p-4 '>
<legend className='text-sm py-2 px-1'>Disciplines:</legend>
<div className='flex flex-col gap-5'>
<div className='flex items-center gap-3 flex-wrap'>
<Checkbox label='Sport' index={index} discipline='sport' formContext={formContext} />
<Checkbox label='Trad' index={index} discipline='trad' formContext={formContext} />
<Checkbox label='Bouldering' index={index} discipline='bouldering' formContext={formContext} />
</div>

<div className='flex items-center gap-3 flex-wrap'>
<Checkbox label='Aid' index={index} discipline='aid' formContext={formContext} />
<Checkbox label='Top Rope' index={index} discipline='tr' formContext={formContext} />
<Checkbox label='Deep Water Soloing' index={index} discipline='deepwatersolo' formContext={formContext} />
</div>

<div className='flex items-center gap-3 flex-wrap'>
<Checkbox label='Mixed' index={index} discipline='mixed' formContext={formContext} />
<Checkbox label='Ice' index={index} discipline='ice' formContext={formContext} />
<Checkbox label='Snow' index={index} discipline='snow' formContext={formContext} />
</div>

<div className='self-end'>
<button className='btn btn-link btn-sm' disabled={numberOfCheckedDisciplines === 0} onClick={clearAll}>Clear all</button>
</div>

<div className='label-text-alt text-error'>
{hasError && 'Please select at least one discipline'}
</div>
</div>

</fieldset>

<div className='mt-2'>
<GradeInput formContext={formContext} index={index} gradeContext={gradeContext} />
</div>
</>
)
}

/**
* Checkbox for each discipline
*/
const Checkbox: React.FC<{ label: string, discipline: keyof ClimbDisciplineRecord } & Omit<FieldArrayInputProps, 'gradeContext'>> = ({ label, index, discipline, formContext }) => {
const { register, watch } = formContext
const fieldName = `${CLIMB_ARRAY_FIELD_NAME}[${index}].disciplines.${discipline}` as keyof ClimbDisciplineRecord

// @ts-expect-error
const checked = watch(fieldName) as boolean

return (
<label className={clx('cursor-pointer rounded-btn border px-2.5 py-1.5 flex items-center gap-2', checked ? 'border-base-content/80' : '')}>
<input
type='checkbox' className='checkbox'
// @ts-expect-error
{...register(fieldName)}
/>
<span className='uppercase text-sm select-none'>{label}</span>
</label>
)
}

/**
* Grade textbox
*/
const GradeInput: React.FC<FieldArrayInputProps> = ({ formContext, index, gradeContext }) => {
const [validationRules, setValidationRules] = useState<RulesType | undefined>()
const [gradeScale, setGradeScale] = useState<ReturnType<typeof getScale>>()

const { watch, formState: { errors } } = formContext

const fieldName = `${CLIMB_ARRAY_FIELD_NAME}[${index}].grade`
// @ts-expect-error
const disciplines = watch(`${CLIMB_ARRAY_FIELD_NAME}[${index}].disciplines`) as ClimbDisciplineRecord

useEffect(() => {
const rules = getGradeValationRules(gradeContext, disciplines)
setValidationRules(rules?.rules)
setGradeScale(rules?.scale)
}, [JSON.stringify(disciplines)])

const disciplinesError = errors?.climbList?.[index]?.grade?.message as string

const gradeScaleDisplay = gradeScale?.displayName?.toUpperCase() ?? 'Unknown'
return (
<div>
<label className='label' htmlFor='grade'>
<span className='label-text flex items-center gap-2'>
Grade
<span className='badge badge-sm bg-base-300/60'>Context={gradeContext.toUpperCase()}</span>
<span className='badge badge-sm bg-blue-300'>Scale={gradeScaleDisplay}</span>
</span>
</label>
<BaseInput
name={fieldName}
// @ts-expect-error
formContext={formContext}
registerOptions={validationRules}
/>
<label className='label'>
{disciplinesError != null
? (
<div className='label-text-alt text-error'>
{disciplinesError != null && disciplinesError}
</div>)
: null}
</label>
</div>
)
}

interface ValidationRules {
rules: RulesType
scale: ReturnType<typeof getScale>
}

const getGradeValationRules = (gradeContext: GradeContexts, disciplines: Partial<ClimbDisciplineRecord>): ValidationRules | undefined => {
const gradescales = gradeContextToGradeScales?.[gradeContext]
if (gradescales == null) {
throw new Error('Unknown grade context')
}

const getValidationRules = (discipline: keyof ClimbDisciplineRecord): ValidationRules => {
const gradeScale = getScale(gradescales[discipline])

const isValidGrade = (userInput: string): string | undefined => {
if (userInput == null || userInput === '') return undefined // possible to have unknown grade (Ex: route under development)
const score = gradeScale?.getScore(userInput) ?? -1
return Array.isArray(score) || score >= 0 ? undefined : 'Invalid grade'
}
return {
scale: gradeScale,
rules: {
validate: {
isValidGrade
}
}
}
}

// Processing priority
if (disciplines?.tr ?? false) {
return getValidationRules('tr')
}

if (disciplines?.sport ?? false) {
return getValidationRules('sport')
}

if (disciplines?.trad ?? false) {
return getValidationRules('trad')
}

if (disciplines?.bouldering ?? false) {
return getValidationRules('bouldering')
}

if (disciplines?.deepwatersolo ?? false) {
return getValidationRules('deepwatersolo')
}

if (disciplines?.aid ?? false) {
return getValidationRules('aid')
}

if (disciplines?.ice ?? false) {
return getValidationRules('ice')
}

if (disciplines?.mixed ?? false) {
return getValidationRules('mixed')
}

if (disciplines?.alpine ?? false) {
return getValidationRules('alpine')
}

if (disciplines?.snow ?? false) {
return getValidationRules('snow')
}

return undefined
}
Loading

0 comments on commit ba9af1d

Please sign in to comment.