@betterstart/cli 0.1.81 → 0.1.82
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/cli.js +15 -2
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/templates/init/components/layout/cms-search.tsx +6 -5
- package/templates/init/components/layout/cms-sidebar.tsx +3 -7
- package/templates/init/data/navigation.ts +6 -6
- package/templates/init/lib/actions/profile.ts +4 -0
- package/templates/init/lib/auth/auth.ts +17 -0
- package/templates/init/pages/forgot-password-form.tsx +130 -0
- package/templates/init/pages/forgot-password-page.tsx +20 -0
- package/templates/init/pages/login-form.tsx +17 -1
- package/templates/init/pages/login-page.tsx +12 -2
- package/templates/init/pages/profile/profile-form.tsx +65 -137
- package/templates/init/pages/reset-password-form.tsx +159 -0
- package/templates/init/pages/reset-password-page.tsx +20 -0
- package/templates/ui/button.tsx +1 -1
- package/templates/ui/card.tsx +2 -2
- package/templates/ui/curriculum-editor.tsx +2 -2
- package/templates/ui/image-upload-field.tsx +2 -2
- package/templates/ui/input.tsx +5 -4
- package/templates/ui/media-upload-field.tsx +2 -2
- package/templates/ui/placeholder.tsx +1 -1
- package/templates/ui/sidebar.tsx +1 -1
- package/templates/ui/video-upload-field.tsx +2 -2
package/package.json
CHANGED
|
@@ -3,17 +3,18 @@ import { Command, Search } from 'lucide-react'
|
|
|
3
3
|
|
|
4
4
|
export const CmsSearch = () => {
|
|
5
5
|
return (
|
|
6
|
-
<div className="flex items-center gap-2 relative w-full max-w-
|
|
6
|
+
<div className="flex items-center gap-2 relative w-full max-w-60">
|
|
7
7
|
<Button
|
|
8
8
|
variant="outline"
|
|
9
|
-
|
|
9
|
+
size="sm"
|
|
10
|
+
className="w-full text-left items-center pr-2! pl-3.5 py-0 rounded-full bg-white"
|
|
10
11
|
>
|
|
11
|
-
<Search className="shrink-0 size-3.5 -ml-0.5 text-muted-foreground
|
|
12
|
-
<span className="w-full font-normal text-sm text-muted-foreground
|
|
12
|
+
<Search className="shrink-0 size-3.5 -ml-0.5 text-muted-foreground" />
|
|
13
|
+
<span className="w-full font-normal text-sm text-muted-foreground [text-box-trim:trim-both]">
|
|
13
14
|
Search
|
|
14
15
|
</span>
|
|
15
16
|
<div className="flex items-center gap-1 py-0.5 border rounded-full corner-squircle px-2 border-border bg-background">
|
|
16
|
-
<Command className="size-3
|
|
17
|
+
<Command className="size-3 text-muted-foreground" />
|
|
17
18
|
<span className="font-mono text-xs font-medium">K</span>
|
|
18
19
|
</div>
|
|
19
20
|
</Button>
|
|
@@ -56,9 +56,6 @@ export async function CmsSidebar(props: React.ComponentProps<typeof Sidebar>) {
|
|
|
56
56
|
const user = session?.user ?? null
|
|
57
57
|
const groups = groupNavItems(cmsNavigation)
|
|
58
58
|
|
|
59
|
-
console.log({ settings });
|
|
60
|
-
|
|
61
|
-
|
|
62
59
|
return (
|
|
63
60
|
<Sidebar collapsible="icon" {...props}>
|
|
64
61
|
<SidebarHeader className="border-b border-border h-14 items-center flex w-full">
|
|
@@ -79,7 +76,7 @@ export async function CmsSidebar(props: React.ComponentProps<typeof Sidebar>) {
|
|
|
79
76
|
</div>
|
|
80
77
|
</SidebarHeader>
|
|
81
78
|
<SidebarContent className='pb-2'>
|
|
82
|
-
<SidebarGroup className="pb-
|
|
79
|
+
<SidebarGroup className="pb-0">
|
|
83
80
|
<CmsSearch />
|
|
84
81
|
</SidebarGroup>
|
|
85
82
|
|
|
@@ -141,12 +138,11 @@ export async function CmsSidebar(props: React.ComponentProps<typeof Sidebar>) {
|
|
|
141
138
|
</div>
|
|
142
139
|
|
|
143
140
|
<div className="group-data-[collapsible=icon]:hidden">
|
|
144
|
-
<span className={cn(buttonVariants({ variant: 'outline', size: 'icon' }), 'rounded-full group-hover:bg-white size-
|
|
145
|
-
<Ellipsis strokeWidth={2} />
|
|
141
|
+
<span className={cn(buttonVariants({ variant: 'outline', size: 'icon' }), 'rounded-full group-hover:bg-white size-7')}>
|
|
142
|
+
<Ellipsis strokeWidth={2} className='size-4' />
|
|
146
143
|
</span>
|
|
147
144
|
</div>
|
|
148
145
|
</Button>
|
|
149
|
-
|
|
150
146
|
</DropdownMenuTrigger>
|
|
151
147
|
<DropdownMenuContent align="start" className="w-68 p-0">
|
|
152
148
|
<DropdownMenuGroup className='py-2 border-b border-border px-2'>
|
|
@@ -24,16 +24,16 @@ export const cmsNavigation: CmsNavigationItem[] = [
|
|
|
24
24
|
href: '/cms/media',
|
|
25
25
|
icon: ImagePlay,
|
|
26
26
|
},
|
|
27
|
-
{
|
|
28
|
-
label: 'Categories',
|
|
29
|
-
href: '/cms/categories',
|
|
30
|
-
icon: Tag,
|
|
31
|
-
group: 'Blog',
|
|
32
|
-
},
|
|
33
27
|
{
|
|
34
28
|
label: 'Posts',
|
|
35
29
|
href: '/cms/posts',
|
|
36
30
|
icon: FileText,
|
|
37
31
|
group: 'Blog',
|
|
38
32
|
},
|
|
33
|
+
{
|
|
34
|
+
label: 'Categories',
|
|
35
|
+
href: '/cms/categories',
|
|
36
|
+
icon: Tag,
|
|
37
|
+
group: 'Blog',
|
|
38
|
+
},
|
|
39
39
|
]
|
|
@@ -6,6 +6,10 @@ import db from '@cms/db'
|
|
|
6
6
|
import { user } from '@cms/db/schema'
|
|
7
7
|
import { eq } from 'drizzle-orm'
|
|
8
8
|
|
|
9
|
+
export async function isEmailConfigured(): Promise<boolean> {
|
|
10
|
+
return !!process.env.BETTERSTART_RESEND_API_KEY
|
|
11
|
+
}
|
|
12
|
+
|
|
9
13
|
export interface UpdateEmailResult {
|
|
10
14
|
success: boolean
|
|
11
15
|
error?: string
|
|
@@ -3,6 +3,7 @@ import * as schema from '@cms/db/schema'
|
|
|
3
3
|
import { betterAuth } from 'better-auth'
|
|
4
4
|
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
|
|
5
5
|
import { toNextJsHandler } from 'better-auth/next-js'
|
|
6
|
+
import { Resend } from 'resend'
|
|
6
7
|
|
|
7
8
|
export { toNextJsHandler }
|
|
8
9
|
|
|
@@ -22,6 +23,22 @@ export const auth = betterAuth({
|
|
|
22
23
|
emailAndPassword: {
|
|
23
24
|
enabled: true,
|
|
24
25
|
minPasswordLength: 8,
|
|
26
|
+
revokeSessionsOnPasswordReset: true,
|
|
27
|
+
sendResetPassword: async ({ user, url }: { user: { email: string; name: string }; url: string }) => {
|
|
28
|
+
const apiKey = process.env.BETTERSTART_RESEND_API_KEY
|
|
29
|
+
if (!apiKey) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
'Email is not configured. Set BETTERSTART_RESEND_API_KEY to enable password reset emails.'
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
const resend = new Resend(apiKey)
|
|
35
|
+
await resend.emails.send({
|
|
36
|
+
from: process.env.BETTERSTART_EMAIL_FROM || 'noreply@yourdomain.com',
|
|
37
|
+
to: user.email,
|
|
38
|
+
subject: 'Reset your password',
|
|
39
|
+
html: `<p>Hi ${user.name},</p><p>Click the link below to reset your password:</p><p><a href="${url}">Reset Password</a></p><p>This link will expire in 1 hour. If you didn't request this, you can safely ignore this email.</p>`,
|
|
40
|
+
})
|
|
41
|
+
},
|
|
25
42
|
},
|
|
26
43
|
session: {
|
|
27
44
|
expiresIn: 60 * 60 * 24 * 7, // 7 days
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { isEmailConfigured } from '@cms/actions/profile'
|
|
4
|
+
import { authClient } from '@cms/auth/client'
|
|
5
|
+
import { Button } from '@cms/components/ui/button'
|
|
6
|
+
import { Card, CardContent } from '@cms/components/ui/card'
|
|
7
|
+
import { Field, FieldGroup, FieldLabel } from '@cms/components/ui/field'
|
|
8
|
+
import { Input } from '@cms/components/ui/input'
|
|
9
|
+
import { cn } from '@cms/utils/cn'
|
|
10
|
+
import { LoaderCircle } from 'lucide-react'
|
|
11
|
+
import Link from 'next/link'
|
|
12
|
+
import * as React from 'react'
|
|
13
|
+
|
|
14
|
+
export function ForgotPasswordForm({
|
|
15
|
+
className,
|
|
16
|
+
...props
|
|
17
|
+
}: React.ComponentProps<'div'>) {
|
|
18
|
+
const [email, setEmail] = React.useState('')
|
|
19
|
+
const [error, setError] = React.useState<string | null>(null)
|
|
20
|
+
const [success, setSuccess] = React.useState(false)
|
|
21
|
+
const [isPending, startTransition] = React.useTransition()
|
|
22
|
+
|
|
23
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
24
|
+
e.preventDefault()
|
|
25
|
+
setError(null)
|
|
26
|
+
|
|
27
|
+
startTransition(async () => {
|
|
28
|
+
try {
|
|
29
|
+
const emailReady = await isEmailConfigured()
|
|
30
|
+
if (!emailReady) {
|
|
31
|
+
setError(
|
|
32
|
+
'Email is not configured. Set BETTERSTART_RESEND_API_KEY to enable password reset emails.'
|
|
33
|
+
)
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
const { error } = await authClient.requestPasswordReset({
|
|
37
|
+
email,
|
|
38
|
+
redirectTo: '/cms/reset-password',
|
|
39
|
+
})
|
|
40
|
+
if (error) {
|
|
41
|
+
setError(error.message || 'Failed to send reset link')
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
setSuccess(true)
|
|
45
|
+
} catch {
|
|
46
|
+
setError('An error occurred. Please try again.')
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div className={cn('flex flex-col gap-6', className)} {...props}>
|
|
53
|
+
<Card className="overflow-hidden p-0">
|
|
54
|
+
<CardContent className="grid p-0 md:grid-cols-[5fr_7fr]">
|
|
55
|
+
<div className="relative hidden bg-muted md:block">
|
|
56
|
+
<img
|
|
57
|
+
src="https://assets.betterstart.dev/assets/placeholder.png"
|
|
58
|
+
alt="Image"
|
|
59
|
+
className="absolute inset-0 h-full w-full object-cover dark:brightness-[0.2] dark:grayscale"
|
|
60
|
+
/>
|
|
61
|
+
</div>
|
|
62
|
+
<div className="p-6 md:p-20">
|
|
63
|
+
{success ? (
|
|
64
|
+
<FieldGroup className="pb-12 gap-10">
|
|
65
|
+
<div className="flex flex-col items-start">
|
|
66
|
+
<h1 className="text-xl font-medium">Check your email</h1>
|
|
67
|
+
<p className="text-balance text-muted-foreground">
|
|
68
|
+
If an account exists for {email}, we sent a password reset
|
|
69
|
+
link. Check your inbox and spam folder.
|
|
70
|
+
</p>
|
|
71
|
+
</div>
|
|
72
|
+
<Link
|
|
73
|
+
href="/cms/login"
|
|
74
|
+
className="text-sm underline-offset-4 hover:underline"
|
|
75
|
+
>
|
|
76
|
+
Back to login
|
|
77
|
+
</Link>
|
|
78
|
+
</FieldGroup>
|
|
79
|
+
) : (
|
|
80
|
+
<form onSubmit={handleSubmit}>
|
|
81
|
+
<FieldGroup className="pb-12 gap-10">
|
|
82
|
+
<div className="flex flex-col items-start">
|
|
83
|
+
<h1 className="text-xl font-medium">Forgot password</h1>
|
|
84
|
+
<p className="text-balance text-muted-foreground">
|
|
85
|
+
Enter your email and we'll send you a reset link
|
|
86
|
+
</p>
|
|
87
|
+
</div>
|
|
88
|
+
{error ? (
|
|
89
|
+
<div className="bg-destructive/10 text-destructive text-sm p-3 rounded-md">
|
|
90
|
+
{error}
|
|
91
|
+
</div>
|
|
92
|
+
) : null}
|
|
93
|
+
<div className="flex flex-col gap-4">
|
|
94
|
+
<Field>
|
|
95
|
+
<FieldLabel htmlFor="email">Email</FieldLabel>
|
|
96
|
+
<Input
|
|
97
|
+
id="email"
|
|
98
|
+
type="email"
|
|
99
|
+
autoComplete="email"
|
|
100
|
+
placeholder="m@example.com"
|
|
101
|
+
value={email}
|
|
102
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
103
|
+
required
|
|
104
|
+
disabled={isPending}
|
|
105
|
+
/>
|
|
106
|
+
</Field>
|
|
107
|
+
<Field className="pt-4">
|
|
108
|
+
<Button type="submit" disabled={isPending}>
|
|
109
|
+
{isPending && (
|
|
110
|
+
<LoaderCircle className="animate-spin" />
|
|
111
|
+
)}
|
|
112
|
+
{isPending ? 'Sending...' : 'Send Reset Link'}
|
|
113
|
+
</Button>
|
|
114
|
+
</Field>
|
|
115
|
+
<Link
|
|
116
|
+
href="/cms/login"
|
|
117
|
+
className="text-sm text-center underline-offset-4 hover:underline"
|
|
118
|
+
>
|
|
119
|
+
Back to login
|
|
120
|
+
</Link>
|
|
121
|
+
</div>
|
|
122
|
+
</FieldGroup>
|
|
123
|
+
</form>
|
|
124
|
+
)}
|
|
125
|
+
</div>
|
|
126
|
+
</CardContent>
|
|
127
|
+
</Card>
|
|
128
|
+
</div>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Metadata } from 'next'
|
|
2
|
+
import { Suspense } from 'react'
|
|
3
|
+
import { ForgotPasswordForm } from './forgot-password-form'
|
|
4
|
+
|
|
5
|
+
export const metadata: Metadata = {
|
|
6
|
+
title: 'Forgot Password',
|
|
7
|
+
robots: { index: false, follow: false },
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default function ForgotPasswordPage() {
|
|
11
|
+
return (
|
|
12
|
+
<div className="flex min-h-svh flex-col items-center justify-center bg-background p-6 md:p-10">
|
|
13
|
+
<div className="w-full max-w-sm md:max-w-4xl">
|
|
14
|
+
<Suspense>
|
|
15
|
+
<ForgotPasswordForm />
|
|
16
|
+
</Suspense>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -11,7 +11,8 @@ import {
|
|
|
11
11
|
import { Input } from '@cms/components/ui/input'
|
|
12
12
|
import { cn } from '@cms/utils/cn'
|
|
13
13
|
import { LoaderCircle } from 'lucide-react'
|
|
14
|
-
import
|
|
14
|
+
import Link from 'next/link'
|
|
15
|
+
import { useRouter, useSearchParams } from 'next/navigation'
|
|
15
16
|
import * as React from 'react'
|
|
16
17
|
|
|
17
18
|
export function LoginForm({
|
|
@@ -19,6 +20,8 @@ export function LoginForm({
|
|
|
19
20
|
...props
|
|
20
21
|
}: React.ComponentProps<'div'>) {
|
|
21
22
|
const router = useRouter()
|
|
23
|
+
const searchParams = useSearchParams()
|
|
24
|
+
const resetSuccess = searchParams.get('reset') === 'success'
|
|
22
25
|
const [email, setEmail] = React.useState('')
|
|
23
26
|
const [password, setPassword] = React.useState('')
|
|
24
27
|
const [error, setError] = React.useState<string | null>(null)
|
|
@@ -67,6 +70,12 @@ export function LoginForm({
|
|
|
67
70
|
Login to your account
|
|
68
71
|
</p>
|
|
69
72
|
</div>
|
|
73
|
+
{resetSuccess ? (
|
|
74
|
+
<div className="bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 text-sm p-3 rounded-md">
|
|
75
|
+
Password reset successfully. Please log in with your new
|
|
76
|
+
password.
|
|
77
|
+
</div>
|
|
78
|
+
) : null}
|
|
70
79
|
{error ? (
|
|
71
80
|
<div className="bg-destructive/10 text-destructive text-sm p-3 rounded-md">
|
|
72
81
|
{error}
|
|
@@ -89,6 +98,13 @@ export function LoginForm({
|
|
|
89
98
|
<Field>
|
|
90
99
|
<div className="flex items-center">
|
|
91
100
|
<FieldLabel htmlFor="password">Password</FieldLabel>
|
|
101
|
+
<Link
|
|
102
|
+
href="/cms/forgot-password"
|
|
103
|
+
className="ml-auto text-sm underline-offset-4 hover:underline"
|
|
104
|
+
tabIndex={-1}
|
|
105
|
+
>
|
|
106
|
+
Forgot password?
|
|
107
|
+
</Link>
|
|
92
108
|
</div>
|
|
93
109
|
<Input
|
|
94
110
|
id="password"
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { Metadata } from 'next'
|
|
2
|
+
import { redirect } from 'next/navigation'
|
|
3
|
+
import { Suspense } from 'react'
|
|
4
|
+
import { getSession } from '@cms/auth/middleware'
|
|
2
5
|
import { LoginForm } from './login-form'
|
|
3
6
|
|
|
4
7
|
export const metadata: Metadata = {
|
|
@@ -6,11 +9,18 @@ export const metadata: Metadata = {
|
|
|
6
9
|
robots: { index: false, follow: false },
|
|
7
10
|
}
|
|
8
11
|
|
|
9
|
-
export default function LoginPage() {
|
|
12
|
+
export default async function LoginPage() {
|
|
13
|
+
const session = await getSession()
|
|
14
|
+
if (session?.user) {
|
|
15
|
+
redirect('/cms')
|
|
16
|
+
}
|
|
17
|
+
|
|
10
18
|
return (
|
|
11
19
|
<div className="flex min-h-svh flex-col items-center justify-center bg-background p-6 md:p-10">
|
|
12
20
|
<div className="w-full max-w-sm md:max-w-4xl">
|
|
13
|
-
<
|
|
21
|
+
<Suspense>
|
|
22
|
+
<LoginForm />
|
|
23
|
+
</Suspense>
|
|
14
24
|
</div>
|
|
15
25
|
</div>
|
|
16
26
|
)
|
|
@@ -5,7 +5,7 @@ import { useForm } from 'react-hook-form'
|
|
|
5
5
|
import { toast } from 'sonner'
|
|
6
6
|
import { z } from 'zod/v3'
|
|
7
7
|
import { authClient } from '@cms/auth/client'
|
|
8
|
-
import { updateEmail } from '@cms/actions/profile'
|
|
8
|
+
import { isEmailConfigured, updateEmail } from '@cms/actions/profile'
|
|
9
9
|
import { Avatar, AvatarFallback, AvatarImage } from '@cms/components/ui/avatar'
|
|
10
10
|
import { Button } from '@cms/components/ui/button'
|
|
11
11
|
import { Input } from '@cms/components/ui/input'
|
|
@@ -42,19 +42,6 @@ const profileSchema = z.object({
|
|
|
42
42
|
|
|
43
43
|
type ProfileValues = z.infer<typeof profileSchema>
|
|
44
44
|
|
|
45
|
-
const passwordSchema = z
|
|
46
|
-
.object({
|
|
47
|
-
currentPassword: z.string().min(1, 'Current password is required'),
|
|
48
|
-
newPassword: z.string().min(8, 'Password must be at least 8 characters'),
|
|
49
|
-
confirmPassword: z.string().min(1, 'Please confirm your new password'),
|
|
50
|
-
})
|
|
51
|
-
.refine((data) => data.newPassword === data.confirmPassword, {
|
|
52
|
-
message: 'Passwords do not match',
|
|
53
|
-
path: ['confirmPassword'],
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
type PasswordValues = z.infer<typeof passwordSchema>
|
|
57
|
-
|
|
58
45
|
interface ProfileFormProps {
|
|
59
46
|
user: {
|
|
60
47
|
name: string
|
|
@@ -66,7 +53,7 @@ interface ProfileFormProps {
|
|
|
66
53
|
export function ProfileForm({ user }: ProfileFormProps) {
|
|
67
54
|
const router = useRouter()
|
|
68
55
|
const [profilePending, setProfilePending] = useState(false)
|
|
69
|
-
const [
|
|
56
|
+
const [resetPending, setResetPending] = useState(false)
|
|
70
57
|
|
|
71
58
|
const profileForm = useForm<ProfileValues>({
|
|
72
59
|
resolver: zodResolver(profileSchema),
|
|
@@ -78,15 +65,6 @@ export function ProfileForm({ user }: ProfileFormProps) {
|
|
|
78
65
|
},
|
|
79
66
|
})
|
|
80
67
|
|
|
81
|
-
const passwordForm = useForm<PasswordValues>({
|
|
82
|
-
resolver: zodResolver(passwordSchema),
|
|
83
|
-
defaultValues: {
|
|
84
|
-
currentPassword: '',
|
|
85
|
-
newPassword: '',
|
|
86
|
-
confirmPassword: '',
|
|
87
|
-
},
|
|
88
|
-
})
|
|
89
|
-
|
|
90
68
|
const emailDirty = profileForm.watch('email') !== user.email
|
|
91
69
|
const imageValue = profileForm.watch('image')
|
|
92
70
|
|
|
@@ -141,27 +119,29 @@ export function ProfileForm({ user }: ProfileFormProps) {
|
|
|
141
119
|
}
|
|
142
120
|
}
|
|
143
121
|
|
|
144
|
-
async function
|
|
145
|
-
|
|
146
|
-
|
|
122
|
+
async function handleResetPassword() {
|
|
123
|
+
setResetPending(true)
|
|
147
124
|
try {
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
125
|
+
const emailReady = await isEmailConfigured()
|
|
126
|
+
if (!emailReady) {
|
|
127
|
+
toast.error(
|
|
128
|
+
'Email is not configured. Set BETTERSTART_RESEND_API_KEY to enable password reset emails.'
|
|
129
|
+
)
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
const { error } = await authClient.requestPasswordReset({
|
|
133
|
+
email: user.email,
|
|
134
|
+
redirectTo: '/cms/reset-password',
|
|
151
135
|
})
|
|
152
|
-
|
|
153
136
|
if (error) {
|
|
154
|
-
toast.error(error.message || 'Failed to
|
|
155
|
-
setPasswordPending(false)
|
|
137
|
+
toast.error(error.message || 'Failed to send reset link')
|
|
156
138
|
return
|
|
157
139
|
}
|
|
158
|
-
|
|
159
|
-
toast.success('Password changed successfully')
|
|
160
|
-
passwordForm.reset()
|
|
140
|
+
toast.success('Check your email for a password reset link')
|
|
161
141
|
} catch {
|
|
162
|
-
toast.error('An error occurred while
|
|
142
|
+
toast.error('An error occurred while sending the reset link')
|
|
163
143
|
} finally {
|
|
164
|
-
|
|
144
|
+
setResetPending(false)
|
|
165
145
|
}
|
|
166
146
|
}
|
|
167
147
|
|
|
@@ -187,6 +167,27 @@ export function ProfileForm({ user }: ProfileFormProps) {
|
|
|
187
167
|
</div>
|
|
188
168
|
</CardHeader>
|
|
189
169
|
<CardContent className="space-y-6">
|
|
170
|
+
<FormField
|
|
171
|
+
control={profileForm.control}
|
|
172
|
+
name="image"
|
|
173
|
+
render={({ field: formField }) => (
|
|
174
|
+
<FormItem>
|
|
175
|
+
<FormLabel>Profile Picture</FormLabel>
|
|
176
|
+
<FormControl>
|
|
177
|
+
<ImageUploadField
|
|
178
|
+
value={formField.value}
|
|
179
|
+
onChange={formField.onChange}
|
|
180
|
+
onBlur={formField.onBlur}
|
|
181
|
+
disabled={profilePending}
|
|
182
|
+
maxSizeInMB={5}
|
|
183
|
+
label=""
|
|
184
|
+
/>
|
|
185
|
+
</FormControl>
|
|
186
|
+
<FormMessage />
|
|
187
|
+
</FormItem>
|
|
188
|
+
)}
|
|
189
|
+
/>
|
|
190
|
+
|
|
190
191
|
<FormField
|
|
191
192
|
control={profileForm.control}
|
|
192
193
|
name="name"
|
|
@@ -246,27 +247,6 @@ export function ProfileForm({ user }: ProfileFormProps) {
|
|
|
246
247
|
)}
|
|
247
248
|
/>
|
|
248
249
|
)}
|
|
249
|
-
|
|
250
|
-
<FormField
|
|
251
|
-
control={profileForm.control}
|
|
252
|
-
name="image"
|
|
253
|
-
render={({ field: formField }) => (
|
|
254
|
-
<FormItem>
|
|
255
|
-
<FormLabel>Profile Picture</FormLabel>
|
|
256
|
-
<FormControl>
|
|
257
|
-
<ImageUploadField
|
|
258
|
-
value={formField.value}
|
|
259
|
-
onChange={formField.onChange}
|
|
260
|
-
onBlur={formField.onBlur}
|
|
261
|
-
disabled={profilePending}
|
|
262
|
-
maxSizeInMB={5}
|
|
263
|
-
label=""
|
|
264
|
-
/>
|
|
265
|
-
</FormControl>
|
|
266
|
-
<FormMessage />
|
|
267
|
-
</FormItem>
|
|
268
|
-
)}
|
|
269
|
-
/>
|
|
270
250
|
</CardContent>
|
|
271
251
|
<CardFooter>
|
|
272
252
|
<Button type="submit" disabled={profilePending || !profileForm.formState.isDirty} size="sm" className="ml-auto min-w-25">
|
|
@@ -278,84 +258,32 @@ export function ProfileForm({ user }: ProfileFormProps) {
|
|
|
278
258
|
</form>
|
|
279
259
|
</Form>
|
|
280
260
|
|
|
281
|
-
<
|
|
282
|
-
<
|
|
283
|
-
<
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
</FormItem>
|
|
308
|
-
)}
|
|
309
|
-
/>
|
|
310
|
-
|
|
311
|
-
<FormField
|
|
312
|
-
control={passwordForm.control}
|
|
313
|
-
name="newPassword"
|
|
314
|
-
render={({ field: formField }) => (
|
|
315
|
-
<FormItem>
|
|
316
|
-
<FormLabel>New Password</FormLabel>
|
|
317
|
-
<FormControl>
|
|
318
|
-
<Input
|
|
319
|
-
type="password"
|
|
320
|
-
placeholder="Enter new password"
|
|
321
|
-
autoComplete="new-password"
|
|
322
|
-
disabled={passwordPending}
|
|
323
|
-
{...formField}
|
|
324
|
-
/>
|
|
325
|
-
</FormControl>
|
|
326
|
-
<FormMessage />
|
|
327
|
-
</FormItem>
|
|
328
|
-
)}
|
|
329
|
-
/>
|
|
330
|
-
|
|
331
|
-
<FormField
|
|
332
|
-
control={passwordForm.control}
|
|
333
|
-
name="confirmPassword"
|
|
334
|
-
render={({ field: formField }) => (
|
|
335
|
-
<FormItem>
|
|
336
|
-
<FormLabel>Confirm New Password</FormLabel>
|
|
337
|
-
<FormControl>
|
|
338
|
-
<Input
|
|
339
|
-
type="password"
|
|
340
|
-
placeholder="Confirm new password"
|
|
341
|
-
autoComplete="new-password"
|
|
342
|
-
disabled={passwordPending}
|
|
343
|
-
{...formField}
|
|
344
|
-
/>
|
|
345
|
-
</FormControl>
|
|
346
|
-
<FormMessage />
|
|
347
|
-
</FormItem>
|
|
348
|
-
)}
|
|
349
|
-
/>
|
|
350
|
-
</CardContent>
|
|
351
|
-
<CardFooter>
|
|
352
|
-
<Button type="submit" disabled={passwordPending || !passwordForm.formState.isDirty} size="sm" className="ml-auto min-w-25">
|
|
353
|
-
{passwordPending ? 'Changing...' : 'Change Password'}
|
|
354
|
-
</Button>
|
|
355
|
-
</CardFooter>
|
|
356
|
-
</Card>
|
|
357
|
-
</form>
|
|
358
|
-
</Form>
|
|
261
|
+
<Card className="material-sm!">
|
|
262
|
+
<CardHeader>
|
|
263
|
+
<CardTitle>Reset Password</CardTitle>
|
|
264
|
+
<CardDescription>
|
|
265
|
+
Send a password reset link to your email address
|
|
266
|
+
</CardDescription>
|
|
267
|
+
</CardHeader>
|
|
268
|
+
<CardContent>
|
|
269
|
+
<p className="text-sm text-muted-foreground">
|
|
270
|
+
A reset link will be sent to{' '}
|
|
271
|
+
<span className="font-medium text-foreground">{user.email}</span>
|
|
272
|
+
</p>
|
|
273
|
+
</CardContent>
|
|
274
|
+
<CardFooter>
|
|
275
|
+
<Button
|
|
276
|
+
type="button"
|
|
277
|
+
size="sm"
|
|
278
|
+
className="ml-auto min-w-25"
|
|
279
|
+
disabled={resetPending}
|
|
280
|
+
onClick={handleResetPassword}
|
|
281
|
+
>
|
|
282
|
+
{resetPending && <LoaderCircle className="animate-spin" />}
|
|
283
|
+
{resetPending ? 'Sending...' : 'Reset Password'}
|
|
284
|
+
</Button>
|
|
285
|
+
</CardFooter>
|
|
286
|
+
</Card>
|
|
359
287
|
</div>
|
|
360
288
|
)
|
|
361
289
|
}
|