From 43ae8026907b77373bd3f23fa266d7b1a75006fe Mon Sep 17 00:00:00 2001 From: Lewis Blackburn Date: Wed, 4 Sep 2024 18:05:47 +0100 Subject: [PATCH] feat: Update UI components and fix formatting issues --- app/components/banner.tsx | 2 +- app/components/chart/bar-chart.tsx | 9 +- app/components/chart/pie.tsx | 14 +- app/components/footer.tsx | 2 +- app/components/form/ErrorList.tsx | 21 + app/components/form/Field.tsx | 17 + app/components/form/MultiSelectField.tsx | 189 +++++++ app/components/form/RegularFIeld.tsx | 34 ++ app/components/form/conform/Checkbox.tsx | 39 ++ app/components/form/conform/CheckboxGroup.tsx | 56 ++ app/components/form/conform/CountryPicker.tsx | 101 ++++ app/components/form/conform/DatePicker.tsx | 60 +++ .../form/conform/DepartmentPicker.tsx | 102 ++++ app/components/form/conform/Input.tsx | 19 + app/components/form/conform/InputOTP.tsx | 66 +++ app/components/form/conform/JobPicker.tsx | 96 ++++ .../form/conform/LanguagePicker.tsx | 101 ++++ app/components/form/conform/MultiSelect.tsx | 0 app/components/form/conform/RadioGroup.tsx | 51 ++ app/components/form/conform/SearchSelect.tsx | 124 +++++ app/components/form/conform/Select.tsx | 71 +++ app/components/form/conform/Slider.tsx | 51 ++ app/components/form/conform/Switch.tsx | 35 ++ app/components/form/conform/Textarea.tsx | 12 + app/components/form/conform/ToggleGroup.tsx | 48 ++ app/components/forms.tsx | 204 ------- app/components/image.tsx | 54 ++ app/components/infinite-scroll.tsx | 46 ++ app/components/navigation-bar.tsx | 10 +- app/components/pagination-bar.tsx | 82 +++ .../table/data-table-pagination.tsx | 4 +- app/components/ui/avatar.tsx | 2 +- app/components/ui/badge.tsx | 2 +- app/components/ui/calendar.tsx | 65 +++ app/components/ui/card.tsx | 2 +- app/components/ui/command.tsx | 153 ++++++ app/components/ui/dialog.tsx | 119 ++++ app/components/ui/pagination.tsx | 117 ++++ app/components/ui/popover.tsx | 2 +- app/components/ui/progress.tsx | 2 +- app/components/ui/radio-group.tsx | 41 ++ app/components/ui/scroll-area.tsx | 2 +- app/components/ui/sheet.tsx | 2 +- app/components/ui/skeleton.tsx | 15 + app/components/ui/slider.tsx | 25 + app/components/ui/switch.tsx | 26 + app/components/ui/table.tsx | 2 +- app/routes/_auth+/forgot-password.tsx | 31 +- app/routes/_auth+/login.tsx | 77 +-- app/routes/_auth+/onboarding.tsx | 128 +++-- app/routes/_auth+/onboarding_.$provider.tsx | 85 +-- app/routes/_auth+/reset-password.tsx | 54 +- app/routes/_auth+/signup.tsx | 36 +- app/routes/_auth+/verify.tsx | 30 +- app/routes/_home+/index.tsx | 2 +- app/routes/_home.tsx | 5 +- app/routes/admin+/cache.tsx | 6 +- .../changes+/edits-table/columns.tsx | 2 +- .../dashboard+/changes+/edits-table/data.tsx | 2 +- app/routes/dashboard+/charts/BarChart.tsx | 5 +- app/routes/dashboard+/charts/GenreChart.tsx | 2 +- app/routes/dashboard+/films+/index.tsx | 52 +- app/routes/dashboard.tsx | 6 +- app/routes/resources+/notifications.tsx | 5 +- app/routes/settings+/profile.change-email.tsx | 26 +- app/routes/settings+/profile.index.tsx | 34 +- app/routes/settings+/profile.password.tsx | 69 ++- .../settings+/profile.password_.create.tsx | 48 +- app/routes/settings+/profile.photo.tsx | 2 +- .../settings+/profile.two-factor.verify.tsx | 32 +- .../users+/$username_+/__note-editor.tsx | 34 +- .../users+/$username_+/notes.$noteId.tsx | 2 +- app/routes/users+/index.tsx | 2 +- app/utils/auth.server.ts | 2 +- app/utils/constants.ts | 2 +- other/svg-icons/calendar.svg | 13 + package-lock.json | 507 ++++++++++++++++++ package.json | 6 + prisma/seed.ts | 2 +- 79 files changed, 3010 insertions(+), 596 deletions(-) create mode 100644 app/components/form/ErrorList.tsx create mode 100644 app/components/form/Field.tsx create mode 100644 app/components/form/MultiSelectField.tsx create mode 100644 app/components/form/RegularFIeld.tsx create mode 100644 app/components/form/conform/Checkbox.tsx create mode 100644 app/components/form/conform/CheckboxGroup.tsx create mode 100644 app/components/form/conform/CountryPicker.tsx create mode 100644 app/components/form/conform/DatePicker.tsx create mode 100644 app/components/form/conform/DepartmentPicker.tsx create mode 100644 app/components/form/conform/Input.tsx create mode 100644 app/components/form/conform/InputOTP.tsx create mode 100644 app/components/form/conform/JobPicker.tsx create mode 100644 app/components/form/conform/LanguagePicker.tsx create mode 100644 app/components/form/conform/MultiSelect.tsx create mode 100644 app/components/form/conform/RadioGroup.tsx create mode 100644 app/components/form/conform/SearchSelect.tsx create mode 100644 app/components/form/conform/Select.tsx create mode 100644 app/components/form/conform/Slider.tsx create mode 100644 app/components/form/conform/Switch.tsx create mode 100644 app/components/form/conform/Textarea.tsx create mode 100644 app/components/form/conform/ToggleGroup.tsx delete mode 100644 app/components/forms.tsx create mode 100644 app/components/image.tsx create mode 100644 app/components/infinite-scroll.tsx create mode 100644 app/components/pagination-bar.tsx create mode 100644 app/components/ui/calendar.tsx create mode 100644 app/components/ui/command.tsx create mode 100644 app/components/ui/dialog.tsx create mode 100644 app/components/ui/pagination.tsx create mode 100644 app/components/ui/radio-group.tsx create mode 100644 app/components/ui/skeleton.tsx create mode 100644 app/components/ui/slider.tsx create mode 100644 app/components/ui/switch.tsx create mode 100644 other/svg-icons/calendar.svg diff --git a/app/components/banner.tsx b/app/components/banner.tsx index 8b933f2..f14cf8e 100644 --- a/app/components/banner.tsx +++ b/app/components/banner.tsx @@ -1,6 +1,6 @@ +import { Link } from '@remix-run/react' import { useEffect, useState } from 'react' import { Icon } from './ui/icon' -import { Link } from '@remix-run/react' export default function Banner() { // TODO: This should be set back to false if there is a new feature announcement diff --git a/app/components/chart/bar-chart.tsx b/app/components/chart/bar-chart.tsx index cc05632..0cd05af 100644 --- a/app/components/chart/bar-chart.tsx +++ b/app/components/chart/bar-chart.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { BarChart as ReBarChart, XAxis, Bar, BarProps } from 'recharts' +import { BarChart as ReBarChart, XAxis, Bar } from 'recharts' import { Card, CardHeader, @@ -7,12 +7,7 @@ import { CardDescription, CardContent, } from '../ui/card' -import { - ChartConfig, - ChartContainer, - ChartTooltip, - ChartTooltipContent, -} from '../ui/chart' +import { ChartContainer, ChartTooltip, ChartTooltipContent } from '../ui/chart' interface ChartData { [key: string]: string | number diff --git a/app/components/chart/pie.tsx b/app/components/chart/pie.tsx index 7ddea93..8f0980b 100644 --- a/app/components/chart/pie.tsx +++ b/app/components/chart/pie.tsx @@ -1,20 +1,12 @@ import React from 'react' +import { Label, Pie, PieChart as RePieChart } from 'recharts' +import { Card, CardContent } from '../ui/card' import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from '../ui/card' -import { - ChartConfig, + type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent, } from '../ui/chart' -import { Label, Pie, PieChart as RePieChart } from 'recharts' -import { Icon } from '../ui/icon' interface ChartData { [key: string]: string | number diff --git a/app/components/footer.tsx b/app/components/footer.tsx index 05f76db..76a1525 100644 --- a/app/components/footer.tsx +++ b/app/components/footer.tsx @@ -2,8 +2,8 @@ import { Form, Link } from '@remix-run/react' import { type SVGProps } from 'react' import { Icon } from './ui/icon' -import { Label } from './ui/label' import { Input } from './ui/input' +import { Label } from './ui/label' // Define the navigation types type NavigationItem = { diff --git a/app/components/form/ErrorList.tsx b/app/components/form/ErrorList.tsx new file mode 100644 index 0000000..33b4b9e --- /dev/null +++ b/app/components/form/ErrorList.tsx @@ -0,0 +1,21 @@ +import { FieldError } from './Field' + +export type ListOfErrors = Array | null | undefined + +export function ErrorList({ + id, + errors, +}: { + errors?: ListOfErrors + id?: string +}) { + const errorsToRender = errors?.filter(Boolean) + if (!errorsToRender?.length) return null + return ( + + ) +} diff --git a/app/components/form/Field.tsx b/app/components/form/Field.tsx new file mode 100644 index 0000000..3e63ccd --- /dev/null +++ b/app/components/form/Field.tsx @@ -0,0 +1,17 @@ +import { cn } from '#app/utils/misc.js' + +export const Field = ({ + children, + className, +}: { + children: React.ReactNode + className?: string +}) => { + return ( +
{children}
+ ) +} + +export const FieldError = ({ children }: { children: React.ReactNode }) => { + return
  • {children}
  • +} diff --git a/app/components/form/MultiSelectField.tsx b/app/components/form/MultiSelectField.tsx new file mode 100644 index 0000000..e2d06f8 --- /dev/null +++ b/app/components/form/MultiSelectField.tsx @@ -0,0 +1,189 @@ +import { useInputControl } from '@conform-to/react' +import type * as PopoverPrimitive from '@radix-ui/react-popover' + +import React, { useId } from 'react' +import { cn } from '#app/utils/misc.js' +import { Badge } from '../ui/badge' +import { Button } from '../ui/button' +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from '../ui/command' +import { Icon } from '../ui/icon' +import { Label } from '../ui/label' +import { Popover, PopoverTrigger, PopoverContent } from '../ui/popover' +import { ErrorList, type ListOfErrors } from './ErrorList' + +export type OptionType = { + label: string + value: string +} + +export type PopoverProps = Omit< + React.ComponentPropsWithoutRef, + 'type' +> & { + type?: string +} + +export function MultiSelectField({ + labelProps, + buttonProps, + options, + errors, + className, +}: { + labelProps: React.LabelHTMLAttributes + buttonProps: PopoverProps & { + name: string + form: string + value?: string + } + options: OptionType[] + errors?: ListOfErrors + className?: string +}) { + const [open, setOpen] = React.useState(false) + const [selected, setSelected] = React.useState( + // @ts-expect-error fix later + buttonProps.defaultValue ?? [], + ) + + const { key, name, ...selectProps } = buttonProps + const fallbackId = useId() + const input = useInputControl({ + key, + name: buttonProps.name, + formId: buttonProps.form, + initialValue: buttonProps.defaultValue?.toString(), + }) + const id = buttonProps.id ?? fallbackId + const errorId = errors?.length ? `${id}-error` : undefined + + const handleUnselect = (item: string) => { + setSelected(selected.filter((i) => i !== item)) + } + + return ( +
    + {}} + value={JSON.stringify(selected)} + className="hidden" + /> +
    + + + + + + + + + No {labelProps.children?.toString().toLowerCase()} found. + + + {options.map((option) => ( + { + setSelected( + selected.includes(option.value) + ? selected.filter((item) => item !== option.value) + : [...selected, option.value], + ) + setOpen(true) + }} + > + + {option.label} + + ))} + + + + + +
    + {errorId ? : null} +
    + + ) +} diff --git a/app/components/form/RegularFIeld.tsx b/app/components/form/RegularFIeld.tsx new file mode 100644 index 0000000..f1bdef8 --- /dev/null +++ b/app/components/form/RegularFIeld.tsx @@ -0,0 +1,34 @@ +import { useId } from 'react' +import { Input } from '../ui/input' +import { Label } from '../ui/label' +import { ErrorList, type ListOfErrors } from './ErrorList' + +export function RegularField({ + labelProps, + inputProps, + errors, + className, +}: { + labelProps: React.LabelHTMLAttributes + inputProps: React.InputHTMLAttributes + errors?: ListOfErrors + className?: string +}) { + const fallbackId = useId() + const id = inputProps.id ?? fallbackId + const errorId = errors?.length ? `${id}-error` : undefined + return ( +
    +
    + ) +} diff --git a/app/components/form/conform/Checkbox.tsx b/app/components/form/conform/Checkbox.tsx new file mode 100644 index 0000000..7a5e75b --- /dev/null +++ b/app/components/form/conform/Checkbox.tsx @@ -0,0 +1,39 @@ +import { + type FieldMetadata, + unstable_useControl as useControl, +} from '@conform-to/react' +import { useRef, type ElementRef } from 'react' +import { Checkbox } from '../../ui/checkbox' + +export function CheckboxConform({ + meta, +}: { + meta: FieldMetadata +}) { + const checkboxRef = useRef>(null) + const control = useControl(meta) + + return ( + <> + checkboxRef.current?.focus()} + /> + { + control.change(checked ? 'on' : '') + }} + onBlur={control.blur} + className="focus:ring-2 focus:ring-stone-950 focus:ring-offset-2" + /> + + ) +} diff --git a/app/components/form/conform/CheckboxGroup.tsx b/app/components/form/conform/CheckboxGroup.tsx new file mode 100644 index 0000000..0f06253 --- /dev/null +++ b/app/components/form/conform/CheckboxGroup.tsx @@ -0,0 +1,56 @@ +import { + unstable_Control as Control, + type FieldMetadata, +} from '@conform-to/react' +import { Checkbox } from '../../ui/checkbox' + +export function CheckboxGroupConform({ + meta, + items, +}: { + meta: FieldMetadata + items: Array<{ name: string; value: string }> +}) { + const initialValue = + typeof meta.initialValue === 'string' + ? [meta.initialValue] + : (meta.initialValue ?? []) + + return ( + <> + {items.map((item) => ( + v == item.value) + ? [item.value] + : '', + }} + render={(control) => ( +
    { + control.register(element?.querySelector('input')) + }} + > + + control.change(value.valueOf() ? item.value : '') + } + onBlur={control.blur} + className="focus:ring-2 focus:ring-stone-950 focus:ring-offset-2" + /> + +
    + )} + /> + ))} + + ) +} diff --git a/app/components/form/conform/CountryPicker.tsx b/app/components/form/conform/CountryPicker.tsx new file mode 100644 index 0000000..b09385e --- /dev/null +++ b/app/components/form/conform/CountryPicker.tsx @@ -0,0 +1,101 @@ +import { + type FieldMetadata, + unstable_useControl as useControl, +} from '@conform-to/react' +import React from 'react' +import { COUNTRIES } from '#app/utils/constants.js' +import { cn } from '#app/utils/misc.js' +import { Button } from '../../ui/button' +import { + Command, + CommandInput, + CommandEmpty, + CommandGroup, + CommandItem, + CommandList, +} from '../../ui/command' +import { Icon } from '../../ui/icon' +import { Popover, PopoverTrigger, PopoverContent } from '../../ui/popover' + +const countries = COUNTRIES.map((country) => ({ + label: country.name, + value: country.name, +})) + +export function CountryPickerConform({ + meta, +}: { + meta: FieldMetadata +}) { + const triggerRef = React.useRef(null) + const control = useControl(meta) + + return ( +
    + { + triggerRef.current?.focus() + }} + /> + + + + + + + + + No country found. + + {countries.map((country) => ( + { + control.change(country.value) + }} + > + + {country.label} + + ))} + + + + + +
    + ) +} diff --git a/app/components/form/conform/DatePicker.tsx b/app/components/form/conform/DatePicker.tsx new file mode 100644 index 0000000..4767c13 --- /dev/null +++ b/app/components/form/conform/DatePicker.tsx @@ -0,0 +1,60 @@ +import { + type FieldMetadata, + unstable_useControl as useControl, +} from '@conform-to/react' +import { format } from 'date-fns' +import * as React from 'react' +import { cn } from '#app/utils/misc' +import { Button } from '../../ui/button' +import { Calendar } from '../../ui/calendar' +import { Icon } from '../../ui/icon' +import { Popover, PopoverTrigger, PopoverContent } from '../../ui/popover' + +export function DatePickerConform({ meta }: { meta: FieldMetadata }) { + const triggerRef = React.useRef(null) + const control = useControl(meta) + + return ( +
    + { + triggerRef.current?.focus() + }} + /> + + + + + + control.change(value?.toISOString() ?? '')} + /> + + +
    + ) +} diff --git a/app/components/form/conform/DepartmentPicker.tsx b/app/components/form/conform/DepartmentPicker.tsx new file mode 100644 index 0000000..7326e0b --- /dev/null +++ b/app/components/form/conform/DepartmentPicker.tsx @@ -0,0 +1,102 @@ +import { + type FieldMetadata, + unstable_useControl as useControl, +} from '@conform-to/react' +import React from 'react' +import { ROLES } from '#app/utils/constants.js' +import { cn } from '#app/utils/misc.js' +import { Button } from '../../ui/button' +import { + Command, + CommandInput, + CommandEmpty, + CommandGroup, + CommandItem, + CommandList, +} from '../../ui/command' +import { Icon } from '../../ui/icon' +import { Popover, PopoverTrigger, PopoverContent } from '../../ui/popover' + +const departments = ROLES.map((role) => ({ + label: role.department, + value: role.department, +})) + +export function DepartmentPickerConform({ + meta, +}: { + meta: FieldMetadata +}) { + const triggerRef = React.useRef(null) + const control = useControl(meta) + + return ( +
    + { + triggerRef.current?.focus() + }} + /> + + + + + + + + + No department found. + + {departments.map((department) => ( + { + control.change(department.value) + }} + > + + {department.label} + + ))} + + + + + +
    + ) +} diff --git a/app/components/form/conform/Input.tsx b/app/components/form/conform/Input.tsx new file mode 100644 index 0000000..396e3ab --- /dev/null +++ b/app/components/form/conform/Input.tsx @@ -0,0 +1,19 @@ +import { type FieldMetadata, getInputProps } from '@conform-to/react' +import { type ComponentProps } from 'react' +import { Input } from '../../ui/input' + +export const InputConform = ({ + meta, + type, + ...props +}: { + meta: FieldMetadata + type: Parameters[1]['type'] +} & ComponentProps) => { + return ( + + ) +} diff --git a/app/components/form/conform/InputOTP.tsx b/app/components/form/conform/InputOTP.tsx new file mode 100644 index 0000000..6eb3b84 --- /dev/null +++ b/app/components/form/conform/InputOTP.tsx @@ -0,0 +1,66 @@ +import { + type FieldMetadata, + unstable_useControl as useControl, +} from '@conform-to/react' +import { REGEXP_ONLY_DIGITS_AND_CHARS } from 'input-otp' +import { type ElementRef, useRef, type ComponentProps } from 'react' +import { + InputOTP, + InputOTPGroup, + InputOTPSlot, +} from '#app/components/ui/input-otp.js' + +export function InputOTPConform({ + meta, + length = 6, + pattern = REGEXP_ONLY_DIGITS_AND_CHARS, + autoCapitalise, + ...props +}: { + meta: FieldMetadata + length: number + pattern?: string + autoCapitalise?: boolean +} & Partial>) { + const inputOTPRef = useRef>(null) + const control = useControl(meta) + + const capitaliseInput = (value: string) => { + return value.toUpperCase() + } + + return ( + <> + { + inputOTPRef.current?.focus() + }} + /> + { + if (!autoCapitalise) return control.change(newValue) + return control.change(capitaliseInput(newValue)) + }} + onBlur={control.blur} + maxLength={6} + pattern={pattern} + render={undefined} + > + + {new Array(length).fill(0).map((_, index) => ( + + ))} + + + + ) +} diff --git a/app/components/form/conform/JobPicker.tsx b/app/components/form/conform/JobPicker.tsx new file mode 100644 index 0000000..60475ba --- /dev/null +++ b/app/components/form/conform/JobPicker.tsx @@ -0,0 +1,96 @@ +import { + type FieldMetadata, + unstable_useControl as useControl, +} from '@conform-to/react' +import React from 'react' +import { getAllJobs } from '#app/utils/constants.js' +import { cn } from '#app/utils/misc.js' +import { Button } from '../../ui/button' +import { + Command, + CommandInput, + CommandEmpty, + CommandGroup, + CommandItem, + CommandList, +} from '../../ui/command' +import { Icon } from '../../ui/icon' +import { Popover, PopoverTrigger, PopoverContent } from '../../ui/popover' + +const jobs = getAllJobs().map((job) => ({ + label: job, + value: job, +})) + +export function JobPickerConform({ meta }: { meta: FieldMetadata }) { + const triggerRef = React.useRef(null) + const control = useControl(meta) + + return ( +
    + { + triggerRef.current?.focus() + }} + /> + + + + + + + + + No job found. + + {jobs.map((job) => ( + { + control.change(job.value) + }} + > + + {job.label} + + ))} + + + + + +
    + ) +} diff --git a/app/components/form/conform/LanguagePicker.tsx b/app/components/form/conform/LanguagePicker.tsx new file mode 100644 index 0000000..dcb02f9 --- /dev/null +++ b/app/components/form/conform/LanguagePicker.tsx @@ -0,0 +1,101 @@ +import { + type FieldMetadata, + unstable_useControl as useControl, +} from '@conform-to/react' +import React from 'react' +import { LANGUAGES } from '#app/utils/constants.js' +import { cn } from '#app/utils/misc.js' +import { Button } from '../../ui/button' +import { + Command, + CommandInput, + CommandEmpty, + CommandGroup, + CommandItem, + CommandList, +} from '../../ui/command' +import { Icon } from '../../ui/icon' +import { Popover, PopoverTrigger, PopoverContent } from '../../ui/popover' + +const langauges = LANGUAGES.map((language) => ({ + label: language.name, + value: language.name, +})) + +export function LanguagePickerConform({ + meta, +}: { + meta: FieldMetadata +}) { + const triggerRef = React.useRef(null) + const control = useControl(meta) + + return ( +
    + { + triggerRef.current?.focus() + }} + /> + + + + + + + + + No language found. + + {langauges.map((language) => ( + { + control.change(language.value) + }} + > + + {language.label} + + ))} + + + + + +
    + ) +} diff --git a/app/components/form/conform/MultiSelect.tsx b/app/components/form/conform/MultiSelect.tsx new file mode 100644 index 0000000..e69de29 diff --git a/app/components/form/conform/RadioGroup.tsx b/app/components/form/conform/RadioGroup.tsx new file mode 100644 index 0000000..3aa1a5e --- /dev/null +++ b/app/components/form/conform/RadioGroup.tsx @@ -0,0 +1,51 @@ +import { + type FieldMetadata, + unstable_useControl as useControl, +} from '@conform-to/react' +import { type ElementRef, useRef } from 'react' +import { RadioGroup, RadioGroupItem } from '../../ui/radio-group' + +export function RadioGroupConform({ + meta, + items, +}: { + meta: FieldMetadata + items: Array<{ value: string; label: string }> +}) { + const radioGroupRef = useRef>(null) + const control = useControl(meta) + + return ( + <> + { + radioGroupRef.current?.focus() + }} + /> + + {items.map((item) => { + return ( +
    + + +
    + ) + })} +
    + + ) +} diff --git a/app/components/form/conform/SearchSelect.tsx b/app/components/form/conform/SearchSelect.tsx new file mode 100644 index 0000000..436cb56 --- /dev/null +++ b/app/components/form/conform/SearchSelect.tsx @@ -0,0 +1,124 @@ +import { + type FieldMetadata, + unstable_useControl as useControl, +} from '@conform-to/react' +import React, { type FormEventHandler } from 'react' +import Image from '#app/components/image.js' +import { cn } from '#app/utils/misc.js' +import { Button } from '../../ui/button' +import { + Command, + CommandInput, + CommandEmpty, + CommandGroup, + CommandItem, + CommandList, +} from '../../ui/command' +import { Icon } from '../../ui/icon' +import { Popover, PopoverTrigger, PopoverContent } from '../../ui/popover' + +export function SearchSelectConform({ + meta, + items, + onInput, +}: { + meta: FieldMetadata + items: { label: string; value: string; image?: string | null }[] + onInput: FormEventHandler +}) { + const [selectedItem, setSelectedItem] = React.useState<{ + label: string + value: string + }>() + const triggerRef = React.useRef(null) + const control = useControl(meta) + + // if selectedItem and is not already in items, add it + if ( + selectedItem && + !items.find((item) => item.value === selectedItem.value) + ) { + items.push(selectedItem) + } + + return ( +
    + { + triggerRef.current?.focus() + }} + /> + + + + + + + + + No item found. + + {items.map((item) => ( + { + control.change(item.value) + setSelectedItem(item) + }} + > + {item.image ? ( + {item.value} + ) : ( + + )} + {item.label} + + ))} + + + + + +
    + ) +} diff --git a/app/components/form/conform/Select.tsx b/app/components/form/conform/Select.tsx new file mode 100644 index 0000000..91a755e --- /dev/null +++ b/app/components/form/conform/Select.tsx @@ -0,0 +1,71 @@ +import { + unstable_useControl as useControl, + type FieldMetadata, +} from '@conform-to/react' +import { useRef, type ElementRef, type ComponentProps } from 'react' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '../../ui/select' + +export const SelectConform = ({ + meta, + items, + placeholder, + ...props +}: { + meta: FieldMetadata + items: Array<{ name: string; value: string }> + placeholder: string +} & ComponentProps) => { + const selectRef = useRef>(null) + const control = useControl(meta) + + return ( + <> + + + + + ) +} diff --git a/app/components/form/conform/Slider.tsx b/app/components/form/conform/Slider.tsx new file mode 100644 index 0000000..1d1bbe5 --- /dev/null +++ b/app/components/form/conform/Slider.tsx @@ -0,0 +1,51 @@ +import { + type FieldMetadata, + unstable_useControl as useControl, +} from '@conform-to/react' +import { type ComponentProps, type ElementRef, useRef } from 'react' +import { Slider } from '../../ui/slider' + +export function SliderConform({ + meta, + ...props +}: { + meta: FieldMetadata + ariaLabel?: string +} & ComponentProps) { + const sliderRef = useRef>(null) + const control = useControl(meta) + + return ( + <> + { + const sliderSpan = sliderRef.current?.querySelector('[role="slider"]') + if (sliderSpan instanceof HTMLElement) { + sliderSpan.focus() + } + }} + /> +
    + { + if (value[0] !== undefined) { + control.change(value[0].toString()) + } + }} + onBlur={control.blur} + className="w-[280px]" + /> +
    {control.value}
    +
    + + ) +} diff --git a/app/components/form/conform/Switch.tsx b/app/components/form/conform/Switch.tsx new file mode 100644 index 0000000..b5ccda6 --- /dev/null +++ b/app/components/form/conform/Switch.tsx @@ -0,0 +1,35 @@ +import { + unstable_useControl as useControl, + type FieldMetadata, +} from '@conform-to/react' +import { useRef, type ElementRef } from 'react' +import { Switch } from '#app/components/ui/switch.js' + +export function SwitchConform({ meta }: { meta: FieldMetadata }) { + const switchRef = useRef>(null) + const control = useControl(meta) + + return ( + <> + { + switchRef.current?.focus() + }} + /> + { + control.change(checked ? 'on' : '') + }} + onBlur={control.blur} + className="focus:ring-2 focus:ring-stone-950 focus:ring-offset-2" + > + + ) +} diff --git a/app/components/form/conform/Textarea.tsx b/app/components/form/conform/Textarea.tsx new file mode 100644 index 0000000..1df0566 --- /dev/null +++ b/app/components/form/conform/Textarea.tsx @@ -0,0 +1,12 @@ +import { type FieldMetadata, getTextareaProps } from '@conform-to/react' +import { type ComponentProps } from 'react' +import { Textarea } from '../../ui/textarea' + +export const TextareaConform = ({ + meta, + ...props +}: { + meta: FieldMetadata +} & ComponentProps) => { + return