stackkit 0.3.4 → 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.
Files changed (203) hide show
  1. package/README.md +50 -42
  2. package/dist/cli/add.js +122 -56
  3. package/dist/cli/create.d.ts +2 -0
  4. package/dist/cli/create.js +271 -95
  5. package/dist/cli/doctor.js +1 -0
  6. package/dist/cli/list.d.ts +1 -1
  7. package/dist/cli/list.js +6 -4
  8. package/dist/index.js +234 -191
  9. package/dist/lib/constants.d.ts +4 -0
  10. package/dist/lib/constants.js +4 -0
  11. package/dist/lib/discovery/module-discovery.d.ts +4 -0
  12. package/dist/lib/discovery/module-discovery.js +56 -0
  13. package/dist/lib/generation/code-generator.d.ts +11 -2
  14. package/dist/lib/generation/code-generator.js +42 -3
  15. package/dist/lib/generation/generator-utils.js +3 -1
  16. package/dist/lib/pm/package-manager.js +16 -13
  17. package/dist/lib/ui/logger.js +3 -2
  18. package/dist/lib/utils/path-resolver.d.ts +2 -0
  19. package/dist/lib/utils/path-resolver.js +8 -0
  20. package/dist/meta.json +8312 -0
  21. package/modules/auth/better-auth/files/{shared → express}/config/env.ts +48 -52
  22. package/modules/auth/better-auth/files/express/middlewares/authorize.ts +20 -1
  23. package/modules/auth/better-auth/files/express/modules/auth.controller.ts +349 -0
  24. package/modules/auth/better-auth/files/express/modules/{auth/auth.route.ts → auth.route.ts} +12 -7
  25. package/modules/auth/better-auth/files/express/modules/auth.service.ts +664 -0
  26. package/modules/auth/better-auth/files/express/modules/{auth/auth.type.ts → auth.type.ts} +22 -9
  27. package/modules/auth/better-auth/files/{shared/mongoose/auth/constants.ts → express/mongo-modules/auth.constants.ts} +0 -1
  28. package/modules/auth/better-auth/files/{shared/mongoose/auth/helper.ts → express/mongo-modules/auth.helper.ts} +11 -1
  29. package/modules/auth/better-auth/files/express/types/express.d.ts +11 -0
  30. package/modules/auth/better-auth/files/nextjs/api-route.ts +74 -0
  31. package/modules/auth/better-auth/files/nextjs/dashboard/pages/(user)/page.tsx +6 -0
  32. package/modules/auth/better-auth/files/nextjs/dashboard/pages/admin/page.tsx +6 -0
  33. package/modules/auth/better-auth/files/nextjs/dashboard/pages/layout.tsx +48 -0
  34. package/modules/auth/better-auth/files/nextjs/dashboard/pages/my-profile/page.tsx +5 -0
  35. package/modules/auth/better-auth/files/nextjs/features/services/auth.service.ts +102 -0
  36. package/modules/auth/better-auth/files/nextjs/layout/layout.tsx +13 -0
  37. package/modules/auth/better-auth/files/nextjs/lib/axios/http.ts +158 -0
  38. package/modules/auth/better-auth/files/nextjs/lib/env.ts +35 -0
  39. package/modules/auth/better-auth/files/nextjs/lib/utils/auth.ts +75 -0
  40. package/modules/auth/better-auth/files/nextjs/lib/utils/cookie.ts +29 -0
  41. package/modules/auth/better-auth/files/nextjs/lib/utils/jwt.ts +28 -0
  42. package/modules/auth/better-auth/files/nextjs/lib/utils/token.ts +49 -0
  43. package/modules/auth/better-auth/files/nextjs/pages/forgot-password/page.tsx +5 -0
  44. package/modules/auth/better-auth/files/nextjs/pages/layout.tsx +11 -0
  45. package/modules/auth/better-auth/files/nextjs/pages/login/page.tsx +9 -0
  46. package/modules/auth/better-auth/files/nextjs/pages/register/page.tsx +5 -0
  47. package/modules/auth/better-auth/files/nextjs/pages/reset-password/page.tsx +10 -0
  48. package/modules/auth/better-auth/files/nextjs/pages/verify-email/page.tsx +10 -0
  49. package/modules/auth/better-auth/files/nextjs/proxy.ts +157 -22
  50. package/modules/auth/better-auth/files/nextjs/theme/providers/theme-provider.tsx +11 -0
  51. package/modules/auth/better-auth/files/nextjs/types/api.types.ts +18 -0
  52. package/modules/auth/better-auth/files/react/components/protected-route.tsx +39 -0
  53. package/modules/auth/better-auth/files/react/components/route-guards.tsx +13 -0
  54. package/modules/auth/better-auth/files/react/dashboard/admin/pages/overview.tsx +3 -0
  55. package/modules/auth/better-auth/files/react/dashboard/pages/overview.tsx +3 -0
  56. package/modules/auth/better-auth/files/react/features/pages/forgot-password.tsx +5 -0
  57. package/modules/auth/better-auth/files/react/features/pages/login.tsx +5 -0
  58. package/modules/auth/better-auth/files/react/features/pages/my-profile.tsx +5 -0
  59. package/modules/auth/better-auth/files/react/features/pages/oauth-callback.tsx +59 -0
  60. package/modules/auth/better-auth/files/react/features/pages/register.tsx +5 -0
  61. package/modules/auth/better-auth/files/react/features/pages/reset-password.tsx +10 -0
  62. package/modules/auth/better-auth/files/react/features/pages/verify-email.tsx +10 -0
  63. package/modules/auth/better-auth/files/react/layout/dashboard-layout.tsx +54 -0
  64. package/modules/auth/better-auth/files/react/lib/axios/http.ts +68 -0
  65. package/modules/auth/better-auth/files/react/lib/env.ts +25 -0
  66. package/modules/auth/better-auth/files/react/router.tsx +73 -0
  67. package/modules/auth/better-auth/files/react/theme/components/providers/theme-provider-context.ts +13 -0
  68. package/modules/auth/better-auth/files/react/theme/components/providers/theme-provider.tsx +51 -0
  69. package/modules/auth/better-auth/files/react/theme/hooks/use-theme.ts +8 -0
  70. package/modules/auth/better-auth/files/shared/features/components/change-password-dialog.tsx +113 -0
  71. package/modules/auth/better-auth/files/shared/features/components/forgot-password-form.tsx +84 -0
  72. package/modules/auth/better-auth/files/shared/features/components/login-form.tsx +134 -0
  73. package/modules/auth/better-auth/files/shared/features/components/my-profile.tsx +147 -0
  74. package/modules/auth/better-auth/files/shared/features/components/profile-form.tsx +205 -0
  75. package/modules/auth/better-auth/files/shared/features/components/register-form.tsx +100 -0
  76. package/modules/auth/better-auth/files/shared/features/components/reset-password-form.tsx +111 -0
  77. package/modules/auth/better-auth/files/shared/features/components/social-login-buttons.tsx +47 -0
  78. package/modules/auth/better-auth/files/shared/features/components/user-profile-menu.tsx +106 -0
  79. package/modules/auth/better-auth/files/shared/features/components/verify-email-form.tsx +110 -0
  80. package/modules/auth/better-auth/files/shared/features/queries/auth.mutations.tsx +312 -0
  81. package/modules/auth/better-auth/files/shared/features/queries/auth.querie.ts +19 -0
  82. package/modules/auth/better-auth/files/shared/features/services/auth.api.ts +81 -0
  83. package/modules/auth/better-auth/files/shared/features/types/auth.type.ts +47 -0
  84. package/modules/auth/better-auth/files/shared/features/validators/change-password.validator.ts +18 -0
  85. package/modules/auth/better-auth/files/shared/features/validators/forgot.validator.ts +7 -0
  86. package/modules/auth/better-auth/files/shared/features/validators/login.validator.ts +14 -0
  87. package/modules/auth/better-auth/files/shared/features/validators/profile.validator.ts +8 -0
  88. package/modules/auth/better-auth/files/shared/features/validators/register.validator.ts +9 -0
  89. package/modules/auth/better-auth/files/shared/features/validators/reset.validator.ts +9 -0
  90. package/modules/auth/better-auth/files/shared/features/validators/verify.validator.ts +8 -0
  91. package/modules/auth/better-auth/files/shared/lib/auth-client.ts +2 -1
  92. package/modules/auth/better-auth/files/shared/lib/auth.ts +10 -29
  93. package/modules/auth/better-auth/files/shared/lib/constant/dashboard.ts +90 -0
  94. package/modules/auth/better-auth/files/shared/prisma/enums.prisma +0 -1
  95. package/modules/auth/better-auth/files/shared/theme/mode-toggle.tsx +30 -0
  96. package/modules/auth/better-auth/files/shared/ui/shadcn/components/dashboard/dashboard-header.tsx +94 -0
  97. package/modules/auth/better-auth/files/shared/ui/shadcn/components/dashboard/dashboard-sidebar.tsx +255 -0
  98. package/modules/auth/better-auth/files/shared/ui/shadcn/components/footer.tsx +35 -0
  99. package/modules/auth/better-auth/files/shared/ui/shadcn/components/navbar.tsx +145 -0
  100. package/modules/auth/better-auth/files/shared/ui/shadcn/form-field/input-field.tsx +440 -0
  101. package/modules/auth/better-auth/files/shared/utils/email.ts +20 -18
  102. package/modules/auth/better-auth/generator.json +174 -53
  103. package/modules/auth/better-auth/module.json +2 -2
  104. package/modules/components/files/shared/hooks/use-file-upload.ts +412 -0
  105. package/modules/components/files/shared/lib/utils/url-helpers.ts +110 -0
  106. package/modules/components/files/shared/shadcn/dashboard/data-table-column-selector.tsx +52 -0
  107. package/modules/components/files/shared/shadcn/dashboard/data-table-footer.tsx +156 -0
  108. package/modules/components/files/shared/shadcn/dashboard/data-table.tsx +405 -0
  109. package/modules/components/files/shared/shadcn/global/form-field/input-field.tsx +440 -0
  110. package/modules/components/files/shared/shadcn/global/form-field/media-uploader-field.tsx +745 -0
  111. package/modules/components/files/shared/shadcn/global/form-field/multi-select-field.tsx +207 -0
  112. package/modules/components/files/shared/shadcn/global/form-field/select-field.tsx +247 -0
  113. package/modules/components/files/shared/shadcn/global/form-field/textarea-field.tsx +277 -0
  114. package/modules/components/files/shared/shadcn/global/form-field/tiptap-editor-field.tsx +35 -0
  115. package/modules/components/files/shared/shadcn/global/no-results.tsx +41 -0
  116. package/modules/components/files/shared/shadcn/tiptap-editor/editor-menu-bar.tsx +217 -0
  117. package/modules/components/files/shared/shadcn/tiptap-editor/tiptap-editor.tsx +104 -0
  118. package/modules/components/files/shared/url/load-more.tsx +93 -0
  119. package/modules/components/files/shared/url/search-bar.tsx +131 -0
  120. package/modules/components/files/shared/url/sort-select.tsx +118 -0
  121. package/modules/components/files/shared/url/url-tabs.tsx +77 -0
  122. package/modules/components/generator.json +109 -0
  123. package/modules/components/module.json +11 -0
  124. package/modules/database/mongoose/generator.json +3 -14
  125. package/modules/database/mongoose/module.json +2 -2
  126. package/modules/database/prisma/generator.json +6 -12
  127. package/modules/database/prisma/module.json +2 -2
  128. package/modules/storage/cloudinary/files/express/config/env.ts +65 -0
  129. package/modules/storage/cloudinary/files/express/config/media.ts +103 -0
  130. package/modules/storage/cloudinary/files/express/modules/media/media.controller.ts +59 -0
  131. package/modules/storage/cloudinary/files/express/modules/media/media.route.ts +29 -0
  132. package/modules/storage/cloudinary/files/express/modules/media/media.service.ts +113 -0
  133. package/modules/storage/cloudinary/files/express/modules/media/media.type.ts +32 -0
  134. package/modules/storage/cloudinary/generator.json +34 -0
  135. package/modules/storage/cloudinary/module.json +11 -0
  136. package/modules/ui/shadcn/generator.json +21 -0
  137. package/modules/ui/shadcn/module.json +11 -0
  138. package/package.json +24 -26
  139. package/templates/express/README.md +11 -16
  140. package/templates/express/src/config/env.ts +7 -5
  141. package/templates/nextjs/README.md +13 -18
  142. package/templates/nextjs/app/favicon.ico +0 -0
  143. package/templates/nextjs/app/layout.tsx +6 -4
  144. package/templates/nextjs/components/providers/query-provider.tsx +3 -0
  145. package/templates/nextjs/env.example +3 -1
  146. package/templates/nextjs/lib/axios/http.ts +23 -0
  147. package/templates/nextjs/lib/env.ts +7 -5
  148. package/templates/nextjs/package.json +2 -1
  149. package/templates/nextjs/template.json +1 -2
  150. package/templates/react/README.md +9 -14
  151. package/templates/react/index.html +1 -1
  152. package/templates/react/package.json +1 -1
  153. package/templates/react/src/assets/favicon.ico +0 -0
  154. package/templates/react/src/components/providers/query-provider.tsx +38 -0
  155. package/templates/react/src/{shared/components → components}/seo.tsx +4 -8
  156. package/templates/react/src/lib/axios/http.ts +24 -0
  157. package/templates/react/src/main.tsx +8 -11
  158. package/templates/react/src/{features/about/pages → pages}/about.tsx +1 -1
  159. package/templates/react/src/{features/home/pages → pages}/home.tsx +1 -1
  160. package/templates/react/src/router.tsx +6 -6
  161. package/templates/react/src/vite-env.d.ts +2 -1
  162. package/templates/react/template.json +0 -1
  163. package/templates/react/tsconfig.app.json +6 -0
  164. package/templates/react/tsconfig.json +7 -1
  165. package/templates/react/vite.config.ts +12 -0
  166. package/modules/auth/authjs/files/nextjs/api/auth/[...nextauth]/route.ts +0 -3
  167. package/modules/auth/authjs/files/nextjs/proxy.ts +0 -1
  168. package/modules/auth/authjs/files/shared/lib/auth.ts +0 -119
  169. package/modules/auth/authjs/files/shared/prisma/schema.prisma +0 -61
  170. package/modules/auth/authjs/generator.json +0 -64
  171. package/modules/auth/authjs/module.json +0 -13
  172. package/modules/auth/better-auth/files/express/modules/auth/auth.controller.ts +0 -264
  173. package/modules/auth/better-auth/files/express/modules/auth/auth.service.ts +0 -537
  174. package/modules/auth/better-auth/files/express/templates/google-redirect.ejs +0 -24
  175. package/modules/auth/better-auth/files/nextjs/api/auth/[...all]/route.ts +0 -4
  176. package/modules/auth/better-auth/files/nextjs/lib/auth/auth-guards.ts +0 -41
  177. package/modules/auth/better-auth/files/nextjs/templates/email-otp.tsx +0 -74
  178. package/templates/express/node_modules/.bin/acorn +0 -17
  179. package/templates/express/node_modules/.bin/eslint +0 -17
  180. package/templates/express/node_modules/.bin/tsc +0 -17
  181. package/templates/express/node_modules/.bin/tsserver +0 -17
  182. package/templates/express/node_modules/.bin/tsx +0 -17
  183. package/templates/nextjs/lib/api/http.ts +0 -40
  184. package/templates/nextjs/next-env.d.ts +0 -6
  185. package/templates/react/dist/assets/index-D4AHT4dU.js +0 -193
  186. package/templates/react/dist/assets/index-rpwj5ZOX.css +0 -1
  187. package/templates/react/dist/index.html +0 -14
  188. package/templates/react/dist/vite.svg +0 -1
  189. package/templates/react/public/vite.svg +0 -1
  190. package/templates/react/src/app/layouts/dashboard-layout.tsx +0 -8
  191. package/templates/react/src/app/layouts/public-layout.tsx +0 -5
  192. package/templates/react/src/app/providers.tsx +0 -20
  193. package/templates/react/src/app/router.tsx +0 -21
  194. package/templates/react/src/assets/react.svg +0 -1
  195. package/templates/react/src/shared/api/http.ts +0 -39
  196. package/templates/react/src/shared/components/loading.tsx +0 -8
  197. package/templates/react/src/shared/lib/query-client.ts +0 -12
  198. package/templates/react/src/utils/storage.ts +0 -35
  199. package/templates/react/src/utils/utils.ts +0 -3
  200. /package/templates/nextjs/app/{page.tsx → (public)/(root)/page.tsx} +0 -0
  201. /package/templates/react/src/{shared/components → components}/error-boundary.tsx +0 -0
  202. /package/templates/react/src/{shared/components → components}/layout.tsx +0 -0
  203. /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
+ }