Skip to content

Commit

Permalink
Merge pull request #18 from delta10/feat/5-additional-questions
Browse files Browse the repository at this point in the history
Add ability to show additional questions
  • Loading branch information
Robbert authored Oct 25, 2024
2 parents 1bc6b3d + 4bd87ae commit ea67bc3
Show file tree
Hide file tree
Showing 21 changed files with 1,684 additions and 97 deletions.
1,083 changes: 1,080 additions & 3 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"react-hook-form": "7.53.0",
"react-icons": "4.12.0",
"react-map-gl": "7.1.7",
"react-markdown": "9.0.1",
"react-select": "5.8.1",
"tailwind-merge": "2.5.4",
"validator": "13.12.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,67 +1,94 @@
'use client'

import { IncidentFormFooter } from '@/app/[locale]/incident/components/IncidentFormFooter'
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/Form'
import * as z from 'zod'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { useTranslations } from 'next-intl'
import { useEffect, useState } from 'react'
import { fetchAdditionalQuestions } from '@/services/additional-questions'
import { useFormStore } from '@/store/form_store'
import { IncidentFormFooter } from '@/app/[locale]/incident/components/IncidentFormFooter'
import { useStepperStore } from '@/store/stepper_store'
import { useRouter } from '@/routing/navigation'
import { LocationMap } from '@/components/ui/LocationMap'
import { Button } from '@/components/ui/Button'
import { MapDialog } from '@/app/[locale]/incident/add/components/MapDialog'
import { useEffect } from 'react'
import { useFormStore } from '@/store/form_store'
import { PublicQuestion } from '@/types/form'
import { RenderDynamicFields } from '@/app/[locale]/incident/add/components/questions/RenderDynamicFields'

const IncidentQuestionsLocationForm = () => {
const t = useTranslations('describe-add.form')
const { updateForm, formState } = useFormStore()
export const IncidentQuestionsLocationForm = () => {
const { formState: formStoreState, updateForm } = useFormStore()
const [loading, setLoading] = useState<boolean>(true)
const [additionalQuestions, setAdditionalQuestions] = useState<
PublicQuestion[]
>([])
const { addOneStep, setLastCompletedStep } = useStepperStore()
const router = useRouter()
const marker = formState.coordinates!

const incidentQuestionAndLocationFormSchema = z.object({
map: z.object({
lng: z.number().min(0.00000001, t('errors.location_required')),
lat: z.number().min(0.00000001, t('errors.location_required')),
}),
})

const form = useForm<z.infer<typeof incidentQuestionAndLocationFormSchema>>({
resolver: zodResolver(incidentQuestionAndLocationFormSchema),
defaultValues: {
map: {
lng: formState.coordinates[0],
lat: formState.coordinates[1],
},
},
})

const {
setValue,
register,
handleSubmit,
formState: { errors },
} = form
} = useForm()

useEffect(() => {
router.prefetch('/incident/contact')
}, [router])

useEffect(() => {
if (marker[0] !== 0 && marker[1] !== 0) {
setValue('map', { lng: marker[0], lat: marker[1] })
const appendAdditionalQuestions = async () => {
try {
const additionalQuestions = (await fetchAdditionalQuestions(
formStoreState.main_category,
formStoreState.sub_category
)) as unknown as PublicQuestion[]

setAdditionalQuestions(additionalQuestions)
setLoading(false)
} catch (e) {
console.error('Could not fetch additional questions', e)
setLoading(false)
}
}
}, [marker])

const onSubmit = (
values: z.infer<typeof incidentQuestionAndLocationFormSchema>
) => {
appendAdditionalQuestions()
}, [formStoreState.main_category, formStoreState.sub_category])

const onSubmit = (data: any) => {
const questionKeys = Object.keys(data)
const questionsToSubmit = additionalQuestions.filter(
(question) =>
questionKeys.includes(question.key) &&
data[question.key] !== null &&
data[question.key] !== ''
)

const answers = questionsToSubmit.map((question) => {
const id = data[question.key]
const checkboxAnswers: string[] = Array.isArray(id)
? id.filter((value: any) => value !== false && value !== 'empty')
: []

// If checkboxAnswers has a length, map over them to return a list of answer objects
const answer =
checkboxAnswers.length > 0
? checkboxAnswers.map((answerId) => ({
id: answerId,
label: question.meta.values[answerId],
info: '',
}))
: question.meta?.values?.[id]
? {
id: id,
label: question.meta.values[id],
info: '',
}
: data[question.key]

return {
id: question.key,
label: question.meta.label,
category_url: `/signals/v1/public/terms/categories/${formStoreState.sub_category}/sub_categories/${formStoreState.main_category}`,
answer,
}
})

updateForm({
...formState,
coordinates: [values.map.lng, values.map.lat],
...formStoreState,
extra_properties: answers,
})

setLastCompletedStep(2)
Expand All @@ -71,46 +98,23 @@ const IncidentQuestionsLocationForm = () => {
}

return (
<div>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-8 items-start"
>
<FormField
name={'map'}
control={form.control}
render={({ field, formState: { errors } }) => (
<FormItem className="w-full relative">
<div>
<FormLabel>{t('add_map_heading')}</FormLabel>
<FormMessage customError={errors.map?.lng} />
</div>
<FormControl className="w-full bg-red-400 relative">
<>
<LocationMap />
{/* TODO: I can not find the reason why not every element inside this dialog is focusable */}
<MapDialog
marker={marker}
trigger={
<Button
className="absolute top-1/2 mt-5 -translate-y-1/2 left-1/2 -translate-x-1/2 border-none"
type="button"
>
{t('add_choose_location_button')}
</Button>
}
/>
</>
</FormControl>
</FormItem>
)}
/>
<IncidentFormFooter />
</form>
</Form>
</div>
<form
onSubmit={handleSubmit(onSubmit)}
className="flex flex-col gap-8 items-start"
>
{additionalQuestions.length ? (
<RenderDynamicFields
data={additionalQuestions}
register={register}
errors={errors}
/>
) : loading ? (
/* TODO: Implement nice loading state */
<p>Laden...</p>
) : (
<p>TODO: Laat hier een LocationSelect zien</p>
)}
<IncidentFormFooter />
</form>
)
}

export { IncidentQuestionsLocationForm }
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { QuestionField } from '@/types/form'

interface AssetSelectProps extends Omit<QuestionField, 'register' | 'errors'> {}

export const AssetSelect = ({ field }: AssetSelectProps) => {
return <p>{field.key}</p>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { QuestionField } from '@/types/form'
import { useTranslations } from 'next-intl'
import { useFormStore } from '@/store/form_store'
import { getValidators } from '@/lib/utils/form-validator'
import React from 'react'

interface CheckboxInputProps extends QuestionField {}

export const CheckboxInput = ({
field,
register,
errors,
}: CheckboxInputProps) => {
const t = useTranslations('general.errors')
const { formState } = useFormStore()

const errorMessage = errors[field.key]?.message as string

// Check if the user has already answered a specific question.
// Returns true if an answer exists where the id is same as the value key.
// This is used to determine if the 'defaultChecked' property of a checkbox input should be set.
const getDefaultValueCheckboxInput = (id: string, key: string) => {
const extraProperties = formState.extra_properties.filter(
(question) => question.id === id
)

if (!extraProperties.length) {
return false
}

if (Array.isArray(extraProperties[0].answer)) {
return (
extraProperties[0].answer.filter((answer) => answer?.id === key)
.length > 0
)
}

return false
}

return (
<fieldset aria-invalid={!!errorMessage}>
<legend>
{field.meta.label}{' '}
<span> {field.required ? '' : `(${t('not_required_short')})`}</span>
{field.meta.subtitle && <span>{field.meta.subtitle}</span>}
</legend>
{errorMessage && (
<p
id={`${field.key}-error`}
aria-live="assertive"
style={{ color: 'red' }}
>
{errorMessage}
</p>
)}
{Object.keys(field.meta.values).map((key: string) => (
<div key={key}>
<input
{...register(`${field.key}.${key}`, getValidators(field, t))}
type="checkbox"
id={`${field.key}-${key}`}
value={key}
aria-describedby={errorMessage ? `${field.key}-error` : undefined}
defaultChecked={getDefaultValueCheckboxInput(field.key, key)}
/>
<label htmlFor={`${field.key}-${key}`}>
{field.meta.values[key]}
</label>
</div>
))}
</fieldset>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { LocationMap } from '@/components/ui/LocationMap'
import { MapDialog } from '@/app/[locale]/incident/add/components/MapDialog'
import { Button } from '@/components/ui/Button'

export const LocationSelect = (props: any) => {
return (
<div className="relative">
<LocationMap />
{/* TODO: I can not find the reason why not every element inside this dialog is focusable */}
<MapDialog
marker={props.marker}
trigger={
<Button
className="absolute top-1/2 -translate-y-1/2 left-1/2 -translate-x-1/2 border-none"
type="button"
>
Kies locatie
</Button>
}
/>
</div>
)
}
15 changes: 15 additions & 0 deletions src/app/[locale]/incident/add/components/questions/PlainText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { QuestionField } from '@/types/form'
import Markdown from 'react-markdown'

interface PlainTextProps extends Omit<QuestionField, 'register' | 'errors'> {}

export const PlainText = ({ field }: PlainTextProps) => {
// TODO: Discuss if alert is the only used PlainText type in Signalen, style Markdown
return field.meta.value ? (
<div className="bg-red-100 rounded-lg p-4">
<Markdown>{field.meta.value}</Markdown>
</div>
) : (
<></>
)
}
Loading

0 comments on commit ea67bc3

Please sign in to comment.