betterstart-cli 0.0.108 → 0.0.109
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/adapters/next/templates/init/admin-globals.css +1 -1
- package/dist/assets/adapters/next/templates/init/components/shared/media/edit-media-dialog-content.tsx +1 -0
- package/dist/assets/adapters/next/templates/init/components/shared/media/media-url-importer.tsx +89 -78
- package/dist/assets/adapters/next/templates/init/components/shared/search-input.tsx +5 -1
- package/dist/assets/adapters/next/templates/init/pages/forgot-password-form.tsx +41 -36
- package/dist/assets/adapters/next/templates/init/pages/login-form.tsx +94 -70
- package/dist/assets/adapters/next/templates/init/pages/profile/profile-form.tsx +7 -7
- package/dist/assets/adapters/next/templates/init/pages/reset-password-form.tsx +86 -67
- package/dist/assets/adapters/next/templates/init/pages/settings/forms/form-notifications-drawer.tsx +1 -0
- package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhook-endpoint-dialog.tsx +26 -8
- package/dist/assets/adapters/next/templates/init/pages/users/change-password-dialog.tsx +8 -10
- package/dist/assets/adapters/next/templates/init/pages/users/create-user-dialog.tsx +8 -5
- package/dist/assets/adapters/next/templates/init/pages/users/delete-user-dialog.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/pages/users/edit-role-dialog.tsx +76 -33
- package/dist/assets/shared-assets/react-admin/ui/input-group.tsx +6 -0
- package/dist/cli.js +208 -166
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
--warning-foreground: oklch(0.625 0.165 57.4);
|
|
99
99
|
--border: oklch(0.93 0 0);
|
|
100
100
|
--highlight: oklch(0.9 0.004 286.32);
|
|
101
|
-
--input: oklch(0.
|
|
101
|
+
--input: oklch(0.93 0 0);
|
|
102
102
|
--ring: oklch(0.708 0 0);
|
|
103
103
|
--chart-1: oklch(0.845 0.143 164.978);
|
|
104
104
|
--chart-2: oklch(0.696 0.17 162.48);
|
package/dist/assets/adapters/next/templates/init/components/shared/media/media-url-importer.tsx
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import type { UploadFileResult } from '@admin/types'
|
|
5
|
+
import type { FieldErrors } from 'react-hook-form'
|
|
5
6
|
import { Card, CardContent } from '@admin/components/ui/card'
|
|
7
|
+
import { Form } from '@admin/components/ui/form'
|
|
6
8
|
import {
|
|
7
9
|
InputGroup,
|
|
8
10
|
InputGroupAddon,
|
|
@@ -15,19 +17,29 @@ import { getMediaUrlPlaceholder } from '@admin/utils/media/get-media-url-placeho
|
|
|
15
17
|
import { cn } from '@admin/utils/shared/cn'
|
|
16
18
|
import { isValidUrl } from '@admin/utils/validation/is-valid-url'
|
|
17
19
|
import { Upload } from 'lucide-react'
|
|
20
|
+
import { useForm } from 'react-hook-form'
|
|
21
|
+
import { toast } from 'sonner'
|
|
18
22
|
|
|
19
23
|
interface MediaUrlImporterProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
20
24
|
onImportFromUrl: (url: string) => Promise<UploadFileResult>
|
|
21
25
|
accept?: string
|
|
22
26
|
}
|
|
23
27
|
|
|
28
|
+
interface MediaUrlFormValues {
|
|
29
|
+
url: string
|
|
30
|
+
}
|
|
31
|
+
|
|
24
32
|
export function MediaUrlImporter({ onImportFromUrl, accept, className }: MediaUrlImporterProps) {
|
|
25
|
-
const [urlValue, setUrlValue] = React.useState('')
|
|
26
|
-
const [urlError, setUrlError] = React.useState<string | null>(null)
|
|
27
33
|
const [isImporting, startImportTransition] = React.useTransition()
|
|
28
34
|
const mountedRef = React.useRef(true)
|
|
29
35
|
const importInFlightRef = React.useRef(false)
|
|
30
36
|
const importRequestIdRef = React.useRef(0)
|
|
37
|
+
const form = useForm<MediaUrlFormValues>({
|
|
38
|
+
defaultValues: { url: '' },
|
|
39
|
+
mode: 'onSubmit',
|
|
40
|
+
reValidateMode: 'onChange'
|
|
41
|
+
})
|
|
42
|
+
const urlField = form.register('url', { required: 'Enter a direct media URL to import.' })
|
|
31
43
|
|
|
32
44
|
React.useEffect(() => {
|
|
33
45
|
mountedRef.current = true
|
|
@@ -42,93 +54,92 @@ export function MediaUrlImporter({ onImportFromUrl, accept, className }: MediaUr
|
|
|
42
54
|
const urlHelpText = getMediaUrlHelpText(accept)
|
|
43
55
|
const urlPlaceholder = getMediaUrlPlaceholder(accept)
|
|
44
56
|
|
|
45
|
-
const handleImport = React.useCallback(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const nextUrl = urlValue.trim()
|
|
49
|
-
|
|
50
|
-
if (!nextUrl) {
|
|
51
|
-
setUrlError('Enter a direct media URL to import.')
|
|
52
|
-
return
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (!isValidUrl(nextUrl)) {
|
|
56
|
-
setUrlError('Enter a valid HTTP or HTTPS URL.')
|
|
57
|
-
return
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
setUrlError(null)
|
|
61
|
-
const importRequestId = importRequestIdRef.current + 1
|
|
62
|
-
importRequestIdRef.current = importRequestId
|
|
63
|
-
importInFlightRef.current = true
|
|
57
|
+
const handleImport = React.useCallback(
|
|
58
|
+
(values: MediaUrlFormValues) => {
|
|
59
|
+
if (isImporting || importInFlightRef.current) return
|
|
64
60
|
|
|
65
|
-
|
|
66
|
-
try {
|
|
67
|
-
const result = await onImportFromUrl(nextUrl)
|
|
61
|
+
const nextUrl = values.url.trim()
|
|
68
62
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (!result.success) {
|
|
74
|
-
setUrlError(result.error ?? 'Failed to import media from URL.')
|
|
75
|
-
return
|
|
76
|
-
}
|
|
63
|
+
if (!isValidUrl(nextUrl)) {
|
|
64
|
+
toast.error('Enter a valid HTTP or HTTPS URL.')
|
|
65
|
+
return
|
|
66
|
+
}
|
|
77
67
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
68
|
+
const importRequestId = importRequestIdRef.current + 1
|
|
69
|
+
importRequestIdRef.current = importRequestId
|
|
70
|
+
importInFlightRef.current = true
|
|
71
|
+
|
|
72
|
+
startImportTransition(async () => {
|
|
73
|
+
try {
|
|
74
|
+
const result = await onImportFromUrl(nextUrl)
|
|
75
|
+
|
|
76
|
+
if (!mountedRef.current || importRequestIdRef.current !== importRequestId) {
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!result.success) {
|
|
81
|
+
toast.error(result.error ?? 'Failed to import media from URL.')
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
form.reset()
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if (!mountedRef.current || importRequestIdRef.current !== importRequestId) {
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
toast.error(error instanceof Error ? error.message : 'Failed to import media from URL.')
|
|
92
|
+
} finally {
|
|
93
|
+
if (importRequestIdRef.current === importRequestId) {
|
|
94
|
+
importInFlightRef.current = false
|
|
95
|
+
}
|
|
82
96
|
}
|
|
97
|
+
})
|
|
98
|
+
},
|
|
99
|
+
[form, isImporting, onImportFromUrl]
|
|
100
|
+
)
|
|
83
101
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
})
|
|
91
|
-
}, [isImporting, onImportFromUrl, urlValue])
|
|
102
|
+
const handleInvalid = (errors: FieldErrors<MediaUrlFormValues>) => {
|
|
103
|
+
const message = errors.url?.message
|
|
104
|
+
toast.error(typeof message === 'string' ? message : 'Enter a direct media URL to import.')
|
|
105
|
+
}
|
|
92
106
|
|
|
93
107
|
return (
|
|
94
108
|
<Card className={cn('flex items-center justify-center', className)}>
|
|
95
109
|
<CardContent className="h-full w-full">
|
|
96
110
|
<div className="flex h-full w-full flex-col items-center justify-center gap-3 text-center">
|
|
97
|
-
<
|
|
98
|
-
<
|
|
99
|
-
type="url"
|
|
100
|
-
name="url"
|
|
101
|
-
aria-label="Media URL"
|
|
102
|
-
value={urlValue}
|
|
103
|
-
placeholder={urlPlaceholder}
|
|
111
|
+
<Form {...form}>
|
|
112
|
+
<form
|
|
104
113
|
autoComplete="off"
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
114
|
+
noValidate
|
|
115
|
+
onSubmit={form.handleSubmit(handleImport, handleInvalid)}
|
|
116
|
+
className="w-full"
|
|
117
|
+
>
|
|
118
|
+
<InputGroup className="mx-auto max-w-2xl bg-background">
|
|
119
|
+
<InputGroupInput
|
|
120
|
+
type="url"
|
|
121
|
+
aria-label="Media URL"
|
|
122
|
+
placeholder={urlPlaceholder}
|
|
123
|
+
autoComplete="off"
|
|
124
|
+
spellCheck={false}
|
|
125
|
+
{...urlField}
|
|
126
|
+
disabled={isImporting}
|
|
127
|
+
/>
|
|
128
|
+
<InputGroupAddon align="inline-end">
|
|
129
|
+
<InputGroupButton
|
|
130
|
+
type="submit"
|
|
131
|
+
variant="outline"
|
|
132
|
+
disabled={isImporting}
|
|
133
|
+
aria-busy={isImporting}
|
|
134
|
+
className="h-full px-5"
|
|
135
|
+
>
|
|
136
|
+
{isImporting ? <Spinner /> : <Upload />}
|
|
137
|
+
{isImporting ? 'Importing…' : 'Import'}
|
|
138
|
+
</InputGroupButton>
|
|
139
|
+
</InputGroupAddon>
|
|
140
|
+
</InputGroup>
|
|
141
|
+
</form>
|
|
142
|
+
</Form>
|
|
132
143
|
|
|
133
144
|
<div className="flex w-full flex-col items-center text-center">
|
|
134
145
|
<p className="text-sm text-muted-foreground">{urlHelpText}</p>
|
|
@@ -40,7 +40,11 @@ export function SearchInput({
|
|
|
40
40
|
|
|
41
41
|
return (
|
|
42
42
|
<Form {...form}>
|
|
43
|
-
<form
|
|
43
|
+
<form
|
|
44
|
+
autoComplete="off"
|
|
45
|
+
onSubmit={form.handleSubmit(onSubmit)}
|
|
46
|
+
className="flex w-full items-center"
|
|
47
|
+
>
|
|
44
48
|
<FormField
|
|
45
49
|
control={form.control}
|
|
46
50
|
name="search"
|
|
@@ -1,49 +1,68 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
+
import type { FieldErrors } from 'react-hook-form'
|
|
4
5
|
import { isEmailConfigured } from '@admin/actions/profile'
|
|
5
6
|
import { authClient } from '@admin/auth/client'
|
|
6
7
|
import { Button } from '@admin/components/ui/button'
|
|
7
8
|
import { Card, CardContent } from '@admin/components/ui/card'
|
|
8
9
|
import { Field, FieldGroup, FieldLabel } from '@admin/components/ui/field'
|
|
10
|
+
import { Form } from '@admin/components/ui/form'
|
|
9
11
|
import { Input } from '@admin/components/ui/input'
|
|
10
12
|
import { Spinner } from '@admin/components/ui/spinner'
|
|
11
13
|
import { cn } from '@admin/utils/shared/cn'
|
|
14
|
+
import { standardSchemaResolver } from '@hookform/resolvers/standard-schema'
|
|
12
15
|
import Image from 'next/image'
|
|
13
16
|
import Link from 'next/link'
|
|
17
|
+
import { useForm } from 'react-hook-form'
|
|
18
|
+
import { toast } from 'sonner'
|
|
19
|
+
import { z } from 'zod/v3'
|
|
20
|
+
|
|
21
|
+
const forgotPasswordSchema = z.object({
|
|
22
|
+
email: z.string().trim().min(1, 'Email is required').email('Enter a valid email address')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
type ForgotPasswordFormValues = z.infer<typeof forgotPasswordSchema>
|
|
14
26
|
|
|
15
27
|
export function ForgotPasswordForm({ className, ...props }: React.ComponentProps<'div'>) {
|
|
16
|
-
const [email, setEmail] = React.useState('')
|
|
17
|
-
const [error, setError] = React.useState<string | null>(null)
|
|
18
|
-
const [success, setSuccess] = React.useState(false)
|
|
19
28
|
const [isPending, startTransition] = React.useTransition()
|
|
29
|
+
const form = useForm<ForgotPasswordFormValues>({
|
|
30
|
+
resolver: standardSchemaResolver(forgotPasswordSchema),
|
|
31
|
+
defaultValues: { email: '' },
|
|
32
|
+
mode: 'onChange',
|
|
33
|
+
reValidateMode: 'onChange'
|
|
34
|
+
})
|
|
20
35
|
|
|
21
|
-
const handleSubmit = (
|
|
22
|
-
e.preventDefault()
|
|
23
|
-
setError(null)
|
|
24
|
-
|
|
36
|
+
const handleSubmit = (values: ForgotPasswordFormValues) => {
|
|
25
37
|
startTransition(async () => {
|
|
26
38
|
try {
|
|
27
39
|
const emailReady = await isEmailConfigured()
|
|
28
40
|
if (!emailReady) {
|
|
29
|
-
|
|
41
|
+
toast.error('Email delivery is not configured for this project.')
|
|
30
42
|
return
|
|
31
43
|
}
|
|
32
44
|
const { error } = await authClient.requestPasswordReset({
|
|
33
|
-
email,
|
|
45
|
+
email: values.email,
|
|
34
46
|
redirectTo: '/admin/reset-password'
|
|
35
47
|
})
|
|
36
48
|
if (error) {
|
|
37
|
-
|
|
49
|
+
toast.error(error.message || 'Failed to send reset link')
|
|
38
50
|
return
|
|
39
51
|
}
|
|
40
|
-
|
|
52
|
+
toast.success(
|
|
53
|
+
'If an account exists, we sent a password reset link. Check your inbox and spam folder.'
|
|
54
|
+
)
|
|
41
55
|
} catch {
|
|
42
|
-
|
|
56
|
+
toast.error('Failed to send the reset link. Try again.')
|
|
43
57
|
}
|
|
44
58
|
})
|
|
45
59
|
}
|
|
46
60
|
|
|
61
|
+
const handleInvalid = (errors: FieldErrors<ForgotPasswordFormValues>) => {
|
|
62
|
+
const message = errors.email?.message
|
|
63
|
+
toast.error(typeof message === 'string' ? message : 'Enter a valid email address.')
|
|
64
|
+
}
|
|
65
|
+
|
|
47
66
|
return (
|
|
48
67
|
<div className={cn('flex flex-col gap-6', className)} {...props}>
|
|
49
68
|
<Card className="overflow-hidden p-0">
|
|
@@ -58,21 +77,12 @@ export function ForgotPasswordForm({ className, ...props }: React.ComponentProps
|
|
|
58
77
|
/>
|
|
59
78
|
</div>
|
|
60
79
|
<div className="p-6 md:p-20">
|
|
61
|
-
{
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
inbox and spam folder.
|
|
68
|
-
</p>
|
|
69
|
-
</div>
|
|
70
|
-
<Link href="/admin/login" className="text-sm underline-offset-4 hover:underline">
|
|
71
|
-
Back to sign in
|
|
72
|
-
</Link>
|
|
73
|
-
</FieldGroup>
|
|
74
|
-
) : (
|
|
75
|
-
<form onSubmit={handleSubmit}>
|
|
80
|
+
<Form {...form}>
|
|
81
|
+
<form
|
|
82
|
+
autoComplete="off"
|
|
83
|
+
noValidate
|
|
84
|
+
onSubmit={form.handleSubmit(handleSubmit, handleInvalid)}
|
|
85
|
+
>
|
|
76
86
|
<FieldGroup className="gap-10 pb-12">
|
|
77
87
|
<div className="flex flex-col items-start">
|
|
78
88
|
<h1 className="text-xl font-medium">Forgot password</h1>
|
|
@@ -81,21 +91,16 @@ export function ForgotPasswordForm({ className, ...props }: React.ComponentProps
|
|
|
81
91
|
enabled
|
|
82
92
|
</p>
|
|
83
93
|
</div>
|
|
84
|
-
{error ? (
|
|
85
|
-
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
|
|
86
|
-
{error}
|
|
87
|
-
</div>
|
|
88
|
-
) : null}
|
|
89
94
|
<div className="flex flex-col gap-4">
|
|
90
95
|
<Field>
|
|
91
96
|
<FieldLabel htmlFor="email">Email</FieldLabel>
|
|
92
97
|
<Input
|
|
93
98
|
id="email"
|
|
94
99
|
type="email"
|
|
95
|
-
autoComplete="
|
|
100
|
+
autoComplete="off"
|
|
96
101
|
placeholder="m@example.com"
|
|
97
|
-
|
|
98
|
-
|
|
102
|
+
aria-invalid={Boolean(form.formState.errors.email)}
|
|
103
|
+
{...form.register('email')}
|
|
99
104
|
required
|
|
100
105
|
disabled={isPending}
|
|
101
106
|
/>
|
|
@@ -115,7 +120,7 @@ export function ForgotPasswordForm({ className, ...props }: React.ComponentProps
|
|
|
115
120
|
</div>
|
|
116
121
|
</FieldGroup>
|
|
117
122
|
</form>
|
|
118
|
-
|
|
123
|
+
</Form>
|
|
119
124
|
</div>
|
|
120
125
|
</CardContent>
|
|
121
126
|
</Card>
|
|
@@ -1,41 +1,64 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
+
import type { FieldErrors } from 'react-hook-form'
|
|
4
5
|
import { authClient } from '@admin/auth/client'
|
|
5
6
|
import { Button } from '@admin/components/ui/button'
|
|
6
7
|
import { Card, CardContent } from '@admin/components/ui/card'
|
|
7
8
|
import { Field, FieldGroup, FieldLabel } from '@admin/components/ui/field'
|
|
9
|
+
import { Form } from '@admin/components/ui/form'
|
|
8
10
|
import { Input } from '@admin/components/ui/input'
|
|
9
11
|
import { Spinner } from '@admin/components/ui/spinner'
|
|
10
12
|
import { getAdminPostLoginPath } from '@admin/utils/auth/get-admin-post-login-path'
|
|
11
13
|
import { cn } from '@admin/utils/shared/cn'
|
|
14
|
+
import { standardSchemaResolver } from '@hookform/resolvers/standard-schema'
|
|
12
15
|
import Image from 'next/image'
|
|
13
16
|
import Link from 'next/link'
|
|
14
17
|
import { useRouter } from 'next/navigation'
|
|
15
18
|
import { parseAsStringLiteral, useQueryState } from 'nuqs'
|
|
19
|
+
import { useForm } from 'react-hook-form'
|
|
20
|
+
import { toast } from 'sonner'
|
|
21
|
+
import { z } from 'zod/v3'
|
|
22
|
+
|
|
23
|
+
const loginSchema = z.object({
|
|
24
|
+
email: z.string().trim().min(1, 'Email is required').email('Enter a valid email address'),
|
|
25
|
+
password: z.string().min(1, 'Password is required')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
type LoginFormValues = z.infer<typeof loginSchema>
|
|
16
29
|
|
|
17
30
|
export function LoginForm({ className, ...props }: React.ComponentProps<'div'>) {
|
|
18
31
|
const router = useRouter()
|
|
19
32
|
const [reset] = useQueryState('reset', parseAsStringLiteral(['success']))
|
|
20
|
-
const resetSuccess = reset === 'success'
|
|
21
|
-
const [email, setEmail] = React.useState('')
|
|
22
|
-
const [password, setPassword] = React.useState('')
|
|
23
|
-
const [error, setError] = React.useState<string | null>(null)
|
|
24
33
|
const [isPending, startTransition] = React.useTransition()
|
|
34
|
+
const form = useForm<LoginFormValues>({
|
|
35
|
+
resolver: standardSchemaResolver(loginSchema),
|
|
36
|
+
defaultValues: {
|
|
37
|
+
email: '',
|
|
38
|
+
password: ''
|
|
39
|
+
},
|
|
40
|
+
mode: 'onChange',
|
|
41
|
+
reValidateMode: 'onChange'
|
|
42
|
+
})
|
|
25
43
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
44
|
+
React.useEffect(() => {
|
|
45
|
+
if (reset === 'success') {
|
|
46
|
+
toast.success('Password reset. Sign in with your new password.', {
|
|
47
|
+
id: 'admin-login-password-reset-success'
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
}, [reset])
|
|
29
51
|
|
|
52
|
+
const handleSubmit = (values: LoginFormValues) => {
|
|
30
53
|
startTransition(async () => {
|
|
31
54
|
try {
|
|
32
55
|
const result = await authClient.signIn.email({
|
|
33
|
-
email,
|
|
34
|
-
password
|
|
56
|
+
email: values.email,
|
|
57
|
+
password: values.password
|
|
35
58
|
})
|
|
36
59
|
|
|
37
60
|
if (result.error) {
|
|
38
|
-
|
|
61
|
+
toast.error(result.error.message || 'Invalid email or password')
|
|
39
62
|
return
|
|
40
63
|
}
|
|
41
64
|
|
|
@@ -45,11 +68,16 @@ export function LoginForm({ className, ...props }: React.ComponentProps<'div'>)
|
|
|
45
68
|
router.replace(getAdminPostLoginPath(role))
|
|
46
69
|
router.refresh()
|
|
47
70
|
} catch {
|
|
48
|
-
|
|
71
|
+
toast.error('Failed to sign you in. Try again.')
|
|
49
72
|
}
|
|
50
73
|
})
|
|
51
74
|
}
|
|
52
75
|
|
|
76
|
+
const handleInvalid = (errors: FieldErrors<LoginFormValues>) => {
|
|
77
|
+
const message = errors.email?.message ?? errors.password?.message
|
|
78
|
+
toast.error(typeof message === 'string' ? message : 'Please check your sign-in details.')
|
|
79
|
+
}
|
|
80
|
+
|
|
53
81
|
return (
|
|
54
82
|
<div className={cn('flex flex-col gap-6', className)} {...props}>
|
|
55
83
|
<Card className="overflow-hidden p-0">
|
|
@@ -63,67 +91,63 @@ export function LoginForm({ className, ...props }: React.ComponentProps<'div'>)
|
|
|
63
91
|
className="object-cover"
|
|
64
92
|
/>
|
|
65
93
|
</div>
|
|
66
|
-
<
|
|
67
|
-
<
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
94
|
+
<Form {...form}>
|
|
95
|
+
<form
|
|
96
|
+
autoComplete="off"
|
|
97
|
+
noValidate
|
|
98
|
+
onSubmit={form.handleSubmit(handleSubmit, handleInvalid)}
|
|
99
|
+
className="p-6 md:p-20"
|
|
100
|
+
>
|
|
101
|
+
<FieldGroup className="gap-10 pb-12">
|
|
102
|
+
<div className="flex flex-col items-start">
|
|
103
|
+
<h1 className="text-xl font-medium">Sign In</h1>
|
|
75
104
|
</div>
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
105
|
+
<div className="flex flex-col gap-4">
|
|
106
|
+
<Field>
|
|
107
|
+
<FieldLabel htmlFor="email">Email</FieldLabel>
|
|
108
|
+
<Input
|
|
109
|
+
id="email"
|
|
110
|
+
type="email"
|
|
111
|
+
autoComplete="off"
|
|
112
|
+
placeholder="m@example.com"
|
|
113
|
+
aria-invalid={Boolean(form.formState.errors.email)}
|
|
114
|
+
{...form.register('email')}
|
|
115
|
+
required
|
|
116
|
+
disabled={isPending}
|
|
117
|
+
/>
|
|
118
|
+
</Field>
|
|
119
|
+
<Field>
|
|
120
|
+
<div className="flex items-center">
|
|
121
|
+
<FieldLabel htmlFor="password">Password</FieldLabel>
|
|
122
|
+
<Link
|
|
123
|
+
href="/admin/forgot-password"
|
|
124
|
+
className="ml-auto text-sm underline-offset-4 hover:underline"
|
|
125
|
+
tabIndex={-1}
|
|
126
|
+
>
|
|
127
|
+
Forgot password?
|
|
128
|
+
</Link>
|
|
129
|
+
</div>
|
|
130
|
+
<Input
|
|
131
|
+
id="password"
|
|
132
|
+
type="password"
|
|
133
|
+
autoComplete="off"
|
|
134
|
+
placeholder="Enter your password"
|
|
135
|
+
aria-invalid={Boolean(form.formState.errors.password)}
|
|
136
|
+
{...form.register('password')}
|
|
137
|
+
required
|
|
138
|
+
disabled={isPending}
|
|
139
|
+
/>
|
|
140
|
+
</Field>
|
|
141
|
+
<Field className="pt-4">
|
|
142
|
+
<Button type="submit" disabled={isPending} aria-busy={isPending}>
|
|
143
|
+
{isPending && <Spinner />}
|
|
144
|
+
{isPending ? 'Signing in…' : 'Sign in'}
|
|
145
|
+
</Button>
|
|
146
|
+
</Field>
|
|
80
147
|
</div>
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
<FieldLabel htmlFor="email">Email</FieldLabel>
|
|
85
|
-
<Input
|
|
86
|
-
id="email"
|
|
87
|
-
type="email"
|
|
88
|
-
autoComplete="email"
|
|
89
|
-
placeholder="m@example.com"
|
|
90
|
-
value={email}
|
|
91
|
-
onChange={(e) => setEmail(e.target.value)}
|
|
92
|
-
required
|
|
93
|
-
disabled={isPending}
|
|
94
|
-
/>
|
|
95
|
-
</Field>
|
|
96
|
-
<Field>
|
|
97
|
-
<div className="flex items-center">
|
|
98
|
-
<FieldLabel htmlFor="password">Password</FieldLabel>
|
|
99
|
-
<Link
|
|
100
|
-
href="/admin/forgot-password"
|
|
101
|
-
className="ml-auto text-sm underline-offset-4 hover:underline"
|
|
102
|
-
tabIndex={-1}
|
|
103
|
-
>
|
|
104
|
-
Forgot password?
|
|
105
|
-
</Link>
|
|
106
|
-
</div>
|
|
107
|
-
<Input
|
|
108
|
-
id="password"
|
|
109
|
-
type="password"
|
|
110
|
-
autoComplete="current-password"
|
|
111
|
-
placeholder="Enter your password"
|
|
112
|
-
value={password}
|
|
113
|
-
onChange={(e) => setPassword(e.target.value)}
|
|
114
|
-
required
|
|
115
|
-
disabled={isPending}
|
|
116
|
-
/>
|
|
117
|
-
</Field>
|
|
118
|
-
<Field className="pt-4">
|
|
119
|
-
<Button type="submit" disabled={isPending} aria-busy={isPending}>
|
|
120
|
-
{isPending && <Spinner />}
|
|
121
|
-
{isPending ? 'Signing in…' : 'Sign in'}
|
|
122
|
-
</Button>
|
|
123
|
-
</Field>
|
|
124
|
-
</div>
|
|
125
|
-
</FieldGroup>
|
|
126
|
-
</form>
|
|
148
|
+
</FieldGroup>
|
|
149
|
+
</form>
|
|
150
|
+
</Form>
|
|
127
151
|
</CardContent>
|
|
128
152
|
</Card>
|
|
129
153
|
</div>
|
|
@@ -90,7 +90,7 @@ export function ProfileForm({ user }: ProfileFormProps) {
|
|
|
90
90
|
newPassword: '',
|
|
91
91
|
confirmPassword: ''
|
|
92
92
|
},
|
|
93
|
-
mode: '
|
|
93
|
+
mode: 'onChange',
|
|
94
94
|
reValidateMode: 'onChange'
|
|
95
95
|
})
|
|
96
96
|
|
|
@@ -195,7 +195,7 @@ export function ProfileForm({ user }: ProfileFormProps) {
|
|
|
195
195
|
return (
|
|
196
196
|
<div className="space-y-6">
|
|
197
197
|
<Form {...profileForm}>
|
|
198
|
-
<form onSubmit={profileForm.handleSubmit(onProfileSubmit)}>
|
|
198
|
+
<form autoComplete="off" onSubmit={profileForm.handleSubmit(onProfileSubmit)}>
|
|
199
199
|
<Card>
|
|
200
200
|
<CardHeader>
|
|
201
201
|
<div className="flex items-center gap-4 pl-1">
|
|
@@ -282,7 +282,7 @@ export function ProfileForm({ user }: ProfileFormProps) {
|
|
|
282
282
|
<Input
|
|
283
283
|
type="password"
|
|
284
284
|
placeholder="Enter current password to change email"
|
|
285
|
-
autoComplete="
|
|
285
|
+
autoComplete="off"
|
|
286
286
|
disabled={profilePending}
|
|
287
287
|
{...formField}
|
|
288
288
|
/>
|
|
@@ -309,7 +309,7 @@ export function ProfileForm({ user }: ProfileFormProps) {
|
|
|
309
309
|
</Form>
|
|
310
310
|
|
|
311
311
|
<Form {...passwordForm}>
|
|
312
|
-
<form onSubmit={passwordForm.handleSubmit(onPasswordSubmit)}>
|
|
312
|
+
<form autoComplete="off" onSubmit={passwordForm.handleSubmit(onPasswordSubmit)}>
|
|
313
313
|
<Card>
|
|
314
314
|
<CardHeader>
|
|
315
315
|
<CardTitle>Change Password</CardTitle>
|
|
@@ -325,7 +325,7 @@ export function ProfileForm({ user }: ProfileFormProps) {
|
|
|
325
325
|
<FormControl>
|
|
326
326
|
<Input
|
|
327
327
|
type="password"
|
|
328
|
-
autoComplete="
|
|
328
|
+
autoComplete="off"
|
|
329
329
|
disabled={passwordPending}
|
|
330
330
|
{...field}
|
|
331
331
|
/>
|
|
@@ -343,7 +343,7 @@ export function ProfileForm({ user }: ProfileFormProps) {
|
|
|
343
343
|
<FormControl>
|
|
344
344
|
<Input
|
|
345
345
|
type="password"
|
|
346
|
-
autoComplete="
|
|
346
|
+
autoComplete="off"
|
|
347
347
|
disabled={passwordPending}
|
|
348
348
|
{...field}
|
|
349
349
|
/>
|
|
@@ -362,7 +362,7 @@ export function ProfileForm({ user }: ProfileFormProps) {
|
|
|
362
362
|
<FormControl>
|
|
363
363
|
<Input
|
|
364
364
|
type="password"
|
|
365
|
-
autoComplete="
|
|
365
|
+
autoComplete="off"
|
|
366
366
|
disabled={passwordPending}
|
|
367
367
|
{...field}
|
|
368
368
|
/>
|