stackkit 0.3.5 → 0.3.6
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/README.md +50 -42
- package/dist/cli/add.js +122 -56
- package/dist/cli/create.d.ts +2 -0
- package/dist/cli/create.js +271 -95
- package/dist/cli/doctor.js +1 -0
- package/dist/cli/list.d.ts +1 -1
- package/dist/cli/list.js +6 -4
- package/dist/index.js +234 -191
- package/dist/lib/constants.d.ts +4 -0
- package/dist/lib/constants.js +4 -0
- package/dist/lib/discovery/module-discovery.d.ts +4 -0
- package/dist/lib/discovery/module-discovery.js +56 -0
- package/dist/lib/generation/code-generator.d.ts +11 -2
- package/dist/lib/generation/code-generator.js +42 -3
- package/dist/lib/generation/generator-utils.js +3 -1
- package/dist/lib/pm/package-manager.js +16 -13
- package/dist/lib/ui/logger.js +3 -2
- package/dist/lib/utils/path-resolver.d.ts +2 -0
- package/dist/lib/utils/path-resolver.js +8 -0
- package/dist/meta.json +8312 -0
- package/modules/auth/better-auth/files/{shared → express}/config/env.ts +48 -50
- package/modules/auth/better-auth/files/express/middlewares/authorize.ts +20 -1
- package/modules/auth/better-auth/files/express/modules/auth.controller.ts +349 -0
- package/modules/auth/better-auth/files/express/modules/{auth/auth.route.ts → auth.route.ts} +9 -4
- package/modules/auth/better-auth/files/express/modules/auth.service.ts +664 -0
- package/modules/auth/better-auth/files/express/modules/{auth/auth.type.ts → auth.type.ts} +22 -9
- package/modules/auth/better-auth/files/{shared/mongoose/auth/helper.ts → express/mongo-modules/auth.helper.ts} +11 -1
- package/modules/auth/better-auth/files/express/types/express.d.ts +11 -0
- package/modules/auth/better-auth/files/nextjs/api-route.ts +74 -0
- package/modules/auth/better-auth/files/nextjs/dashboard/pages/(user)/page.tsx +6 -0
- package/modules/auth/better-auth/files/nextjs/dashboard/pages/admin/page.tsx +6 -0
- package/modules/auth/better-auth/files/nextjs/dashboard/pages/layout.tsx +48 -0
- package/modules/auth/better-auth/files/nextjs/dashboard/pages/my-profile/page.tsx +5 -0
- package/modules/auth/better-auth/files/nextjs/features/services/auth.service.ts +102 -0
- package/modules/auth/better-auth/files/nextjs/layout/layout.tsx +13 -0
- package/modules/auth/better-auth/files/nextjs/lib/axios/http.ts +158 -0
- package/modules/auth/better-auth/files/nextjs/lib/env.ts +35 -0
- package/modules/auth/better-auth/files/nextjs/lib/utils/auth.ts +75 -0
- package/modules/auth/better-auth/files/nextjs/lib/utils/cookie.ts +29 -0
- package/modules/auth/better-auth/files/nextjs/lib/utils/jwt.ts +28 -0
- package/modules/auth/better-auth/files/nextjs/lib/utils/token.ts +49 -0
- package/modules/auth/better-auth/files/nextjs/pages/forgot-password/page.tsx +5 -0
- package/modules/auth/better-auth/files/nextjs/pages/layout.tsx +11 -0
- package/modules/auth/better-auth/files/nextjs/pages/login/page.tsx +9 -0
- package/modules/auth/better-auth/files/nextjs/pages/register/page.tsx +5 -0
- package/modules/auth/better-auth/files/nextjs/pages/reset-password/page.tsx +10 -0
- package/modules/auth/better-auth/files/nextjs/pages/verify-email/page.tsx +10 -0
- package/modules/auth/better-auth/files/nextjs/proxy.ts +154 -42
- package/modules/auth/better-auth/files/nextjs/theme/providers/theme-provider.tsx +11 -0
- package/modules/auth/better-auth/files/nextjs/types/api.types.ts +18 -0
- package/modules/auth/better-auth/files/react/components/protected-route.tsx +39 -0
- package/modules/auth/better-auth/files/react/components/route-guards.tsx +13 -0
- package/modules/auth/better-auth/files/react/dashboard/admin/pages/overview.tsx +3 -0
- package/modules/auth/better-auth/files/react/dashboard/pages/overview.tsx +3 -0
- package/modules/auth/better-auth/files/react/features/pages/forgot-password.tsx +5 -0
- package/modules/auth/better-auth/files/react/features/pages/login.tsx +5 -0
- package/modules/auth/better-auth/files/react/features/pages/my-profile.tsx +5 -0
- package/modules/auth/better-auth/files/react/features/pages/oauth-callback.tsx +59 -0
- package/modules/auth/better-auth/files/react/features/pages/register.tsx +5 -0
- package/modules/auth/better-auth/files/react/features/pages/reset-password.tsx +10 -0
- package/modules/auth/better-auth/files/react/features/pages/verify-email.tsx +10 -0
- package/modules/auth/better-auth/files/react/layout/dashboard-layout.tsx +54 -0
- package/modules/auth/better-auth/files/react/lib/axios/http.ts +68 -0
- package/modules/auth/better-auth/files/react/lib/env.ts +25 -0
- package/modules/auth/better-auth/files/react/router.tsx +73 -0
- package/modules/auth/better-auth/files/react/theme/components/providers/theme-provider-context.ts +13 -0
- package/modules/auth/better-auth/files/react/theme/components/providers/theme-provider.tsx +51 -0
- package/modules/auth/better-auth/files/react/theme/hooks/use-theme.ts +8 -0
- package/modules/auth/better-auth/files/shared/features/components/change-password-dialog.tsx +113 -0
- package/modules/auth/better-auth/files/shared/features/components/forgot-password-form.tsx +84 -0
- package/modules/auth/better-auth/files/shared/features/components/login-form.tsx +134 -0
- package/modules/auth/better-auth/files/shared/features/components/my-profile.tsx +147 -0
- package/modules/auth/better-auth/files/shared/features/components/profile-form.tsx +205 -0
- package/modules/auth/better-auth/files/shared/features/components/register-form.tsx +100 -0
- package/modules/auth/better-auth/files/shared/features/components/reset-password-form.tsx +111 -0
- package/modules/auth/better-auth/files/shared/features/components/social-login-buttons.tsx +47 -0
- package/modules/auth/better-auth/files/shared/features/components/user-profile-menu.tsx +106 -0
- package/modules/auth/better-auth/files/shared/features/components/verify-email-form.tsx +110 -0
- package/modules/auth/better-auth/files/shared/features/queries/auth.mutations.tsx +312 -0
- package/modules/auth/better-auth/files/shared/features/queries/auth.querie.ts +19 -0
- package/modules/auth/better-auth/files/shared/features/services/auth.api.ts +81 -0
- package/modules/auth/better-auth/files/shared/features/types/auth.type.ts +47 -0
- package/modules/auth/better-auth/files/shared/features/validators/change-password.validator.ts +18 -0
- package/modules/auth/better-auth/files/shared/features/validators/forgot.validator.ts +7 -0
- package/modules/auth/better-auth/files/shared/features/validators/login.validator.ts +14 -0
- package/modules/auth/better-auth/files/shared/features/validators/profile.validator.ts +8 -0
- package/modules/auth/better-auth/files/shared/features/validators/register.validator.ts +9 -0
- package/modules/auth/better-auth/files/shared/features/validators/reset.validator.ts +9 -0
- package/modules/auth/better-auth/files/shared/features/validators/verify.validator.ts +8 -0
- package/modules/auth/better-auth/files/shared/lib/auth-client.ts +2 -1
- package/modules/auth/better-auth/files/shared/lib/auth.ts +5 -19
- package/modules/auth/better-auth/files/shared/lib/constant/dashboard.ts +90 -0
- package/modules/auth/better-auth/files/shared/theme/mode-toggle.tsx +30 -0
- package/modules/auth/better-auth/files/shared/ui/shadcn/components/dashboard/dashboard-header.tsx +94 -0
- package/modules/auth/better-auth/files/shared/ui/shadcn/components/dashboard/dashboard-sidebar.tsx +255 -0
- package/modules/auth/better-auth/files/shared/ui/shadcn/components/footer.tsx +35 -0
- package/modules/auth/better-auth/files/shared/ui/shadcn/components/navbar.tsx +145 -0
- package/modules/auth/better-auth/files/shared/ui/shadcn/form-field/input-field.tsx +440 -0
- package/modules/auth/better-auth/files/shared/utils/email.ts +2 -17
- package/modules/auth/better-auth/generator.json +172 -51
- package/modules/auth/better-auth/module.json +2 -2
- package/modules/components/files/shared/hooks/use-file-upload.ts +412 -0
- package/modules/components/files/shared/lib/utils/url-helpers.ts +110 -0
- package/modules/components/files/shared/shadcn/dashboard/data-table-column-selector.tsx +52 -0
- package/modules/components/files/shared/shadcn/dashboard/data-table-footer.tsx +156 -0
- package/modules/components/files/shared/shadcn/dashboard/data-table.tsx +405 -0
- package/modules/components/files/shared/shadcn/global/form-field/input-field.tsx +440 -0
- package/modules/components/files/shared/shadcn/global/form-field/media-uploader-field.tsx +745 -0
- package/modules/components/files/shared/shadcn/global/form-field/multi-select-field.tsx +207 -0
- package/modules/components/files/shared/shadcn/global/form-field/select-field.tsx +247 -0
- package/modules/components/files/shared/shadcn/global/form-field/textarea-field.tsx +277 -0
- package/modules/components/files/shared/shadcn/global/form-field/tiptap-editor-field.tsx +35 -0
- package/modules/components/files/shared/shadcn/global/no-results.tsx +41 -0
- package/modules/components/files/shared/shadcn/tiptap-editor/editor-menu-bar.tsx +217 -0
- package/modules/components/files/shared/shadcn/tiptap-editor/tiptap-editor.tsx +104 -0
- package/modules/components/files/shared/url/load-more.tsx +93 -0
- package/modules/components/files/shared/url/search-bar.tsx +131 -0
- package/modules/components/files/shared/url/sort-select.tsx +118 -0
- package/modules/components/files/shared/url/url-tabs.tsx +77 -0
- package/modules/components/generator.json +109 -0
- package/modules/components/module.json +11 -0
- package/modules/database/mongoose/generator.json +3 -14
- package/modules/database/mongoose/module.json +2 -2
- package/modules/database/prisma/generator.json +6 -12
- package/modules/database/prisma/module.json +2 -2
- package/modules/storage/cloudinary/files/express/config/env.ts +65 -0
- package/modules/storage/cloudinary/files/express/config/media.ts +103 -0
- package/modules/storage/cloudinary/files/express/modules/media/media.controller.ts +59 -0
- package/modules/storage/cloudinary/files/express/modules/media/media.route.ts +29 -0
- package/modules/storage/cloudinary/files/express/modules/media/media.service.ts +113 -0
- package/modules/storage/cloudinary/files/express/modules/media/media.type.ts +32 -0
- package/modules/storage/cloudinary/generator.json +34 -0
- package/modules/storage/cloudinary/module.json +11 -0
- package/modules/ui/shadcn/generator.json +21 -0
- package/modules/ui/shadcn/module.json +11 -0
- package/package.json +24 -26
- package/templates/express/README.md +11 -16
- package/templates/express/src/config/env.ts +7 -5
- package/templates/nextjs/README.md +13 -18
- package/templates/nextjs/app/favicon.ico +0 -0
- package/templates/nextjs/app/layout.tsx +6 -4
- package/templates/nextjs/components/providers/query-provider.tsx +3 -0
- package/templates/nextjs/env.example +3 -1
- package/templates/nextjs/lib/axios/http.ts +23 -0
- package/templates/nextjs/lib/env.ts +7 -5
- package/templates/nextjs/package.json +2 -1
- package/templates/nextjs/template.json +1 -2
- package/templates/react/README.md +9 -14
- package/templates/react/index.html +1 -1
- package/templates/react/package.json +1 -1
- package/templates/react/src/assets/favicon.ico +0 -0
- package/templates/react/src/components/providers/query-provider.tsx +38 -0
- package/templates/react/src/{shared/components → components}/seo.tsx +4 -8
- package/templates/react/src/lib/axios/http.ts +24 -0
- package/templates/react/src/main.tsx +8 -11
- package/templates/react/src/{features/about/pages → pages}/about.tsx +1 -1
- package/templates/react/src/{features/home/pages → pages}/home.tsx +1 -1
- package/templates/react/src/router.tsx +6 -6
- package/templates/react/src/vite-env.d.ts +2 -1
- package/templates/react/template.json +0 -1
- package/templates/react/tsconfig.app.json +6 -0
- package/templates/react/tsconfig.json +7 -1
- package/templates/react/vite.config.ts +12 -0
- package/modules/auth/authjs/files/nextjs/api/auth/[...nextauth]/route.ts +0 -3
- package/modules/auth/authjs/files/nextjs/proxy.ts +0 -1
- package/modules/auth/authjs/files/shared/lib/auth.ts +0 -119
- package/modules/auth/authjs/files/shared/prisma/schema.prisma +0 -61
- package/modules/auth/authjs/generator.json +0 -64
- package/modules/auth/authjs/module.json +0 -13
- package/modules/auth/better-auth/files/express/modules/auth/auth.controller.ts +0 -264
- package/modules/auth/better-auth/files/express/modules/auth/auth.service.ts +0 -549
- package/modules/auth/better-auth/files/express/templates/google-redirect.ejs +0 -24
- package/modules/auth/better-auth/files/nextjs/api/auth/[...all]/route.ts +0 -4
- package/modules/auth/better-auth/files/nextjs/lib/auth/auth-guards.ts +0 -31
- package/modules/auth/better-auth/files/nextjs/templates/email-otp.tsx +0 -74
- package/templates/nextjs/lib/api/http.ts +0 -40
- package/templates/react/public/vite.svg +0 -1
- package/templates/react/src/app/layouts/dashboard-layout.tsx +0 -8
- package/templates/react/src/app/layouts/public-layout.tsx +0 -5
- package/templates/react/src/app/providers.tsx +0 -20
- package/templates/react/src/app/router.tsx +0 -21
- package/templates/react/src/assets/react.svg +0 -1
- package/templates/react/src/shared/api/http.ts +0 -39
- package/templates/react/src/shared/components/loading.tsx +0 -8
- package/templates/react/src/shared/lib/query-client.ts +0 -12
- package/templates/react/src/utils/storage.ts +0 -35
- package/templates/react/src/utils/utils.ts +0 -3
- /package/modules/auth/better-auth/files/{shared/mongoose/auth/constants.ts → express/mongo-modules/auth.constants.ts} +0 -0
- /package/templates/nextjs/app/{page.tsx → (public)/(root)/page.tsx} +0 -0
- /package/templates/react/src/{shared/components → components}/error-boundary.tsx +0 -0
- /package/templates/react/src/{shared/components → components}/layout.tsx +0 -0
- /package/templates/react/src/{shared/pages → pages}/not-found.tsx +0 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import InputField from "@/components/global/form-field/input-field";
|
|
4
|
+
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
import { Separator } from "@/components/ui/separator";
|
|
7
|
+
import { useUpdateProfileMutation } from "@/features/auth/queries/auth.mutations";
|
|
8
|
+
import {
|
|
9
|
+
profileZodSchema,
|
|
10
|
+
type IProfilePayload,
|
|
11
|
+
} from "@/features/auth/validators/profile.validator";
|
|
12
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
13
|
+
import { Camera, Loader2, Lock } from "lucide-react";
|
|
14
|
+
import { useState } from "react";
|
|
15
|
+
import { FormProvider, useForm, useWatch } from "react-hook-form";
|
|
16
|
+
import { toast } from "sonner";
|
|
17
|
+
import ChangePasswordDialog from "./change-password-dialog";
|
|
18
|
+
|
|
19
|
+
interface User {
|
|
20
|
+
id: string;
|
|
21
|
+
name?: string | null;
|
|
22
|
+
email?: string | null;
|
|
23
|
+
image?: string | null;
|
|
24
|
+
role?: string;
|
|
25
|
+
createdAt?: Date | string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface ProfileFormProps {
|
|
29
|
+
user: User;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default function ProfileForm({ user }: ProfileFormProps) {
|
|
33
|
+
const mutation = useUpdateProfileMutation();
|
|
34
|
+
const [showPasswordDialog, setShowPasswordDialog] = useState(false);
|
|
35
|
+
|
|
36
|
+
const form = useForm<IProfilePayload>({
|
|
37
|
+
mode: "onTouched",
|
|
38
|
+
resolver: zodResolver(profileZodSchema),
|
|
39
|
+
defaultValues: {
|
|
40
|
+
name: user.name || "",
|
|
41
|
+
image: user.image || "",
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const imageValue = useWatch({ control: form.control, name: "image" });
|
|
46
|
+
const nameValue = useWatch({ control: form.control, name: "name" });
|
|
47
|
+
|
|
48
|
+
async function onSubmit(values: IProfilePayload) {
|
|
49
|
+
try {
|
|
50
|
+
await mutation.mutateAsync(values);
|
|
51
|
+
} catch {
|
|
52
|
+
// Error handling is done in the mutation's onError callback
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
57
|
+
const file = e.target.files?.[0];
|
|
58
|
+
if (!file) return;
|
|
59
|
+
|
|
60
|
+
if (file.size > 2 * 1024 * 1024) {
|
|
61
|
+
toast.error("Image size must be less than 2MB");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const reader = new FileReader();
|
|
66
|
+
reader.onloadend = () => {
|
|
67
|
+
form.setValue("image", reader.result as string);
|
|
68
|
+
toast.info("Image loaded. Click 'Save Changes' to update.");
|
|
69
|
+
};
|
|
70
|
+
reader.readAsDataURL(file);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<FormProvider {...form}>
|
|
75
|
+
<form
|
|
76
|
+
onSubmit={form.handleSubmit(onSubmit)}
|
|
77
|
+
className="space-y-6 rounded-lg border bg-card p-6"
|
|
78
|
+
>
|
|
79
|
+
<div>
|
|
80
|
+
<h2 className="mb-2 text-xl font-semibold">Personal Information</h2>
|
|
81
|
+
<p className="text-sm text-muted-foreground">
|
|
82
|
+
Update your personal details and profile picture
|
|
83
|
+
</p>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
{/* Profile Picture */}
|
|
87
|
+
<div className="flex items-center gap-6">
|
|
88
|
+
<div className="relative">
|
|
89
|
+
<Avatar className="h-24 w-24">
|
|
90
|
+
<AvatarImage
|
|
91
|
+
src={imageValue}
|
|
92
|
+
alt={nameValue}
|
|
93
|
+
className="object-cover"
|
|
94
|
+
referrerPolicy="no-referrer"
|
|
95
|
+
/>
|
|
96
|
+
<AvatarFallback className="text-2xl">
|
|
97
|
+
{nameValue?.[0]?.toUpperCase() || "U"}
|
|
98
|
+
</AvatarFallback>
|
|
99
|
+
</Avatar>
|
|
100
|
+
<label
|
|
101
|
+
htmlFor="avatar-upload"
|
|
102
|
+
className="absolute bottom-0 right-0 flex h-8 w-8 cursor-pointer items-center justify-center rounded-full border-2 border-background bg-primary text-primary-foreground shadow-lg transition-colors hover:bg-primary/90"
|
|
103
|
+
>
|
|
104
|
+
<Camera className="h-4 w-4" />
|
|
105
|
+
<input
|
|
106
|
+
id="avatar-upload"
|
|
107
|
+
type="file"
|
|
108
|
+
accept="image/*"
|
|
109
|
+
className="hidden"
|
|
110
|
+
onChange={handleImageUpload}
|
|
111
|
+
disabled={mutation.isPending}
|
|
112
|
+
/>
|
|
113
|
+
</label>
|
|
114
|
+
</div>
|
|
115
|
+
<div className="space-y-1">
|
|
116
|
+
<p className="text-sm font-medium">Profile Picture</p>
|
|
117
|
+
<p className="text-xs text-muted-foreground">
|
|
118
|
+
JPG, PNG or GIF. Max size 2MB.
|
|
119
|
+
</p>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
{/* Name Field */}
|
|
124
|
+
<InputField
|
|
125
|
+
name="name"
|
|
126
|
+
label="Full Name"
|
|
127
|
+
placeholder="Enter your full name"
|
|
128
|
+
disabled={mutation.isPending}
|
|
129
|
+
/>
|
|
130
|
+
|
|
131
|
+
{/* Email Field (read-only) */}
|
|
132
|
+
<div className="space-y-2">
|
|
133
|
+
<InputField
|
|
134
|
+
label="Email Address"
|
|
135
|
+
placeholder="your.email@example.com"
|
|
136
|
+
type="email"
|
|
137
|
+
value={user.email || ""}
|
|
138
|
+
disabled
|
|
139
|
+
className="cursor-not-allowed opacity-70"
|
|
140
|
+
hint="Email cannot be changed. Contact support if needed."
|
|
141
|
+
/>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<Separator className="my-6" />
|
|
145
|
+
|
|
146
|
+
{/* Security Section */}
|
|
147
|
+
<div>
|
|
148
|
+
<h3 className="mb-2 text-lg font-semibold">Security</h3>
|
|
149
|
+
<p className="mb-4 text-sm text-muted-foreground">
|
|
150
|
+
Manage your password and account security
|
|
151
|
+
</p>
|
|
152
|
+
|
|
153
|
+
<div className="flex items-center justify-between rounded-lg border bg-muted/30 p-4">
|
|
154
|
+
<div className="flex items-center gap-3">
|
|
155
|
+
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10">
|
|
156
|
+
<Lock className="h-5 w-5 text-primary" />
|
|
157
|
+
</div>
|
|
158
|
+
<div>
|
|
159
|
+
<p className="font-medium">Password</p>
|
|
160
|
+
<p className="text-sm text-muted-foreground">
|
|
161
|
+
Last changed recently
|
|
162
|
+
</p>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
<Button
|
|
166
|
+
type="button"
|
|
167
|
+
variant="outline"
|
|
168
|
+
size="sm"
|
|
169
|
+
onClick={() => setShowPasswordDialog(true)}
|
|
170
|
+
disabled={mutation.isPending}
|
|
171
|
+
>
|
|
172
|
+
Change Password
|
|
173
|
+
</Button>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
{/* Action Buttons */}
|
|
178
|
+
<div className="flex items-center justify-end gap-3 border-t pt-6">
|
|
179
|
+
<Button
|
|
180
|
+
type="button"
|
|
181
|
+
variant="outline"
|
|
182
|
+
disabled={mutation.isPending}
|
|
183
|
+
onClick={() => {
|
|
184
|
+
form.reset({ name: user.name || "", image: user.image || "" });
|
|
185
|
+
toast.info("Changes discarded");
|
|
186
|
+
}}
|
|
187
|
+
>
|
|
188
|
+
Cancel
|
|
189
|
+
</Button>
|
|
190
|
+
<Button type="submit" disabled={mutation.isPending}>
|
|
191
|
+
{mutation.isPending && (
|
|
192
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
193
|
+
)}
|
|
194
|
+
Save Changes
|
|
195
|
+
</Button>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<ChangePasswordDialog
|
|
199
|
+
open={showPasswordDialog}
|
|
200
|
+
onOpenChange={setShowPasswordDialog}
|
|
201
|
+
/>
|
|
202
|
+
</form>
|
|
203
|
+
</FormProvider>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
{{#if framework == "nextjs"}}
|
|
2
|
+
"use client";
|
|
3
|
+
{{/if}}
|
|
4
|
+
|
|
5
|
+
import InputField from "@/components/global/form-field/input-field";
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
import {
|
|
8
|
+
Card,
|
|
9
|
+
CardContent,
|
|
10
|
+
CardDescription,
|
|
11
|
+
CardFooter,
|
|
12
|
+
CardHeader,
|
|
13
|
+
CardTitle,
|
|
14
|
+
} from "@/components/ui/card";
|
|
15
|
+
import { useRegisterMutation } from "@/features/auth/queries/auth.mutations";
|
|
16
|
+
import { registerZodSchema } from "@/features/auth/validators/register.validator";
|
|
17
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
18
|
+
{{#if framework == "nextjs"}}
|
|
19
|
+
import Link from "next/link";
|
|
20
|
+
{{else}}
|
|
21
|
+
import { Link } from "react-router";
|
|
22
|
+
{{/if}}
|
|
23
|
+
import { FormProvider, useForm } from "react-hook-form";
|
|
24
|
+
import SocialLoginButtons from "./social-login-buttons";
|
|
25
|
+
|
|
26
|
+
type RegisterFormValues = {
|
|
27
|
+
name: string;
|
|
28
|
+
email: string;
|
|
29
|
+
password: string;
|
|
30
|
+
confirmPassword?: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default function RegisterForm() {
|
|
34
|
+
const mutation = useRegisterMutation();
|
|
35
|
+
|
|
36
|
+
const form = useForm<RegisterFormValues>({
|
|
37
|
+
mode: "onTouched",
|
|
38
|
+
resolver: zodResolver(registerZodSchema),
|
|
39
|
+
defaultValues: { name: "", email: "", password: "", confirmPassword: "" },
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
async function onSubmit(values: RegisterFormValues) {
|
|
43
|
+
try {
|
|
44
|
+
await mutation.mutateAsync({
|
|
45
|
+
name: values.name,
|
|
46
|
+
email: values.email,
|
|
47
|
+
password: values.password,
|
|
48
|
+
});
|
|
49
|
+
} catch {
|
|
50
|
+
// Error handling is done in the mutation's onError callback
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<Card className="w-full max-w-md">
|
|
56
|
+
<CardHeader>
|
|
57
|
+
<CardTitle>Create your account</CardTitle>
|
|
58
|
+
<CardDescription>
|
|
59
|
+
Enter your details to create an account
|
|
60
|
+
</CardDescription>
|
|
61
|
+
</CardHeader>
|
|
62
|
+
<CardContent className="p-4">
|
|
63
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
|
64
|
+
<FormProvider {...form}>
|
|
65
|
+
<InputField name="name" label="Name" placeholder="Your full name" />
|
|
66
|
+
<InputField
|
|
67
|
+
name="email"
|
|
68
|
+
label="Email"
|
|
69
|
+
placeholder="you@example.com"
|
|
70
|
+
type="email"
|
|
71
|
+
/>
|
|
72
|
+
<InputField name="password" label="Password" type="password" />
|
|
73
|
+
|
|
74
|
+
<CardFooter className="flex flex-col gap-4">
|
|
75
|
+
<div className="flex w-full items-center justify-between">
|
|
76
|
+
{{#if framework == "nextjs"}}
|
|
77
|
+
<Link href="/login" className="text-muted-foreground underline">
|
|
78
|
+
Already have an account? Sign in
|
|
79
|
+
</Link>
|
|
80
|
+
{{else}}
|
|
81
|
+
<Link to="/login" className="text-muted-foreground underline">
|
|
82
|
+
Already have an account? Sign in
|
|
83
|
+
</Link>
|
|
84
|
+
{{/if}}
|
|
85
|
+
|
|
86
|
+
<Button type="submit" disabled={form.formState.isSubmitting}>
|
|
87
|
+
{form.formState.isSubmitting
|
|
88
|
+
? "Creating..."
|
|
89
|
+
: "Create account"}
|
|
90
|
+
</Button>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<SocialLoginButtons />
|
|
94
|
+
</CardFooter>
|
|
95
|
+
</FormProvider>
|
|
96
|
+
</form>
|
|
97
|
+
</CardContent>
|
|
98
|
+
</Card>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
{{#if framework == "nextjs"}}
|
|
2
|
+
"use client";
|
|
3
|
+
{{/if}}
|
|
4
|
+
|
|
5
|
+
import InputField from "@/components/global/form-field/input-field";
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
import {
|
|
8
|
+
Card,
|
|
9
|
+
CardContent,
|
|
10
|
+
CardDescription,
|
|
11
|
+
CardHeader,
|
|
12
|
+
CardTitle,
|
|
13
|
+
} from "@/components/ui/card";
|
|
14
|
+
import { useResetPasswordMutation } from "@/features/auth/queries/auth.mutations";
|
|
15
|
+
import { resetZodSchema } from "@/features/auth/validators/reset.validator";
|
|
16
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
17
|
+
{{#if framework == "nextjs"}}
|
|
18
|
+
import { useSearchParams } from "next/navigation";
|
|
19
|
+
{{else}}
|
|
20
|
+
import { useSearchParams } from "react-router";
|
|
21
|
+
{{/if}}
|
|
22
|
+
import { FormProvider, useForm } from "react-hook-form";
|
|
23
|
+
|
|
24
|
+
type ResetValues = {
|
|
25
|
+
email: string;
|
|
26
|
+
otp: string;
|
|
27
|
+
newPassword: string;
|
|
28
|
+
confirmPassword?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export default function ResetPasswordForm() {
|
|
32
|
+
{{#if framework == "nextjs"}}
|
|
33
|
+
const params = useSearchParams();
|
|
34
|
+
const prefillEmail = params?.get("email") || "";
|
|
35
|
+
{{else}}
|
|
36
|
+
const [searchParams] = useSearchParams();
|
|
37
|
+
const prefillEmail = searchParams.get("email") || "";
|
|
38
|
+
{{/if}}
|
|
39
|
+
|
|
40
|
+
const mutation = useResetPasswordMutation();
|
|
41
|
+
|
|
42
|
+
const form = useForm<ResetValues>({
|
|
43
|
+
mode: "onTouched",
|
|
44
|
+
resolver: zodResolver(resetZodSchema),
|
|
45
|
+
defaultValues: {
|
|
46
|
+
email: prefillEmail,
|
|
47
|
+
otp: "",
|
|
48
|
+
newPassword: "",
|
|
49
|
+
confirmPassword: "",
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
async function onSubmit(values: ResetValues) {
|
|
54
|
+
if (values.newPassword !== values.confirmPassword) {
|
|
55
|
+
form.setError("confirmPassword", { message: "Passwords do not match" });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
await mutation.mutateAsync({
|
|
61
|
+
email: values.email,
|
|
62
|
+
otp: values.otp,
|
|
63
|
+
newPassword: values.newPassword,
|
|
64
|
+
});
|
|
65
|
+
} catch {
|
|
66
|
+
// Error handling is done in the mutation's onError callback
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<Card className="w-full max-w-md">
|
|
72
|
+
<CardHeader>
|
|
73
|
+
<CardTitle>Reset password</CardTitle>
|
|
74
|
+
<CardDescription>
|
|
75
|
+
Enter your email and new password to reset your password
|
|
76
|
+
</CardDescription>
|
|
77
|
+
</CardHeader>
|
|
78
|
+
<CardContent className="p-8">
|
|
79
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
|
80
|
+
<FormProvider {...form}>
|
|
81
|
+
<InputField
|
|
82
|
+
name="email"
|
|
83
|
+
label="Email"
|
|
84
|
+
type="email"
|
|
85
|
+
placeholder="you@example.com"
|
|
86
|
+
/>
|
|
87
|
+
<InputField name="otp" label="OTP" placeholder="Enter OTP" />
|
|
88
|
+
<InputField
|
|
89
|
+
name="newPassword"
|
|
90
|
+
label="New password"
|
|
91
|
+
type="password"
|
|
92
|
+
/>
|
|
93
|
+
<InputField
|
|
94
|
+
name="confirmPassword"
|
|
95
|
+
label="Confirm new password"
|
|
96
|
+
type="password"
|
|
97
|
+
/>
|
|
98
|
+
|
|
99
|
+
<div className="flex items-center justify-between mt-2">
|
|
100
|
+
<Button type="submit" disabled={form.formState.isSubmitting}>
|
|
101
|
+
{form.formState.isSubmitting
|
|
102
|
+
? "Resetting..."
|
|
103
|
+
: "Reset password"}
|
|
104
|
+
</Button>
|
|
105
|
+
</div>
|
|
106
|
+
</FormProvider>
|
|
107
|
+
</form>
|
|
108
|
+
</CardContent>
|
|
109
|
+
</Card>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button } from "@/components/ui/button";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { useSocialLoginMutation } from "../queries/auth.mutations";
|
|
6
|
+
|
|
7
|
+
function GoogleIcon(props: React.SVGProps<SVGSVGElement>) {
|
|
8
|
+
return (
|
|
9
|
+
<svg viewBox="0 0 533.5 544.3" width="20" height="20" {...props}>
|
|
10
|
+
<path
|
|
11
|
+
fill="#4285F4"
|
|
12
|
+
d="M533.5 278.4c0-18.5-1.5-36.3-4.4-53.6H272v101.5h147.1c-6.4 34.6-25.3 63.9-54 83.5v69.4h87.2c51-47 80.2-116.3 80.2-200.8z"
|
|
13
|
+
/>
|
|
14
|
+
<path
|
|
15
|
+
fill="#34A853"
|
|
16
|
+
d="M272 544.3c73.6 0 135.4-24.4 180.6-66.2l-87.2-69.4c-24.2 16.3-55.3 25.9-93.4 25.9-71.7 0-132.6-48.3-154.3-113.3H28.8v71.1C73.8 492.9 166.8 544.3 272 544.3z"
|
|
17
|
+
/>
|
|
18
|
+
<path
|
|
19
|
+
fill="#FBBC05"
|
|
20
|
+
d="M117.7 324.3c-10.8-32.3-10.8-67 0-99.3V153.9H28.8c-36.7 73.6-36.7 160.6 0 234.2l88.9-63.8z"
|
|
21
|
+
/>
|
|
22
|
+
<path
|
|
23
|
+
fill="#EA4335"
|
|
24
|
+
d="M272 109.1c39.9 0 75.8 13.7 104.1 40.6l78-78C402.1 28 339.7 0 272 0 166.8 0 73.8 51.4 28.8 125.1l88.9 71.1C139.4 157.4 200.3 109.1 272 109.1z"
|
|
25
|
+
/>
|
|
26
|
+
</svg>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default function SocialLoginButtons() {
|
|
31
|
+
const { mutate: socialLogin, isPending } = useSocialLoginMutation();
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<Button
|
|
35
|
+
variant="outline"
|
|
36
|
+
size="lg"
|
|
37
|
+
onClick={() => socialLogin("google")}
|
|
38
|
+
disabled={isPending}
|
|
39
|
+
className="w-full bg-background text-accent-foreground"
|
|
40
|
+
>
|
|
41
|
+
<span className="flex items-center justify-center w-6 h-6">
|
|
42
|
+
<GoogleIcon />
|
|
43
|
+
</span>
|
|
44
|
+
{isPending ? "Redirecting..." : "Continue with Google"}
|
|
45
|
+
</Button>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
{{#if framework == "nextjs"}}
|
|
2
|
+
"use client";
|
|
3
|
+
{{/if}}
|
|
4
|
+
|
|
5
|
+
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
import {
|
|
8
|
+
DropdownMenu,
|
|
9
|
+
DropdownMenuContent,
|
|
10
|
+
DropdownMenuItem,
|
|
11
|
+
DropdownMenuSeparator,
|
|
12
|
+
DropdownMenuTrigger,
|
|
13
|
+
} from "@/components/ui/dropdown-menu";
|
|
14
|
+
import { LayoutDashboard, LogOut, User } from "lucide-react";
|
|
15
|
+
{{#if framework == "nextjs"}}
|
|
16
|
+
import Link from "next/link";
|
|
17
|
+
{{else}}
|
|
18
|
+
import { Link } from "react-router";
|
|
19
|
+
{{/if}}
|
|
20
|
+
import { useLogoutMutation } from "../queries/auth.mutations";
|
|
21
|
+
import { useMeQuery } from "../queries/auth.querie";
|
|
22
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
23
|
+
|
|
24
|
+
export default function UserProfileMenu() {
|
|
25
|
+
const { data: user, isLoading } = useMeQuery();
|
|
26
|
+
const { mutate: logout, isPending } = useLogoutMutation();
|
|
27
|
+
|
|
28
|
+
if (isLoading) {
|
|
29
|
+
return <Skeleton className="h-8 w-8 rounded-full" />;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!user) {
|
|
33
|
+
return (
|
|
34
|
+
<Button
|
|
35
|
+
render={
|
|
36
|
+
{{#if framework == "nextjs"}}<Link href="/login" />{{else}}<Link to="/login" />{{/if}}
|
|
37
|
+
}
|
|
38
|
+
variant="ghost"
|
|
39
|
+
size="icon"
|
|
40
|
+
aria-label="Sign in"
|
|
41
|
+
nativeButton={false}
|
|
42
|
+
>
|
|
43
|
+
<User />
|
|
44
|
+
</Button>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const initials = user.name
|
|
49
|
+
? user.name.split(" ").map((n) => n[0]).join("").toUpperCase().slice(0, 2)
|
|
50
|
+
: "U";
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<DropdownMenu>
|
|
54
|
+
<DropdownMenuTrigger
|
|
55
|
+
className="group flex items-center gap-2 rounded-full p-0.5 outline-none ring-2 ring-transparent transition-all hover:ring-border focus-visible:ring-ring"
|
|
56
|
+
aria-label="User menu"
|
|
57
|
+
>
|
|
58
|
+
<Avatar size="default">
|
|
59
|
+
<AvatarImage src={user.image || ""} alt={user.name} referrerPolicy="no-referrer" />
|
|
60
|
+
<AvatarFallback className="bg-primary text-primary-foreground text-xs font-semibold">
|
|
61
|
+
{initials}
|
|
62
|
+
</AvatarFallback>
|
|
63
|
+
</Avatar>
|
|
64
|
+
</DropdownMenuTrigger>
|
|
65
|
+
|
|
66
|
+
<DropdownMenuContent align="end" className="w-60">
|
|
67
|
+
{/* User info header */}
|
|
68
|
+
<div className="flex items-center gap-3 px-2 py-2.5">
|
|
69
|
+
<Avatar size="lg">
|
|
70
|
+
<AvatarImage src={user.image || ""} alt={user.name} referrerPolicy="no-referrer" />
|
|
71
|
+
<AvatarFallback className="bg-primary text-primary-foreground text-sm font-semibold">
|
|
72
|
+
{initials}
|
|
73
|
+
</AvatarFallback>
|
|
74
|
+
</Avatar>
|
|
75
|
+
<div className="flex flex-col min-w-0">
|
|
76
|
+
<span className="truncate text-sm font-medium text-foreground">{user.name}</span>
|
|
77
|
+
<span className="truncate text-xs text-muted-foreground">{user.email}</span>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<DropdownMenuSeparator />
|
|
82
|
+
|
|
83
|
+
<DropdownMenuItem
|
|
84
|
+
render={
|
|
85
|
+
{{#if framework == "nextjs"}}<Link href={user.role === "ADMIN" ? "/dashboard/admin" : "/dashboard"} />{{else}}<Link to={user.role === "ADMIN" ? "/dashboard/admin" : "/dashboard"} />{{/if}}
|
|
86
|
+
}
|
|
87
|
+
className="gap-2"
|
|
88
|
+
>
|
|
89
|
+
<LayoutDashboard className="size-4" /> Dashboard
|
|
90
|
+
</DropdownMenuItem>
|
|
91
|
+
|
|
92
|
+
<DropdownMenuSeparator />
|
|
93
|
+
|
|
94
|
+
<DropdownMenuItem
|
|
95
|
+
variant="destructive"
|
|
96
|
+
disabled={isPending}
|
|
97
|
+
onClick={() => logout()}
|
|
98
|
+
className="gap-2"
|
|
99
|
+
>
|
|
100
|
+
<LogOut className="size-4" />
|
|
101
|
+
{isPending ? "Signing out…" : "Sign out"}
|
|
102
|
+
</DropdownMenuItem>
|
|
103
|
+
</DropdownMenuContent>
|
|
104
|
+
</DropdownMenu>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
{{#if framework == "nextjs"}}
|
|
2
|
+
"use client";
|
|
3
|
+
{{/if}}
|
|
4
|
+
|
|
5
|
+
import InputField from "@/components/global/form-field/input-field";
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
import {
|
|
8
|
+
Card,
|
|
9
|
+
CardContent,
|
|
10
|
+
CardDescription,
|
|
11
|
+
CardHeader,
|
|
12
|
+
CardTitle,
|
|
13
|
+
} from "@/components/ui/card";
|
|
14
|
+
import {
|
|
15
|
+
useResendOTPMutation,
|
|
16
|
+
useVerifyEmailMutation,
|
|
17
|
+
} from "@/features/auth/queries/auth.mutations";
|
|
18
|
+
import { verifyZodSchema } from "@/features/auth/validators/verify.validator";
|
|
19
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
20
|
+
{{#if framework == "nextjs"}}
|
|
21
|
+
import { useSearchParams } from "next/navigation";
|
|
22
|
+
{{else}}
|
|
23
|
+
import { useSearchParams } from "react-router";
|
|
24
|
+
{{/if}}
|
|
25
|
+
import { FormProvider, useForm } from "react-hook-form";
|
|
26
|
+
import { toast } from "sonner";
|
|
27
|
+
|
|
28
|
+
type VerifyValues = {
|
|
29
|
+
email: string;
|
|
30
|
+
otp: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default function VerifyEmailForm() {
|
|
34
|
+
const mutation = useVerifyEmailMutation();
|
|
35
|
+
const resendMutation = useResendOTPMutation();
|
|
36
|
+
|
|
37
|
+
{{#if framework == "nextjs"}}
|
|
38
|
+
const params = useSearchParams();
|
|
39
|
+
const prefillEmail = params?.get("email") || "";
|
|
40
|
+
{{else}}
|
|
41
|
+
const [searchParams] = useSearchParams();
|
|
42
|
+
const prefillEmail = searchParams.get("email") || "";
|
|
43
|
+
{{/if}}
|
|
44
|
+
|
|
45
|
+
const form = useForm<VerifyValues>({
|
|
46
|
+
mode: "onTouched",
|
|
47
|
+
resolver: zodResolver(verifyZodSchema),
|
|
48
|
+
defaultValues: { email: prefillEmail, otp: "" },
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
async function onSubmit(values: VerifyValues) {
|
|
52
|
+
try {
|
|
53
|
+
await mutation.mutateAsync(values);
|
|
54
|
+
} catch {
|
|
55
|
+
// Error handling is done in the mutation's onError callback
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const resend = async () => {
|
|
60
|
+
const email = form.getValues("email") || prefillEmail;
|
|
61
|
+
|
|
62
|
+
if (!email) {
|
|
63
|
+
toast.error("Email not available to resend OTP");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
await resendMutation.mutateAsync({ email });
|
|
69
|
+
} catch {
|
|
70
|
+
// Error handling is done in the mutation's onError callback
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<Card className="w-full max-w-md">
|
|
76
|
+
<CardHeader>
|
|
77
|
+
<CardTitle>Verify your email</CardTitle>
|
|
78
|
+
<CardDescription>
|
|
79
|
+
Enter the OTP sent to your email to verify your account
|
|
80
|
+
</CardDescription>
|
|
81
|
+
</CardHeader>
|
|
82
|
+
<CardContent className="p-8">
|
|
83
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
|
84
|
+
<FormProvider {...form}>
|
|
85
|
+
{!prefillEmail ? (
|
|
86
|
+
<InputField
|
|
87
|
+
name="email"
|
|
88
|
+
label="Email"
|
|
89
|
+
type="email"
|
|
90
|
+
placeholder="you@example.com"
|
|
91
|
+
/>
|
|
92
|
+
) : null}
|
|
93
|
+
<InputField name="otp" label="OTP" placeholder="Enter OTP" />
|
|
94
|
+
|
|
95
|
+
<div className="flex items-center justify-between mt-2 gap-2">
|
|
96
|
+
<div className="flex gap-2">
|
|
97
|
+
<Button type="button" variant="ghost" onClick={resend}>
|
|
98
|
+
Resend code
|
|
99
|
+
</Button>
|
|
100
|
+
</div>
|
|
101
|
+
<Button type="submit" disabled={form.formState.isSubmitting}>
|
|
102
|
+
{form.formState.isSubmitting ? "Verifying..." : "Verify email"}
|
|
103
|
+
</Button>
|
|
104
|
+
</div>
|
|
105
|
+
</FormProvider>
|
|
106
|
+
</form>
|
|
107
|
+
</CardContent>
|
|
108
|
+
</Card>
|
|
109
|
+
);
|
|
110
|
+
}
|