A daily game
1"use client"
2
3import * as LabelPrimitive from "@radix-ui/react-label"
4import { Slot } from "@radix-ui/react-slot"
5import {
6 Controller,
7 FormProvider,
8 useFormContext,
9 useFormState,
10 type ControllerProps,
11 type FieldPath,
12 type FieldValues,
13} from "react-hook-form"
14
15import * as React from "react"
16
17import { Label } from "@/components/ui/label"
18
19import { cn } from "@/lib/utils"
20
21const Form = FormProvider
22
23type FormFieldContextValue<
24 TFieldValues extends FieldValues = FieldValues,
25 TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
26> = {
27 name: TName
28}
29
30const FormFieldContext = React.createContext<FormFieldContextValue>(
31 {} as FormFieldContextValue,
32)
33
34function FormField<
35 TFieldValues extends FieldValues = FieldValues,
36 TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
37>({ ...props }: ControllerProps<TFieldValues, TName>) {
38 return (
39 <FormFieldContext.Provider value={{ name: props.name }}>
40 <Controller {...props} />
41 </FormFieldContext.Provider>
42 )
43}
44
45const useFormField = () => {
46 const fieldContext = React.useContext(FormFieldContext)
47 const itemContext = React.useContext(FormItemContext)
48 const { getFieldState } = useFormContext()
49 const formState = useFormState({ name: fieldContext.name })
50 const fieldState = getFieldState(fieldContext.name, formState)
51
52 if (!fieldContext) {
53 throw new Error("useFormField should be used within <FormField>")
54 }
55
56 const { id } = itemContext
57
58 return {
59 id,
60 name: fieldContext.name,
61 formItemId: `${id}-form-item`,
62 formDescriptionId: `${id}-form-item-description`,
63 formMessageId: `${id}-form-item-message`,
64 ...fieldState,
65 }
66}
67
68type FormItemContextValue = {
69 id: string
70}
71
72const FormItemContext = React.createContext<FormItemContextValue>(
73 {} as FormItemContextValue,
74)
75
76function FormItem({ className, ...props }: React.ComponentProps<"div">) {
77 const id = React.useId()
78
79 return (
80 <FormItemContext.Provider value={{ id }}>
81 <div
82 data-slot="form-item"
83 className={cn("grid gap-2", className)}
84 {...props}
85 />
86 </FormItemContext.Provider>
87 )
88}
89
90function FormLabel({
91 className,
92 ...props
93}: React.ComponentProps<typeof LabelPrimitive.Root>) {
94 const { error, formItemId } = useFormField()
95
96 return (
97 <Label
98 data-slot="form-label"
99 data-error={!!error}
100 className={cn("font-heading", className)}
101 htmlFor={formItemId}
102 {...props}
103 />
104 )
105}
106
107function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
108 const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
109
110 return (
111 <Slot
112 data-slot="form-control"
113 id={formItemId}
114 aria-describedby={
115 !error
116 ? `${formDescriptionId}`
117 : `${formDescriptionId} ${formMessageId}`
118 }
119 aria-invalid={!!error}
120 {...props}
121 />
122 )
123}
124
125function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
126 const { formDescriptionId } = useFormField()
127
128 return (
129 <p
130 data-slot="form-description"
131 id={formDescriptionId}
132 className={cn("text-sm font-base text-foreground", className)}
133 {...props}
134 />
135 )
136}
137
138function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
139 const { error, formMessageId } = useFormField()
140 const body = error ? String(error?.message ?? "") : props.children
141
142 if (!body) {
143 return null
144 }
145
146 return (
147 <p
148 data-slot="form-message"
149 id={formMessageId}
150 className={cn("text-sm font-base text-red-500", className)}
151 {...props}
152 >
153 {body}
154 </p>
155 )
156}
157
158export {
159 useFormField,
160 Form,
161 FormItem,
162 FormLabel,
163 FormControl,
164 FormDescription,
165 FormMessage,
166 FormField,
167}