-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
migrate forms to zod/conform validation
- Loading branch information
1 parent
fb84993
commit 2ea5f7d
Showing
49 changed files
with
7,859 additions
and
3,676 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import { | ||
unstable_Control as Control, | ||
type FieldMetadata, | ||
} from "@conform-to/react"; | ||
|
||
import { Checkbox } from "~/components/ui/checkbox"; | ||
|
||
export function CheckboxGroupConform({ | ||
meta, | ||
items, | ||
}: { | ||
meta: FieldMetadata<string[]>; | ||
items: { name: string; value: string }[]; | ||
}) { | ||
const initialValue = | ||
typeof meta.initialValue === "string" | ||
? [meta.initialValue] | ||
: meta.initialValue ?? []; | ||
|
||
return ( | ||
<> | ||
{items.map((item) => ( | ||
<Control | ||
key={item.value} | ||
meta={{ | ||
key: meta.key, | ||
initialValue: initialValue.find((v) => v == item.value) | ||
? [item.value] | ||
: "", | ||
}} | ||
render={(control) => ( | ||
<div | ||
className="flex items-center gap-2" | ||
ref={(element) => { | ||
control.register(element?.querySelector("input")); | ||
}} | ||
> | ||
<Checkbox | ||
type="button" | ||
id={`${meta.name}-${item.value}`} | ||
name={meta.name} | ||
value={item.value} | ||
checked={control.value == item.value} | ||
onCheckedChange={(value) => | ||
control.change(value.valueOf() ? item.value : "") | ||
} | ||
onBlur={control.blur} | ||
className="focus:ring-stone-950 focus:ring-2 focus:ring-offset-2" | ||
/> | ||
<label htmlFor={`${meta.name}-${item.value}`}>{item.name}</label> | ||
</div> | ||
)} | ||
/> | ||
))} | ||
</> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import { | ||
unstable_useControl as useControl, | ||
type FieldMetadata, | ||
} from "@conform-to/react"; | ||
import { useRef, type ElementRef } from "react"; | ||
|
||
import { Checkbox } from "~/components/ui/checkbox"; | ||
|
||
export function CheckboxConform({ | ||
meta, | ||
}: { | ||
meta: FieldMetadata<string | boolean | undefined>; | ||
}) { | ||
const checkboxRef = useRef<ElementRef<typeof Checkbox>>(null); | ||
const control = useControl(meta); | ||
|
||
return ( | ||
<> | ||
<input | ||
className="sr-only" | ||
aria-hidden | ||
ref={control.register} | ||
name={meta.name} | ||
tabIndex={-1} | ||
defaultValue={meta.initialValue} | ||
onFocus={() => checkboxRef.current?.focus()} | ||
/> | ||
<Checkbox | ||
ref={checkboxRef} | ||
id={meta.id} | ||
checked={control.value === "on"} | ||
onCheckedChange={(checked) => { | ||
control.change(checked ? "on" : ""); | ||
}} | ||
onBlur={control.blur} | ||
className="focus:ring-stone-950 focus:ring-2 focus:ring-offset-2" | ||
/> | ||
</> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
import { | ||
FieldMetadata, | ||
unstable_useControl as useControl, | ||
} from "@conform-to/react"; | ||
import { Check, ChevronsUpDown } from "lucide-react"; | ||
import React from "react"; | ||
|
||
import { Button } from "~/components/ui/button"; | ||
import { | ||
Command, | ||
CommandEmpty, | ||
CommandGroup, | ||
CommandInput, | ||
CommandItem, | ||
} from "~/components/ui/command"; | ||
import { | ||
Popover, | ||
PopoverContent, | ||
PopoverTrigger, | ||
} from "~/components/ui/popover"; | ||
import { cn } from "~/utils"; | ||
|
||
const countries = [ | ||
{ label: "Afghanistan", value: "AF" }, | ||
{ label: "Åland Islands", value: "AX" }, | ||
{ label: "Albania", value: "AL" }, | ||
{ label: "Algeria", value: "DZ" }, | ||
{ label: "Italy", value: "IT" }, | ||
{ label: "Jamaica", value: "JM" }, | ||
{ label: "Japan", value: "JP" }, | ||
{ label: "United States", value: "US" }, | ||
{ label: "Uruguay", value: "UY" }, | ||
]; | ||
|
||
export function CountryPickerConform({ | ||
meta, | ||
}: { | ||
meta: FieldMetadata<string>; | ||
}) { | ||
const triggerRef = React.useRef<HTMLButtonElement>(null); | ||
const control = useControl(meta); | ||
|
||
return ( | ||
<div> | ||
<input | ||
className="sr-only" | ||
aria-hidden | ||
tabIndex={-1} | ||
ref={control.register} | ||
name={meta.name} | ||
defaultValue={meta.initialValue} | ||
onFocus={() => { | ||
triggerRef.current?.focus(); | ||
}} | ||
/> | ||
<Popover> | ||
<PopoverTrigger asChild> | ||
<Button | ||
ref={triggerRef} | ||
variant="outline" | ||
role="combobox" | ||
className={cn( | ||
"w-[200px] justify-between", | ||
!control.value && "text-muted-foreground", | ||
"focus:ring-2 focus:ring-stone-950 focus:ring-offset-2", | ||
)} | ||
> | ||
{control.value | ||
? countries.find((language) => language.value === control.value) | ||
?.label | ||
: "Select language"} | ||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> | ||
</Button> | ||
</PopoverTrigger> | ||
<PopoverContent className="w-[200px] p-0"> | ||
<Command> | ||
<CommandInput placeholder="Search language..." /> | ||
<CommandEmpty>No language found.</CommandEmpty> | ||
<CommandGroup> | ||
{countries.map((country) => ( | ||
<CommandItem | ||
value={country.label} | ||
key={country.value} | ||
onSelect={() => { | ||
control.change(country.value); | ||
}} | ||
> | ||
<Check | ||
className={cn( | ||
"mr-2 h-4 w-4", | ||
country.value === control.value | ||
? "opacity-100" | ||
: "opacity-0", | ||
)} | ||
/> | ||
{country.label} | ||
</CommandItem> | ||
))} | ||
</CommandGroup> | ||
</Command> | ||
</PopoverContent> | ||
</Popover> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import { | ||
FieldMetadata, | ||
unstable_useControl as useControl, | ||
} from "@conform-to/react"; | ||
import { format } from "date-fns"; | ||
import { Calendar as CalendarIcon } from "lucide-react"; | ||
import * as React from "react"; | ||
|
||
import { Button } from "~/components/ui/button"; | ||
import { Calendar } from "~/components/ui/calendar"; | ||
import { | ||
Popover, | ||
PopoverContent, | ||
PopoverTrigger, | ||
} from "~/components/ui/popover"; | ||
import { cn } from "~/utils"; | ||
|
||
export function DatePickerConform({ meta }: { meta: FieldMetadata<Date> }) { | ||
const triggerRef = React.useRef<HTMLButtonElement>(null); | ||
const control = useControl(meta); | ||
|
||
return ( | ||
<div> | ||
<input | ||
className="sr-only" | ||
aria-hidden | ||
tabIndex={-1} | ||
ref={control.register} | ||
name={meta.name} | ||
defaultValue={ | ||
meta.initialValue ? new Date(meta.initialValue).toISOString() : "" | ||
} | ||
onFocus={() => { | ||
triggerRef.current?.focus(); | ||
}} | ||
/> | ||
<Popover> | ||
<PopoverTrigger asChild> | ||
<Button | ||
ref={triggerRef} | ||
variant={"outline"} | ||
className={cn( | ||
"w-64 justify-start text-left font-normal focus:ring-2 focus:ring-stone-950 focus:ring-offset-2", | ||
!control.value && "text-muted-foreground", | ||
)} | ||
> | ||
<CalendarIcon className="mr-2 h-4 w-4" /> | ||
{control.value ? ( | ||
format(control.value, "PPP") | ||
) : ( | ||
<span>Pick a date</span> | ||
)} | ||
</Button> | ||
</PopoverTrigger> | ||
<PopoverContent className="w-auto p-0"> | ||
<Calendar | ||
mode="single" | ||
selected={new Date(control.value ?? "")} | ||
onSelect={(value) => control.change(value?.toISOString() ?? "")} | ||
initialFocus | ||
/> | ||
</PopoverContent> | ||
</Popover> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import { | ||
type FieldMetadata, | ||
unstable_useControl as useControl, | ||
} from '@conform-to/react'; | ||
import { REGEXP_ONLY_DIGITS_AND_CHARS } from 'input-otp'; | ||
import { type ElementRef, useRef } from 'react'; | ||
|
||
import { InputOTP, InputOTPGroup, InputOTPSlot } from '../ui/input-otp'; | ||
|
||
export function InputOTPConform({ | ||
meta, | ||
length = 6, | ||
pattern = REGEXP_ONLY_DIGITS_AND_CHARS, | ||
}: { | ||
meta: FieldMetadata<string>; | ||
length: number; | ||
pattern?: string; | ||
}) { | ||
const inputOTPRef = useRef<ElementRef<typeof InputOTP>>(null); | ||
const control = useControl(meta); | ||
|
||
return ( | ||
<> | ||
<input | ||
ref={control.register} | ||
name={meta.name} | ||
defaultValue={meta.initialValue} | ||
tabIndex={-1} | ||
className="sr-only" | ||
onFocus={() => { | ||
inputOTPRef.current?.focus(); | ||
}} | ||
/> | ||
<InputOTP | ||
ref={inputOTPRef} | ||
value={control.value ?? ''} | ||
onChange={control.change} | ||
onBlur={control.blur} | ||
maxLength={6} | ||
pattern={pattern} | ||
> | ||
<InputOTPGroup> | ||
{new Array(length).fill(0).map((_, index) => ( | ||
<InputOTPSlot key={index} index={index} /> | ||
))} | ||
</InputOTPGroup> | ||
</InputOTP> | ||
</> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { FieldMetadata, getInputProps } from "@conform-to/react"; | ||
import { ComponentProps } from "react"; | ||
|
||
import { Input } from "../ui/input"; | ||
|
||
export const InputConform = ({ | ||
meta, | ||
type, | ||
...props | ||
}: { | ||
meta: FieldMetadata<string>; | ||
type: Parameters<typeof getInputProps>[1]["type"]; | ||
} & ComponentProps<typeof Input>) => { | ||
return ( | ||
<Input | ||
{...getInputProps(meta, { type, ariaAttributes: true })} | ||
{...props} | ||
/> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import { | ||
FieldMetadata, | ||
unstable_useControl as useControl, | ||
} from "@conform-to/react"; | ||
import { ElementRef, useRef } from "react"; | ||
|
||
import { RadioGroup, RadioGroupItem } from "~/components/ui/radio-group"; | ||
|
||
export function RadioGroupConform({ | ||
meta, | ||
items, | ||
}: { | ||
meta: FieldMetadata<string>; | ||
items: { value: string; label: string }[]; | ||
}) { | ||
const radioGroupRef = useRef<ElementRef<typeof RadioGroup>>(null); | ||
const control = useControl(meta); | ||
|
||
return ( | ||
<> | ||
<input | ||
ref={control.register} | ||
name={meta.name} | ||
defaultValue={meta.initialValue} | ||
tabIndex={-1} | ||
className="sr-only" | ||
onFocus={() => { | ||
radioGroupRef.current?.focus(); | ||
}} | ||
/> | ||
<RadioGroup | ||
ref={radioGroupRef} | ||
className="flex items-center gap-4" | ||
value={control.value ?? ""} | ||
onValueChange={control.change} | ||
onBlur={control.blur} | ||
> | ||
{items.map((item) => { | ||
return ( | ||
<div className="flex items-center gap-2" key={item.value}> | ||
<RadioGroupItem | ||
value={item.value} | ||
id={`${meta.id}-${item.value}`} | ||
/> | ||
<label htmlFor={`${meta.id}-${item.value}`}>{item.label}</label> | ||
</div> | ||
); | ||
})} | ||
</RadioGroup> | ||
</> | ||
); | ||
} |
Oops, something went wrong.