@betterstart/cli 0.1.81 → 0.1.83

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.
Files changed (33) hide show
  1. package/dist/cli.js +176 -305
  2. package/dist/cli.js.map +1 -1
  3. package/package.json +1 -1
  4. package/templates/init/components/data-table/data-table.tsx +1 -1
  5. package/templates/init/components/layout/cms-search.tsx +6 -5
  6. package/templates/init/components/layout/cms-sidebar.tsx +3 -7
  7. package/templates/init/components/shared/page-header.tsx +5 -5
  8. package/templates/init/data/navigation.ts +6 -6
  9. package/templates/init/lib/actions/profile.ts +4 -0
  10. package/templates/init/lib/auth/auth.ts +17 -0
  11. package/templates/init/pages/dashboard-page.tsx +1 -1
  12. package/templates/init/pages/forgot-password-form.tsx +130 -0
  13. package/templates/init/pages/forgot-password-page.tsx +20 -0
  14. package/templates/init/pages/login-form.tsx +17 -1
  15. package/templates/init/pages/login-page.tsx +12 -2
  16. package/templates/init/pages/profile/profile-form.tsx +65 -137
  17. package/templates/init/pages/reset-password-form.tsx +159 -0
  18. package/templates/init/pages/reset-password-page.tsx +20 -0
  19. package/templates/init/pages/users/columns.tsx +13 -10
  20. package/templates/init/pages/users/users-page-content.tsx +9 -2
  21. package/templates/init/pages/users/users-page.tsx +1 -1
  22. package/templates/init/pages/users/users-table.tsx +2 -2
  23. package/templates/ui/badge.tsx +2 -2
  24. package/templates/ui/button.tsx +2 -2
  25. package/templates/ui/card.tsx +2 -2
  26. package/templates/ui/curriculum-editor.tsx +2 -2
  27. package/templates/ui/image-upload-field.tsx +2 -2
  28. package/templates/ui/input.tsx +5 -4
  29. package/templates/ui/media-upload-field.tsx +2 -2
  30. package/templates/ui/placeholder.tsx +1 -1
  31. package/templates/ui/sidebar.tsx +3 -3
  32. package/templates/ui/table.tsx +10 -4
  33. package/templates/ui/video-upload-field.tsx +2 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@betterstart/cli",
3
- "version": "0.1.81",
3
+ "version": "0.1.83",
4
4
  "description": "Scaffold a full-featured CMS into any Next.js 16 application",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -133,7 +133,7 @@ export function DataTable<TData, TValue>({
133
133
  <div className="space-y-6">
134
134
  <div className="bg-card border overflow-hidden rounded-lg corner-squircle">
135
135
  <Table>
136
- <TableHeader className="bg-secondary">
136
+ <TableHeader>
137
137
  {table.getHeaderGroups().map((headerGroup) => (
138
138
  <TableRow key={headerGroup.id}>
139
139
  {headerGroup.headers.map((header) => (
@@ -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-[240px]">
6
+ <div className="flex items-center gap-2 relative w-full max-w-60">
7
7
  <Button
8
8
  variant="outline"
9
- className="w-full text-left items-center pr-2! pl-3 py-0 rounded-lg bg-white"
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/70" />
12
- <span className="w-full font-normal text-sm text-muted-foreground/70 [text-box-trim:trim-both]">
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! text-muted-foreground" />
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-1">
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-6!')}>
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'>
@@ -8,16 +8,16 @@ interface PageHeaderProps {
8
8
 
9
9
  export function PageHeader({ title, children, search, actions, back }: PageHeaderProps) {
10
10
  return (
11
- <div className="grid grid-cols-3 items-center justify-between w-full h-14 px-4 border-b border-border sticky top-0 bg-background/60 backdrop-blur-md z-10">
12
- <div className="flex items-center justify-start gap-2 w-full">{back && back}</div>
13
- <div className="flex items-center justify-center gap-2">
11
+ <header className="grid grid-cols-3 items-center h-14 px-6 border-b border-border fixed top-0 bg-background/60 backdrop-blur-md z-10 left-0 pl-[calc(var(--sidebar-width)+calc(var(--spacing)*6))] right-0 w-svw">
12
+ <div className="flex items-center justify-start gap-2 w-full">{back}</div>
13
+ <div className="flex items-center justify-center gap-2 w-full">
14
14
  <h2 className="text-sm font-medium tracking-tight">{title}</h2>
15
15
  </div>
16
- <div className="flex items-center justify-end gap-2">
16
+ <div className="flex items-center justify-end gap-2 w-full">
17
17
  {children && children}
18
18
  {search && search}
19
19
  {actions && actions}
20
20
  </div>
21
- </div>
21
+ </header>
22
22
  )
23
23
  }
@@ -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
@@ -28,7 +28,7 @@ const quickLinks = [
28
28
 
29
29
  export default function DashboardPage() {
30
30
  return (
31
- <div className="flex flex-col">
31
+ <div className="flex flex-col pt-14">
32
32
  <PageHeader title="Dashboard" />
33
33
  <div className="p-6 space-y-6">
34
34
  <div className="grid gap-4 md:grid-cols-3">
@@ -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&apos;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 { useRouter } from 'next/navigation'
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
- <LoginForm />
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 [passwordPending, setPasswordPending] = useState(false)
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 onPasswordSubmit(values: PasswordValues) {
145
- setPasswordPending(true)
146
-
122
+ async function handleResetPassword() {
123
+ setResetPending(true)
147
124
  try {
148
- const { error } = await authClient.changePassword({
149
- currentPassword: values.currentPassword,
150
- newPassword: values.newPassword,
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 change password')
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 changing your password')
142
+ toast.error('An error occurred while sending the reset link')
163
143
  } finally {
164
- setPasswordPending(false)
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
- <Form {...passwordForm}>
282
- <form onSubmit={passwordForm.handleSubmit(onPasswordSubmit)}>
283
- <Card className="material-sm!">
284
- <CardHeader>
285
- <CardTitle>Change Password</CardTitle>
286
- <CardDescription>
287
- Update your password to keep your account secure
288
- </CardDescription>
289
- </CardHeader>
290
- <CardContent className="space-y-6">
291
- <FormField
292
- control={passwordForm.control}
293
- name="currentPassword"
294
- render={({ field: formField }) => (
295
- <FormItem>
296
- <FormLabel>Current Password</FormLabel>
297
- <FormControl>
298
- <Input
299
- type="password"
300
- placeholder="Enter current password"
301
- autoComplete="current-password"
302
- disabled={passwordPending}
303
- {...formField}
304
- />
305
- </FormControl>
306
- <FormMessage />
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
  }