Skip to content

Commit 75627c7

Browse files
committed
Use shadcn field component
1 parent 489ac04 commit 75627c7

11 files changed

Lines changed: 2417 additions & 341 deletions

File tree

app/frontend/components/delete-user.tsx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { Form } from "@inertiajs/react"
22
import { useRef } from "react"
33

44
import HeadingSmall from "@/components/heading-small"
5-
import InputError from "@/components/input-error"
65
import { Button } from "@/components/ui/button"
76
import {
87
Dialog,
@@ -13,8 +12,8 @@ import {
1312
DialogTitle,
1413
DialogTrigger,
1514
} from "@/components/ui/dialog"
15+
import { Field, FieldError, FieldLabel } from "@/components/ui/field"
1616
import { Input } from "@/components/ui/input"
17-
import { Label } from "@/components/ui/label"
1817
import { usersPath } from "@/routes"
1918

2019
export default function DeleteUser() {
@@ -59,10 +58,10 @@ export default function DeleteUser() {
5958
>
6059
{({ resetAndClearErrors, processing, errors }) => (
6160
<>
62-
<div className="grid gap-2">
63-
<Label htmlFor="password_challenge" className="sr-only">
61+
<Field>
62+
<FieldLabel htmlFor="password_challenge" className="sr-only">
6463
Password
65-
</Label>
64+
</FieldLabel>
6665

6766
<Input
6867
id="password_challenge"
@@ -73,8 +72,8 @@ export default function DeleteUser() {
7372
autoComplete="current-password"
7473
/>
7574

76-
<InputError messages={errors.password_challenge} />
77-
</div>
75+
<FieldError errors={errors.password_challenge?.map((message) => ({ message }))} />
76+
</Field>
7877

7978
<DialogFooter>
8079
<DialogClose asChild>
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
import { type VariantProps, cva } from "class-variance-authority"
2+
import { useMemo } from "react"
3+
4+
import { Label } from "@/components/ui/label"
5+
import { Separator } from "@/components/ui/separator"
6+
import { cn } from "@/lib/utils"
7+
8+
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
9+
return (
10+
<fieldset
11+
data-slot="field-set"
12+
className={cn(
13+
"flex flex-col gap-6",
14+
"has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
15+
className,
16+
)}
17+
{...props}
18+
/>
19+
)
20+
}
21+
22+
function FieldLegend({
23+
className,
24+
variant = "legend",
25+
...props
26+
}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
27+
return (
28+
<legend
29+
data-slot="field-legend"
30+
data-variant={variant}
31+
className={cn(
32+
"mb-3 font-medium",
33+
"data-[variant=legend]:text-base",
34+
"data-[variant=label]:text-sm",
35+
className,
36+
)}
37+
{...props}
38+
/>
39+
)
40+
}
41+
42+
function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
43+
return (
44+
<div
45+
data-slot="field-group"
46+
className={cn(
47+
"group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",
48+
className,
49+
)}
50+
{...props}
51+
/>
52+
)
53+
}
54+
55+
const fieldVariants = cva(
56+
"group/field flex w-full gap-3 data-[invalid=true]:text-destructive",
57+
{
58+
variants: {
59+
orientation: {
60+
vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"],
61+
horizontal: [
62+
"flex-row items-center",
63+
"[&>[data-slot=field-label]]:flex-auto",
64+
"has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
65+
],
66+
responsive: [
67+
"flex-col @md/field-group:flex-row @md/field-group:items-center [&>*]:w-full @md/field-group:[&>*]:w-auto [&>.sr-only]:w-auto",
68+
"@md/field-group:[&>[data-slot=field-label]]:flex-auto",
69+
"@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
70+
],
71+
},
72+
},
73+
defaultVariants: {
74+
orientation: "vertical",
75+
},
76+
},
77+
)
78+
79+
function Field({
80+
className,
81+
orientation = "vertical",
82+
...props
83+
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
84+
return (
85+
<div
86+
role="group"
87+
data-slot="field"
88+
data-orientation={orientation}
89+
className={cn(fieldVariants({ orientation }), className)}
90+
{...props}
91+
/>
92+
)
93+
}
94+
95+
function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
96+
return (
97+
<div
98+
data-slot="field-content"
99+
className={cn(
100+
"group/field-content flex flex-1 flex-col gap-1.5 leading-snug",
101+
className,
102+
)}
103+
{...props}
104+
/>
105+
)
106+
}
107+
108+
function FieldLabel({
109+
className,
110+
...props
111+
}: React.ComponentProps<typeof Label>) {
112+
return (
113+
<Label
114+
data-slot="field-label"
115+
className={cn(
116+
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50",
117+
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4",
118+
"has-data-[state=checked]:border-primary has-data-[state=checked]:bg-primary/5 dark:has-data-[state=checked]:bg-primary/10",
119+
className,
120+
)}
121+
{...props}
122+
/>
123+
)
124+
}
125+
126+
function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
127+
return (
128+
<div
129+
data-slot="field-label"
130+
className={cn(
131+
"flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50",
132+
className,
133+
)}
134+
{...props}
135+
/>
136+
)
137+
}
138+
139+
function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
140+
return (
141+
<p
142+
data-slot="field-description"
143+
className={cn(
144+
"text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance",
145+
"last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5",
146+
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
147+
className,
148+
)}
149+
{...props}
150+
/>
151+
)
152+
}
153+
154+
function FieldSeparator({
155+
children,
156+
className,
157+
...props
158+
}: React.ComponentProps<"div"> & {
159+
children?: React.ReactNode
160+
}) {
161+
return (
162+
<div
163+
data-slot="field-separator"
164+
data-content={!!children}
165+
className={cn(
166+
"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
167+
className,
168+
)}
169+
{...props}
170+
>
171+
<Separator className="absolute inset-0 top-1/2" />
172+
{children && (
173+
<span
174+
className="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
175+
data-slot="field-separator-content"
176+
>
177+
{children}
178+
</span>
179+
)}
180+
</div>
181+
)
182+
}
183+
184+
function FieldError({
185+
className,
186+
children,
187+
errors,
188+
...props
189+
}: React.ComponentProps<"div"> & {
190+
errors?: ({ message?: string } | undefined)[]
191+
}) {
192+
const content = useMemo(() => {
193+
if (children) {
194+
return children
195+
}
196+
197+
if (!errors?.length) {
198+
return null
199+
}
200+
201+
const uniqueErrors = [
202+
...new Map(errors.map((error) => [error?.message, error])).values(),
203+
]
204+
205+
if (uniqueErrors?.length == 1) {
206+
return uniqueErrors[0]?.message
207+
}
208+
209+
return (
210+
<ul className="ml-4 flex list-disc flex-col gap-1">
211+
{uniqueErrors.map(
212+
(error, index) =>
213+
error?.message && <li key={index}>{error.message}</li>,
214+
)}
215+
</ul>
216+
)
217+
}, [children, errors])
218+
219+
if (!content) {
220+
return null
221+
}
222+
223+
return (
224+
<div
225+
role="alert"
226+
data-slot="field-error"
227+
className={cn("text-destructive text-sm font-normal", className)}
228+
{...props}
229+
>
230+
{content}
231+
</div>
232+
)
233+
}
234+
235+
export {
236+
Field,
237+
FieldContent,
238+
FieldDescription,
239+
FieldError,
240+
FieldGroup,
241+
FieldLabel,
242+
FieldLegend,
243+
FieldSeparator,
244+
FieldSet,
245+
FieldTitle,
246+
}

app/frontend/pages/identity/password_resets/edit.tsx

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import { Form, Head } from "@inertiajs/react"
22

3-
import InputError from "@/components/input-error"
43
import { Button } from "@/components/ui/button"
4+
import { Field, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field"
55
import { Input } from "@/components/ui/input"
6-
import { Label } from "@/components/ui/label"
76
import { Spinner } from "@/components/ui/spinner"
87
import AuthLayout from "@/layouts/auth-layout"
98
import { identityPasswordResetPath } from "@/routes"
@@ -27,56 +26,50 @@ export default function ResetPassword({ sid, email }: ResetPasswordProps) {
2726
resetOnSuccess={["password", "password_confirmation"]}
2827
>
2928
{({ processing, errors }) => (
30-
<div className="grid gap-6">
31-
<div className="grid gap-2">
32-
<Label htmlFor="email">Email</Label>
29+
<FieldGroup>
30+
<Field>
31+
<FieldLabel htmlFor="email">Email</FieldLabel>
3332
<Input
3433
id="email"
3534
type="email"
3635
name="email"
3736
autoComplete="email"
3837
value={email}
39-
className="mt-1 block w-full"
4038
readOnly
4139
/>
42-
<InputError messages={errors.email} className="mt-2" />
43-
</div>
40+
<FieldError errors={errors.email?.map((message) => ({ message }))} />
41+
</Field>
4442

45-
<div className="grid gap-2">
46-
<Label htmlFor="password">Password</Label>
43+
<Field>
44+
<FieldLabel htmlFor="password">Password</FieldLabel>
4745
<Input
4846
id="password"
4947
type="password"
5048
name="password"
5149
autoComplete="new-password"
52-
className="mt-1 block w-full"
5350
autoFocus
5451
placeholder="Password"
5552
/>
56-
<InputError messages={errors.password} />
57-
</div>
53+
<FieldError errors={errors.password?.map((message) => ({ message }))} />
54+
</Field>
5855

59-
<div className="grid gap-2">
60-
<Label htmlFor="password_confirmation">Confirm password</Label>
56+
<Field>
57+
<FieldLabel htmlFor="password_confirmation">Confirm password</FieldLabel>
6158
<Input
6259
id="password_confirmation"
6360
type="password"
6461
name="password_confirmation"
6562
autoComplete="new-password"
66-
className="mt-1 block w-full"
6763
placeholder="Confirm password"
6864
/>
69-
<InputError
70-
messages={errors.password_confirmation}
71-
className="mt-2"
72-
/>
73-
</div>
65+
<FieldError errors={errors.password_confirmation?.map((message) => ({ message }))} />
66+
</Field>
7467

7568
<Button type="submit" className="mt-4 w-full" disabled={processing}>
7669
{processing && <Spinner />}
7770
Reset password
7871
</Button>
79-
</div>
72+
</FieldGroup>
8073
)}
8174
</Form>
8275
</AuthLayout>

0 commit comments

Comments
 (0)