backend_extra_exercise_start
  1. formMultiselect.tsx
import type {HTMLProps, ReactNode} from 'react'
import {useMemo, useState} from 'react'
import {Label} from '@/components/ui/label'
import {Input} from '@/components/ui/input'
import {Checkbox} from '@/components/ui/checkbox'
import {Controller, useFormContext, useWatch} from 'react-hook-form'
import FormError from '@/components/form/formError'

interface FormMultiselectProps<T> extends HTMLProps<HTMLInputElement> {
  options: T[]
  filterPredicate?: (option: T, filter: string) => boolean
  valueExtractor: (option: T) => string
  labelExtractor: (option: T) => string
  descriptionExtractor?: (option: T) => string
  name: string
  label?: string
}

function FormMultiselect<T>({
  options,
  filterPredicate,
  valueExtractor,
  labelExtractor,
  descriptionExtractor,
  name,
  label,
}: FormMultiselectProps<T>) {
  const [filter, setFilter] = useState<string>('')
  const form = useFormContext()

  const filteredOptions = useMemo(
    () => (filterPredicate ? options.filter(t => filterPredicate(t, filter)) : options),
    [options, filter, filterPredicate],
  )

  const checkboxIds = (useWatch({control: form.control, name: name}) as string[]) || []

  return (
    <div className="col-span-2 space-y-2">
      <Label>{label}</Label>
      <div className="space-y-3">
        {filterPredicate && (
          <Input placeholder="Filter options..." value={filter} onChange={e => setFilter(e.target.value)} />
        )}
        <div className="border rounded-lg p-4 space-y-3 max-h-48 overflow-y-auto">
          {filteredOptions.length === 0 ? (
            <p className="text-sm text-muted-foreground">
              {filter ? 'No options match your search' : 'No options available'}
            </p>
          ) : (
            <Controller
              name={name}
              render={({field}) => {
                const output: ReactNode[] = []

                for (const option of filteredOptions) {
                  const value = valueExtractor(option)
                  const isChecked = checkboxIds.includes(value)
                  output.push(
                    <div key={valueExtractor(option)} className="flex flex-col items-start space-x-3">
                      <div className="flex flex-row gap-4 items-center">
                        <Checkbox
                          className="mt-1"
                          id={`option-${value}`}
                          checked={isChecked}
                          name={name}
                          value={value}
                          defaultChecked={isChecked}
                          onCheckedChange={() =>
                            field.onChange(isChecked ? checkboxIds.filter(x => x != value) : [...checkboxIds, value])
                          }
                        />
                        <label
                          htmlFor={`option-${value}`}
                          className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer">
                          {labelExtractor(option)}
                        </label>
                      </div>
                      {descriptionExtractor && (
                        <p className="text-xs text-muted-foreground mt-1 ml-8">{descriptionExtractor(option)}</p>
                      )}
                    </div>,
                  )
                }

                return <>{output}</>
              }}
            />
          )}
        </div>
        <FormError path={name} />
      </div>
    </div>
  )
}

export default FormMultiselect