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,409 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const TEMPLATES_DIR = path.resolve(__dirname, '../templates');
|
|
9
|
+
|
|
10
|
+
const TEMPLATE_MAP = {
|
|
11
|
+
ui: {
|
|
12
|
+
shadcn: 'ui/shadcn',
|
|
13
|
+
mui: 'ui/mui',
|
|
14
|
+
antd: 'ui/antd',
|
|
15
|
+
},
|
|
16
|
+
state: {
|
|
17
|
+
redux: 'state/redux',
|
|
18
|
+
zustand: 'state/zustand',
|
|
19
|
+
context: 'state/context',
|
|
20
|
+
},
|
|
21
|
+
api: {
|
|
22
|
+
fetch: 'api/fetch',
|
|
23
|
+
axios: 'api/axios',
|
|
24
|
+
trpc: 'api/trpc',
|
|
25
|
+
},
|
|
26
|
+
auth: {
|
|
27
|
+
'next-auth': 'auth/next-auth',
|
|
28
|
+
clerk: 'auth/clerk',
|
|
29
|
+
},
|
|
30
|
+
forms: {
|
|
31
|
+
'react-hook-form': 'forms/react-hook-form',
|
|
32
|
+
'formik': 'forms/formik',
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const DEPENDENCIES = {
|
|
37
|
+
shadcn: {
|
|
38
|
+
dependencies: ['class-variance-authority', 'clsx', 'tailwind-merge', 'lucide-react', 'react-hook-form', '@hookform/resolvers', 'zod', 'input-otp', 'react-hot-toast', 'react-phone-input-2', '@radix-ui/react-slot', '@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu', '@radix-ui/react-select', '@radix-ui/react-tabs', '@radix-ui/react-toast', '@radix-ui/react-switch', '@radix-ui/react-checkbox', '@radix-ui/react-radio-group', '@radix-ui/react-tooltip', '@radix-ui/react-popover', '@radix-ui/react-scroll-area', '@radix-ui/react-avatar', '@radix-ui/react-progress', '@radix-ui/react-label', '@radix-ui/react-separator'],
|
|
39
|
+
devDependencies: ['tailwindcss', 'postcss', 'autoprefixer', 'tailwindcss-animate'],
|
|
40
|
+
},
|
|
41
|
+
mui: {
|
|
42
|
+
dependencies: ['@mui/material', '@mui/icons-material', '@emotion/react', '@emotion/styled', 'react-hot-toast'],
|
|
43
|
+
},
|
|
44
|
+
antd: {
|
|
45
|
+
dependencies: ['antd', '@ant-design/icons', 'dayjs', 'react-hot-toast'],
|
|
46
|
+
},
|
|
47
|
+
redux: {
|
|
48
|
+
dependencies: ['@reduxjs/toolkit', 'react-redux'],
|
|
49
|
+
},
|
|
50
|
+
zustand: {
|
|
51
|
+
dependencies: ['zustand'],
|
|
52
|
+
},
|
|
53
|
+
context: {},
|
|
54
|
+
fetch: {},
|
|
55
|
+
axios: {
|
|
56
|
+
dependencies: ['axios'],
|
|
57
|
+
},
|
|
58
|
+
trpc: {
|
|
59
|
+
dependencies: ['@trpc/server', '@trpc/client', '@trpc/react-query', '@tanstack/react-query', 'zod', 'superjson'],
|
|
60
|
+
devDependencies: ['@trpc/next'],
|
|
61
|
+
},
|
|
62
|
+
'next-auth': {
|
|
63
|
+
dependencies: ['next-auth'],
|
|
64
|
+
},
|
|
65
|
+
clerk: {
|
|
66
|
+
dependencies: ['@clerk/nextjs'],
|
|
67
|
+
},
|
|
68
|
+
'react-hook-form': {
|
|
69
|
+
dependencies: ['react-hook-form', '@hookform/resolvers', 'zod'],
|
|
70
|
+
},
|
|
71
|
+
formik: {
|
|
72
|
+
dependencies: ['formik', 'yup'],
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const generateProject = async (answers) => {
|
|
77
|
+
const { projectName, uiLibraries, stateManagement, apiLayer, auth, forms, includeExamples } = answers;
|
|
78
|
+
|
|
79
|
+
const outputDir = path.resolve(process.cwd(), projectName);
|
|
80
|
+
|
|
81
|
+
if (fs.existsSync(outputDir)) {
|
|
82
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
83
|
+
const proceed = await confirm({
|
|
84
|
+
message: `Directory "${projectName}" already exists. Overwrite?`,
|
|
85
|
+
default: false,
|
|
86
|
+
});
|
|
87
|
+
if (!proceed) {
|
|
88
|
+
console.log(chalk.yellow(' Aborted.'));
|
|
89
|
+
process.exit(0);
|
|
90
|
+
}
|
|
91
|
+
fs.removeSync(outputDir);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const spinner = ora();
|
|
95
|
+
|
|
96
|
+
spinner.start('Forging base project...');
|
|
97
|
+
await fs.copy(path.join(TEMPLATES_DIR, 'nextjs-base'), outputDir);
|
|
98
|
+
spinner.succeed('Base project created');
|
|
99
|
+
|
|
100
|
+
spinner.start(`Adding UI libraries: ${uiLibraries.join(', ')}...`);
|
|
101
|
+
for (const lib of uiLibraries) {
|
|
102
|
+
const srcPath = path.join(TEMPLATES_DIR, TEMPLATE_MAP.ui[lib]);
|
|
103
|
+
if (fs.existsSync(srcPath)) {
|
|
104
|
+
await fs.copy(srcPath, outputDir);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
spinner.succeed('UI libraries added');
|
|
108
|
+
|
|
109
|
+
spinner.start(`Adding state management: ${stateManagement.join(', ')}...`);
|
|
110
|
+
for (const sm of stateManagement) {
|
|
111
|
+
const srcPath = path.join(TEMPLATES_DIR, TEMPLATE_MAP.state[sm]);
|
|
112
|
+
if (fs.existsSync(srcPath)) {
|
|
113
|
+
await fs.copy(srcPath, outputDir);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
spinner.succeed('State management added');
|
|
117
|
+
|
|
118
|
+
spinner.start(`Adding API layer: ${apiLayer.join(', ')}...`);
|
|
119
|
+
for (const api of apiLayer) {
|
|
120
|
+
const srcPath = path.join(TEMPLATES_DIR, TEMPLATE_MAP.api[api]);
|
|
121
|
+
if (fs.existsSync(srcPath)) {
|
|
122
|
+
await fs.copy(srcPath, outputDir);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
spinner.succeed('API layer added');
|
|
126
|
+
|
|
127
|
+
if (auth !== 'none') {
|
|
128
|
+
spinner.start(`Adding auth: ${auth}...`);
|
|
129
|
+
const srcPath = path.join(TEMPLATES_DIR, TEMPLATE_MAP.auth[auth]);
|
|
130
|
+
if (fs.existsSync(srcPath)) {
|
|
131
|
+
await fs.copy(srcPath, outputDir);
|
|
132
|
+
}
|
|
133
|
+
spinner.succeed('Auth added');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (forms !== 'none') {
|
|
137
|
+
spinner.start(`Adding form handling: ${forms}...`);
|
|
138
|
+
const srcPath = path.join(TEMPLATES_DIR, TEMPLATE_MAP.forms[forms]);
|
|
139
|
+
if (fs.existsSync(srcPath)) {
|
|
140
|
+
await fs.copy(srcPath, outputDir);
|
|
141
|
+
}
|
|
142
|
+
spinner.succeed('Form handling added');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
spinner.start('Copying shared utilities...');
|
|
146
|
+
const sharedPath = path.join(TEMPLATES_DIR, 'shared');
|
|
147
|
+
if (fs.existsSync(sharedPath)) {
|
|
148
|
+
await fs.copy(sharedPath, outputDir);
|
|
149
|
+
}
|
|
150
|
+
spinner.succeed('Shared utilities added');
|
|
151
|
+
|
|
152
|
+
if (!includeExamples) {
|
|
153
|
+
spinner.start('Removing example files...');
|
|
154
|
+
const examplePaths = [
|
|
155
|
+
path.join(outputDir, 'src/app/page.tsx'),
|
|
156
|
+
path.join(outputDir, 'src/app/examples'),
|
|
157
|
+
];
|
|
158
|
+
for (const p of examplePaths) {
|
|
159
|
+
if (fs.existsSync(p)) {
|
|
160
|
+
await fs.remove(p);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
spinner.succeed('Examples removed');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
spinner.start('Generating package.json...');
|
|
167
|
+
await generatePackageJson(outputDir, { projectName, uiLibraries, stateManagement, apiLayer, auth, forms, dualMode: answers.dualMode });
|
|
168
|
+
spinner.succeed('package.json generated');
|
|
169
|
+
|
|
170
|
+
spinner.start('Generating tsconfig.json...');
|
|
171
|
+
await generateTsconfig(outputDir);
|
|
172
|
+
spinner.succeed('tsconfig.json generated');
|
|
173
|
+
|
|
174
|
+
spinner.start('Generating provider composition...');
|
|
175
|
+
await generateProviders(outputDir, answers);
|
|
176
|
+
spinner.succeed('Provider composition generated');
|
|
177
|
+
|
|
178
|
+
spinner.start('Generating project README...');
|
|
179
|
+
await generateReadme(outputDir, answers);
|
|
180
|
+
spinner.succeed('README generated');
|
|
181
|
+
|
|
182
|
+
console.log(chalk.dim(`\n ───────────────────────────────────`));
|
|
183
|
+
console.log(chalk.bold(` Next steps:`));
|
|
184
|
+
console.log(chalk.cyan(` cd ${projectName}`));
|
|
185
|
+
console.log(chalk.cyan(` npm install`));
|
|
186
|
+
console.log(chalk.cyan(` npm run dev`));
|
|
187
|
+
console.log(chalk.dim(` ───────────────────────────────────`));
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const generatePackageJson = async (outputDir, options) => {
|
|
191
|
+
const { projectName, uiLibraries, stateManagement, apiLayer, auth, forms, dualMode } = options;
|
|
192
|
+
|
|
193
|
+
const allDeps = {};
|
|
194
|
+
const allDevDeps = {
|
|
195
|
+
'typescript': '^5.4.0',
|
|
196
|
+
'@types/node': '^20.0.0',
|
|
197
|
+
'@types/react': '^18.3.0',
|
|
198
|
+
'@types/react-dom': '^18.3.0',
|
|
199
|
+
'eslint': '^9.0.0',
|
|
200
|
+
'eslint-config-next': '^15.0.0',
|
|
201
|
+
'@eslint/eslintrc': '^3.0.0',
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const mergeDeps = (selected, category) => {
|
|
205
|
+
for (const item of selected) {
|
|
206
|
+
const config = DEPENDENCIES[item];
|
|
207
|
+
if (config) {
|
|
208
|
+
if (config.dependencies) {
|
|
209
|
+
for (const dep of config.dependencies) {
|
|
210
|
+
allDeps[dep] = '*';
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (config.devDependencies) {
|
|
214
|
+
for (const dep of config.devDependencies) {
|
|
215
|
+
allDevDeps[dep] = '*';
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
mergeDeps(uiLibraries, 'ui');
|
|
223
|
+
mergeDeps(stateManagement, 'state');
|
|
224
|
+
mergeDeps(apiLayer, 'api');
|
|
225
|
+
if (auth !== 'none') mergeDeps([auth], 'auth');
|
|
226
|
+
if (forms !== 'none') mergeDeps([forms], 'forms');
|
|
227
|
+
if (dualMode) allDeps['next-themes'] = '*';
|
|
228
|
+
|
|
229
|
+
const packageJson = {
|
|
230
|
+
name: projectName,
|
|
231
|
+
version: '0.1.0',
|
|
232
|
+
private: true,
|
|
233
|
+
scripts: {
|
|
234
|
+
dev: 'next dev',
|
|
235
|
+
build: 'next build',
|
|
236
|
+
start: 'next start',
|
|
237
|
+
lint: 'next lint',
|
|
238
|
+
typecheck: 'tsc --noEmit',
|
|
239
|
+
},
|
|
240
|
+
dependencies: {
|
|
241
|
+
'next': '^15.0.0',
|
|
242
|
+
'react': '^18.3.0',
|
|
243
|
+
'react-dom': '^18.3.0',
|
|
244
|
+
...allDeps,
|
|
245
|
+
},
|
|
246
|
+
devDependencies: {
|
|
247
|
+
...allDevDeps,
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
await fs.writeJson(path.join(outputDir, 'package.json'), packageJson, { spaces: 2 });
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const generateTsconfig = async (outputDir) => {
|
|
255
|
+
const tsconfig = {
|
|
256
|
+
compilerOptions: {
|
|
257
|
+
target: 'ES2017',
|
|
258
|
+
lib: ['dom', 'dom.iterable', 'esnext'],
|
|
259
|
+
allowJs: true,
|
|
260
|
+
skipLibCheck: true,
|
|
261
|
+
strict: true,
|
|
262
|
+
noEmit: true,
|
|
263
|
+
esModuleInterop: true,
|
|
264
|
+
module: 'esnext',
|
|
265
|
+
moduleResolution: 'bundler',
|
|
266
|
+
resolveJsonModule: true,
|
|
267
|
+
isolatedModules: true,
|
|
268
|
+
jsx: 'preserve',
|
|
269
|
+
incremental: true,
|
|
270
|
+
plugins: [{ name: 'next' }],
|
|
271
|
+
paths: { '@/*': ['./src/*'] },
|
|
272
|
+
},
|
|
273
|
+
include: ['next-env.d.ts', '**/*.ts', '**/*.tsx', '.next/types/**/*.ts'],
|
|
274
|
+
exclude: ['node_modules'],
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
await fs.writeJson(path.join(outputDir, 'tsconfig.json'), tsconfig, { spaces: 2 });
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const generateProviders = async (outputDir, answers) => {
|
|
281
|
+
const { uiLibraries, stateManagement, apiLayer, auth, dualMode } = answers;
|
|
282
|
+
|
|
283
|
+
const providerImports = [];
|
|
284
|
+
const providerComponents = [];
|
|
285
|
+
|
|
286
|
+
providerImports.push("import { ToastProvider } from '@/providers/toast.provider';");
|
|
287
|
+
providerComponents.push('ToastProvider');
|
|
288
|
+
|
|
289
|
+
if (dualMode) {
|
|
290
|
+
providerImports.push("import { ThemeProvider } from '@/providers/theme.provider';");
|
|
291
|
+
providerComponents.push('ThemeProvider');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (uiLibraries.includes('mui')) {
|
|
295
|
+
providerImports.push("import { MuiProvider } from '@/providers/mui.provider';");
|
|
296
|
+
providerComponents.push('MuiProvider');
|
|
297
|
+
}
|
|
298
|
+
if (uiLibraries.includes('antd')) {
|
|
299
|
+
providerImports.push("import { AntdProvider } from '@/providers/antd.provider';");
|
|
300
|
+
providerComponents.push('AntdProvider');
|
|
301
|
+
}
|
|
302
|
+
if (stateManagement.includes('redux')) {
|
|
303
|
+
providerImports.push("import { ReduxProvider } from '@/providers/redux.provider';");
|
|
304
|
+
providerComponents.push('ReduxProvider');
|
|
305
|
+
}
|
|
306
|
+
if (apiLayer.includes('trpc')) {
|
|
307
|
+
providerImports.push("import { TrpcProvider } from '@/providers/trpc.provider';");
|
|
308
|
+
providerComponents.push('TrpcProvider');
|
|
309
|
+
}
|
|
310
|
+
if (auth === 'next-auth') {
|
|
311
|
+
providerImports.push("import { AuthProvider as NextAuthProvider } from '@/providers/session.provider';");
|
|
312
|
+
providerComponents.push('NextAuthProvider');
|
|
313
|
+
}
|
|
314
|
+
if (auth === 'clerk') {
|
|
315
|
+
providerImports.push("import { AuthProvider as ClerkAuthProvider } from '@/providers/auth.provider';");
|
|
316
|
+
providerComponents.push('ClerkAuthProvider');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const nested = providerComponents.reduce(
|
|
320
|
+
(children, provider) => ` <${provider}>${children} </${provider}>`,
|
|
321
|
+
' {children}\n'
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
const content = `'use client';
|
|
325
|
+
${providerImports.join('\n')}
|
|
326
|
+
|
|
327
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
328
|
+
return (
|
|
329
|
+
${nested}
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
`;
|
|
333
|
+
|
|
334
|
+
const providersDir = path.join(outputDir, 'src/providers');
|
|
335
|
+
await fs.ensureDir(providersDir);
|
|
336
|
+
await fs.writeFile(path.join(providersDir, 'index.tsx'), content);
|
|
337
|
+
|
|
338
|
+
const layoutPath = path.join(outputDir, 'src/app/layout.tsx');
|
|
339
|
+
if (fs.existsSync(layoutPath)) {
|
|
340
|
+
let layout = await fs.readFile(layoutPath, 'utf-8');
|
|
341
|
+
layout = layout.replace(
|
|
342
|
+
"import './globals.css';",
|
|
343
|
+
"import './globals.css';\nimport { Providers } from '@/providers';"
|
|
344
|
+
);
|
|
345
|
+
layout = layout.replace(
|
|
346
|
+
'<Sidebar>{children}</Sidebar>',
|
|
347
|
+
'<Providers><Sidebar>{children}</Sidebar></Providers>'
|
|
348
|
+
);
|
|
349
|
+
await fs.writeFile(layoutPath, layout);
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
const generateReadme = async (outputDir, answers) => {
|
|
354
|
+
const { projectName, uiLibraries, stateManagement, apiLayer, auth, forms } = answers;
|
|
355
|
+
|
|
356
|
+
const readme = `# ${projectName}
|
|
357
|
+
|
|
358
|
+
Scaffolded with ⚒️ **Nexstruct**
|
|
359
|
+
|
|
360
|
+
## Stack
|
|
361
|
+
|
|
362
|
+
- **Framework:** Next.js 15 + TypeScript
|
|
363
|
+
- **UI Library:** ${uiLibraries.join(', ')}
|
|
364
|
+
- **State Management:** ${stateManagement.join(', ')}
|
|
365
|
+
- **API Layer:** ${apiLayer.join(', ')}
|
|
366
|
+
- **Auth:** ${auth}
|
|
367
|
+
- **Forms:** ${forms}
|
|
368
|
+
|
|
369
|
+
## Getting Started
|
|
370
|
+
|
|
371
|
+
\`\`\`bash
|
|
372
|
+
npm install
|
|
373
|
+
npm run dev
|
|
374
|
+
\`\`\`
|
|
375
|
+
|
|
376
|
+
## Conventions
|
|
377
|
+
|
|
378
|
+
Files follow the \`[name].[category].ts\` naming convention:
|
|
379
|
+
|
|
380
|
+
| Extension | Type |
|
|
381
|
+
|-----------|------|
|
|
382
|
+
| \`.component.tsx\` | UI components |
|
|
383
|
+
| \`.store.ts\` | State management |
|
|
384
|
+
| \`.api.ts\` | API layer |
|
|
385
|
+
| \`.hook.ts\` | React hooks |
|
|
386
|
+
| \`.service.ts\` | Auth/services |
|
|
387
|
+
| \`.util.ts\` | Utilities |
|
|
388
|
+
| \`.type.ts\` | TypeScript types |
|
|
389
|
+
| \`.provider.tsx\` | Context providers |
|
|
390
|
+
|
|
391
|
+
## Project Structure
|
|
392
|
+
|
|
393
|
+
\`\`\`
|
|
394
|
+
src/
|
|
395
|
+
├── app/ # Next.js App Router pages
|
|
396
|
+
├── components/ # Reusable UI components
|
|
397
|
+
│ └── ui/ # Base UI primitives
|
|
398
|
+
├── hooks/ # Custom React hooks
|
|
399
|
+
├── lib/ # Library configs
|
|
400
|
+
├── store/ # State management
|
|
401
|
+
├── api/ # API layer
|
|
402
|
+
├── auth/ # Authentication
|
|
403
|
+
├── forms/ # Form handling
|
|
404
|
+
└── types/ # TypeScript types
|
|
405
|
+
\`\`\`
|
|
406
|
+
`;
|
|
407
|
+
|
|
408
|
+
await fs.writeFile(path.join(outputDir, 'README.md'), readme);
|
|
409
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { askPrompts } from './prompts.js';
|
|
5
|
+
import { generateProject } from './generator.js';
|
|
6
|
+
|
|
7
|
+
const main = async () => {
|
|
8
|
+
console.log(chalk.bold.cyan('\n ⚒️ NEXSTRUCT\n'));
|
|
9
|
+
console.log(chalk.dim(' Scaffold your production-ready Next.js project\n'));
|
|
10
|
+
|
|
11
|
+
const answers = await askPrompts();
|
|
12
|
+
await generateProject(answers);
|
|
13
|
+
|
|
14
|
+
console.log(chalk.green.bold('\n ✓ Project scaffolded successfully!\n'));
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
main().catch((err) => {
|
|
18
|
+
console.error(chalk.red(' ✗ Error:'), err.message);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { input, select, checkbox, confirm } from '@inquirer/prompts';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
/** Generate a related default project name from selected tech choices */
|
|
6
|
+
const deriveDefaultName = ({ uiLibraries, stateManagement }) => {
|
|
7
|
+
const uiLabel = uiLibraries[0] || 'app';
|
|
8
|
+
const stateLabel = stateManagement[0] || '';
|
|
9
|
+
const parts = [uiLabel, stateLabel, 'app'].filter(Boolean);
|
|
10
|
+
return parts.join('-');
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** Check if a directory with the given name already exists in cwd */
|
|
14
|
+
const isDirectoryTaken = (name) => {
|
|
15
|
+
try {
|
|
16
|
+
return fs.statSync(path.resolve(process.cwd(), name)).isDirectory();
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const askPrompts = async () => {
|
|
23
|
+
const uiLibraries = await checkbox({
|
|
24
|
+
message: 'Select UI libraries:',
|
|
25
|
+
choices: [
|
|
26
|
+
{ name: 'shadcn/ui (with Tailwind CSS)', value: 'shadcn', checked: true },
|
|
27
|
+
{ name: 'Material UI (MUI)', value: 'mui' },
|
|
28
|
+
{ name: 'Ant Design', value: 'antd' },
|
|
29
|
+
],
|
|
30
|
+
required: true,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const stateManagement = await checkbox({
|
|
34
|
+
message: 'Select state management:',
|
|
35
|
+
choices: [
|
|
36
|
+
{ name: 'Redux Toolkit (with RTK Query)', value: 'redux' },
|
|
37
|
+
{ name: 'Zustand', value: 'zustand', checked: true },
|
|
38
|
+
{ name: 'React Context + useReducer', value: 'context' },
|
|
39
|
+
],
|
|
40
|
+
required: true,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const apiLayer = await checkbox({
|
|
44
|
+
message: 'Select API layer:',
|
|
45
|
+
choices: [
|
|
46
|
+
{ name: 'Fetch API (built-in)', value: 'fetch', checked: true },
|
|
47
|
+
{ name: 'Axios', value: 'axios' },
|
|
48
|
+
{ name: 'tRPC', value: 'trpc' },
|
|
49
|
+
],
|
|
50
|
+
required: true,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const auth = await select({
|
|
54
|
+
message: 'Select authentication:',
|
|
55
|
+
choices: [
|
|
56
|
+
{ name: 'None', value: 'none' },
|
|
57
|
+
{ name: 'NextAuth.js', value: 'next-auth' },
|
|
58
|
+
{ name: 'Clerk', value: 'clerk' },
|
|
59
|
+
],
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const forms = await select({
|
|
63
|
+
message: 'Select form handling:',
|
|
64
|
+
choices: [
|
|
65
|
+
{ name: 'None', value: 'none' },
|
|
66
|
+
{ name: 'React Hook Form', value: 'react-hook-form' },
|
|
67
|
+
{ name: 'Formik', value: 'formik' },
|
|
68
|
+
],
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const dualMode = await confirm({
|
|
72
|
+
message: 'Enable dark/light mode toggle?',
|
|
73
|
+
default: true,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const includeExamples = await confirm({
|
|
77
|
+
message: 'Include example components and pages?',
|
|
78
|
+
default: true,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Project name comes last so default can be derived from selections
|
|
82
|
+
const defaultName = deriveDefaultName({ uiLibraries, stateManagement });
|
|
83
|
+
|
|
84
|
+
const projectName = await input({
|
|
85
|
+
message: 'Project name:',
|
|
86
|
+
default: defaultName,
|
|
87
|
+
validate: (v) => {
|
|
88
|
+
if (!/^[a-z0-9-]+$/.test(v)) {
|
|
89
|
+
return 'Use lowercase letters, numbers, and hyphens only';
|
|
90
|
+
}
|
|
91
|
+
if (isDirectoryTaken(v)) {
|
|
92
|
+
return `Directory "${v}" already exists. Choose a unique name`;
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
projectName,
|
|
100
|
+
uiLibraries,
|
|
101
|
+
stateManagement,
|
|
102
|
+
apiLayer,
|
|
103
|
+
auth,
|
|
104
|
+
forms,
|
|
105
|
+
dualMode,
|
|
106
|
+
includeExamples,
|
|
107
|
+
};
|
|
108
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import axios, { type AxiosInstance, type AxiosRequestConfig } from 'axios';
|
|
2
|
+
|
|
3
|
+
const createClient = (config?: AxiosRequestConfig): AxiosInstance => {
|
|
4
|
+
const client = axios.create({
|
|
5
|
+
baseURL: process.env.NEXT_PUBLIC_API_URL || '/api',
|
|
6
|
+
headers: { 'Content-Type': 'application/json' },
|
|
7
|
+
...config,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
client.interceptors.response.use(
|
|
11
|
+
(response) => response,
|
|
12
|
+
(error) => {
|
|
13
|
+
if (error.response) {
|
|
14
|
+
throw new ApiError(error.response.status, error.response.data?.message || error.message);
|
|
15
|
+
}
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
return client;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export class ApiError extends Error {
|
|
24
|
+
constructor(public status: number, message: string) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = 'ApiError';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const api = createClient();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { api } from './client.api';
|
|
2
|
+
|
|
3
|
+
export interface User {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
email: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const usersApi = {
|
|
10
|
+
list: (params?: { page?: number }) => api.get<User[]>('/users', { params }),
|
|
11
|
+
get: (id: string) => api.get<User>(`/users/${id}`),
|
|
12
|
+
create: (data: Partial<User>) => api.post<User>('/users', data),
|
|
13
|
+
update: (id: string, data: Partial<User>) => api.put<User>(`/users/${id}`, data),
|
|
14
|
+
delete: (id: string) => api.delete<void>(`/users/${id}`),
|
|
15
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
type RequestOptions = Omit<RequestInit, 'body'> & {
|
|
2
|
+
body?: unknown;
|
|
3
|
+
params?: Record<string, string>;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
class ApiClient {
|
|
7
|
+
private baseUrl: string;
|
|
8
|
+
|
|
9
|
+
constructor(baseUrl?: string) {
|
|
10
|
+
this.baseUrl = baseUrl || process.env.NEXT_PUBLIC_API_URL || '/api';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
private async request<T>(path: string, options: RequestOptions = {}): Promise<T> {
|
|
14
|
+
const { body, params, ...init } = options;
|
|
15
|
+
let url = `${this.baseUrl}${path}`;
|
|
16
|
+
|
|
17
|
+
if (params) {
|
|
18
|
+
const searchParams = new URLSearchParams(params);
|
|
19
|
+
url += `?${searchParams}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const headers: HeadersInit = {
|
|
23
|
+
'Content-Type': 'application/json',
|
|
24
|
+
...init.headers,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const response = await fetch(url, {
|
|
28
|
+
...init,
|
|
29
|
+
headers,
|
|
30
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
throw new ApiError(response.status, await response.text());
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return response.json();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get<T>(path: string, options?: RequestOptions) {
|
|
41
|
+
return this.request<T>(path, { ...options, method: 'GET' });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
post<T>(path: string, body?: unknown, options?: RequestOptions) {
|
|
45
|
+
return this.request<T>(path, { ...options, method: 'POST', body });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
put<T>(path: string, body?: unknown, options?: RequestOptions) {
|
|
49
|
+
return this.request<T>(path, { ...options, method: 'PUT', body });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
patch<T>(path: string, body?: unknown, options?: RequestOptions) {
|
|
53
|
+
return this.request<T>(path, { ...options, method: 'PATCH', body });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
delete<T>(path: string, options?: RequestOptions) {
|
|
57
|
+
return this.request<T>(path, { ...options, method: 'DELETE' });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export class ApiError extends Error {
|
|
62
|
+
constructor(public status: number, message: string) {
|
|
63
|
+
super(message);
|
|
64
|
+
this.name = 'ApiError';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const api = new ApiClient();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { api } from './client.api';
|
|
2
|
+
|
|
3
|
+
export interface User {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
email: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const usersApi = {
|
|
10
|
+
list: (params?: { page?: string }) => api.get<User[]>('/users', { params }),
|
|
11
|
+
get: (id: string) => api.get<User>(`/users/${id}`),
|
|
12
|
+
create: (data: Partial<User>) => api.post<User>('/users', data),
|
|
13
|
+
update: (id: string, data: Partial<User>) => api.put<User>(`/users/${id}`, data),
|
|
14
|
+
delete: (id: string) => api.delete<void>(`/users/${id}`),
|
|
15
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { initTRPC } from '@trpc/server';
|
|
2
|
+
import superjson from 'superjson';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
const t = initTRPC.create({ transformer: superjson });
|
|
6
|
+
|
|
7
|
+
export const appRouter = t.router({
|
|
8
|
+
hello: t.procedure
|
|
9
|
+
.input(z.object({ name: z.string() }))
|
|
10
|
+
.query(({ input }) => {
|
|
11
|
+
return { greeting: `Hello, ${input.name}!` };
|
|
12
|
+
}),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export type AppRouter = typeof appRouter;
|