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,24 @@
1
+ 'use client';
2
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
3
+ import { httpBatchLink } from '@trpc/client';
4
+ import { useState } from 'react';
5
+ import { trpc } from '@/api/trpc/client.api';
6
+
7
+ export function TrpcProvider({ children }: { children: React.ReactNode }) {
8
+ const [queryClient] = useState(() => new QueryClient());
9
+ const [trpcClient] = useState(() =>
10
+ trpc.createClient({
11
+ links: [
12
+ httpBatchLink({
13
+ url: '/api/trpc',
14
+ }),
15
+ ],
16
+ })
17
+ );
18
+
19
+ return (
20
+ <trpc.Provider client={trpcClient} queryClient={queryClient}>
21
+ <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
22
+ </trpc.Provider>
23
+ );
24
+ }
@@ -0,0 +1,4 @@
1
+ export const clerkAuth = {
2
+ publishableKey: process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY || '',
3
+ secretKey: process.env.CLERK_SECRET_KEY,
4
+ };
@@ -0,0 +1,13 @@
1
+ import { useUser, useAuth as useClerkAuth } from '@clerk/nextjs';
2
+
3
+ export function useAuth() {
4
+ const { user, isLoaded } = useUser();
5
+ const { signOut } = useClerkAuth();
6
+
7
+ return {
8
+ user: user ?? null,
9
+ isAuthenticated: !!user,
10
+ isLoading: !isLoaded,
11
+ logout: () => signOut(),
12
+ };
13
+ }
@@ -0,0 +1,7 @@
1
+ import { clerkMiddleware } from '@clerk/nextjs/server';
2
+
3
+ export default clerkMiddleware();
4
+
5
+ export const config = {
6
+ matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
7
+ };
@@ -0,0 +1,6 @@
1
+ 'use client';
2
+ import { ClerkProvider } from '@clerk/nextjs';
3
+
4
+ export function AuthProvider({ children }: { children: React.ReactNode }) {
5
+ return <ClerkProvider>{children}</ClerkProvider>;
6
+ }
@@ -0,0 +1,5 @@
1
+ import NextAuth from 'next-auth';
2
+ import { authOptions } from '@/auth/next-auth/auth.service';
3
+
4
+ const handler = NextAuth(authOptions);
5
+ export { handler as GET, handler as POST };
@@ -0,0 +1,45 @@
1
+ import type { NextAuthOptions } from 'next-auth';
2
+ import CredentialsProvider from 'next-auth/providers/credentials';
3
+ import GithubProvider from 'next-auth/providers/github';
4
+
5
+ export const authOptions: NextAuthOptions = {
6
+ providers: [
7
+ GithubProvider({
8
+ clientId: process.env.GITHUB_ID || '',
9
+ clientSecret: process.env.GITHUB_SECRET || '',
10
+ }),
11
+ CredentialsProvider({
12
+ name: 'Credentials',
13
+ credentials: {
14
+ email: { label: 'Email', type: 'email' },
15
+ password: { label: 'Password', type: 'password' },
16
+ },
17
+ async authorize(credentials) {
18
+ if (!credentials?.email || !credentials?.password) return null;
19
+ const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/auth/login`, {
20
+ method: 'POST',
21
+ body: JSON.stringify(credentials),
22
+ headers: { 'Content-Type': 'application/json' },
23
+ });
24
+ const user = await res.json();
25
+ if (res.ok && user) return user;
26
+ return null;
27
+ },
28
+ }),
29
+ ],
30
+ pages: {
31
+ signIn: '/auth/login',
32
+ error: '/auth/error',
33
+ },
34
+ session: { strategy: 'jwt' },
35
+ callbacks: {
36
+ async jwt({ token, user }) {
37
+ if (user) token.id = user.id;
38
+ return token;
39
+ },
40
+ async session({ session, token }) {
41
+ if (session.user) session.user.id = token.id as string;
42
+ return session;
43
+ },
44
+ },
45
+ };
@@ -0,0 +1,13 @@
1
+ import { useSession, signIn, signOut } from 'next-auth/react';
2
+
3
+ export function useAuthSession() {
4
+ const { data: session, status } = useSession();
5
+
6
+ return {
7
+ user: session?.user ?? null,
8
+ isAuthenticated: !!session,
9
+ isLoading: status === 'loading',
10
+ login: signIn,
11
+ logout: () => signOut(),
12
+ };
13
+ }
@@ -0,0 +1,6 @@
1
+ 'use client';
2
+ import { SessionProvider } from 'next-auth/react';
3
+
4
+ export function AuthProvider({ children }: { children: React.ReactNode }) {
5
+ return <SessionProvider>{children}</SessionProvider>;
6
+ }
@@ -0,0 +1,30 @@
1
+ 'use client';
2
+ import { useFormik } from 'formik';
3
+ import { loginSchema, type LoginFormData } from '@/forms/formik/schemas/example.schema';
4
+
5
+ export function LoginForm({ onSubmit }: { onSubmit: (values: LoginFormData) => Promise<void> }) {
6
+ const formik = useFormik({
7
+ initialValues: { email: '', password: '' },
8
+ validationSchema: loginSchema,
9
+ onSubmit: async (values, { setSubmitting }) => {
10
+ await onSubmit(values);
11
+ setSubmitting(false);
12
+ },
13
+ });
14
+
15
+ return (
16
+ <form onSubmit={formik.handleSubmit} className="space-y-4">
17
+ <div>
18
+ <input {...formik.getFieldProps('email')} placeholder="Email" className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2" />
19
+ {formik.touched.email && formik.errors.email && <p className="text-sm text-destructive">{formik.errors.email}</p>}
20
+ </div>
21
+ <div>
22
+ <input {...formik.getFieldProps('password')} type="password" placeholder="Password" className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2" />
23
+ {formik.touched.password && formik.errors.password && <p className="text-sm text-destructive">{formik.errors.password}</p>}
24
+ </div>
25
+ <button type="submit" disabled={formik.isSubmitting} className="inline-flex h-10 items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground">
26
+ {formik.isSubmitting ? 'Loading...' : 'Login'}
27
+ </button>
28
+ </form>
29
+ );
30
+ }
@@ -0,0 +1,7 @@
1
+ import { useFormik, type FormikConfig, type FormikValues } from 'formik';
2
+
3
+ export function useFormikForm<T extends FormikValues>(
4
+ config: FormikConfig<T>
5
+ ) {
6
+ return useFormik<T>(config);
7
+ }
@@ -0,0 +1,8 @@
1
+ import * as yup from 'yup';
2
+
3
+ export const loginSchema = yup.object({
4
+ email: yup.string().email('Invalid email').required('Email is required'),
5
+ password: yup.string().min(6, 'Minimum 6 characters').required('Password is required'),
6
+ });
7
+
8
+ export type LoginFormData = yup.InferType<typeof loginSchema>;
@@ -0,0 +1,27 @@
1
+ 'use client';
2
+ import { useZodForm } from '@/forms/react-hook-form/hooks/use-form.hook';
3
+ import { loginSchema, type LoginFormData } from '@/forms/react-hook-form/schemas/example.schema';
4
+
5
+ interface LoginFormProps {
6
+ onSubmit: (data: LoginFormData) => Promise<void>;
7
+ }
8
+
9
+ export function LoginForm({ onSubmit }: LoginFormProps) {
10
+ const { register, handleSubmit, formState: { errors, isSubmitting } } = useZodForm(loginSchema);
11
+
12
+ return (
13
+ <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
14
+ <div>
15
+ <input {...register('email')} placeholder="Email" className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2" />
16
+ {errors.email && <p className="text-sm text-destructive">{errors.email.message}</p>}
17
+ </div>
18
+ <div>
19
+ <input {...register('password')} type="password" placeholder="Password" className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2" />
20
+ {errors.password && <p className="text-sm text-destructive">{errors.password.message}</p>}
21
+ </div>
22
+ <button type="submit" disabled={isSubmitting} className="inline-flex h-10 items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground">
23
+ {isSubmitting ? 'Loading...' : 'Login'}
24
+ </button>
25
+ </form>
26
+ );
27
+ }
@@ -0,0 +1,13 @@
1
+ import { useForm, type UseFormProps } from 'react-hook-form';
2
+ import { zodResolver } from '@hookform/resolvers/zod';
3
+ import { z, type ZodSchema } from 'zod';
4
+
5
+ export function useZodForm<T extends ZodSchema>(
6
+ schema: T,
7
+ options?: Omit<UseFormProps<z.infer<T>>, 'resolver'>
8
+ ) {
9
+ return useForm<z.infer<T>>({
10
+ resolver: zodResolver(schema),
11
+ ...options,
12
+ });
13
+ }
@@ -0,0 +1,15 @@
1
+ import { z } from 'zod';
2
+
3
+ export const loginSchema = z.object({
4
+ email: z.string().email('Invalid email address'),
5
+ password: z.string().min(6, 'Password must be at least 6 characters'),
6
+ });
7
+
8
+ export const signupSchema = z.object({
9
+ name: z.string().min(2, 'Name must be at least 2 characters'),
10
+ email: z.string().email('Invalid email address'),
11
+ password: z.string().min(6, 'Password must be at least 6 characters'),
12
+ });
13
+
14
+ export type LoginFormData = z.infer<typeof loginSchema>;
15
+ export type SignupFormData = z.infer<typeof signupSchema>;
@@ -0,0 +1,5 @@
1
+ import type { NextConfig } from 'next';
2
+
3
+ const nextConfig: NextConfig = {};
4
+
5
+ export default nextConfig;
@@ -0,0 +1,9 @@
1
+ /** @type {import('postcss-load-config').Config} */
2
+ const config = {
3
+ plugins: {
4
+ tailwindcss: {},
5
+ autoprefixer: {},
6
+ },
7
+ };
8
+
9
+ export default config;
@@ -0,0 +1,88 @@
1
+ 'use client';
2
+
3
+ import Link from 'next/link';
4
+ import { usePathname } from 'next/navigation';
5
+ import { useState } from 'react';
6
+ import { ThemeToggle } from '@/components/common/theme-toggle.component';
7
+
8
+ const links = [
9
+ { href: '/lab', label: 'Component Lab' },
10
+ { href: '/test/form', label: 'Form Test' },
11
+ { href: '/test/dialog', label: 'Dialog Test' },
12
+ { href: '/test/table', label: 'Table Test' },
13
+ ];
14
+
15
+ export default function NavBar() {
16
+ const pathname = usePathname();
17
+ const [open, setOpen] = useState(false);
18
+
19
+ return (
20
+ <header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
21
+ <div className="max-w-6xl mx-auto flex h-14 items-center px-6">
22
+ <Link href="/" className="font-bold text-foreground shrink-0 tracking-tight">
23
+ Nex<span className="text-primary">struct</span>
24
+ </Link>
25
+
26
+ <nav className="hidden md:flex items-center gap-1 ml-8">
27
+ {links.map((link) => {
28
+ const active = pathname === link.href || pathname.startsWith(link.href + '/');
29
+ return (
30
+ <Link
31
+ key={link.href}
32
+ href={link.href}
33
+ className={`px-3 py-2 rounded-md text-sm font-medium transition-colors ${
34
+ active
35
+ ? 'bg-primary/10 text-primary'
36
+ : 'text-muted-foreground hover:text-foreground hover:bg-accent'
37
+ }`}
38
+ >
39
+ {link.label}
40
+ </Link>
41
+ );
42
+ })}
43
+ </nav>
44
+
45
+ <div className="ml-auto flex items-center gap-1">
46
+ <ThemeToggle />
47
+ <button
48
+ onClick={() => setOpen(!open)}
49
+ className="md:hidden p-2 rounded-md text-muted-foreground hover:text-foreground hover:bg-accent"
50
+ aria-label="Toggle menu"
51
+ >
52
+ {open ? (
53
+ <svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor">
54
+ <path strokeLinecap="round" strokeLinejoin="round" d="M6 18 18 6M6 6l12 12" />
55
+ </svg>
56
+ ) : (
57
+ <svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor">
58
+ <path strokeLinecap="round" strokeLinejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
59
+ </svg>
60
+ )}
61
+ </button>
62
+ </div>
63
+ </div>
64
+
65
+ {open && (
66
+ <nav className="md:hidden border-t px-6 py-3 space-y-1 bg-background">
67
+ {links.map((link) => {
68
+ const active = pathname === link.href || pathname.startsWith(link.href + '/');
69
+ return (
70
+ <Link
71
+ key={link.href}
72
+ href={link.href}
73
+ onClick={() => setOpen(false)}
74
+ className={`block px-3 py-2 rounded-md text-sm font-medium transition-colors ${
75
+ active
76
+ ? 'bg-primary/10 text-primary'
77
+ : 'text-muted-foreground hover:text-foreground hover:bg-accent'
78
+ }`}
79
+ >
80
+ {link.label}
81
+ </Link>
82
+ );
83
+ })}
84
+ </nav>
85
+ )}
86
+ </header>
87
+ );
88
+ }
@@ -0,0 +1,223 @@
1
+ 'use client';
2
+
3
+ import Link from 'next/link';
4
+ import { usePathname } from 'next/navigation';
5
+ import { useState, useCallback, type ReactNode } from 'react';
6
+ import { ThemeToggle } from '@/components/common/theme-toggle.component';
7
+
8
+ type NavItem = {
9
+ label: string;
10
+ href?: string;
11
+ icon: React.ReactNode;
12
+ children?: { label: string; href: string }[];
13
+ };
14
+
15
+ const navItems: NavItem[] = [
16
+ {
17
+ label: 'Component Lab',
18
+ href: '/lab',
19
+ icon: (
20
+ <svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
21
+ <path strokeLinecap="round" strokeLinejoin="round" d="M9.75 3.104v5.714a2.25 2.25 0 0 1-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 0 1 4.5 0m0 0v5.714c0 .597.237 1.17.659 1.591L19.8 15.3M14.25 3.104c.251.023.501.05.75.082M19.8 15.3l-1.57.393A9.065 9.065 0 0 1 12 15a9.065 9.065 0 0 0-6.23.693L5 14.5m14.8.8 1.402 1.402c1.232 1.232.65 3.318-1.067 3.611A48.309 48.309 0 0 1 12 21c-2.773 0-5.491-.235-8.135-.687-1.718-.293-2.3-2.379-1.067-3.61L5 14.5" />
22
+ </svg>
23
+ ),
24
+ children: [
25
+ { label: 'All Components', href: '/lab' },
26
+ { label: 'Form Fields', href: '/lab?category=field' },
27
+ { label: 'Common', href: '/lab?category=common' },
28
+ ],
29
+ },
30
+ {
31
+ label: 'Form Test',
32
+ href: '/test/form',
33
+ icon: (
34
+ <svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
35
+ <path strokeLinecap="round" strokeLinejoin="round" d="M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 0 1 .865-.501 48.172 48.172 0 0 0 3.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0 0 12 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018Z" />
36
+ </svg>
37
+ ),
38
+ },
39
+ {
40
+ label: 'Dialog Test',
41
+ href: '/test/dialog',
42
+ icon: (
43
+ <svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
44
+ <path strokeLinecap="round" strokeLinejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
45
+ </svg>
46
+ ),
47
+ },
48
+ {
49
+ label: 'Table Test',
50
+ href: '/test/table',
51
+ icon: (
52
+ <svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
53
+ <path strokeLinecap="round" strokeLinejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 0 0 2.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 0 0-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75 2.25 2.25 0 0 0-.1-.664m-5.8 0A2.251 2.251 0 0 1 13.5 2.25H15a2.25 2.25 0 0 1 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25ZM6.75 12h.008v.008H6.75V12Zm0 3h.008v.008H6.75V15Zm0 3h.008v.008H6.75V18Z" />
54
+ </svg>
55
+ ),
56
+ },
57
+ ];
58
+
59
+ export default function Sidebar({ children }: { children: ReactNode }) {
60
+ const pathname = usePathname();
61
+ const [hovered, setHovered] = useState(false);
62
+ const [pinned, setPinned] = useState(false);
63
+ const [openMenus, setOpenMenus] = useState<Set<string>>(new Set());
64
+
65
+ const expanded = hovered || pinned;
66
+
67
+ const toggleMenu = useCallback((label: string) => {
68
+ setOpenMenus((prev) => {
69
+ const next = new Set(prev);
70
+ if (next.has(label)) next.delete(label);
71
+ else next.add(label);
72
+ return next;
73
+ });
74
+ }, []);
75
+
76
+ const isActive = (href?: string) => href && (pathname === href || pathname.startsWith(href + '/'));
77
+
78
+ return (
79
+ <div className="flex min-h-screen">
80
+ <nav
81
+ onMouseEnter={() => setHovered(true)}
82
+ onMouseLeave={() => { if (!pinned) setHovered(false); }}
83
+ className="fixed left-0 top-0 z-40 flex flex-col h-full bg-card border-r border-border overflow-hidden scrollbar-none transition-[width] duration-300 ease-in-out"
84
+ style={{ width: expanded ? '260px' : '60px' }}
85
+ >
86
+ {/* Brand */}
87
+ <div className="flex items-center h-14 px-4 border-b border-border shrink-0">
88
+ <Link href="/" className="flex items-center gap-3 min-w-0 group">
89
+ <div className="h-9 w-9 rounded-xl bg-gradient-to-br from-amber-500 to-orange-600 flex items-center justify-center shrink-0 shadow-sm group-hover:shadow-md transition-shadow relative overflow-hidden">
90
+ <div className="absolute inset-0 bg-[radial-gradient(circle_at_30%_30%,rgba(255,255,255,0.2),transparent_70%)]" />
91
+ <svg className="h-5 w-5 text-white relative" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5}>
92
+ <path strokeLinecap="round" strokeLinejoin="round" d="M11.42 15.17 17.25 21A2.652 2.652 0 0 0 21 17.25l-5.877-5.877M11.42 15.17l2.496-3.03c.317-.384.74-.626 1.208-.766M11.42 15.17l-4.655 5.653a2.548 2.548 0 1 1-3.586-3.586l6.837-5.63m5.108-.233c.55-.164 1.163-.188 1.743-.14a4.5 4.5 0 0 0 4.486-6.336l-3.276 3.277a3.004 3.004 0 0 1-2.25-2.25l3.276-3.276a4.5 4.5 0 0 0-6.336 4.486c.091 1.076-.071 2.264-.904 2.95l-.102.085m-1.745 1.437L5.909 7.5H4.5L2.25 3.75l1.5-1.5L7.5 4.5v1.409l4.087 4.113" />
93
+ </svg>
94
+ </div>
95
+ <span
96
+ className="font-bold text-foreground text-sm whitespace-nowrap transition-opacity duration-300"
97
+ style={{ opacity: expanded ? 1 : 0 }}
98
+ >
99
+ Nex<span className="text-primary">struct</span>
100
+ </span>
101
+ </Link>
102
+ </div>
103
+
104
+ {/* Nav items */}
105
+ <div className="flex-1 py-3 px-2 space-y-0.5 overflow-y-auto scrollbar-none">
106
+ {navItems.map((item) => {
107
+ const active = isActive(item.href);
108
+ const hasChildren = item.children && item.children.length > 0;
109
+ const menuOpen = openMenus.has(item.label);
110
+
111
+ return (
112
+ <div key={item.label}>
113
+ <div
114
+ role="button"
115
+ tabIndex={0}
116
+ onClick={() => {
117
+ if (hasChildren && expanded) {
118
+ toggleMenu(item.label);
119
+ } else if (item.href) {
120
+ window.location.href = item.href;
121
+ }
122
+ }}
123
+ onKeyDown={(e) => {
124
+ if (e.key === 'Enter' || e.key === ' ') {
125
+ if (hasChildren && expanded) toggleMenu(item.label);
126
+ else if (item.href) window.location.href = item.href;
127
+ }
128
+ }}
129
+ className={`flex items-center gap-3 px-3 py-2.5 rounded-lg cursor-pointer transition-colors whitespace-nowrap ${
130
+ active
131
+ ? 'bg-primary/10 text-primary'
132
+ : 'text-muted-foreground hover:text-foreground hover:bg-accent'
133
+ }`}
134
+ >
135
+ <span className="shrink-0">{item.icon}</span>
136
+ <span
137
+ className="text-sm font-medium transition-opacity duration-300"
138
+ style={{ opacity: expanded ? 1 : 0 }}
139
+ >
140
+ {item.label}
141
+ </span>
142
+ {hasChildren && expanded && (
143
+ <svg
144
+ className={`h-4 w-4 ml-auto transition-transform duration-200 ${menuOpen ? 'rotate-90' : ''}`}
145
+ fill="none"
146
+ viewBox="0 0 24 24"
147
+ strokeWidth={2}
148
+ stroke="currentColor"
149
+ >
150
+ <path strokeLinecap="round" strokeLinejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
151
+ </svg>
152
+ )}
153
+ </div>
154
+
155
+ {/* Nested sub-menu */}
156
+ {hasChildren && expanded && (
157
+ <div
158
+ className="overflow-hidden scrollbar-none transition-all duration-250 ease-in-out"
159
+ style={{
160
+ maxHeight: menuOpen ? `${(item.children?.length || 0) * 44}px` : '0',
161
+ opacity: menuOpen ? 1 : 0,
162
+ }}
163
+ >
164
+ {item.children?.map((child) => {
165
+ const childActive = pathname === child.href;
166
+ return (
167
+ <Link
168
+ key={child.href}
169
+ href={child.href}
170
+ onClick={() => setHovered(false)}
171
+ className={`flex items-center gap-3 ml-9 px-3 py-2 rounded-md text-sm transition-colors ${
172
+ childActive
173
+ ? 'text-primary font-medium'
174
+ : 'text-muted-foreground hover:text-foreground hover:bg-accent'
175
+ }`}
176
+ >
177
+ <span className="h-1 w-1 rounded-full bg-current shrink-0" />
178
+ {child.label}
179
+ </Link>
180
+ );
181
+ })}
182
+ </div>
183
+ )}
184
+ </div>
185
+ );
186
+ })}
187
+ </div>
188
+
189
+ {/* Toggle pin + Theme */}
190
+ <div className="border-t border-border px-2 py-2 shrink-0 space-y-1">
191
+ <button
192
+ onClick={() => { setPinned((p) => !p); }}
193
+ className={`flex items-center gap-3 w-full px-3 py-2.5 rounded-lg transition-colors ${
194
+ pinned ? 'bg-primary/10 text-primary' : 'text-muted-foreground hover:text-foreground hover:bg-accent'
195
+ }`}
196
+ >
197
+ <svg className="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
198
+ <path strokeLinecap="round" strokeLinejoin="round" d="M9 9V4.5M9 9H4.5M9 9 3.75 3.75M9 15v4.5M9 15H4.5M9 15l-5.25 5.25M15 9h4.5M15 9V4.5M15 9l5.25-5.25M15 15h4.5M15 15v4.5m0-4.5 5.25 5.25" />
199
+ </svg>
200
+ <span
201
+ className="text-sm font-medium whitespace-nowrap transition-opacity duration-300"
202
+ style={{ opacity: expanded ? 1 : 0 }}
203
+ >
204
+ {pinned ? 'Unpin sidebar' : 'Pin sidebar'}
205
+ </span>
206
+ </button>
207
+
208
+ </div>
209
+ </nav>
210
+
211
+ {/* Main content */}
212
+ <main
213
+ className="flex-1 transition-[padding] duration-300 ease-in-out relative"
214
+ style={{ paddingLeft: expanded ? '260px' : '60px' }}
215
+ >
216
+ <div className="fixed top-3 right-4 z-50">
217
+ <ThemeToggle />
218
+ </div>
219
+ {children}
220
+ </main>
221
+ </div>
222
+ );
223
+ }
@@ -0,0 +1,39 @@
1
+ 'use client';
2
+
3
+ export default function Error({
4
+ error,
5
+ reset,
6
+ }: {
7
+ error: Error & { digest?: string };
8
+ reset: () => void;
9
+ }) {
10
+ return (
11
+ <div className="min-h-screen flex flex-col items-center justify-center bg-background px-4">
12
+ <div className="flex h-20 w-20 items-center justify-center rounded-full bg-destructive/10 mb-6">
13
+ <svg
14
+ className="h-10 w-10 text-destructive"
15
+ fill="none"
16
+ viewBox="0 0 24 24"
17
+ strokeWidth={1.5}
18
+ stroke="currentColor"
19
+ >
20
+ <path
21
+ strokeLinecap="round"
22
+ strokeLinejoin="round"
23
+ d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z"
24
+ />
25
+ </svg>
26
+ </div>
27
+ <h1 className="text-2xl font-semibold text-foreground mb-2">Something went wrong</h1>
28
+ <p className="text-sm text-muted-foreground text-center max-w-md mb-6">
29
+ {error.message || 'An unexpected error occurred. Please try again.'}
30
+ </p>
31
+ <button
32
+ onClick={reset}
33
+ className="inline-flex items-center justify-center rounded-md bg-primary px-6 py-2 text-sm font-medium text-primary-foreground shadow hover:bg-primary/90 transition-colors"
34
+ >
35
+ Try again
36
+ </button>
37
+ </div>
38
+ );
39
+ }