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.
- package/AGENTS.md +122 -0
- package/LICENSE +21 -0
- package/README.md +103 -0
- package/package.json +99 -0
- package/scaffold/generator.js +409 -0
- package/scaffold/index.js +20 -0
- package/scaffold/prompts.js +108 -0
- package/templates/api/axios/src/api/axios/client.api.ts +30 -0
- package/templates/api/axios/src/api/axios/users.api.ts +15 -0
- package/templates/api/fetch/src/api/fetch/client.api.ts +68 -0
- package/templates/api/fetch/src/api/fetch/users.api.ts +15 -0
- package/templates/api/trpc/src/api/trpc/client.api.ts +4 -0
- package/templates/api/trpc/src/api/trpc/router.api.ts +15 -0
- package/templates/api/trpc/src/api/trpc/server.client.api.ts +4 -0
- package/templates/api/trpc/src/providers/trpc.provider.tsx +24 -0
- package/templates/auth/clerk/src/auth/clerk/auth.service.ts +4 -0
- package/templates/auth/clerk/src/hooks/use-auth.hook.ts +13 -0
- package/templates/auth/clerk/src/middleware.ts +7 -0
- package/templates/auth/clerk/src/providers/auth.provider.tsx +6 -0
- package/templates/auth/next-auth/src/app/api/auth/[...nextauth]/route.ts +5 -0
- package/templates/auth/next-auth/src/auth/next-auth/auth.service.ts +45 -0
- package/templates/auth/next-auth/src/hooks/use-session.hook.ts +13 -0
- package/templates/auth/next-auth/src/providers/session.provider.tsx +6 -0
- package/templates/forms/formik/src/components/forms/login-form.component.tsx +30 -0
- package/templates/forms/formik/src/forms/formik/hooks/use-form-config.hook.ts +7 -0
- package/templates/forms/formik/src/forms/formik/schemas/example.schema.ts +8 -0
- package/templates/forms/react-hook-form/src/components/forms/login-form.component.tsx +27 -0
- package/templates/forms/react-hook-form/src/forms/react-hook-form/hooks/use-form.hook.ts +13 -0
- package/templates/forms/react-hook-form/src/forms/react-hook-form/schemas/example.schema.ts +15 -0
- package/templates/nextjs-base/next.config.ts +5 -0
- package/templates/nextjs-base/postcss.config.mjs +9 -0
- package/templates/nextjs-base/src/app/_components/navbar.tsx +88 -0
- package/templates/nextjs-base/src/app/_components/sidebar.tsx +223 -0
- package/templates/nextjs-base/src/app/error.tsx +39 -0
- package/templates/nextjs-base/src/app/globals.css +71 -0
- package/templates/nextjs-base/src/app/layout.tsx +21 -0
- package/templates/nextjs-base/src/app/loading.tsx +13 -0
- package/templates/nextjs-base/src/app/not-found.tsx +22 -0
- package/templates/nextjs-base/src/app/page.tsx +10 -0
- package/templates/nextjs-base/tailwind.config.ts +69 -0
- package/templates/shared/src/components/common/theme-toggle.component.tsx +31 -0
- package/templates/shared/src/components/common/toast/custom-message.component.tsx +18 -0
- package/templates/shared/src/components/common/toast/index.ts +8 -0
- package/templates/shared/src/components/common/toast/toast-message.component.tsx +112 -0
- package/templates/shared/src/hooks/use-debounce.hook.ts +12 -0
- package/templates/shared/src/hooks/use-fetch.hook.ts +42 -0
- package/templates/shared/src/hooks/use-intersection-observer.hook.ts +39 -0
- package/templates/shared/src/hooks/use-local-storage.hook.ts +30 -0
- package/templates/shared/src/hooks/use-media-query.hook.ts +26 -0
- package/templates/shared/src/hooks/use-toggle.hook.ts +12 -0
- package/templates/shared/src/lib/utils.util.ts +361 -0
- package/templates/shared/src/providers/theme.provider.tsx +17 -0
- package/templates/shared/src/providers/toast.provider.tsx +32 -0
- package/templates/shared/src/types/common.type.ts +34 -0
- package/templates/state/context/src/store/context/auth.context.tsx +47 -0
- package/templates/state/context/src/store/context/counter.context.tsx +41 -0
- package/templates/state/context/src/store/context/index.ts +2 -0
- package/templates/state/redux/src/providers/redux.provider.tsx +7 -0
- package/templates/state/redux/src/store/redux/hooks.store.ts +5 -0
- package/templates/state/redux/src/store/redux/index.ts +4 -0
- package/templates/state/redux/src/store/redux/slices/api.slice.ts +8 -0
- package/templates/state/redux/src/store/redux/slices/counter.slice.ts +24 -0
- package/templates/state/redux/src/store/redux/store.store.ts +13 -0
- package/templates/state/zustand/src/store/zustand/counter.store.ts +15 -0
- package/templates/state/zustand/src/store/zustand/index.ts +2 -0
- package/templates/state/zustand/src/store/zustand/user.store.ts +32 -0
- package/templates/ui/antd/COMPONENT_GUIDE.md +326 -0
- package/templates/ui/antd/src/app/examples/dialog/page.tsx +205 -0
- package/templates/ui/antd/src/app/examples/form/page.tsx +160 -0
- package/templates/ui/antd/src/app/examples/layout.tsx +125 -0
- package/templates/ui/antd/src/app/examples/page.tsx +64 -0
- package/templates/ui/antd/src/app/examples/table/page.tsx +118 -0
- package/templates/ui/antd/src/app/page.tsx +283 -0
- package/templates/ui/antd/src/components/common/DynamicTable/dynamic-table.component.tsx +79 -0
- package/templates/ui/antd/src/components/common/button/action-button.component.tsx +63 -0
- package/templates/ui/antd/src/components/common/dialog/dialog-wrapper.component.tsx +63 -0
- package/templates/ui/antd/src/components/common/fields/assets/components/check-field.component.tsx +55 -0
- package/templates/ui/antd/src/components/common/fields/assets/components/date-picker-field.component.tsx +80 -0
- package/templates/ui/antd/src/components/common/fields/assets/components/limit-field.component.tsx +26 -0
- package/templates/ui/antd/src/components/common/fields/assets/components/multi-check-field.component.tsx +56 -0
- package/templates/ui/antd/src/components/common/fields/assets/components/number-field.component.tsx +100 -0
- package/templates/ui/antd/src/components/common/fields/assets/components/otp-field.component.tsx +63 -0
- package/templates/ui/antd/src/components/common/fields/assets/components/password-field.component.tsx +106 -0
- package/templates/ui/antd/src/components/common/fields/assets/components/phone-number-field.component.tsx +78 -0
- package/templates/ui/antd/src/components/common/fields/assets/components/radio-field.component.tsx +55 -0
- package/templates/ui/antd/src/components/common/fields/assets/components/range-date-picker.component.tsx +66 -0
- package/templates/ui/antd/src/components/common/fields/assets/components/search-field.component.tsx +24 -0
- package/templates/ui/antd/src/components/common/fields/assets/components/select-field.component.tsx +82 -0
- package/templates/ui/antd/src/components/common/fields/assets/components/single-check-field.component.tsx +50 -0
- package/templates/ui/antd/src/components/common/fields/assets/components/single-select-field.component.tsx +86 -0
- package/templates/ui/antd/src/components/common/fields/assets/components/string-number-field.component.tsx +80 -0
- package/templates/ui/antd/src/components/common/fields/assets/components/switch-field.component.tsx +62 -0
- package/templates/ui/antd/src/components/common/fields/assets/components/text-area-field.component.tsx +85 -0
- package/templates/ui/antd/src/components/common/fields/assets/components/text-field.component.tsx +88 -0
- package/templates/ui/antd/src/components/common/fields/assets/interface/input-props.type.ts +233 -0
- package/templates/ui/antd/src/components/common/fields/cusInputField.component.tsx +40 -0
- package/templates/ui/antd/src/components/common/pagination/pagination.component.tsx +27 -0
- package/templates/ui/antd/src/components/ui/avatar.component.tsx +8 -0
- package/templates/ui/antd/src/components/ui/badge.component.tsx +8 -0
- package/templates/ui/antd/src/components/ui/button.component.tsx +8 -0
- package/templates/ui/antd/src/components/ui/card.component.tsx +8 -0
- package/templates/ui/antd/src/components/ui/checkbox.component.tsx +8 -0
- package/templates/ui/antd/src/components/ui/dialog.component.tsx +9 -0
- package/templates/ui/antd/src/components/ui/dropdown-menu.component.tsx +10 -0
- package/templates/ui/antd/src/components/ui/form.component.tsx +12 -0
- package/templates/ui/antd/src/components/ui/input.component.tsx +13 -0
- package/templates/ui/antd/src/components/ui/label.component.tsx +18 -0
- package/templates/ui/antd/src/components/ui/popover.component.tsx +8 -0
- package/templates/ui/antd/src/components/ui/progress.component.tsx +8 -0
- package/templates/ui/antd/src/components/ui/radio-group.component.tsx +10 -0
- package/templates/ui/antd/src/components/ui/scroll-area.component.tsx +25 -0
- package/templates/ui/antd/src/components/ui/select.component.tsx +8 -0
- package/templates/ui/antd/src/components/ui/separator.component.tsx +8 -0
- package/templates/ui/antd/src/components/ui/sheet.component.tsx +8 -0
- package/templates/ui/antd/src/components/ui/switch.component.tsx +8 -0
- package/templates/ui/antd/src/components/ui/table.component.tsx +8 -0
- package/templates/ui/antd/src/components/ui/tabs.component.tsx +8 -0
- package/templates/ui/antd/src/components/ui/textarea.component.tsx +9 -0
- package/templates/ui/antd/src/components/ui/tooltip.component.tsx +8 -0
- package/templates/ui/antd/src/lib/theme.util.ts +40 -0
- package/templates/ui/antd/src/providers/antd.provider.tsx +13 -0
- package/templates/ui/mui/src/app/examples/layout.tsx +113 -0
- package/templates/ui/mui/src/app/examples/page.tsx +716 -0
- package/templates/ui/mui/src/app/page.tsx +298 -0
- package/templates/ui/mui/src/components/common/DynamicTable/dynamic-table.component.tsx +131 -0
- package/templates/ui/mui/src/components/common/button/action-button.component.tsx +57 -0
- package/templates/ui/mui/src/components/common/dialog/dialog-wrapper.component.tsx +55 -0
- package/templates/ui/mui/src/components/common/fields/assets/components/check-field.component.tsx +51 -0
- package/templates/ui/mui/src/components/common/fields/assets/components/date-picker-field.component.tsx +50 -0
- package/templates/ui/mui/src/components/common/fields/assets/components/multi-check-field.component.tsx +14 -0
- package/templates/ui/mui/src/components/common/fields/assets/components/number-field.component.tsx +59 -0
- package/templates/ui/mui/src/components/common/fields/assets/components/password-field.component.tsx +87 -0
- package/templates/ui/mui/src/components/common/fields/assets/components/phone-number-field.component.tsx +48 -0
- package/templates/ui/mui/src/components/common/fields/assets/components/radio-field.component.tsx +37 -0
- package/templates/ui/mui/src/components/common/fields/assets/components/search-field.component.tsx +41 -0
- package/templates/ui/mui/src/components/common/fields/assets/components/select-field.component.tsx +77 -0
- package/templates/ui/mui/src/components/common/fields/assets/components/single-check-field.component.tsx +39 -0
- package/templates/ui/mui/src/components/common/fields/assets/components/single-select-field.component.tsx +56 -0
- package/templates/ui/mui/src/components/common/fields/assets/components/string-number-field.component.tsx +52 -0
- package/templates/ui/mui/src/components/common/fields/assets/components/switch-field.component.tsx +35 -0
- package/templates/ui/mui/src/components/common/fields/assets/components/text-area-field.component.tsx +46 -0
- package/templates/ui/mui/src/components/common/fields/assets/components/text-field.component.tsx +51 -0
- package/templates/ui/mui/src/components/common/fields/assets/interface/input-props.type.ts +193 -0
- package/templates/ui/mui/src/components/common/fields/cusInputField.component.tsx +34 -0
- package/templates/ui/mui/src/components/common/pagination/pagination.component.tsx +59 -0
- package/templates/ui/mui/src/components/ui/avatar.component.tsx +19 -0
- package/templates/ui/mui/src/components/ui/badge.component.tsx +18 -0
- package/templates/ui/mui/src/components/ui/button.component.tsx +22 -0
- package/templates/ui/mui/src/components/ui/card.component.tsx +39 -0
- package/templates/ui/mui/src/components/ui/checkbox.component.tsx +21 -0
- package/templates/ui/mui/src/components/ui/dialog.component.tsx +38 -0
- package/templates/ui/mui/src/components/ui/dropdown-menu.component.tsx +43 -0
- package/templates/ui/mui/src/components/ui/form.component.tsx +98 -0
- package/templates/ui/mui/src/components/ui/input.component.tsx +15 -0
- package/templates/ui/mui/src/components/ui/label.component.tsx +15 -0
- package/templates/ui/mui/src/components/ui/popover.component.tsx +20 -0
- package/templates/ui/mui/src/components/ui/progress.component.tsx +19 -0
- package/templates/ui/mui/src/components/ui/radio-group.component.tsx +25 -0
- package/templates/ui/mui/src/components/ui/scroll-area.component.tsx +27 -0
- package/templates/ui/mui/src/components/ui/select.component.tsx +26 -0
- package/templates/ui/mui/src/components/ui/separator.component.tsx +11 -0
- package/templates/ui/mui/src/components/ui/sheet.component.tsx +44 -0
- package/templates/ui/mui/src/components/ui/switch.component.tsx +23 -0
- package/templates/ui/mui/src/components/ui/table.component.tsx +34 -0
- package/templates/ui/mui/src/components/ui/tabs.component.tsx +38 -0
- package/templates/ui/mui/src/components/ui/textarea.component.tsx +18 -0
- package/templates/ui/mui/src/components/ui/tooltip.component.tsx +24 -0
- package/templates/ui/mui/src/lib/theme.util.ts +73 -0
- package/templates/ui/mui/src/providers/mui.provider.tsx +13 -0
- package/templates/ui/shadcn/COMPONENT_GUIDE.md +306 -0
- package/templates/ui/shadcn/src/app/examples/dialog/page.tsx +122 -0
- package/templates/ui/shadcn/src/app/examples/form/page.tsx +107 -0
- package/templates/ui/shadcn/src/app/examples/layout.tsx +24 -0
- package/templates/ui/shadcn/src/app/examples/page.tsx +30 -0
- package/templates/ui/shadcn/src/app/examples/table/page.tsx +77 -0
- package/templates/ui/shadcn/src/app/page.tsx +20 -0
- package/templates/ui/shadcn/src/components/common/DynamicTable/dynamic-table.component.tsx +136 -0
- package/templates/ui/shadcn/src/components/common/button/action-button.component.tsx +68 -0
- package/templates/ui/shadcn/src/components/common/dialog/dialog-wrapper.component.tsx +58 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/check-field.component.tsx +52 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/date-picker-field.component.tsx +62 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/dynamic-file-upload-field.component.tsx +152 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/limit-field.component.tsx +73 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/multi-check-field.component.tsx +46 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/number-field.component.tsx +124 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/otp-field.component.tsx +61 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/password-field.component.tsx +110 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/phone-number-field.component.tsx +90 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/radio-field.component.tsx +41 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/range-date-picker.component.tsx +71 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/rich-text-editor.component.tsx +91 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/search-field.component.tsx +34 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/select-field.component.tsx +231 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/single-check-field.component.tsx +42 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/single-select-field.component.tsx +82 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/string-number-field.component.tsx +68 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/switch-field.component.tsx +61 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/text-area-field.component.tsx +62 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/text-area-with-file.component.tsx +142 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/text-field.component.tsx +80 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/tiny-editor.component.tsx +51 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/upload-profile-picture.component.tsx +103 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/upload-video-file.component.tsx +86 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/interface/input-props.type.ts +198 -0
- package/templates/ui/shadcn/src/components/common/fields/cusInputField.component.tsx +52 -0
- package/templates/ui/shadcn/src/components/common/pagination/pagination.component.tsx +68 -0
- package/templates/ui/shadcn/src/components/ui/avatar.component.tsx +37 -0
- package/templates/ui/shadcn/src/components/ui/badge.component.tsx +28 -0
- package/templates/ui/shadcn/src/components/ui/button.component.tsx +52 -0
- package/templates/ui/shadcn/src/components/ui/card.component.tsx +46 -0
- package/templates/ui/shadcn/src/components/ui/checkbox.component.tsx +25 -0
- package/templates/ui/shadcn/src/components/ui/dialog.component.tsx +98 -0
- package/templates/ui/shadcn/src/components/ui/dropdown-menu.component.tsx +163 -0
- package/templates/ui/shadcn/src/components/ui/form.component.tsx +110 -0
- package/templates/ui/shadcn/src/components/ui/input-otp.component.tsx +64 -0
- package/templates/ui/shadcn/src/components/ui/input.component.tsx +23 -0
- package/templates/ui/shadcn/src/components/ui/label.component.tsx +23 -0
- package/templates/ui/shadcn/src/components/ui/popover.component.tsx +27 -0
- package/templates/ui/shadcn/src/components/ui/progress.component.tsx +22 -0
- package/templates/ui/shadcn/src/components/ui/radio-group.component.tsx +33 -0
- package/templates/ui/shadcn/src/components/ui/scroll-area.component.tsx +37 -0
- package/templates/ui/shadcn/src/components/ui/select.component.tsx +139 -0
- package/templates/ui/shadcn/src/components/ui/separator.component.tsx +23 -0
- package/templates/ui/shadcn/src/components/ui/sheet.component.tsx +89 -0
- package/templates/ui/shadcn/src/components/ui/switch.component.tsx +26 -0
- package/templates/ui/shadcn/src/components/ui/table.component.tsx +71 -0
- package/templates/ui/shadcn/src/components/ui/tabs.component.tsx +52 -0
- package/templates/ui/shadcn/src/components/ui/textarea.component.tsx +20 -0
- package/templates/ui/shadcn/src/components/ui/tooltip.component.tsx +25 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
@layer base {
|
|
6
|
+
html {
|
|
7
|
+
@apply transition-colors duration-300;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
@layer base {
|
|
12
|
+
:root {
|
|
13
|
+
--background: 0 0% 100%;
|
|
14
|
+
--foreground: 222.2 84% 4.9%;
|
|
15
|
+
--card: 0 0% 100%;
|
|
16
|
+
--card-foreground: 222.2 84% 4.9%;
|
|
17
|
+
--primary: 221.2 83.2% 53.3%;
|
|
18
|
+
--primary-foreground: 210 40% 98%;
|
|
19
|
+
--secondary: 210 40% 96.1%;
|
|
20
|
+
--secondary-foreground: 222.2 47.4% 11.2%;
|
|
21
|
+
--muted: 210 40% 96.1%;
|
|
22
|
+
--muted-foreground: 215.4 16.3% 46.9%;
|
|
23
|
+
--accent: 210 40% 96.1%;
|
|
24
|
+
--accent-foreground: 222.2 47.4% 11.2%;
|
|
25
|
+
--destructive: 0 84.2% 60.2%;
|
|
26
|
+
--destructive-foreground: 210 40% 98%;
|
|
27
|
+
--border: 214.3 31.8% 91.4%;
|
|
28
|
+
--input: 214.3 31.8% 91.4%;
|
|
29
|
+
--ring: 221.2 83.2% 53.3%;
|
|
30
|
+
--radius: 0.5rem;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.dark {
|
|
34
|
+
--background: 222.2 84% 4.9%;
|
|
35
|
+
--foreground: 210 40% 98%;
|
|
36
|
+
--card: 222.2 84% 4.9%;
|
|
37
|
+
--card-foreground: 210 40% 98%;
|
|
38
|
+
--primary: 217.2 91.2% 59.8%;
|
|
39
|
+
--primary-foreground: 222.2 47.4% 11.2%;
|
|
40
|
+
--secondary: 217.2 32.6% 17.5%;
|
|
41
|
+
--secondary-foreground: 210 40% 98%;
|
|
42
|
+
--muted: 217.2 32.6% 17.5%;
|
|
43
|
+
--muted-foreground: 215 20.2% 65.1%;
|
|
44
|
+
--accent: 217.2 32.6% 17.5%;
|
|
45
|
+
--accent-foreground: 210 40% 98%;
|
|
46
|
+
--destructive: 0 62.8% 30.6%;
|
|
47
|
+
--destructive-foreground: 210 40% 98%;
|
|
48
|
+
--border: 217.2 32.6% 17.5%;
|
|
49
|
+
--input: 217.2 32.6% 17.5%;
|
|
50
|
+
--ring: 224.3 76.3% 48%;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@layer base {
|
|
55
|
+
* {
|
|
56
|
+
@apply border-border;
|
|
57
|
+
}
|
|
58
|
+
body {
|
|
59
|
+
@apply bg-background text-foreground;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@layer utilities {
|
|
64
|
+
.scrollbar-none {
|
|
65
|
+
-ms-overflow-style: none;
|
|
66
|
+
scrollbar-width: none;
|
|
67
|
+
}
|
|
68
|
+
.scrollbar-none::-webkit-scrollbar {
|
|
69
|
+
display: none;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Metadata } from 'next';
|
|
2
|
+
import { Inter } from 'next/font/google';
|
|
3
|
+
import './globals.css';
|
|
4
|
+
import Sidebar from './_components/sidebar';
|
|
5
|
+
|
|
6
|
+
const inter = Inter({ subsets: ['latin'] });
|
|
7
|
+
|
|
8
|
+
export const metadata: Metadata = {
|
|
9
|
+
title: 'Nexstruct',
|
|
10
|
+
description: 'Scaffolded with Nexstruct',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
14
|
+
return (
|
|
15
|
+
<html lang="en" suppressHydrationWarning>
|
|
16
|
+
<body className={inter.className} suppressHydrationWarning>
|
|
17
|
+
<Sidebar>{children}</Sidebar>
|
|
18
|
+
</body>
|
|
19
|
+
</html>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export default function Loading() {
|
|
2
|
+
return (
|
|
3
|
+
<div className="min-h-screen flex flex-col items-center justify-center bg-background">
|
|
4
|
+
<div className="relative">
|
|
5
|
+
<div className="h-16 w-16 animate-spin rounded-full border-4 border-muted border-t-primary" />
|
|
6
|
+
<div className="absolute inset-0 flex items-center justify-center">
|
|
7
|
+
<div className="h-4 w-4 rounded-full bg-primary animate-pulse" />
|
|
8
|
+
</div>
|
|
9
|
+
</div>
|
|
10
|
+
<p className="mt-6 text-sm text-muted-foreground animate-pulse">Loading…</p>
|
|
11
|
+
</div>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import Link from 'next/link';
|
|
2
|
+
|
|
3
|
+
export default function NotFound() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="min-h-screen flex flex-col items-center justify-center bg-background px-4">
|
|
6
|
+
<div className="text-8xl font-bold text-primary/20 select-none">404</div>
|
|
7
|
+
<h1 className="text-2xl font-semibold text-foreground mt-4 mb-2">Page not found</h1>
|
|
8
|
+
<p className="text-sm text-muted-foreground text-center max-w-md mb-8">
|
|
9
|
+
The page you're looking for doesn't exist or has been moved.
|
|
10
|
+
</p>
|
|
11
|
+
<Link
|
|
12
|
+
href="/"
|
|
13
|
+
className="inline-flex items-center justify-center gap-2 rounded-md bg-primary px-6 py-2 text-sm font-medium text-primary-foreground shadow hover:bg-primary/90 transition-colors"
|
|
14
|
+
>
|
|
15
|
+
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor">
|
|
16
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M9 15 3 9m0 0 6-6M3 9h12a6 6 0 0 1 0 12h-3" />
|
|
17
|
+
</svg>
|
|
18
|
+
Back to home
|
|
19
|
+
</Link>
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export default function Home() {
|
|
2
|
+
return (
|
|
3
|
+
<main className="flex min-h-screen flex-col items-center justify-center p-24">
|
|
4
|
+
<h1 className="text-4xl font-bold">Nexstruct</h1>
|
|
5
|
+
<p className="mt-4 text-lg text-muted-foreground">
|
|
6
|
+
Your project is ready. Start building.
|
|
7
|
+
</p>
|
|
8
|
+
</main>
|
|
9
|
+
);
|
|
10
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { Config } from 'tailwindcss';
|
|
2
|
+
import animate from 'tailwindcss-animate';
|
|
3
|
+
|
|
4
|
+
const config: Config = {
|
|
5
|
+
darkMode: 'class',
|
|
6
|
+
content: ['./src/**/*.{ts,tsx}'],
|
|
7
|
+
theme: {
|
|
8
|
+
container: {
|
|
9
|
+
center: true,
|
|
10
|
+
padding: '2rem',
|
|
11
|
+
screens: { '2xl': '1400px' },
|
|
12
|
+
},
|
|
13
|
+
extend: {
|
|
14
|
+
colors: {
|
|
15
|
+
border: 'hsl(var(--border))',
|
|
16
|
+
input: 'hsl(var(--input))',
|
|
17
|
+
ring: 'hsl(var(--ring))',
|
|
18
|
+
background: 'hsl(var(--background))',
|
|
19
|
+
foreground: 'hsl(var(--foreground))',
|
|
20
|
+
primary: {
|
|
21
|
+
DEFAULT: 'hsl(var(--primary))',
|
|
22
|
+
foreground: 'hsl(var(--primary-foreground))',
|
|
23
|
+
},
|
|
24
|
+
secondary: {
|
|
25
|
+
DEFAULT: 'hsl(var(--secondary))',
|
|
26
|
+
foreground: 'hsl(var(--secondary-foreground))',
|
|
27
|
+
},
|
|
28
|
+
destructive: {
|
|
29
|
+
DEFAULT: 'hsl(var(--destructive))',
|
|
30
|
+
foreground: 'hsl(var(--destructive-foreground))',
|
|
31
|
+
},
|
|
32
|
+
muted: {
|
|
33
|
+
DEFAULT: 'hsl(var(--muted))',
|
|
34
|
+
foreground: 'hsl(var(--muted-foreground))',
|
|
35
|
+
},
|
|
36
|
+
accent: {
|
|
37
|
+
DEFAULT: 'hsl(var(--accent))',
|
|
38
|
+
foreground: 'hsl(var(--accent-foreground))',
|
|
39
|
+
},
|
|
40
|
+
card: {
|
|
41
|
+
DEFAULT: 'hsl(var(--card))',
|
|
42
|
+
foreground: 'hsl(var(--card-foreground))',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
borderRadius: {
|
|
46
|
+
lg: 'var(--radius)',
|
|
47
|
+
md: 'calc(var(--radius) - 2px)',
|
|
48
|
+
sm: 'calc(var(--radius) - 4px)',
|
|
49
|
+
},
|
|
50
|
+
keyframes: {
|
|
51
|
+
'accordion-down': {
|
|
52
|
+
from: { height: '0' },
|
|
53
|
+
to: { height: 'var(--radix-accordion-content-height)' },
|
|
54
|
+
},
|
|
55
|
+
'accordion-up': {
|
|
56
|
+
from: { height: 'var(--radix-accordion-content-height)' },
|
|
57
|
+
to: { height: '0' },
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
animation: {
|
|
61
|
+
'accordion-down': 'accordion-down 0.2s ease-out',
|
|
62
|
+
'accordion-up': 'accordion-up 0.2s ease-out',
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
plugins: [animate],
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export default config;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useTheme } from 'next-themes';
|
|
4
|
+
import { useEffect, useState } from 'react';
|
|
5
|
+
|
|
6
|
+
export function ThemeToggle() {
|
|
7
|
+
const { theme, setTheme } = useTheme();
|
|
8
|
+
const [mounted, setMounted] = useState(false);
|
|
9
|
+
|
|
10
|
+
useEffect(() => setMounted(true), []);
|
|
11
|
+
|
|
12
|
+
if (!mounted) return <div className="h-9 w-9" />;
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<button
|
|
16
|
+
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
|
|
17
|
+
className="rounded-md p-2 text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
|
|
18
|
+
aria-label="Toggle theme"
|
|
19
|
+
>
|
|
20
|
+
{theme === 'dark' ? (
|
|
21
|
+
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
|
|
22
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z" />
|
|
23
|
+
</svg>
|
|
24
|
+
) : (
|
|
25
|
+
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
|
|
26
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z" />
|
|
27
|
+
</svg>
|
|
28
|
+
)}
|
|
29
|
+
</button>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ToastMessageChange — Transform common error/response messages into
|
|
5
|
+
* human-friendly text. Register known patterns here.
|
|
6
|
+
*/
|
|
7
|
+
export const ToastMessageChange = (message: string | any): string => {
|
|
8
|
+
switch (message) {
|
|
9
|
+
case `Prisma: Unique constraint failed on field "name"`:
|
|
10
|
+
return 'Duplicate data found. Please change the title and try again.';
|
|
11
|
+
case `Prisma: Unique constraint failed on field "courseType"`:
|
|
12
|
+
return 'Duplicate data found. Please change the course type and try again.';
|
|
13
|
+
case 'Course total credits is less than modules total credits':
|
|
14
|
+
return 'Please ensure the assigned module credit is within the course credit.';
|
|
15
|
+
default:
|
|
16
|
+
return message;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { ToastMessageShow } from './toast-message.component';
|
|
2
|
+
export { ToastMessageChange } from './custom-message.component';
|
|
3
|
+
export {
|
|
4
|
+
toastSuccessMessage,
|
|
5
|
+
toastErrorMessage,
|
|
6
|
+
toastLoadingMessage,
|
|
7
|
+
toastCustomMessage,
|
|
8
|
+
} from './toast-message.component';
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
'use client';
|
|
3
|
+
|
|
4
|
+
import toast, { ToastOptions } from 'react-hot-toast';
|
|
5
|
+
import { LabelAndPlaceholderTextFormat } from '@/lib/utils.util';
|
|
6
|
+
import { ToastMessageChange } from './custom-message.component';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Available toast notification types
|
|
10
|
+
*/
|
|
11
|
+
type ToastType = 'success' | 'error' | 'loading' | 'custom';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Extended toast options including custom flags
|
|
15
|
+
*/
|
|
16
|
+
type ExtendedToastOptions = ToastOptions & {
|
|
17
|
+
textFormat?: boolean;
|
|
18
|
+
changeMessage?: boolean;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Default toast configuration
|
|
23
|
+
*/
|
|
24
|
+
const defaultOptions: ExtendedToastOptions = {
|
|
25
|
+
duration: 3000,
|
|
26
|
+
position: 'top-center',
|
|
27
|
+
textFormat: true,
|
|
28
|
+
changeMessage: false,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* ToastMessageShow — Unified toast notification system.
|
|
33
|
+
*
|
|
34
|
+
* Supports:
|
|
35
|
+
* - Success / Error / Loading / Custom types
|
|
36
|
+
* - Extracts messages from API error responses
|
|
37
|
+
* - Optional message transformation via ToastMessageChange
|
|
38
|
+
* - Optional text case formatting
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ToastMessageShow('success', 'Profile updated successfully');
|
|
42
|
+
* ToastMessageShow('error', apiErrorResponse);
|
|
43
|
+
* ToastMessageShow('loading', 'Saving...', { textFormat: false });
|
|
44
|
+
*/
|
|
45
|
+
export const ToastMessageShow = (
|
|
46
|
+
type: ToastType,
|
|
47
|
+
message: any,
|
|
48
|
+
options: ExtendedToastOptions = {},
|
|
49
|
+
) => {
|
|
50
|
+
const { textFormat, changeMessage, ...toastOptions } = {
|
|
51
|
+
...defaultOptions,
|
|
52
|
+
...options,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const MessageHandler = (msg: string) => {
|
|
56
|
+
let finalMessage = msg;
|
|
57
|
+
if (changeMessage) finalMessage = ToastMessageChange(finalMessage);
|
|
58
|
+
if (textFormat) finalMessage = LabelAndPlaceholderTextFormat(finalMessage);
|
|
59
|
+
return finalMessage;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
switch (type) {
|
|
63
|
+
case 'success': {
|
|
64
|
+
if (typeof message !== 'string') {
|
|
65
|
+
message = message?.data?.message || message?.message || 'Successfully updated';
|
|
66
|
+
}
|
|
67
|
+
message = MessageHandler(message);
|
|
68
|
+
toast.success(message, toastOptions);
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
case 'error': {
|
|
72
|
+
if (typeof message !== 'string') {
|
|
73
|
+
message =
|
|
74
|
+
message?.response?.data?.message ||
|
|
75
|
+
message?.response?.message ||
|
|
76
|
+
message?.data?.message ||
|
|
77
|
+
message?.message ||
|
|
78
|
+
'Something went wrong';
|
|
79
|
+
}
|
|
80
|
+
message = MessageHandler(message);
|
|
81
|
+
toast.error(message, toastOptions);
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
case 'loading':
|
|
85
|
+
message = MessageHandler(message);
|
|
86
|
+
toast.loading(message, toastOptions);
|
|
87
|
+
break;
|
|
88
|
+
case 'custom':
|
|
89
|
+
message = MessageHandler(message);
|
|
90
|
+
toast(message, toastOptions);
|
|
91
|
+
break;
|
|
92
|
+
default:
|
|
93
|
+
message = MessageHandler(message);
|
|
94
|
+
toast(message, toastOptions);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/** Shortcut: Show a success toast */
|
|
99
|
+
export const toastSuccessMessage = (msg: any, options?: ExtendedToastOptions) =>
|
|
100
|
+
ToastMessageShow('success', msg, options);
|
|
101
|
+
|
|
102
|
+
/** Shortcut: Show an error toast */
|
|
103
|
+
export const toastErrorMessage = (msg: any, options?: ExtendedToastOptions) =>
|
|
104
|
+
ToastMessageShow('error', msg, options);
|
|
105
|
+
|
|
106
|
+
/** Shortcut: Show a loading toast */
|
|
107
|
+
export const toastLoadingMessage = (msg: any, options?: ExtendedToastOptions) =>
|
|
108
|
+
ToastMessageShow('loading', msg, options);
|
|
109
|
+
|
|
110
|
+
/** Shortcut: Show a custom toast */
|
|
111
|
+
export const toastCustomMessage = (msg: any, options?: ExtendedToastOptions) =>
|
|
112
|
+
ToastMessageShow('custom', msg, options);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
export function useDebounce<T>(value: T, delayMs: number = 300): T {
|
|
4
|
+
const [debouncedValue, setDebouncedValue] = useState(value);
|
|
5
|
+
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
const timer = setTimeout(() => setDebouncedValue(value), delayMs);
|
|
8
|
+
return () => clearTimeout(timer);
|
|
9
|
+
}, [value, delayMs]);
|
|
10
|
+
|
|
11
|
+
return debouncedValue;
|
|
12
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
3
|
+
|
|
4
|
+
interface UseFetchResult<T> {
|
|
5
|
+
data: T | null;
|
|
6
|
+
isLoading: boolean;
|
|
7
|
+
error: Error | null;
|
|
8
|
+
refetch: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function useFetch<T>(url: string | null): UseFetchResult<T> {
|
|
12
|
+
const [data, setData] = useState<T | null>(null);
|
|
13
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
14
|
+
const [error, setError] = useState<Error | null>(null);
|
|
15
|
+
const [refetchTrigger, setRefetchTrigger] = useState(0);
|
|
16
|
+
|
|
17
|
+
const refetch = useCallback(() => setRefetchTrigger((n) => n + 1), []);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!url) return;
|
|
21
|
+
|
|
22
|
+
let cancelled = false;
|
|
23
|
+
setIsLoading(true);
|
|
24
|
+
setError(null);
|
|
25
|
+
|
|
26
|
+
fetch(url)
|
|
27
|
+
.then((res) => {
|
|
28
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
29
|
+
return res.json();
|
|
30
|
+
})
|
|
31
|
+
.then((json) => {
|
|
32
|
+
if (!cancelled) { setData(json); setIsLoading(false); }
|
|
33
|
+
})
|
|
34
|
+
.catch((err) => {
|
|
35
|
+
if (!cancelled) { setError(err); setIsLoading(false); }
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return () => { cancelled = true; };
|
|
39
|
+
}, [url, refetchTrigger]);
|
|
40
|
+
|
|
41
|
+
return { data, isLoading, error, refetch };
|
|
42
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect, useRef, useState, type RefObject } from 'react';
|
|
3
|
+
|
|
4
|
+
interface UseIntersectionObserverOptions {
|
|
5
|
+
threshold?: number;
|
|
6
|
+
root?: Element | null;
|
|
7
|
+
rootMargin?: string;
|
|
8
|
+
triggerOnce?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function useIntersectionObserver<T extends Element>(
|
|
12
|
+
options: UseIntersectionObserverOptions = {}
|
|
13
|
+
): [RefObject<T | null>, boolean] {
|
|
14
|
+
const { threshold = 0, root = null, rootMargin = '0px', triggerOnce = false } = options;
|
|
15
|
+
const ref = useRef<T | null>(null);
|
|
16
|
+
const [isIntersecting, setIsIntersecting] = useState(false);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const element = ref.current;
|
|
20
|
+
if (!element) return;
|
|
21
|
+
|
|
22
|
+
const observer = new IntersectionObserver(
|
|
23
|
+
([entry]) => {
|
|
24
|
+
if (entry.isIntersecting) {
|
|
25
|
+
setIsIntersecting(true);
|
|
26
|
+
if (triggerOnce) observer.unobserve(element);
|
|
27
|
+
} else if (!triggerOnce) {
|
|
28
|
+
setIsIntersecting(false);
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
{ threshold, root, rootMargin }
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
observer.observe(element);
|
|
35
|
+
return () => observer.disconnect();
|
|
36
|
+
}, [threshold, root, rootMargin, triggerOnce]);
|
|
37
|
+
|
|
38
|
+
return [ref, isIntersecting];
|
|
39
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
3
|
+
|
|
4
|
+
export function useLocalStorage<T>(key: string, initialValue: T) {
|
|
5
|
+
const [storedValue, setStoredValue] = useState<T>(initialValue);
|
|
6
|
+
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
try {
|
|
9
|
+
const item = window.localStorage.getItem(key);
|
|
10
|
+
if (item) setStoredValue(JSON.parse(item));
|
|
11
|
+
} catch {
|
|
12
|
+
console.warn(`Error reading localStorage key "${key}":`);
|
|
13
|
+
}
|
|
14
|
+
}, [key]);
|
|
15
|
+
|
|
16
|
+
const setValue = useCallback(
|
|
17
|
+
(value: T | ((val: T) => T)) => {
|
|
18
|
+
const valueToStore = value instanceof Function ? value(storedValue) : value;
|
|
19
|
+
setStoredValue(valueToStore);
|
|
20
|
+
try {
|
|
21
|
+
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
|
22
|
+
} catch {
|
|
23
|
+
console.warn(`Error setting localStorage key "${key}":`);
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
[key, storedValue]
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
return [storedValue, setValue] as const;
|
|
30
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
|
|
4
|
+
export function useMediaQuery(query: string): boolean {
|
|
5
|
+
const [matches, setMatches] = useState(false);
|
|
6
|
+
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
const media = window.matchMedia(query);
|
|
9
|
+
setMatches(media.matches);
|
|
10
|
+
|
|
11
|
+
const listener = (event: MediaQueryListEvent) => setMatches(event.matches);
|
|
12
|
+
media.addEventListener('change', listener);
|
|
13
|
+
return () => media.removeEventListener('change', listener);
|
|
14
|
+
}, [query]);
|
|
15
|
+
|
|
16
|
+
return matches;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function useIsMobile(): boolean {
|
|
20
|
+
return useMediaQuery('(max-width: 768px)');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function useIsDarkMode(): boolean {
|
|
24
|
+
const prefersDark = useMediaQuery('(prefers-color-scheme: dark)');
|
|
25
|
+
return prefersDark;
|
|
26
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useState, useCallback } from 'react';
|
|
3
|
+
|
|
4
|
+
export function useToggle(initialValue: boolean = false) {
|
|
5
|
+
const [value, setValue] = useState(initialValue);
|
|
6
|
+
|
|
7
|
+
const toggle = useCallback(() => setValue((v) => !v), []);
|
|
8
|
+
const setTrue = useCallback(() => setValue(true), []);
|
|
9
|
+
const setFalse = useCallback(() => setValue(false), []);
|
|
10
|
+
|
|
11
|
+
return { value, toggle, setTrue, setFalse, setValue } as const;
|
|
12
|
+
}
|