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.
Files changed (192) 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 -50
  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} +9 -4
  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/helper.ts → express/mongo-modules/auth.helper.ts} +11 -1
  28. package/modules/auth/better-auth/files/express/types/express.d.ts +11 -0
  29. package/modules/auth/better-auth/files/nextjs/api-route.ts +74 -0
  30. package/modules/auth/better-auth/files/nextjs/dashboard/pages/(user)/page.tsx +6 -0
  31. package/modules/auth/better-auth/files/nextjs/dashboard/pages/admin/page.tsx +6 -0
  32. package/modules/auth/better-auth/files/nextjs/dashboard/pages/layout.tsx +48 -0
  33. package/modules/auth/better-auth/files/nextjs/dashboard/pages/my-profile/page.tsx +5 -0
  34. package/modules/auth/better-auth/files/nextjs/features/services/auth.service.ts +102 -0
  35. package/modules/auth/better-auth/files/nextjs/layout/layout.tsx +13 -0
  36. package/modules/auth/better-auth/files/nextjs/lib/axios/http.ts +158 -0
  37. package/modules/auth/better-auth/files/nextjs/lib/env.ts +35 -0
  38. package/modules/auth/better-auth/files/nextjs/lib/utils/auth.ts +75 -0
  39. package/modules/auth/better-auth/files/nextjs/lib/utils/cookie.ts +29 -0
  40. package/modules/auth/better-auth/files/nextjs/lib/utils/jwt.ts +28 -0
  41. package/modules/auth/better-auth/files/nextjs/lib/utils/token.ts +49 -0
  42. package/modules/auth/better-auth/files/nextjs/pages/forgot-password/page.tsx +5 -0
  43. package/modules/auth/better-auth/files/nextjs/pages/layout.tsx +11 -0
  44. package/modules/auth/better-auth/files/nextjs/pages/login/page.tsx +9 -0
  45. package/modules/auth/better-auth/files/nextjs/pages/register/page.tsx +5 -0
  46. package/modules/auth/better-auth/files/nextjs/pages/reset-password/page.tsx +10 -0
  47. package/modules/auth/better-auth/files/nextjs/pages/verify-email/page.tsx +10 -0
  48. package/modules/auth/better-auth/files/nextjs/proxy.ts +154 -42
  49. package/modules/auth/better-auth/files/nextjs/theme/providers/theme-provider.tsx +11 -0
  50. package/modules/auth/better-auth/files/nextjs/types/api.types.ts +18 -0
  51. package/modules/auth/better-auth/files/react/components/protected-route.tsx +39 -0
  52. package/modules/auth/better-auth/files/react/components/route-guards.tsx +13 -0
  53. package/modules/auth/better-auth/files/react/dashboard/admin/pages/overview.tsx +3 -0
  54. package/modules/auth/better-auth/files/react/dashboard/pages/overview.tsx +3 -0
  55. package/modules/auth/better-auth/files/react/features/pages/forgot-password.tsx +5 -0
  56. package/modules/auth/better-auth/files/react/features/pages/login.tsx +5 -0
  57. package/modules/auth/better-auth/files/react/features/pages/my-profile.tsx +5 -0
  58. package/modules/auth/better-auth/files/react/features/pages/oauth-callback.tsx +59 -0
  59. package/modules/auth/better-auth/files/react/features/pages/register.tsx +5 -0
  60. package/modules/auth/better-auth/files/react/features/pages/reset-password.tsx +10 -0
  61. package/modules/auth/better-auth/files/react/features/pages/verify-email.tsx +10 -0
  62. package/modules/auth/better-auth/files/react/layout/dashboard-layout.tsx +54 -0
  63. package/modules/auth/better-auth/files/react/lib/axios/http.ts +68 -0
  64. package/modules/auth/better-auth/files/react/lib/env.ts +25 -0
  65. package/modules/auth/better-auth/files/react/router.tsx +73 -0
  66. package/modules/auth/better-auth/files/react/theme/components/providers/theme-provider-context.ts +13 -0
  67. package/modules/auth/better-auth/files/react/theme/components/providers/theme-provider.tsx +51 -0
  68. package/modules/auth/better-auth/files/react/theme/hooks/use-theme.ts +8 -0
  69. package/modules/auth/better-auth/files/shared/features/components/change-password-dialog.tsx +113 -0
  70. package/modules/auth/better-auth/files/shared/features/components/forgot-password-form.tsx +84 -0
  71. package/modules/auth/better-auth/files/shared/features/components/login-form.tsx +134 -0
  72. package/modules/auth/better-auth/files/shared/features/components/my-profile.tsx +147 -0
  73. package/modules/auth/better-auth/files/shared/features/components/profile-form.tsx +205 -0
  74. package/modules/auth/better-auth/files/shared/features/components/register-form.tsx +100 -0
  75. package/modules/auth/better-auth/files/shared/features/components/reset-password-form.tsx +111 -0
  76. package/modules/auth/better-auth/files/shared/features/components/social-login-buttons.tsx +47 -0
  77. package/modules/auth/better-auth/files/shared/features/components/user-profile-menu.tsx +106 -0
  78. package/modules/auth/better-auth/files/shared/features/components/verify-email-form.tsx +110 -0
  79. package/modules/auth/better-auth/files/shared/features/queries/auth.mutations.tsx +312 -0
  80. package/modules/auth/better-auth/files/shared/features/queries/auth.querie.ts +19 -0
  81. package/modules/auth/better-auth/files/shared/features/services/auth.api.ts +81 -0
  82. package/modules/auth/better-auth/files/shared/features/types/auth.type.ts +47 -0
  83. package/modules/auth/better-auth/files/shared/features/validators/change-password.validator.ts +18 -0
  84. package/modules/auth/better-auth/files/shared/features/validators/forgot.validator.ts +7 -0
  85. package/modules/auth/better-auth/files/shared/features/validators/login.validator.ts +14 -0
  86. package/modules/auth/better-auth/files/shared/features/validators/profile.validator.ts +8 -0
  87. package/modules/auth/better-auth/files/shared/features/validators/register.validator.ts +9 -0
  88. package/modules/auth/better-auth/files/shared/features/validators/reset.validator.ts +9 -0
  89. package/modules/auth/better-auth/files/shared/features/validators/verify.validator.ts +8 -0
  90. package/modules/auth/better-auth/files/shared/lib/auth-client.ts +2 -1
  91. package/modules/auth/better-auth/files/shared/lib/auth.ts +5 -19
  92. package/modules/auth/better-auth/files/shared/lib/constant/dashboard.ts +90 -0
  93. package/modules/auth/better-auth/files/shared/theme/mode-toggle.tsx +30 -0
  94. package/modules/auth/better-auth/files/shared/ui/shadcn/components/dashboard/dashboard-header.tsx +94 -0
  95. package/modules/auth/better-auth/files/shared/ui/shadcn/components/dashboard/dashboard-sidebar.tsx +255 -0
  96. package/modules/auth/better-auth/files/shared/ui/shadcn/components/footer.tsx +35 -0
  97. package/modules/auth/better-auth/files/shared/ui/shadcn/components/navbar.tsx +145 -0
  98. package/modules/auth/better-auth/files/shared/ui/shadcn/form-field/input-field.tsx +440 -0
  99. package/modules/auth/better-auth/files/shared/utils/email.ts +2 -17
  100. package/modules/auth/better-auth/generator.json +172 -51
  101. package/modules/auth/better-auth/module.json +2 -2
  102. package/modules/components/files/shared/hooks/use-file-upload.ts +412 -0
  103. package/modules/components/files/shared/lib/utils/url-helpers.ts +110 -0
  104. package/modules/components/files/shared/shadcn/dashboard/data-table-column-selector.tsx +52 -0
  105. package/modules/components/files/shared/shadcn/dashboard/data-table-footer.tsx +156 -0
  106. package/modules/components/files/shared/shadcn/dashboard/data-table.tsx +405 -0
  107. package/modules/components/files/shared/shadcn/global/form-field/input-field.tsx +440 -0
  108. package/modules/components/files/shared/shadcn/global/form-field/media-uploader-field.tsx +745 -0
  109. package/modules/components/files/shared/shadcn/global/form-field/multi-select-field.tsx +207 -0
  110. package/modules/components/files/shared/shadcn/global/form-field/select-field.tsx +247 -0
  111. package/modules/components/files/shared/shadcn/global/form-field/textarea-field.tsx +277 -0
  112. package/modules/components/files/shared/shadcn/global/form-field/tiptap-editor-field.tsx +35 -0
  113. package/modules/components/files/shared/shadcn/global/no-results.tsx +41 -0
  114. package/modules/components/files/shared/shadcn/tiptap-editor/editor-menu-bar.tsx +217 -0
  115. package/modules/components/files/shared/shadcn/tiptap-editor/tiptap-editor.tsx +104 -0
  116. package/modules/components/files/shared/url/load-more.tsx +93 -0
  117. package/modules/components/files/shared/url/search-bar.tsx +131 -0
  118. package/modules/components/files/shared/url/sort-select.tsx +118 -0
  119. package/modules/components/files/shared/url/url-tabs.tsx +77 -0
  120. package/modules/components/generator.json +109 -0
  121. package/modules/components/module.json +11 -0
  122. package/modules/database/mongoose/generator.json +3 -14
  123. package/modules/database/mongoose/module.json +2 -2
  124. package/modules/database/prisma/generator.json +6 -12
  125. package/modules/database/prisma/module.json +2 -2
  126. package/modules/storage/cloudinary/files/express/config/env.ts +65 -0
  127. package/modules/storage/cloudinary/files/express/config/media.ts +103 -0
  128. package/modules/storage/cloudinary/files/express/modules/media/media.controller.ts +59 -0
  129. package/modules/storage/cloudinary/files/express/modules/media/media.route.ts +29 -0
  130. package/modules/storage/cloudinary/files/express/modules/media/media.service.ts +113 -0
  131. package/modules/storage/cloudinary/files/express/modules/media/media.type.ts +32 -0
  132. package/modules/storage/cloudinary/generator.json +34 -0
  133. package/modules/storage/cloudinary/module.json +11 -0
  134. package/modules/ui/shadcn/generator.json +21 -0
  135. package/modules/ui/shadcn/module.json +11 -0
  136. package/package.json +24 -26
  137. package/templates/express/README.md +11 -16
  138. package/templates/express/src/config/env.ts +7 -5
  139. package/templates/nextjs/README.md +13 -18
  140. package/templates/nextjs/app/favicon.ico +0 -0
  141. package/templates/nextjs/app/layout.tsx +6 -4
  142. package/templates/nextjs/components/providers/query-provider.tsx +3 -0
  143. package/templates/nextjs/env.example +3 -1
  144. package/templates/nextjs/lib/axios/http.ts +23 -0
  145. package/templates/nextjs/lib/env.ts +7 -5
  146. package/templates/nextjs/package.json +2 -1
  147. package/templates/nextjs/template.json +1 -2
  148. package/templates/react/README.md +9 -14
  149. package/templates/react/index.html +1 -1
  150. package/templates/react/package.json +1 -1
  151. package/templates/react/src/assets/favicon.ico +0 -0
  152. package/templates/react/src/components/providers/query-provider.tsx +38 -0
  153. package/templates/react/src/{shared/components → components}/seo.tsx +4 -8
  154. package/templates/react/src/lib/axios/http.ts +24 -0
  155. package/templates/react/src/main.tsx +8 -11
  156. package/templates/react/src/{features/about/pages → pages}/about.tsx +1 -1
  157. package/templates/react/src/{features/home/pages → pages}/home.tsx +1 -1
  158. package/templates/react/src/router.tsx +6 -6
  159. package/templates/react/src/vite-env.d.ts +2 -1
  160. package/templates/react/template.json +0 -1
  161. package/templates/react/tsconfig.app.json +6 -0
  162. package/templates/react/tsconfig.json +7 -1
  163. package/templates/react/vite.config.ts +12 -0
  164. package/modules/auth/authjs/files/nextjs/api/auth/[...nextauth]/route.ts +0 -3
  165. package/modules/auth/authjs/files/nextjs/proxy.ts +0 -1
  166. package/modules/auth/authjs/files/shared/lib/auth.ts +0 -119
  167. package/modules/auth/authjs/files/shared/prisma/schema.prisma +0 -61
  168. package/modules/auth/authjs/generator.json +0 -64
  169. package/modules/auth/authjs/module.json +0 -13
  170. package/modules/auth/better-auth/files/express/modules/auth/auth.controller.ts +0 -264
  171. package/modules/auth/better-auth/files/express/modules/auth/auth.service.ts +0 -549
  172. package/modules/auth/better-auth/files/express/templates/google-redirect.ejs +0 -24
  173. package/modules/auth/better-auth/files/nextjs/api/auth/[...all]/route.ts +0 -4
  174. package/modules/auth/better-auth/files/nextjs/lib/auth/auth-guards.ts +0 -31
  175. package/modules/auth/better-auth/files/nextjs/templates/email-otp.tsx +0 -74
  176. package/templates/nextjs/lib/api/http.ts +0 -40
  177. package/templates/react/public/vite.svg +0 -1
  178. package/templates/react/src/app/layouts/dashboard-layout.tsx +0 -8
  179. package/templates/react/src/app/layouts/public-layout.tsx +0 -5
  180. package/templates/react/src/app/providers.tsx +0 -20
  181. package/templates/react/src/app/router.tsx +0 -21
  182. package/templates/react/src/assets/react.svg +0 -1
  183. package/templates/react/src/shared/api/http.ts +0 -39
  184. package/templates/react/src/shared/components/loading.tsx +0 -8
  185. package/templates/react/src/shared/lib/query-client.ts +0 -12
  186. package/templates/react/src/utils/storage.ts +0 -35
  187. package/templates/react/src/utils/utils.ts +0 -3
  188. /package/modules/auth/better-auth/files/{shared/mongoose/auth/constants.ts → express/mongo-modules/auth.constants.ts} +0 -0
  189. /package/templates/nextjs/app/{page.tsx → (public)/(root)/page.tsx} +0 -0
  190. /package/templates/react/src/{shared/components → components}/error-boundary.tsx +0 -0
  191. /package/templates/react/src/{shared/components → components}/layout.tsx +0 -0
  192. /package/templates/react/src/{shared/pages → pages}/not-found.tsx +0 -0
@@ -0,0 +1,277 @@
1
+ "use client";
2
+
3
+ import {
4
+ Field,
5
+ FieldContent,
6
+ FieldDescription,
7
+ FieldError,
8
+ FieldLabel,
9
+ } from "@/components/ui/field";
10
+ import { Textarea } from "@/components/ui/textarea";
11
+ import { cn } from "@/lib/utils";
12
+ import type { LucideIcon } from "lucide-react";
13
+ import * as React from "react";
14
+ import type { FieldPath, FieldValues } from "react-hook-form";
15
+ import { Controller } from "react-hook-form";
16
+
17
+ type BaseProps = {
18
+ id?: string;
19
+ icon?: LucideIcon;
20
+ label?: React.ReactNode;
21
+ placeholder?: string;
22
+ description?: React.ReactNode;
23
+ className?: string;
24
+ wrapperClassName?: string;
25
+ textareaClassName?: string;
26
+ rows?: number;
27
+ minHeight?: string;
28
+ maxLength?: number;
29
+ showCount?: boolean;
30
+ disabled?: boolean;
31
+ required?: boolean;
32
+ autoResize?: boolean;
33
+ trimOnBlur?: boolean;
34
+ readOnly?: boolean;
35
+ value?: string;
36
+ onValueChange?: (value: string) => void;
37
+ onChange?: React.ChangeEventHandler<HTMLTextAreaElement>;
38
+ onKeyUp?: React.KeyboardEventHandler<HTMLTextAreaElement>;
39
+ onPaste?: React.ClipboardEventHandler<HTMLTextAreaElement>;
40
+ onBlur?: React.FocusEventHandler<HTMLTextAreaElement>;
41
+ defaultValue?: string;
42
+ error?: React.ReactNode;
43
+ };
44
+
45
+ export type TextareaFieldProps<
46
+ TFieldValues extends FieldValues = FieldValues,
47
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
48
+ > = BaseProps & { name?: TName };
49
+
50
+ const TextareaField = React.forwardRef<
51
+ HTMLTextAreaElement,
52
+ TextareaFieldProps<FieldValues, string>
53
+ >((props, ref) => {
54
+ const {
55
+ name,
56
+ id,
57
+ icon: Icon,
58
+ label,
59
+ placeholder,
60
+ description,
61
+ className,
62
+ wrapperClassName,
63
+ textareaClassName,
64
+ rows = 4,
65
+ minHeight,
66
+ maxLength,
67
+ showCount = false,
68
+ disabled = false,
69
+ required = false,
70
+ autoResize = false,
71
+ trimOnBlur = false,
72
+ readOnly = false,
73
+ value: externalValue,
74
+ onValueChange,
75
+ onChange,
76
+ onKeyUp,
77
+ onPaste,
78
+ onBlur,
79
+ defaultValue,
80
+ error,
81
+ } = props;
82
+ const innerRef = React.useRef<HTMLTextAreaElement | null>(null);
83
+ const setMergedRef = React.useCallback(
84
+ (el: HTMLTextAreaElement | null) => {
85
+ innerRef.current = el;
86
+ if (typeof ref === "function") ref(el);
87
+ else if (ref)
88
+ (ref as React.MutableRefObject<HTMLTextAreaElement | null>).current =
89
+ el;
90
+ },
91
+ [ref]
92
+ );
93
+
94
+ const autoId = React.useId();
95
+ const textareaId = id ?? autoId;
96
+
97
+ const [internal, setInternal] = React.useState<string>(defaultValue ?? "");
98
+ const standaloneValue = externalValue ?? internal;
99
+
100
+ const resizeNow = React.useCallback(() => {
101
+ if (!autoResize || !innerRef.current) return;
102
+ const el = innerRef.current;
103
+ el.style.height = "auto";
104
+ el.style.height = `${el.scrollHeight}px`;
105
+ }, [autoResize]);
106
+
107
+ React.useEffect(() => {
108
+ resizeNow();
109
+ }, [resizeNow]);
110
+
111
+ if (!name) {
112
+ const value = standaloneValue ?? "";
113
+
114
+ const handleChange: React.ChangeEventHandler<HTMLTextAreaElement> = (e) => {
115
+ const next = e.target.value;
116
+ void (onValueChange ? onValueChange(next) : setInternal(next));
117
+ onChange?.(e);
118
+ resizeNow();
119
+ };
120
+
121
+ const handleBlur: React.FocusEventHandler<HTMLTextAreaElement> = (e) => {
122
+ if (trimOnBlur) {
123
+ const trimmed = e.target.value.trim();
124
+ if (trimmed !== e.target.value) {
125
+ void (onValueChange ? onValueChange(trimmed) : setInternal(trimmed));
126
+ }
127
+ }
128
+ onBlur?.(e);
129
+ };
130
+
131
+ const count = typeof value === "string" ? value.length : 0;
132
+
133
+ return (
134
+ <div className={className}>
135
+ {label ? (
136
+ <FieldLabel htmlFor={textareaId} className="mb-2 inline-flex items-center gap-2">
137
+ {Icon && <Icon className="w-4 h-4" />}
138
+ {label}
139
+ {required && <span className="ml-0.5 text-destructive">*</span>}
140
+ </FieldLabel>
141
+ ) : null}
142
+
143
+ <div
144
+ className={cn(
145
+ "overflow-hidden rounded-md dark:bg-transparent",
146
+ wrapperClassName
147
+ )}
148
+ >
149
+ <Textarea
150
+ ref={setMergedRef}
151
+ id={textareaId}
152
+ placeholder={placeholder}
153
+ value={value}
154
+ onChange={handleChange}
155
+ onBlur={handleBlur}
156
+ onKeyUp={onKeyUp}
157
+ onPaste={onPaste}
158
+ disabled={disabled}
159
+ readOnly={readOnly}
160
+ rows={rows}
161
+ maxLength={maxLength}
162
+ className={cn("w-full", minHeight, textareaClassName)}
163
+ aria-required={required || undefined}
164
+ />
165
+ </div>
166
+
167
+ {description ? (
168
+ <FieldDescription>{description}</FieldDescription>
169
+ ) : null}
170
+
171
+ {showCount && (
172
+ <div
173
+ className={cn(
174
+ "mt-1 text-right text-xs",
175
+ maxLength && count >= maxLength * 0.95
176
+ ? "text-destructive"
177
+ : "text-muted-foreground"
178
+ )}
179
+ >
180
+ {count}
181
+ {maxLength ? ` / ${maxLength}` : ""}
182
+ </div>
183
+ )}
184
+
185
+ {error ? (
186
+ <p className="text-[0.8rem] font-medium text-destructive">{error}</p>
187
+ ) : null}
188
+ </div>
189
+ );
190
+ }
191
+
192
+ return (
193
+ <Controller
194
+ name={name}
195
+ render={({ field, fieldState }) => {
196
+ const value = externalValue ?? field.value ?? "";
197
+ const count = typeof value === "string" ? value.length : 0;
198
+
199
+ return (
200
+ <Field className={className} data-invalid={fieldState.invalid}>
201
+ {label ? (
202
+ <FieldLabel className="mb-2 inline-flex items-center gap-2" htmlFor={textareaId}>
203
+ {Icon && <Icon className="w-4 h-4" />}
204
+ {label}
205
+ {required && <span className="ml-0.5 text-destructive">*</span>}
206
+ </FieldLabel>
207
+ ) : null}
208
+
209
+ <FieldContent>
210
+ <div
211
+ className={cn(
212
+ "overflow-hidden rounded-md bg-light dark:bg-transparent",
213
+ wrapperClassName
214
+ )}
215
+ >
216
+ <Textarea
217
+ ref={setMergedRef}
218
+ id={textareaId}
219
+ placeholder={placeholder}
220
+ value={value}
221
+ onChange={(e) => {
222
+ field.onChange(e.target.value);
223
+ onValueChange?.(e.target.value);
224
+ onChange?.(e);
225
+ resizeNow();
226
+ }}
227
+ onBlur={(e) => {
228
+ if (trimOnBlur) {
229
+ const trimmed = e.target.value.trim();
230
+ if (trimmed !== e.target.value) {
231
+ field.onChange(trimmed);
232
+ onValueChange?.(trimmed);
233
+ }
234
+ }
235
+ field.onBlur();
236
+ onBlur?.(e);
237
+ }}
238
+ onKeyUp={onKeyUp}
239
+ onPaste={onPaste}
240
+ disabled={disabled || field.disabled}
241
+ readOnly={readOnly}
242
+ rows={rows}
243
+ maxLength={maxLength}
244
+ className={cn("w-full", minHeight, textareaClassName)}
245
+ aria-required={required || undefined}
246
+ />
247
+ </div>
248
+ </FieldContent>
249
+
250
+ {description ? (
251
+ <FieldDescription>{description}</FieldDescription>
252
+ ) : null}
253
+
254
+ {showCount && (
255
+ <div
256
+ className={cn(
257
+ "mt-1 text-right text-xs",
258
+ maxLength && count >= maxLength * 0.95
259
+ ? "text-destructive"
260
+ : "text-muted-foreground"
261
+ )}
262
+ >
263
+ {count}
264
+ {maxLength ? ` / ${maxLength}` : ""}
265
+ </div>
266
+ )}
267
+
268
+ {fieldState.invalid && <FieldError errors={[fieldState.error]} />}
269
+ </Field>
270
+ );
271
+ }}
272
+ />
273
+ );
274
+ });
275
+
276
+ TextareaField.displayName = "TextareaField";
277
+ export default TextareaField;
@@ -0,0 +1,35 @@
1
+ import TiptapEditor from "@/components/tiptap-editor/tiptap-editor";
2
+ import {
3
+ Field,
4
+ FieldContent,
5
+ FieldError,
6
+ FieldLabel,
7
+ } from "@/components/ui/field";
8
+ import { Controller } from "react-hook-form";
9
+
10
+ interface Props {
11
+ name: string;
12
+ label: string;
13
+ className?: string;
14
+ }
15
+
16
+ export default function TiptapEditorField({ name, label, className }: Props) {
17
+ return (
18
+ <Controller
19
+ name={name}
20
+ render={({ field, fieldState }) => (
21
+ <Field className={className} data-invalid={fieldState.invalid}>
22
+ <FieldLabel>{label}</FieldLabel>
23
+ <FieldContent>
24
+ <TiptapEditor
25
+ content={field.value}
26
+ onChange={field.onChange}
27
+ className="min-h-50 bg-input/50 rounded-md border px-3 py-2 focus:border-primary"
28
+ />
29
+ </FieldContent>
30
+ {fieldState.invalid && <FieldError errors={[fieldState.error]} />}
31
+ </Field>
32
+ )}
33
+ />
34
+ );
35
+ }
@@ -0,0 +1,41 @@
1
+ import { Card, CardContent } from "@/components/ui/card";
2
+ import { cn } from "@/lib/utils";
3
+ import { SearchX } from "lucide-react";
4
+
5
+ interface NoResultsProps {
6
+ title?: string;
7
+ description?: string;
8
+ className?: string;
9
+ }
10
+
11
+ export default function NoResults({
12
+ title = "No Results Found",
13
+ description = "We couldn't find what you're looking for. Try adjusting your search or filters.",
14
+ className,
15
+ }: NoResultsProps) {
16
+ return (
17
+ <Card className={cn("relative", className)}>
18
+ <CardContent className="flex min-h-[400px] flex-col items-center justify-center p-6">
19
+ {/* Main content */}
20
+ <div className="relative flex flex-col items-center">
21
+ {/* Icon with modern styling */}
22
+ <div className="group relative mb-8">
23
+ <div className="bg-card ring-border/50 relative flex h-16 w-16 items-center justify-center rounded-2xl shadow-sm ring-1 transition-all duration-300 group-hover:scale-105">
24
+ <SearchX className="text-muted-foreground group-hover:text-foreground h-8 w-8 transition-colors duration-300" />
25
+ </div>
26
+ </div>
27
+
28
+ {/* Text content */}
29
+ <div className="text-center">
30
+ <h3 className="text-foreground mb-3 text-2xl font-semibold">
31
+ {title}
32
+ </h3>
33
+ <p className="text-muted-foreground mx-auto max-w-md text-base">
34
+ {description}
35
+ </p>
36
+ </div>
37
+ </div>
38
+ </CardContent>
39
+ </Card>
40
+ );
41
+ }
@@ -0,0 +1,217 @@
1
+ "use client";
2
+
3
+ import type { Editor } from "@tiptap/core";
4
+ import {
5
+ AlignCenter,
6
+ AlignLeft,
7
+ AlignRight,
8
+ Bold as BoldIcon,
9
+ Columns,
10
+ Columns3,
11
+ Heading1,
12
+ Heading2,
13
+ Heading3,
14
+ Highlighter,
15
+ Italic as ItalicIcon,
16
+ Link as LinkIcon,
17
+ List,
18
+ ListOrdered,
19
+ ListPlus,
20
+ ListX,
21
+ Quote,
22
+ Redo,
23
+ Strikethrough as StrikeIcon,
24
+ Table as TableIcon,
25
+ Type,
26
+ Underline as UnderlineIcon,
27
+ Undo,
28
+ } from "lucide-react";
29
+ import { Toggle } from "../ui/toggle";
30
+
31
+ import "@tiptap/extension-blockquote";
32
+ import "@tiptap/extension-bold";
33
+ import "@tiptap/extension-bullet-list";
34
+ import "@tiptap/extension-code";
35
+ import "@tiptap/extension-heading";
36
+ import "@tiptap/extension-highlight";
37
+ import "@tiptap/extension-history";
38
+ import "@tiptap/extension-horizontal-rule";
39
+ import "@tiptap/extension-italic";
40
+ import "@tiptap/extension-link";
41
+ import "@tiptap/extension-list-item";
42
+ import "@tiptap/extension-ordered-list";
43
+ import "@tiptap/extension-strike";
44
+ import "@tiptap/extension-table";
45
+ import "@tiptap/extension-table/cell";
46
+ import "@tiptap/extension-table/header";
47
+ import "@tiptap/extension-table/row";
48
+ import "@tiptap/extension-text-align";
49
+ import "@tiptap/extension-underline";
50
+
51
+ export default function MenuBar({ editor }: { editor: Editor | null }) {
52
+ if (!editor) return null;
53
+
54
+ const opts = [
55
+ // Headings
56
+ {
57
+ icon: <Heading1 className="h-4 w-4" />,
58
+ action: () => editor.chain().focus().toggleHeading({ level: 1 }).run(),
59
+ pressed: editor.isActive("heading", { level: 1 }),
60
+ },
61
+ {
62
+ icon: <Heading2 className="h-4 w-4" />,
63
+ action: () => editor.chain().focus().toggleHeading({ level: 2 }).run(),
64
+ pressed: editor.isActive("heading", { level: 2 }),
65
+ },
66
+ {
67
+ icon: <Heading3 className="h-4 w-4" />,
68
+ action: () => editor.chain().focus().toggleHeading({ level: 3 }).run(),
69
+ pressed: editor.isActive("heading", { level: 3 }),
70
+ },
71
+
72
+ // Marks
73
+ {
74
+ icon: <BoldIcon className="h-4 w-4" />,
75
+ action: () => editor.chain().focus().toggleBold().run(),
76
+ pressed: editor.isActive("bold"),
77
+ },
78
+ {
79
+ icon: <ItalicIcon className="h-4 w-4" />,
80
+ action: () => editor.chain().focus().toggleItalic().run(),
81
+ pressed: editor.isActive("italic"),
82
+ },
83
+ {
84
+ icon: <UnderlineIcon className="h-4 w-4" />,
85
+ action: () => editor.chain().focus().toggleUnderline().run(),
86
+ pressed: editor.isActive("underline"),
87
+ },
88
+ {
89
+ icon: <StrikeIcon className="h-4 w-4" />,
90
+ action: () => editor.chain().focus().toggleStrike().run(),
91
+ pressed: editor.isActive("strike"),
92
+ },
93
+ {
94
+ icon: <Highlighter className="h-4 w-4" />,
95
+ action: () => editor.chain().focus().toggleHighlight().run(),
96
+ pressed: editor.isActive("highlight"),
97
+ },
98
+
99
+ // Link
100
+ {
101
+ icon: <LinkIcon className="h-4 w-4" />,
102
+ action: () => {
103
+ if (editor.isActive("link")) {
104
+ editor.chain().focus().unsetLink().run();
105
+ return;
106
+ }
107
+ const url =
108
+ typeof window !== "undefined" ? window.prompt("Enter URL") : null;
109
+ if (url) editor.chain().focus().setLink({ href: url }).run();
110
+ },
111
+ pressed: editor.isActive("link"),
112
+ },
113
+
114
+ // Blockquote / HR
115
+ {
116
+ icon: <Quote className="h-4 w-4" />,
117
+ action: () => editor.chain().focus().toggleBlockquote().run(),
118
+ pressed: editor.isActive("blockquote"),
119
+ },
120
+ {
121
+ icon: <Type className="h-4 w-4" />,
122
+ action: () => editor.chain().focus().setHorizontalRule().run(),
123
+ pressed: false,
124
+ },
125
+
126
+ // Align
127
+ {
128
+ icon: <AlignLeft className="h-4 w-4" />,
129
+ action: () => editor.chain().focus().setTextAlign("left").run(),
130
+ pressed: editor.isActive({ textAlign: "left" }),
131
+ },
132
+ {
133
+ icon: <AlignCenter className="h-4 w-4" />,
134
+ action: () => editor.chain().focus().setTextAlign("center").run(),
135
+ pressed: editor.isActive({ textAlign: "center" }),
136
+ },
137
+ {
138
+ icon: <AlignRight className="h-4 w-4" />,
139
+ action: () => editor.chain().focus().setTextAlign("right").run(),
140
+ pressed: editor.isActive({ textAlign: "right" }),
141
+ },
142
+
143
+ // Lists
144
+ {
145
+ icon: <List className="h-4 w-4" />,
146
+ action: () => editor.chain().focus().toggleBulletList().run(),
147
+ pressed: editor.isActive("bulletList"),
148
+ },
149
+ {
150
+ icon: <ListOrdered className="h-4 w-4" />,
151
+ action: () => editor.chain().focus().toggleOrderedList().run(),
152
+ pressed: editor.isActive("orderedList"),
153
+ },
154
+
155
+ // Table controls
156
+ {
157
+ icon: <TableIcon className="h-4 w-4" />,
158
+ action: () =>
159
+ editor.isActive("table")
160
+ ? editor.chain().focus().deleteTable().run()
161
+ : editor
162
+ .chain()
163
+ .focus()
164
+ .insertTable({ rows: 3, cols: 2, withHeaderRow: true })
165
+ .run(),
166
+ pressed: editor.isActive("table"),
167
+ },
168
+ {
169
+ icon: <ListPlus className="h-4 w-4" />,
170
+ action: () => editor.chain().focus().addRowAfter().run(),
171
+ pressed: false,
172
+ },
173
+ {
174
+ icon: <Columns className="h-4 w-4" />,
175
+ action: () => editor.chain().focus().addColumnAfter().run(),
176
+ pressed: false,
177
+ },
178
+ {
179
+ icon: <ListX className="h-4 w-4" />,
180
+ action: () => editor.chain().focus().deleteRow().run(),
181
+ pressed: false,
182
+ },
183
+ {
184
+ icon: <Columns3 className="h-4 w-4" />,
185
+ action: () => editor.chain().focus().deleteColumn().run(),
186
+ pressed: false,
187
+ },
188
+
189
+ // History
190
+ {
191
+ icon: <Undo className="h-4 w-4" />,
192
+ action: () => editor.chain().focus().undo().run(),
193
+ pressed: false,
194
+ },
195
+ {
196
+ icon: <Redo className="h-4 w-4" />,
197
+ action: () => editor.chain().focus().redo().run(),
198
+ pressed: false,
199
+ },
200
+ ];
201
+
202
+ return (
203
+ <div className="flex flex-wrap gap-1 rounded-md border border-border p-1">
204
+ {opts.map((o, i) => (
205
+ <Toggle
206
+ key={i as number}
207
+ pressed={o.pressed}
208
+ onPressedChange={() => o.action()}
209
+ aria-pressed={o.pressed}
210
+ className="data-[state=on]:bg-primary/10"
211
+ >
212
+ {o.icon}
213
+ </Toggle>
214
+ ))}
215
+ </div>
216
+ );
217
+ }
@@ -0,0 +1,104 @@
1
+ "use client";
2
+
3
+ import { cn } from "@/lib/utils";
4
+ import Blockquote from "@tiptap/extension-blockquote";
5
+ import HardBreak from "@tiptap/extension-hard-break";
6
+ import Highlight from "@tiptap/extension-highlight";
7
+ import HorizontalRule from "@tiptap/extension-horizontal-rule";
8
+ import Link from "@tiptap/extension-link";
9
+ import { Table } from "@tiptap/extension-table";
10
+ import { TableCell } from "@tiptap/extension-table/cell";
11
+ import { TableHeader } from "@tiptap/extension-table/header";
12
+ import { TableRow } from "@tiptap/extension-table/row";
13
+ import TextAlign from "@tiptap/extension-text-align";
14
+ import Underline from "@tiptap/extension-underline";
15
+ import { EditorContent, useEditor } from "@tiptap/react";
16
+ import StarterKit from "@tiptap/starter-kit";
17
+ import MenuBar from "./editor-menu-bar";
18
+
19
+ type EditorProps = {
20
+ content: string;
21
+ onChange: (content: string) => void;
22
+ editable?: boolean;
23
+ className?: string;
24
+ };
25
+
26
+ export default function TiptapEditor({
27
+ content,
28
+ onChange,
29
+ editable = true,
30
+ className,
31
+ }: EditorProps) {
32
+ const editor = useEditor({
33
+ editable,
34
+ content,
35
+ immediatelyRender: false,
36
+ extensions: [
37
+ StarterKit.configure({
38
+ bulletList: { HTMLAttributes: { class: "list-disc ml-4" } },
39
+ orderedList: { HTMLAttributes: { class: "list-decimal ml-4" } },
40
+ link: false,
41
+ underline: false,
42
+ blockquote: false,
43
+ horizontalRule: false,
44
+ hardBreak: false,
45
+ codeBlock: false,
46
+ code: false,
47
+ }),
48
+
49
+ Table.configure({
50
+ resizable: true,
51
+ HTMLAttributes: { class: "w-full table-auto border border-border" },
52
+ }),
53
+ TableRow,
54
+ TableHeader.configure({
55
+ HTMLAttributes: {
56
+ class:
57
+ "border border-border bg-muted text-foreground font-semibold px-2 py-1",
58
+ },
59
+ }),
60
+ TableCell.configure({
61
+ HTMLAttributes: { class: "border border-border px-2 py-1 text-sm" },
62
+ }),
63
+
64
+ TextAlign.configure({ types: ["heading", "paragraph"] }),
65
+
66
+ Highlight,
67
+ Underline,
68
+ Link.configure({
69
+ openOnClick: false,
70
+ autolink: true,
71
+ HTMLAttributes: { class: "text-primary underline" },
72
+ }),
73
+
74
+ Blockquote.configure({
75
+ HTMLAttributes: {
76
+ class: "border-l-4 border-border pl-4 italic text-muted-foreground",
77
+ },
78
+ }),
79
+ HorizontalRule,
80
+ HardBreak,
81
+ ],
82
+ editorProps: {
83
+ attributes: {
84
+ class: cn(
85
+ "prose dark:prose-invert max-w-none focus:outline-none min-h-[200px]",
86
+ className,
87
+ ),
88
+ },
89
+ },
90
+ onUpdate({ editor }) {
91
+ onChange(editor.getHTML());
92
+ },
93
+ autofocus: true,
94
+ });
95
+
96
+ return (
97
+ <>
98
+ <div className="sticky top-1 z-50 backdrop-blur-sm shadow-md">
99
+ {editable && <MenuBar editor={editor} />}
100
+ </div>
101
+ <EditorContent className="focus:border-primary" editor={editor} />
102
+ </>
103
+ );
104
+ }