nexstruct 1.0.0

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 (229) hide show
  1. package/AGENTS.md +122 -0
  2. package/LICENSE +21 -0
  3. package/README.md +103 -0
  4. package/package.json +99 -0
  5. package/scaffold/generator.js +409 -0
  6. package/scaffold/index.js +20 -0
  7. package/scaffold/prompts.js +108 -0
  8. package/templates/api/axios/src/api/axios/client.api.ts +30 -0
  9. package/templates/api/axios/src/api/axios/users.api.ts +15 -0
  10. package/templates/api/fetch/src/api/fetch/client.api.ts +68 -0
  11. package/templates/api/fetch/src/api/fetch/users.api.ts +15 -0
  12. package/templates/api/trpc/src/api/trpc/client.api.ts +4 -0
  13. package/templates/api/trpc/src/api/trpc/router.api.ts +15 -0
  14. package/templates/api/trpc/src/api/trpc/server.client.api.ts +4 -0
  15. package/templates/api/trpc/src/providers/trpc.provider.tsx +24 -0
  16. package/templates/auth/clerk/src/auth/clerk/auth.service.ts +4 -0
  17. package/templates/auth/clerk/src/hooks/use-auth.hook.ts +13 -0
  18. package/templates/auth/clerk/src/middleware.ts +7 -0
  19. package/templates/auth/clerk/src/providers/auth.provider.tsx +6 -0
  20. package/templates/auth/next-auth/src/app/api/auth/[...nextauth]/route.ts +5 -0
  21. package/templates/auth/next-auth/src/auth/next-auth/auth.service.ts +45 -0
  22. package/templates/auth/next-auth/src/hooks/use-session.hook.ts +13 -0
  23. package/templates/auth/next-auth/src/providers/session.provider.tsx +6 -0
  24. package/templates/forms/formik/src/components/forms/login-form.component.tsx +30 -0
  25. package/templates/forms/formik/src/forms/formik/hooks/use-form-config.hook.ts +7 -0
  26. package/templates/forms/formik/src/forms/formik/schemas/example.schema.ts +8 -0
  27. package/templates/forms/react-hook-form/src/components/forms/login-form.component.tsx +27 -0
  28. package/templates/forms/react-hook-form/src/forms/react-hook-form/hooks/use-form.hook.ts +13 -0
  29. package/templates/forms/react-hook-form/src/forms/react-hook-form/schemas/example.schema.ts +15 -0
  30. package/templates/nextjs-base/next.config.ts +5 -0
  31. package/templates/nextjs-base/postcss.config.mjs +9 -0
  32. package/templates/nextjs-base/src/app/_components/navbar.tsx +88 -0
  33. package/templates/nextjs-base/src/app/_components/sidebar.tsx +223 -0
  34. package/templates/nextjs-base/src/app/error.tsx +39 -0
  35. package/templates/nextjs-base/src/app/globals.css +71 -0
  36. package/templates/nextjs-base/src/app/layout.tsx +21 -0
  37. package/templates/nextjs-base/src/app/loading.tsx +13 -0
  38. package/templates/nextjs-base/src/app/not-found.tsx +22 -0
  39. package/templates/nextjs-base/src/app/page.tsx +10 -0
  40. package/templates/nextjs-base/tailwind.config.ts +69 -0
  41. package/templates/shared/src/components/common/theme-toggle.component.tsx +31 -0
  42. package/templates/shared/src/components/common/toast/custom-message.component.tsx +18 -0
  43. package/templates/shared/src/components/common/toast/index.ts +8 -0
  44. package/templates/shared/src/components/common/toast/toast-message.component.tsx +112 -0
  45. package/templates/shared/src/hooks/use-debounce.hook.ts +12 -0
  46. package/templates/shared/src/hooks/use-fetch.hook.ts +42 -0
  47. package/templates/shared/src/hooks/use-intersection-observer.hook.ts +39 -0
  48. package/templates/shared/src/hooks/use-local-storage.hook.ts +30 -0
  49. package/templates/shared/src/hooks/use-media-query.hook.ts +26 -0
  50. package/templates/shared/src/hooks/use-toggle.hook.ts +12 -0
  51. package/templates/shared/src/lib/utils.util.ts +361 -0
  52. package/templates/shared/src/providers/theme.provider.tsx +17 -0
  53. package/templates/shared/src/providers/toast.provider.tsx +32 -0
  54. package/templates/shared/src/types/common.type.ts +34 -0
  55. package/templates/state/context/src/store/context/auth.context.tsx +47 -0
  56. package/templates/state/context/src/store/context/counter.context.tsx +41 -0
  57. package/templates/state/context/src/store/context/index.ts +2 -0
  58. package/templates/state/redux/src/providers/redux.provider.tsx +7 -0
  59. package/templates/state/redux/src/store/redux/hooks.store.ts +5 -0
  60. package/templates/state/redux/src/store/redux/index.ts +4 -0
  61. package/templates/state/redux/src/store/redux/slices/api.slice.ts +8 -0
  62. package/templates/state/redux/src/store/redux/slices/counter.slice.ts +24 -0
  63. package/templates/state/redux/src/store/redux/store.store.ts +13 -0
  64. package/templates/state/zustand/src/store/zustand/counter.store.ts +15 -0
  65. package/templates/state/zustand/src/store/zustand/index.ts +2 -0
  66. package/templates/state/zustand/src/store/zustand/user.store.ts +32 -0
  67. package/templates/ui/antd/COMPONENT_GUIDE.md +326 -0
  68. package/templates/ui/antd/src/app/examples/dialog/page.tsx +205 -0
  69. package/templates/ui/antd/src/app/examples/form/page.tsx +160 -0
  70. package/templates/ui/antd/src/app/examples/layout.tsx +125 -0
  71. package/templates/ui/antd/src/app/examples/page.tsx +64 -0
  72. package/templates/ui/antd/src/app/examples/table/page.tsx +118 -0
  73. package/templates/ui/antd/src/app/page.tsx +283 -0
  74. package/templates/ui/antd/src/components/common/DynamicTable/dynamic-table.component.tsx +79 -0
  75. package/templates/ui/antd/src/components/common/button/action-button.component.tsx +63 -0
  76. package/templates/ui/antd/src/components/common/dialog/dialog-wrapper.component.tsx +63 -0
  77. package/templates/ui/antd/src/components/common/fields/assets/components/check-field.component.tsx +55 -0
  78. package/templates/ui/antd/src/components/common/fields/assets/components/date-picker-field.component.tsx +80 -0
  79. package/templates/ui/antd/src/components/common/fields/assets/components/limit-field.component.tsx +26 -0
  80. package/templates/ui/antd/src/components/common/fields/assets/components/multi-check-field.component.tsx +56 -0
  81. package/templates/ui/antd/src/components/common/fields/assets/components/number-field.component.tsx +100 -0
  82. package/templates/ui/antd/src/components/common/fields/assets/components/otp-field.component.tsx +63 -0
  83. package/templates/ui/antd/src/components/common/fields/assets/components/password-field.component.tsx +106 -0
  84. package/templates/ui/antd/src/components/common/fields/assets/components/phone-number-field.component.tsx +78 -0
  85. package/templates/ui/antd/src/components/common/fields/assets/components/radio-field.component.tsx +55 -0
  86. package/templates/ui/antd/src/components/common/fields/assets/components/range-date-picker.component.tsx +66 -0
  87. package/templates/ui/antd/src/components/common/fields/assets/components/search-field.component.tsx +24 -0
  88. package/templates/ui/antd/src/components/common/fields/assets/components/select-field.component.tsx +82 -0
  89. package/templates/ui/antd/src/components/common/fields/assets/components/single-check-field.component.tsx +50 -0
  90. package/templates/ui/antd/src/components/common/fields/assets/components/single-select-field.component.tsx +86 -0
  91. package/templates/ui/antd/src/components/common/fields/assets/components/string-number-field.component.tsx +80 -0
  92. package/templates/ui/antd/src/components/common/fields/assets/components/switch-field.component.tsx +62 -0
  93. package/templates/ui/antd/src/components/common/fields/assets/components/text-area-field.component.tsx +85 -0
  94. package/templates/ui/antd/src/components/common/fields/assets/components/text-field.component.tsx +88 -0
  95. package/templates/ui/antd/src/components/common/fields/assets/interface/input-props.type.ts +233 -0
  96. package/templates/ui/antd/src/components/common/fields/cusInputField.component.tsx +40 -0
  97. package/templates/ui/antd/src/components/common/pagination/pagination.component.tsx +27 -0
  98. package/templates/ui/antd/src/components/ui/avatar.component.tsx +8 -0
  99. package/templates/ui/antd/src/components/ui/badge.component.tsx +8 -0
  100. package/templates/ui/antd/src/components/ui/button.component.tsx +8 -0
  101. package/templates/ui/antd/src/components/ui/card.component.tsx +8 -0
  102. package/templates/ui/antd/src/components/ui/checkbox.component.tsx +8 -0
  103. package/templates/ui/antd/src/components/ui/dialog.component.tsx +9 -0
  104. package/templates/ui/antd/src/components/ui/dropdown-menu.component.tsx +10 -0
  105. package/templates/ui/antd/src/components/ui/form.component.tsx +12 -0
  106. package/templates/ui/antd/src/components/ui/input.component.tsx +13 -0
  107. package/templates/ui/antd/src/components/ui/label.component.tsx +18 -0
  108. package/templates/ui/antd/src/components/ui/popover.component.tsx +8 -0
  109. package/templates/ui/antd/src/components/ui/progress.component.tsx +8 -0
  110. package/templates/ui/antd/src/components/ui/radio-group.component.tsx +10 -0
  111. package/templates/ui/antd/src/components/ui/scroll-area.component.tsx +25 -0
  112. package/templates/ui/antd/src/components/ui/select.component.tsx +8 -0
  113. package/templates/ui/antd/src/components/ui/separator.component.tsx +8 -0
  114. package/templates/ui/antd/src/components/ui/sheet.component.tsx +8 -0
  115. package/templates/ui/antd/src/components/ui/switch.component.tsx +8 -0
  116. package/templates/ui/antd/src/components/ui/table.component.tsx +8 -0
  117. package/templates/ui/antd/src/components/ui/tabs.component.tsx +8 -0
  118. package/templates/ui/antd/src/components/ui/textarea.component.tsx +9 -0
  119. package/templates/ui/antd/src/components/ui/tooltip.component.tsx +8 -0
  120. package/templates/ui/antd/src/lib/theme.util.ts +40 -0
  121. package/templates/ui/antd/src/providers/antd.provider.tsx +13 -0
  122. package/templates/ui/mui/src/app/examples/layout.tsx +113 -0
  123. package/templates/ui/mui/src/app/examples/page.tsx +716 -0
  124. package/templates/ui/mui/src/app/page.tsx +298 -0
  125. package/templates/ui/mui/src/components/common/DynamicTable/dynamic-table.component.tsx +131 -0
  126. package/templates/ui/mui/src/components/common/button/action-button.component.tsx +57 -0
  127. package/templates/ui/mui/src/components/common/dialog/dialog-wrapper.component.tsx +55 -0
  128. package/templates/ui/mui/src/components/common/fields/assets/components/check-field.component.tsx +51 -0
  129. package/templates/ui/mui/src/components/common/fields/assets/components/date-picker-field.component.tsx +50 -0
  130. package/templates/ui/mui/src/components/common/fields/assets/components/multi-check-field.component.tsx +14 -0
  131. package/templates/ui/mui/src/components/common/fields/assets/components/number-field.component.tsx +59 -0
  132. package/templates/ui/mui/src/components/common/fields/assets/components/password-field.component.tsx +87 -0
  133. package/templates/ui/mui/src/components/common/fields/assets/components/phone-number-field.component.tsx +48 -0
  134. package/templates/ui/mui/src/components/common/fields/assets/components/radio-field.component.tsx +37 -0
  135. package/templates/ui/mui/src/components/common/fields/assets/components/search-field.component.tsx +41 -0
  136. package/templates/ui/mui/src/components/common/fields/assets/components/select-field.component.tsx +77 -0
  137. package/templates/ui/mui/src/components/common/fields/assets/components/single-check-field.component.tsx +39 -0
  138. package/templates/ui/mui/src/components/common/fields/assets/components/single-select-field.component.tsx +56 -0
  139. package/templates/ui/mui/src/components/common/fields/assets/components/string-number-field.component.tsx +52 -0
  140. package/templates/ui/mui/src/components/common/fields/assets/components/switch-field.component.tsx +35 -0
  141. package/templates/ui/mui/src/components/common/fields/assets/components/text-area-field.component.tsx +46 -0
  142. package/templates/ui/mui/src/components/common/fields/assets/components/text-field.component.tsx +51 -0
  143. package/templates/ui/mui/src/components/common/fields/assets/interface/input-props.type.ts +193 -0
  144. package/templates/ui/mui/src/components/common/fields/cusInputField.component.tsx +34 -0
  145. package/templates/ui/mui/src/components/common/pagination/pagination.component.tsx +59 -0
  146. package/templates/ui/mui/src/components/ui/avatar.component.tsx +19 -0
  147. package/templates/ui/mui/src/components/ui/badge.component.tsx +18 -0
  148. package/templates/ui/mui/src/components/ui/button.component.tsx +22 -0
  149. package/templates/ui/mui/src/components/ui/card.component.tsx +39 -0
  150. package/templates/ui/mui/src/components/ui/checkbox.component.tsx +21 -0
  151. package/templates/ui/mui/src/components/ui/dialog.component.tsx +38 -0
  152. package/templates/ui/mui/src/components/ui/dropdown-menu.component.tsx +43 -0
  153. package/templates/ui/mui/src/components/ui/form.component.tsx +98 -0
  154. package/templates/ui/mui/src/components/ui/input.component.tsx +15 -0
  155. package/templates/ui/mui/src/components/ui/label.component.tsx +15 -0
  156. package/templates/ui/mui/src/components/ui/popover.component.tsx +20 -0
  157. package/templates/ui/mui/src/components/ui/progress.component.tsx +19 -0
  158. package/templates/ui/mui/src/components/ui/radio-group.component.tsx +25 -0
  159. package/templates/ui/mui/src/components/ui/scroll-area.component.tsx +27 -0
  160. package/templates/ui/mui/src/components/ui/select.component.tsx +26 -0
  161. package/templates/ui/mui/src/components/ui/separator.component.tsx +11 -0
  162. package/templates/ui/mui/src/components/ui/sheet.component.tsx +44 -0
  163. package/templates/ui/mui/src/components/ui/switch.component.tsx +23 -0
  164. package/templates/ui/mui/src/components/ui/table.component.tsx +34 -0
  165. package/templates/ui/mui/src/components/ui/tabs.component.tsx +38 -0
  166. package/templates/ui/mui/src/components/ui/textarea.component.tsx +18 -0
  167. package/templates/ui/mui/src/components/ui/tooltip.component.tsx +24 -0
  168. package/templates/ui/mui/src/lib/theme.util.ts +73 -0
  169. package/templates/ui/mui/src/providers/mui.provider.tsx +13 -0
  170. package/templates/ui/shadcn/COMPONENT_GUIDE.md +306 -0
  171. package/templates/ui/shadcn/src/app/examples/dialog/page.tsx +122 -0
  172. package/templates/ui/shadcn/src/app/examples/form/page.tsx +107 -0
  173. package/templates/ui/shadcn/src/app/examples/layout.tsx +24 -0
  174. package/templates/ui/shadcn/src/app/examples/page.tsx +30 -0
  175. package/templates/ui/shadcn/src/app/examples/table/page.tsx +77 -0
  176. package/templates/ui/shadcn/src/app/page.tsx +20 -0
  177. package/templates/ui/shadcn/src/components/common/DynamicTable/dynamic-table.component.tsx +136 -0
  178. package/templates/ui/shadcn/src/components/common/button/action-button.component.tsx +68 -0
  179. package/templates/ui/shadcn/src/components/common/dialog/dialog-wrapper.component.tsx +58 -0
  180. package/templates/ui/shadcn/src/components/common/fields/assets/components/check-field.component.tsx +52 -0
  181. package/templates/ui/shadcn/src/components/common/fields/assets/components/date-picker-field.component.tsx +62 -0
  182. package/templates/ui/shadcn/src/components/common/fields/assets/components/dynamic-file-upload-field.component.tsx +152 -0
  183. package/templates/ui/shadcn/src/components/common/fields/assets/components/limit-field.component.tsx +73 -0
  184. package/templates/ui/shadcn/src/components/common/fields/assets/components/multi-check-field.component.tsx +46 -0
  185. package/templates/ui/shadcn/src/components/common/fields/assets/components/number-field.component.tsx +124 -0
  186. package/templates/ui/shadcn/src/components/common/fields/assets/components/otp-field.component.tsx +61 -0
  187. package/templates/ui/shadcn/src/components/common/fields/assets/components/password-field.component.tsx +110 -0
  188. package/templates/ui/shadcn/src/components/common/fields/assets/components/phone-number-field.component.tsx +90 -0
  189. package/templates/ui/shadcn/src/components/common/fields/assets/components/radio-field.component.tsx +41 -0
  190. package/templates/ui/shadcn/src/components/common/fields/assets/components/range-date-picker.component.tsx +71 -0
  191. package/templates/ui/shadcn/src/components/common/fields/assets/components/rich-text-editor.component.tsx +91 -0
  192. package/templates/ui/shadcn/src/components/common/fields/assets/components/search-field.component.tsx +34 -0
  193. package/templates/ui/shadcn/src/components/common/fields/assets/components/select-field.component.tsx +231 -0
  194. package/templates/ui/shadcn/src/components/common/fields/assets/components/single-check-field.component.tsx +42 -0
  195. package/templates/ui/shadcn/src/components/common/fields/assets/components/single-select-field.component.tsx +82 -0
  196. package/templates/ui/shadcn/src/components/common/fields/assets/components/string-number-field.component.tsx +68 -0
  197. package/templates/ui/shadcn/src/components/common/fields/assets/components/switch-field.component.tsx +61 -0
  198. package/templates/ui/shadcn/src/components/common/fields/assets/components/text-area-field.component.tsx +62 -0
  199. package/templates/ui/shadcn/src/components/common/fields/assets/components/text-area-with-file.component.tsx +142 -0
  200. package/templates/ui/shadcn/src/components/common/fields/assets/components/text-field.component.tsx +80 -0
  201. package/templates/ui/shadcn/src/components/common/fields/assets/components/tiny-editor.component.tsx +51 -0
  202. package/templates/ui/shadcn/src/components/common/fields/assets/components/upload-profile-picture.component.tsx +103 -0
  203. package/templates/ui/shadcn/src/components/common/fields/assets/components/upload-video-file.component.tsx +86 -0
  204. package/templates/ui/shadcn/src/components/common/fields/assets/interface/input-props.type.ts +198 -0
  205. package/templates/ui/shadcn/src/components/common/fields/cusInputField.component.tsx +52 -0
  206. package/templates/ui/shadcn/src/components/common/pagination/pagination.component.tsx +68 -0
  207. package/templates/ui/shadcn/src/components/ui/avatar.component.tsx +37 -0
  208. package/templates/ui/shadcn/src/components/ui/badge.component.tsx +28 -0
  209. package/templates/ui/shadcn/src/components/ui/button.component.tsx +52 -0
  210. package/templates/ui/shadcn/src/components/ui/card.component.tsx +46 -0
  211. package/templates/ui/shadcn/src/components/ui/checkbox.component.tsx +25 -0
  212. package/templates/ui/shadcn/src/components/ui/dialog.component.tsx +98 -0
  213. package/templates/ui/shadcn/src/components/ui/dropdown-menu.component.tsx +163 -0
  214. package/templates/ui/shadcn/src/components/ui/form.component.tsx +110 -0
  215. package/templates/ui/shadcn/src/components/ui/input-otp.component.tsx +64 -0
  216. package/templates/ui/shadcn/src/components/ui/input.component.tsx +23 -0
  217. package/templates/ui/shadcn/src/components/ui/label.component.tsx +23 -0
  218. package/templates/ui/shadcn/src/components/ui/popover.component.tsx +27 -0
  219. package/templates/ui/shadcn/src/components/ui/progress.component.tsx +22 -0
  220. package/templates/ui/shadcn/src/components/ui/radio-group.component.tsx +33 -0
  221. package/templates/ui/shadcn/src/components/ui/scroll-area.component.tsx +37 -0
  222. package/templates/ui/shadcn/src/components/ui/select.component.tsx +139 -0
  223. package/templates/ui/shadcn/src/components/ui/separator.component.tsx +23 -0
  224. package/templates/ui/shadcn/src/components/ui/sheet.component.tsx +89 -0
  225. package/templates/ui/shadcn/src/components/ui/switch.component.tsx +26 -0
  226. package/templates/ui/shadcn/src/components/ui/table.component.tsx +71 -0
  227. package/templates/ui/shadcn/src/components/ui/tabs.component.tsx +52 -0
  228. package/templates/ui/shadcn/src/components/ui/textarea.component.tsx +20 -0
  229. package/templates/ui/shadcn/src/components/ui/tooltip.component.tsx +25 -0
@@ -0,0 +1,361 @@
1
+ import { type ClassValue, clsx } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ /* ─────────────────────────────────────────────
5
+ * cn — Merge Tailwind classes
6
+ * ───────────────────────────────────────────── */
7
+ export function cn(...inputs: ClassValue[]) {
8
+ return twMerge(clsx(inputs));
9
+ }
10
+
11
+ /* ─────────────────────────────────────────────
12
+ * Date / Time Formatter
13
+ * ───────────────────────────────────────────── */
14
+
15
+ /**
16
+ * Format a date into a human-readable string e.g. "June 17, 2026"
17
+ */
18
+ export function formatDate(date: Date | string): string {
19
+ return new Intl.DateTimeFormat('en-US', {
20
+ year: 'numeric',
21
+ month: 'long',
22
+ day: 'numeric',
23
+ }).format(new Date(date));
24
+ }
25
+
26
+ /**
27
+ * Returns a relative time string e.g. "just now", "3m ago", "2h ago", "5d ago"
28
+ */
29
+ export function formatRelativeTime(date: Date | string): string {
30
+ const now = new Date();
31
+ const then = new Date(date);
32
+ const diffMs = now.getTime() - then.getTime();
33
+ const diffSecs = Math.floor(diffMs / 1000);
34
+ const diffMins = Math.floor(diffSecs / 60);
35
+ const diffHours = Math.floor(diffMins / 60);
36
+ const diffDays = Math.floor(diffHours / 24);
37
+
38
+ if (diffSecs < 60) return 'just now';
39
+ if (diffMins < 60) return `${diffMins}m ago`;
40
+ if (diffHours < 24) return `${diffHours}h ago`;
41
+ if (diffDays < 7) return `${diffDays}d ago`;
42
+ return formatDate(date);
43
+ }
44
+
45
+ /**
46
+ * Returns the month and year e.g. "Jun 2025"
47
+ */
48
+ export function toMonthYear(
49
+ input: string | number | Date,
50
+ options: { local?: boolean } = {},
51
+ ): string {
52
+ const { local = false } = options;
53
+ let date = new Date(input);
54
+ if (isNaN(date.getTime())) return 'Invalid Date';
55
+
56
+ if (local && typeof input === 'string' && input.includes('Z')) {
57
+ date = new Date(date.getTime() + date.getTimezoneOffset() * 60_000);
58
+ }
59
+
60
+ return new Intl.DateTimeFormat('en-US', {
61
+ month: 'short',
62
+ year: 'numeric',
63
+ }).format(date);
64
+ }
65
+
66
+ /**
67
+ * Returns time in 12-hour format e.g. "2:30 PM"
68
+ * When showRelative is true, returns "Today, 2:30 PM" etc.
69
+ */
70
+ export function time12h(
71
+ input: string | Date,
72
+ options: { local?: boolean } = {},
73
+ showRelative: boolean | 'day' = false,
74
+ ): string {
75
+ const { local = false } = options;
76
+ let date = new Date(input);
77
+ if (isNaN(date.getTime())) return 'Invalid Date';
78
+
79
+ if (local && typeof input === 'string' && input.includes('Z')) {
80
+ date = new Date(date.getTime() + date.getTimezoneOffset() * 60_000);
81
+ }
82
+
83
+ const timeString = new Intl.DateTimeFormat('en-US', {
84
+ hour: 'numeric',
85
+ minute: '2-digit',
86
+ hour12: true,
87
+ }).format(date);
88
+
89
+ if (!showRelative) return timeString;
90
+
91
+ const now = new Date();
92
+ const diffMs = now.getTime() - date.getTime();
93
+ const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
94
+
95
+ if (showRelative === 'day' && diffDays > 7) {
96
+ const dateStr = new Intl.DateTimeFormat('en-US', {
97
+ month: 'short',
98
+ day: 'numeric',
99
+ year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined,
100
+ }).format(date);
101
+ return `${dateStr}, ${timeString}`;
102
+ }
103
+
104
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
105
+ const inputDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
106
+
107
+ if (inputDate.getTime() === today.getTime()) {
108
+ return `Today, ${timeString}`;
109
+ }
110
+ if (inputDate.getTime() === today.getTime() - 86_400_000) {
111
+ return `Yesterday, ${timeString}`;
112
+ }
113
+ if (diffDays < 7) {
114
+ const dayName = new Intl.DateTimeFormat('en-US', { weekday: 'long' }).format(date);
115
+ return `${dayName}, ${timeString}`;
116
+ }
117
+
118
+ const dateStr = new Intl.DateTimeFormat('en-US', {
119
+ month: 'short',
120
+ day: 'numeric',
121
+ year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined,
122
+ }).format(date);
123
+ return `${dateStr}, ${timeString}`;
124
+ }
125
+
126
+ /**
127
+ * Returns time in 24-hour format e.g. "14:30"
128
+ */
129
+ export function time24h(
130
+ input: string | Date,
131
+ options: { local?: boolean } = {},
132
+ ): string {
133
+ const { local = false } = options;
134
+ let date = new Date(input);
135
+ if (isNaN(date.getTime())) return 'Invalid Date';
136
+
137
+ if (local && typeof input === 'string' && input.includes('Z')) {
138
+ date = new Date(date.getTime() + date.getTimezoneOffset() * 60_000);
139
+ }
140
+
141
+ return new Intl.DateTimeFormat('en-GB', {
142
+ hour: '2-digit',
143
+ minute: '2-digit',
144
+ hour12: false,
145
+ }).format(date);
146
+ }
147
+
148
+ /**
149
+ * Returns a date in a custom format: "YYYY-MM-DD", "DD-MM-YYYY", or "YYYY"
150
+ */
151
+ export function customFormatDate(
152
+ input: Date | string,
153
+ format: 'YYYY-MM-DD' | 'DD-MM-YYYY' | 'YYYY' = 'YYYY-MM-DD',
154
+ options: { local?: boolean } = {},
155
+ ): string {
156
+ const { local = false } = options;
157
+ let date = new Date(input);
158
+ if (isNaN(date.getTime())) return 'Invalid Date';
159
+
160
+ if (local && typeof input === 'string' && input.includes('Z')) {
161
+ date = new Date(date.getTime() + date.getTimezoneOffset() * 60_000);
162
+ }
163
+
164
+ const y = date.getFullYear();
165
+ const m = String(date.getMonth() + 1).padStart(2, '0');
166
+ const d = String(date.getDate()).padStart(2, '0');
167
+
168
+ switch (format) {
169
+ case 'YYYY':
170
+ return String(y);
171
+ case 'DD-MM-YYYY':
172
+ return `${d}-${m}-${y}`;
173
+ default:
174
+ return `${y}-${m}-${d}`;
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Returns a full date-time string e.g. "24 August 2025, 03:30 PM"
180
+ */
181
+ export function fullDateTime(
182
+ input: Date | string,
183
+ options: { showTime?: boolean; local?: boolean } = {},
184
+ ): string {
185
+ const { showTime = true, local = false } = options;
186
+ let date = new Date(input);
187
+ if (isNaN(date.getTime())) return 'Invalid Date';
188
+
189
+ if (local && typeof input === 'string' && input.includes('Z')) {
190
+ date = new Date(date.getTime() + date.getTimezoneOffset() * 60_000);
191
+ }
192
+
193
+ return new Intl.DateTimeFormat('en-GB', {
194
+ day: '2-digit',
195
+ month: 'long',
196
+ year: 'numeric',
197
+ ...(showTime && { hour: '2-digit', minute: '2-digit', hour12: true }),
198
+ }).format(date);
199
+ }
200
+
201
+ /**
202
+ * Returns a duration string between two dates e.g. "2 years 3 months"
203
+ */
204
+ export function duration(start: Date | string, end?: Date | string | null): string {
205
+ const e = !end || end === 'no' ? new Date() : new Date(end);
206
+ const s = new Date(start);
207
+
208
+ if (isNaN(s.getTime()) || isNaN(e.getTime())) return 'Invalid Date';
209
+ if (e < s) return 'End date is before start date';
210
+
211
+ let years = e.getFullYear() - s.getFullYear();
212
+ let months = e.getMonth() - s.getMonth();
213
+
214
+ if (months < 0) { years--; months += 12; }
215
+
216
+ const diffMs = e.getTime() - s.getTime();
217
+ const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
218
+ const hours = Math.floor(diffMs / (1000 * 60 * 60)) % 24;
219
+ const minutes = Math.floor(diffMs / (1000 * 60)) % 60;
220
+ const seconds = Math.floor(diffMs / 1000) % 60;
221
+
222
+ if (years > 0) return months > 0 ? `${years}y ${months}m` : `${years}y`;
223
+ if (months > 0) return `${months}m`;
224
+ if (days > 0) return hours > 0 ? `${days}d ${hours}h` : `${days}d`;
225
+ if (hours > 0) return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
226
+ if (minutes > 0) return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
227
+ return `${seconds}s`;
228
+ }
229
+
230
+ /**
231
+ * Converts a local Date to ISO string at UTC midnight
232
+ */
233
+ export function localDateToISO(date?: Date | null): string | null {
234
+ if (!date) return null;
235
+ return new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0, 0)).toISOString();
236
+ }
237
+
238
+ /**
239
+ * Returns "20-10-2025 at 10:07 am" format
240
+ */
241
+ export function localDateTime(input: string | Date): string {
242
+ const date = new Date(input);
243
+ if (isNaN(date.getTime())) return 'Invalid Date';
244
+
245
+ const day = String(date.getDate()).padStart(2, '0');
246
+ const month = String(date.getMonth() + 1).padStart(2, '0');
247
+ const year = date.getFullYear();
248
+ const time = date.toLocaleTimeString('en-US', {
249
+ hour: '2-digit', minute: '2-digit', hour12: true,
250
+ }).toLowerCase().replace(' ', '');
251
+
252
+ return `${day}-${month}-${year} at ${time}`;
253
+ }
254
+
255
+ /* ─────────────────────────────────────────────
256
+ * String Utilities
257
+ * ───────────────────────────────────────────── */
258
+
259
+ export function truncate(str: string, length: number): string {
260
+ if (str.length <= length) return str;
261
+ return str.slice(0, length) + '...';
262
+ }
263
+
264
+ export function slugify(str: string): string {
265
+ return str
266
+ .toLowerCase()
267
+ .replace(/[^\w\s-]/g, '')
268
+ .replace(/[\s_]+/g, '-')
269
+ .replace(/-+/g, '-')
270
+ .trim();
271
+ }
272
+
273
+ /**
274
+ * Mask a string with a custom character, showing the first N and last N chars
275
+ * e.g. maskString("HelloWorld", 3, 2) → "Hel**ld"
276
+ */
277
+ export function maskString(
278
+ value: string,
279
+ showStart = 3,
280
+ showEnd = 0,
281
+ maskChar = '*',
282
+ ): string {
283
+ if (!value) return '';
284
+ const length = value.length;
285
+ if (showStart + showEnd >= length) return value;
286
+
287
+ const start = value.slice(0, showStart);
288
+ const end = value.slice(length - showEnd);
289
+ const masked = maskChar.repeat(length - showStart - showEnd);
290
+
291
+ return `${start}${masked}${end}`;
292
+ }
293
+
294
+ /**
295
+ * Mask an email address e.g. "j***@example.com"
296
+ */
297
+ export function maskEmail(email: string): string {
298
+ if (!email) return '';
299
+ const [user, domain] = email.split('@');
300
+ if (!domain) return maskString(email, 1, 0, '*');
301
+ return `${maskString(user, 1, 0, '*')}@${domain}`;
302
+ }
303
+
304
+ /* ─────────────────────────────────────────────
305
+ * Password Strength Rules
306
+ * ───────────────────────────────────────────── */
307
+
308
+ export interface PasswordRule {
309
+ label: string;
310
+ test: (val: string) => boolean;
311
+ }
312
+
313
+ /**
314
+ * Standard password validation rules used by the PasswordField when mode="validate"
315
+ */
316
+ export const passwordRules: PasswordRule[] = [
317
+ { label: 'At least 8 characters', test: (val) => val.length >= 8 },
318
+ { label: 'At least one uppercase letter', test: (val) => /[A-Z]/.test(val) },
319
+ { label: 'At least one number', test: (val) => /\d/.test(val) },
320
+ { label: 'At least one special character', test: (val) => /[!@#$%^&*(),.?":{}|<>_\-+=~`[\]\\;/]/.test(val) },
321
+ { label: 'No sequential numbers (e.g. 1234)', test: (val) => !/012|123|234|345|456|567|678|789/.test(val) },
322
+ ];
323
+
324
+ /**
325
+ * Check if a password passes all rules
326
+ */
327
+ export function isPasswordStrong(password: string): boolean {
328
+ return passwordRules.every((rule) => rule.test(password));
329
+ }
330
+
331
+ /* ─────────────────────────────────────────────
332
+ * Label & Placeholder Formatting
333
+ * ───────────────────────────────────────────── */
334
+
335
+ const prepositions = [
336
+ 'is', 'the', 'within', 'are', 'was', 'were', 'been', 'being',
337
+ 'have', 'has', 'had', 'having', 'do', 'does', 'did', 'doing',
338
+ 'will', 'would', 'shall', 'should', 'may', 'might', 'must', 'can', 'could',
339
+ 'a', 'an', 'and', 'or', 'but', 'if', 'because', 'as', 'until', 'while',
340
+ 'of', 'at', 'by', 'for', 'with', 'about', 'against', 'between', 'into',
341
+ 'through', 'during', 'before', 'after', 'above', 'below', 'to', 'from',
342
+ 'up', 'down', 'in', 'out', 'on', 'off', 'over', 'under', 'again',
343
+ 'further', 'then', 'once', 'here', 'there', 'each', 'few', 'more',
344
+ 'most', 'other', 'some', 'such', 'no', 'nor', 'not', 'only', 'own',
345
+ 'same', 'so', 'than', 'too', 'very', 'just', 'also',
346
+ ];
347
+
348
+ export function LabelAndPlaceholderTextFormat(input: string): string {
349
+ if (!input) return '';
350
+ const capitalWords: string[] = [];
351
+
352
+ return input
353
+ .toLowerCase()
354
+ .split(' ')
355
+ .map((word: string, index: number) => {
356
+ if (prepositions.includes(word) && index !== 0) return word;
357
+ if (capitalWords.includes(word)) return word.toUpperCase();
358
+ return word.charAt(0).toUpperCase() + word.slice(1);
359
+ })
360
+ .join(' ');
361
+ }
@@ -0,0 +1,17 @@
1
+ 'use client';
2
+
3
+ import { ThemeProvider as NextThemesProvider } from 'next-themes';
4
+ import type { ReactNode } from 'react';
5
+
6
+ export function ThemeProvider({ children }: { children: ReactNode }) {
7
+ return (
8
+ <NextThemesProvider
9
+ attribute="class"
10
+ defaultTheme="system"
11
+ enableSystem
12
+ disableTransitionOnChange
13
+ >
14
+ {children}
15
+ </NextThemesProvider>
16
+ );
17
+ }
@@ -0,0 +1,32 @@
1
+ 'use client';
2
+
3
+ import { Toaster } from 'react-hot-toast';
4
+
5
+ export function ToastProvider({ children }: { children: React.ReactNode }) {
6
+ return (
7
+ <>
8
+ {children}
9
+ <Toaster
10
+ position="top-center"
11
+ reverseOrder={false}
12
+ toastOptions={{
13
+ duration: 3000,
14
+ style: {
15
+ borderRadius: '8px',
16
+ background: 'var(--background, #fff)',
17
+ color: 'var(--foreground, #000)',
18
+ border: '1px solid var(--border, #e2e8f0)',
19
+ boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
20
+ fontSize: '14px',
21
+ },
22
+ success: {
23
+ iconTheme: { primary: '#22c55e', secondary: '#fff' },
24
+ },
25
+ error: {
26
+ iconTheme: { primary: '#ef4444', secondary: '#fff' },
27
+ },
28
+ }}
29
+ />
30
+ </>
31
+ );
32
+ }
@@ -0,0 +1,34 @@
1
+ export interface PaginatedResponse<T> {
2
+ data: T[];
3
+ total: number;
4
+ page: number;
5
+ pageSize: number;
6
+ totalPages: number;
7
+ }
8
+
9
+ export interface ApiResponse<T> {
10
+ success: boolean;
11
+ data: T;
12
+ message?: string;
13
+ }
14
+
15
+ export interface ApiError {
16
+ success: false;
17
+ message: string;
18
+ status: number;
19
+ errors?: Record<string, string[]>;
20
+ }
21
+
22
+ export interface SelectOption<T = string> {
23
+ label: string;
24
+ value: T;
25
+ disabled?: boolean;
26
+ }
27
+
28
+ export interface NavItem {
29
+ label: string;
30
+ href: string;
31
+ icon?: React.ComponentType;
32
+ children?: NavItem[];
33
+ disabled?: boolean;
34
+ }
@@ -0,0 +1,47 @@
1
+ 'use client';
2
+ import { createContext, useContext, useState, type ReactNode } from 'react';
3
+
4
+ interface User { id: string; name: string; email: string }
5
+
6
+ interface AuthContextType {
7
+ user: User | null;
8
+ isLoading: boolean;
9
+ login: (email: string, password: string) => Promise<void>;
10
+ logout: () => void;
11
+ }
12
+
13
+ const AuthContext = createContext<AuthContextType | undefined>(undefined);
14
+
15
+ export function AuthProvider({ children }: { children: ReactNode }) {
16
+ const [user, setUser] = useState<User | null>(null);
17
+ const [isLoading, setIsLoading] = useState(false);
18
+
19
+ const login = async (email: string, password: string) => {
20
+ setIsLoading(true);
21
+ try {
22
+ const res = await fetch('/api/auth/login', {
23
+ method: 'POST',
24
+ headers: { 'Content-Type': 'application/json' },
25
+ body: JSON.stringify({ email, password }),
26
+ });
27
+ const data = await res.json();
28
+ setUser(data.user);
29
+ } finally {
30
+ setIsLoading(false);
31
+ }
32
+ };
33
+
34
+ const logout = () => setUser(null);
35
+
36
+ return (
37
+ <AuthContext.Provider value={{ user, isLoading, login, logout }}>
38
+ {children}
39
+ </AuthContext.Provider>
40
+ );
41
+ }
42
+
43
+ export function useAuth() {
44
+ const context = useContext(AuthContext);
45
+ if (!context) throw new Error('useAuth must be used within AuthProvider');
46
+ return context;
47
+ }
@@ -0,0 +1,41 @@
1
+ 'use client';
2
+ import { createContext, useContext, useReducer, type ReactNode } from 'react';
3
+
4
+ interface CounterState { count: number }
5
+
6
+ type CounterAction =
7
+ | { type: 'INCREMENT' }
8
+ | { type: 'DECREMENT' }
9
+ | { type: 'SET'; payload: number };
10
+
11
+ const counterReducer = (state: CounterState, action: CounterAction): CounterState => {
12
+ switch (action.type) {
13
+ case 'INCREMENT': return { count: state.count + 1 };
14
+ case 'DECREMENT': return { count: state.count - 1 };
15
+ case 'SET': return { count: action.payload };
16
+ default: return state;
17
+ }
18
+ };
19
+
20
+ interface CounterContextType extends CounterState {
21
+ dispatch: React.Dispatch<CounterAction>;
22
+ }
23
+
24
+ const CounterContext = createContext<CounterContextType | undefined>(undefined);
25
+
26
+ const initialState: CounterState = { count: 0 };
27
+
28
+ export function CounterProvider({ children }: { children: ReactNode }) {
29
+ const [state, dispatch] = useReducer(counterReducer, initialState);
30
+ return (
31
+ <CounterContext.Provider value={{ ...state, dispatch }}>
32
+ {children}
33
+ </CounterContext.Provider>
34
+ );
35
+ }
36
+
37
+ export function useCounter() {
38
+ const context = useContext(CounterContext);
39
+ if (!context) throw new Error('useCounter must be used within CounterProvider');
40
+ return context;
41
+ }
@@ -0,0 +1,2 @@
1
+ export { CounterProvider, useCounter } from './counter.context';
2
+ export { AuthProvider, useAuth } from './auth.context';
@@ -0,0 +1,7 @@
1
+ 'use client';
2
+ import { Provider } from 'react-redux';
3
+ import { store } from '@/store/redux/store.store';
4
+
5
+ export function ReduxProvider({ children }: { children: React.ReactNode }) {
6
+ return <Provider store={store}>{children}</Provider>;
7
+ }
@@ -0,0 +1,5 @@
1
+ import { useDispatch, useSelector, useStore } from 'react-redux';
2
+ import type { RootState, AppDispatch } from './store.store';
3
+
4
+ export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
5
+ export const useAppSelector = useSelector.withTypes<RootState>();
@@ -0,0 +1,4 @@
1
+ export { store } from './store.store';
2
+ export { useAppDispatch, useAppSelector } from './hooks.store';
3
+ export { increment, decrement, setValue, selectCount } from './slices/counter.slice';
4
+ export { apiSlice } from './slices/api.slice';
@@ -0,0 +1,8 @@
1
+ import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
2
+
3
+ export const apiSlice = createApi({
4
+ reducerPath: 'api',
5
+ baseQuery: fetchBaseQuery({ baseUrl: process.env.NEXT_PUBLIC_API_URL || '/api' }),
6
+ tagTypes: [],
7
+ endpoints: () => ({}),
8
+ });
@@ -0,0 +1,24 @@
1
+ import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
2
+
3
+ interface CounterState {
4
+ value: number;
5
+ }
6
+
7
+ const initialState: CounterState = { value: 0 };
8
+
9
+ export const counterSlice = createSlice({
10
+ name: 'counter',
11
+ initialState,
12
+ reducers: {
13
+ increment: (state) => { state.value += 1; },
14
+ decrement: (state) => { state.value -= 1; },
15
+ setValue: (state, action: PayloadAction<number>) => { state.value = action.payload; },
16
+ },
17
+ selectors: {
18
+ selectCount: (state) => state.value,
19
+ },
20
+ });
21
+
22
+ export const { increment, decrement, setValue } = counterSlice.actions;
23
+ export const { selectCount } = counterSlice.selectors;
24
+ export default counterSlice.reducer;
@@ -0,0 +1,13 @@
1
+ import { configureStore } from '@reduxjs/toolkit';
2
+ import { apiSlice } from './slices/api.slice';
3
+
4
+ export const store = configureStore({
5
+ reducer: {
6
+ [apiSlice.reducerPath]: apiSlice.reducer,
7
+ },
8
+ middleware: (getDefaultMiddleware) =>
9
+ getDefaultMiddleware().concat(apiSlice.middleware),
10
+ });
11
+
12
+ export type RootState = ReturnType<typeof store.getState>;
13
+ export type AppDispatch = typeof store.dispatch;
@@ -0,0 +1,15 @@
1
+ import { create } from 'zustand';
2
+
3
+ interface CounterState {
4
+ count: number;
5
+ increment: () => void;
6
+ decrement: () => void;
7
+ setCount: (value: number) => void;
8
+ }
9
+
10
+ export const useCounterStore = create<CounterState>((set) => ({
11
+ count: 0,
12
+ increment: () => set((state) => ({ count: state.count + 1 })),
13
+ decrement: () => set((state) => ({ count: state.count - 1 })),
14
+ setCount: (value: number) => set({ count: value }),
15
+ }));
@@ -0,0 +1,2 @@
1
+ export { useCounterStore } from './counter.store';
2
+ export { useUserStore } from './user.store';
@@ -0,0 +1,32 @@
1
+ import { create } from 'zustand';
2
+
3
+ interface User {
4
+ id: string;
5
+ name: string;
6
+ email: string;
7
+ }
8
+
9
+ interface UserState {
10
+ user: User | null;
11
+ isLoading: boolean;
12
+ setUser: (user: User | null) => void;
13
+ fetchUser: (id: string) => Promise<void>;
14
+ clearUser: () => void;
15
+ }
16
+
17
+ export const useUserStore = create<UserState>((set) => ({
18
+ user: null,
19
+ isLoading: false,
20
+ setUser: (user) => set({ user }),
21
+ fetchUser: async (id: string) => {
22
+ set({ isLoading: true });
23
+ try {
24
+ const res = await fetch(`/api/users/${id}`);
25
+ const user = await res.json();
26
+ set({ user, isLoading: false });
27
+ } catch {
28
+ set({ isLoading: false });
29
+ }
30
+ },
31
+ clearUser: () => set({ user: null }),
32
+ }));