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,440 @@
1
+ "use client";
2
+
3
+ import { Button } from "@/components/ui/button";
4
+ import { Checkbox } from "@/components/ui/checkbox";
5
+ import {
6
+ Field,
7
+ FieldContent,
8
+ FieldDescription,
9
+ FieldError,
10
+ FieldLabel,
11
+ } from "@/components/ui/field";
12
+ import {
13
+ InputGroup,
14
+ InputGroupAddon,
15
+ InputGroupInput,
16
+ } from "@/components/ui/input-group";
17
+ import { CloudUpload, Eye, EyeOff, type LucideIcon } from "lucide-react";
18
+ import type { ChangeEvent, HTMLInputTypeAttribute } from "react";
19
+ import * as React from "react";
20
+ import type { FieldPath, FieldValues } from "react-hook-form";
21
+ import { Controller } from "react-hook-form";
22
+
23
+ // Types
24
+ type ButtonVariant =
25
+ | "default"
26
+ | "outline"
27
+ | "destructive"
28
+ | "secondary"
29
+ | "ghost"
30
+ | "link";
31
+ type ButtonSize = "default" | "sm" | "lg" | "icon";
32
+
33
+ /** Base props shared across standalone & RHF modes */
34
+ type BaseProps = Omit<
35
+ React.InputHTMLAttributes<HTMLInputElement>,
36
+ "onChange" | "type" | "value"
37
+ > & {
38
+ icon?: LucideIcon;
39
+ startIcon?: LucideIcon;
40
+ label?: React.ReactNode;
41
+ labelNode?: React.ReactNode;
42
+ disable?: boolean;
43
+ isDisabled?: boolean;
44
+ value?: string | number | boolean;
45
+ defaultValue?: string | number | boolean;
46
+ requiredMark?: boolean;
47
+ hint?: React.ReactNode;
48
+ onChange?: React.ChangeEventHandler<HTMLInputElement>;
49
+ type?: HTMLInputTypeAttribute;
50
+ parseNumber?: boolean;
51
+ preventWheelChange?: boolean;
52
+ id?: string;
53
+ className?: string;
54
+ inputClassName?: string;
55
+ multiple?: boolean;
56
+ accept?: string;
57
+ onFilesChange?: (files: File[] | null) => void;
58
+ fileIcon?: LucideIcon;
59
+ fileButtonLabel?: string;
60
+ fileButtonVariant?: ButtonVariant;
61
+ fileButtonSize?: ButtonSize;
62
+ };
63
+
64
+ /** Props for generic + optional RHF name */
65
+ export type InputFieldProps<
66
+ TFieldValues extends FieldValues = FieldValues,
67
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
68
+ > = BaseProps & {
69
+ name?: TName;
70
+ };
71
+
72
+ // Helpers
73
+ function toTextInputValue(v: unknown): string | number {
74
+ if (v === null || v === undefined) return "";
75
+ if (typeof v === "boolean") return v ? "1" : "";
76
+ if (typeof v === "string" || typeof v === "number") return v;
77
+ return "";
78
+ }
79
+
80
+ function makeSyntheticCheckboxChange(
81
+ checked: boolean,
82
+ ): React.ChangeEvent<HTMLInputElement> {
83
+ const input = document.createElement("input");
84
+ input.type = "checkbox";
85
+ input.checked = checked;
86
+ const evt = new Event("change", { bubbles: true });
87
+ Object.defineProperty(evt, "target", { writable: false, value: input });
88
+ return evt as unknown as React.ChangeEvent<HTMLInputElement>;
89
+ }
90
+
91
+ // Component
92
+ export default function InputField<
93
+ TFieldValues extends FieldValues = FieldValues,
94
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
95
+ >({
96
+ name,
97
+ icon: LabelIcon,
98
+ startIcon,
99
+ label,
100
+ labelNode,
101
+ placeholder,
102
+ type = "text",
103
+ disabled,
104
+ isDisabled,
105
+ disable,
106
+ className,
107
+ inputClassName,
108
+ value,
109
+ defaultValue,
110
+ requiredMark,
111
+ hint,
112
+ onChange,
113
+ id,
114
+ parseNumber,
115
+ preventWheelChange,
116
+ multiple,
117
+ accept,
118
+ onFilesChange,
119
+ fileIcon: FileIcon,
120
+ fileButtonLabel = "Import",
121
+ fileButtonVariant = "outline",
122
+ fileButtonSize = "default",
123
+ ...rest
124
+ }: InputFieldProps<TFieldValues, TName>) {
125
+ const effectiveDisabled = disabled ?? isDisabled ?? disable ?? false;
126
+ const isFile = type === "file";
127
+ const isCheckbox = type === "checkbox";
128
+ const shouldParseNumber = !isCheckbox && (parseNumber ?? type === "number");
129
+ const shouldPreventWheel =
130
+ !isCheckbox && (preventWheelChange ?? shouldParseNumber);
131
+ const labelContent = labelNode ?? label;
132
+
133
+ const hiddenFileRef = React.useRef<HTMLInputElement | null>(null);
134
+ const chooseFile = React.useCallback(
135
+ () => hiddenFileRef.current?.click(),
136
+ [],
137
+ );
138
+
139
+ const [internal, setInternal] = React.useState<
140
+ string | number | boolean | undefined
141
+ >(defaultValue);
142
+
143
+ const [showPassword, setShowPassword] = React.useState(false);
144
+ const isPassword = type === "password";
145
+ const effectiveType =
146
+ isPassword && showPassword ? ("text" as HTMLInputTypeAttribute) : type;
147
+
148
+ const inFormMode = Boolean(name);
149
+
150
+ const toNumberIfNeeded = (raw: string): string | number => {
151
+ if (!shouldParseNumber) return raw;
152
+ return raw === "" ? "" : Number(raw);
153
+ };
154
+
155
+ const LabelIconEffective: LucideIcon | undefined = startIcon ?? LabelIcon;
156
+ const FileButtonIcon: LucideIcon = FileIcon ?? CloudUpload;
157
+
158
+ if (inFormMode) {
159
+ // RHF mode using Controller + Field components
160
+ return (
161
+ <Controller<TFieldValues, TName>
162
+ name={name as TName}
163
+ render={({ field, fieldState }) => {
164
+ const inputId = id ?? (field.name as string);
165
+
166
+ const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
167
+ const fl = e.target.files;
168
+ field.onChange(fl);
169
+ onFilesChange?.(fl ? Array.from(fl) : null);
170
+ };
171
+
172
+ const handleChangeTextOrNumber = (
173
+ e: ChangeEvent<HTMLInputElement>,
174
+ ) => {
175
+ const raw = e.target.value;
176
+ const val = toNumberIfNeeded(raw);
177
+ field.onChange(val);
178
+ onChange?.(e);
179
+ };
180
+
181
+ const assignRefs = (el: HTMLInputElement | null) => {
182
+ hiddenFileRef.current = el;
183
+ const ref = field.ref as
184
+ | ((instance: HTMLInputElement | null) => void)
185
+ | React.MutableRefObject<HTMLInputElement | null>
186
+ | undefined;
187
+ if (typeof ref === "function") ref(el);
188
+ else if (ref && "current" in ref) ref.current = el;
189
+ };
190
+
191
+ return (
192
+ <Field className={className} data-invalid={fieldState.invalid}>
193
+ {!isCheckbox && labelContent ? (
194
+ <FieldLabel
195
+ className="mb-2 inline-flex items-center gap-2"
196
+ htmlFor={inputId}
197
+ >
198
+ {LabelIcon ? (
199
+ <LabelIcon className="h-4 w-4" />
200
+ ) : LabelIconEffective ? (
201
+ <LabelIconEffective className="h-4 w-4" />
202
+ ) : null}
203
+ {labelContent}
204
+ {requiredMark ? (
205
+ <span className="ml-0.5 text-destructive">*</span>
206
+ ) : null}
207
+ </FieldLabel>
208
+ ) : null}
209
+
210
+ <FieldContent>
211
+ {isFile ? (
212
+ <div className="flex items-center gap-2">
213
+ <input
214
+ id={inputId}
215
+ ref={assignRefs}
216
+ type="file"
217
+ className="hidden"
218
+ disabled={effectiveDisabled}
219
+ multiple={multiple}
220
+ accept={accept}
221
+ onChange={handleFileChange}
222
+ {...rest}
223
+ />
224
+ <Button
225
+ type="button"
226
+ variant={fileButtonVariant}
227
+ size={fileButtonSize}
228
+ onClick={chooseFile}
229
+ disabled={effectiveDisabled}
230
+ className="gap-2"
231
+ >
232
+ <FileButtonIcon className="h-4 w-4" />
233
+ {fileButtonLabel}
234
+ </Button>
235
+ </div>
236
+ ) : isCheckbox ? (
237
+ <div className="flex items-center gap-2">
238
+ <Checkbox
239
+ id={inputId}
240
+ checked={Boolean(field.value)}
241
+ onCheckedChange={(v) => field.onChange(Boolean(v))}
242
+ disabled={effectiveDisabled}
243
+ />
244
+ {labelContent ? (
245
+ <FieldLabel
246
+ htmlFor={inputId}
247
+ className="cursor-pointer select-none"
248
+ >
249
+ {labelContent}
250
+ {requiredMark ? (
251
+ <span className="ml-0.5 text-destructive">*</span>
252
+ ) : null}
253
+ </FieldLabel>
254
+ ) : null}
255
+ </div>
256
+ ) : (
257
+ <InputGroup>
258
+ <InputGroupInput
259
+ id={inputId}
260
+ type={effectiveType}
261
+ placeholder={placeholder}
262
+ disabled={effectiveDisabled}
263
+ value={toTextInputValue(field.value)}
264
+ onChange={handleChangeTextOrNumber}
265
+ onWheel={
266
+ shouldPreventWheel
267
+ ? (e) => (e.currentTarget as HTMLInputElement).blur()
268
+ : undefined
269
+ }
270
+ className={inputClassName}
271
+ ref={field.ref}
272
+ {...rest}
273
+ />
274
+ {isPassword ? (
275
+ <InputGroupAddon align="inline-end">
276
+ <Button
277
+ type="button"
278
+ variant="ghost"
279
+ size="icon"
280
+ className="h-full px-3 py-2 hover:bg-transparent"
281
+ onClick={() => setShowPassword(!showPassword)}
282
+ disabled={effectiveDisabled}
283
+ >
284
+ {showPassword ? (
285
+ <EyeOff className="h-4 w-4" />
286
+ ) : (
287
+ <Eye className="h-4 w-4" />
288
+ )}
289
+ </Button>
290
+ </InputGroupAddon>
291
+ ) : null}
292
+ </InputGroup>
293
+ )}
294
+ </FieldContent>
295
+
296
+ {hint ? <FieldDescription>{hint}</FieldDescription> : null}
297
+ {fieldState.invalid && <FieldError errors={[fieldState.error]} />}
298
+ </Field>
299
+ );
300
+ }}
301
+ />
302
+ );
303
+ }
304
+
305
+ // Standalone mode
306
+ const isControlled = value !== undefined;
307
+ const currentValue = isControlled ? value : internal;
308
+
309
+ const inputId =
310
+ id ??
311
+ (typeof label === "string"
312
+ ? `input-${label.toLowerCase().replace(/\s+/g, "-")}`
313
+ : undefined);
314
+
315
+ const handleStandaloneChange: React.ChangeEventHandler<HTMLInputElement> = (
316
+ e,
317
+ ) => {
318
+ const raw = e.target.value;
319
+ const val = toNumberIfNeeded(raw);
320
+ if (!isControlled) setInternal(val);
321
+ onChange?.(e);
322
+ };
323
+
324
+ const handleStandaloneFileChange: React.ChangeEventHandler<
325
+ HTMLInputElement
326
+ > = (e) => {
327
+ const fl = e.target.files;
328
+ onFilesChange?.(fl ? Array.from(fl) : null);
329
+ onChange?.(e);
330
+ };
331
+
332
+ const handleStandaloneCheckboxChange = (checked: boolean) => {
333
+ if (!isControlled) setInternal(checked);
334
+ if (onChange) onChange(makeSyntheticCheckboxChange(checked));
335
+ };
336
+
337
+ return (
338
+ <div className={className}>
339
+ {!isCheckbox && labelContent ? (
340
+ <FieldLabel
341
+ className="mb-2 inline-flex items-center gap-2"
342
+ htmlFor={inputId}
343
+ >
344
+ {LabelIcon ? <LabelIcon className="h-4 w-4" /> : null}
345
+ {labelContent}
346
+ {requiredMark ? (
347
+ <span className="ml-0.5 text-destructive">*</span>
348
+ ) : null}
349
+ </FieldLabel>
350
+ ) : null}
351
+
352
+ {isFile ? (
353
+ <div className="flex items-center gap-2">
354
+ <input
355
+ id={inputId}
356
+ ref={hiddenFileRef}
357
+ type="file"
358
+ className="hidden"
359
+ disabled={effectiveDisabled}
360
+ multiple={multiple}
361
+ accept={accept}
362
+ onChange={handleStandaloneFileChange}
363
+ {...rest}
364
+ />
365
+ <Button
366
+ type="button"
367
+ variant={fileButtonVariant}
368
+ size={fileButtonSize}
369
+ onClick={chooseFile}
370
+ disabled={effectiveDisabled}
371
+ className="gap-2"
372
+ >
373
+ <FileButtonIcon className="h-4 w-4" />
374
+ {fileButtonLabel}
375
+ </Button>
376
+ </div>
377
+ ) : isCheckbox ? (
378
+ <div className="flex items-center gap-2">
379
+ <Checkbox
380
+ id={inputId}
381
+ checked={Boolean(currentValue)}
382
+ onCheckedChange={(v) => handleStandaloneCheckboxChange(Boolean(v))}
383
+ disabled={effectiveDisabled}
384
+ />
385
+ {labelContent ? (
386
+ <FieldLabel
387
+ htmlFor={inputId}
388
+ className="cursor-pointer select-none"
389
+ >
390
+ {labelContent}
391
+ {requiredMark ? (
392
+ <span className="ml-0.5 text-destructive">*</span>
393
+ ) : null}
394
+ </FieldLabel>
395
+ ) : null}
396
+ </div>
397
+ ) : (
398
+ <InputGroup>
399
+ <InputGroupInput
400
+ id={inputId}
401
+ type={effectiveType}
402
+ placeholder={placeholder}
403
+ disabled={effectiveDisabled}
404
+ value={toTextInputValue(currentValue)}
405
+ onChange={handleStandaloneChange}
406
+ onWheel={
407
+ shouldPreventWheel
408
+ ? (e) => (e.currentTarget as HTMLInputElement).blur()
409
+ : undefined
410
+ }
411
+ className={inputClassName}
412
+ {...rest}
413
+ />
414
+ {isPassword ? (
415
+ <InputGroupAddon align="inline-end">
416
+ <Button
417
+ type="button"
418
+ variant="ghost"
419
+ size="icon"
420
+ className="h-full px-3 py-2 hover:bg-transparent"
421
+ onClick={() => setShowPassword(!showPassword)}
422
+ disabled={effectiveDisabled}
423
+ >
424
+ {showPassword ? (
425
+ <EyeOff className="h-4 w-4" />
426
+ ) : (
427
+ <Eye className="h-4 w-4" />
428
+ )}
429
+ </Button>
430
+ </InputGroupAddon>
431
+ ) : null}
432
+ </InputGroup>
433
+ )}
434
+
435
+ {hint ? (
436
+ <p className="mt-1 text-xs text-muted-foreground">{hint}</p>
437
+ ) : null}
438
+ </div>
439
+ );
440
+ }
@@ -1,15 +1,9 @@
1
- import nodemailer from "nodemailer";
2
- {{#if framework == "express"}}
3
1
  import ejs from "ejs";
4
2
  import status from "http-status";
3
+ import nodemailer from "nodemailer";
5
4
  import path from "path";
6
5
  import { envVars } from "../../config/env";
7
6
  import { AppError } from "../errors/app-error";
8
- {{/if}}
9
- {{#if framework == "nextjs"}}
10
- import { renderEmailTemplate } from "../email/otp-template";
11
- import { envVars } from "../env";
12
- {{/if}}
13
7
 
14
8
  const transporter = nodemailer.createTransport({
15
9
  host: envVars.EMAIL_SENDER.SMTP_HOST,
@@ -21,6 +15,8 @@ const transporter = nodemailer.createTransport({
21
15
  port: Number(envVars.EMAIL_SENDER.SMTP_PORT),
22
16
  });
23
17
 
18
+ transporter.verify().catch(() => null);
19
+
24
20
  interface SendEmailOptions {
25
21
  to: string;
26
22
  subject: string;
@@ -41,17 +37,27 @@ export const sendEmail = async ({
41
37
  attachments,
42
38
  }: SendEmailOptions) => {
43
39
  try {
44
- {{#if framework == "express"}}
45
- const templatePath = path.resolve(
40
+ const templatePath = path.resolve(
46
41
  process.cwd(),
47
42
  `src/templates/${templateName}.ejs`,
48
43
  );
49
44
 
50
- const html = await ejs.renderFile(templatePath, templateData);
51
- {{/if}}
52
- {{#if framework == "nextjs"}}
53
- const html = renderEmailTemplate(templateName, templateData);
54
- {{/if}}
45
+ const td = templateData as Record<string, unknown>;
46
+ const expiresVal =
47
+ td && Object.prototype.hasOwnProperty.call(td, "expiresInMinutes")
48
+ ? td["expiresInMinutes"]
49
+ : undefined;
50
+ const expiresInMinutes = typeof expiresVal === "number" ? expiresVal : 5;
51
+
52
+ const templateDataWithDefaults: Record<string, unknown> = {
53
+ appName: envVars.APP_NAME ?? "Your App",
54
+ supportEmail: envVars.EMAIL_SENDER.SMTP_FROM ?? "support@example.com",
55
+ year: new Date().getFullYear(),
56
+ expiresInMinutes,
57
+ ...td,
58
+ };
59
+
60
+ const html = await ejs.renderFile(templatePath, templateDataWithDefaults);
55
61
 
56
62
  await transporter.sendMail({
57
63
  from: envVars.EMAIL_SENDER.SMTP_FROM,
@@ -65,10 +71,6 @@ export const sendEmail = async ({
65
71
  })),
66
72
  });
67
73
  } catch {
68
- {{#if framework == "express"}}
69
74
  throw new AppError(status.INTERNAL_SERVER_ERROR, `Failed to send email to ${to}`);
70
- {{else}}
71
- throw new Error(`Failed to send email to ${to}`);
72
- {{/if}}
73
75
  }
74
76
  };