create-react-starter-kit 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/LICENSE +21 -0
- package/README.md +119 -0
- package/bin/create-react-starter-kit.js +7 -0
- package/dist/cli-args.d.ts +15 -0
- package/dist/cli-args.d.ts.map +1 -0
- package/dist/cli-args.js +78 -0
- package/dist/cli-args.js.map +1 -0
- package/dist/generator/dependencies.d.ts +8 -0
- package/dist/generator/dependencies.d.ts.map +1 -0
- package/dist/generator/dependencies.js +356 -0
- package/dist/generator/dependencies.js.map +1 -0
- package/dist/generator/index.d.ts +5 -0
- package/dist/generator/index.d.ts.map +1 -0
- package/dist/generator/index.js +358 -0
- package/dist/generator/index.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -0
- package/dist/prompts/index.d.ts +3 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +267 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/templates/.env.example.ejs +21 -0
- package/dist/templates/.husky/commit-msg +2 -0
- package/dist/templates/.husky/pre-commit +2 -0
- package/dist/templates/README.md.ejs +169 -0
- package/dist/templates/__js__/jsconfig.json +9 -0
- package/dist/templates/__ts__/src/vite-env.d.ts +16 -0
- package/dist/templates/__ts__/tsconfig.json +24 -0
- package/dist/templates/__when_buildTool_esbuild__/scripts/esbuild.build.mjs.ejs +23 -0
- package/dist/templates/__when_buildTool_esbuild__/scripts/esbuild.dev.mjs.ejs +48 -0
- package/dist/templates/__when_buildTool_parcel__/.parcelrc +9 -0
- package/dist/templates/__when_buildTool_rollup__/rollup.config.js.ejs +33 -0
- package/dist/templates/__when_buildTool_rspack__/rspack.config.js.ejs +36 -0
- package/dist/templates/__when_buildTool_vite__/vite.config.ts.ejs +37 -0
- package/dist/templates/__when_buildTool_webpack__/webpack.config.js.ejs +68 -0
- package/dist/templates/__when_e2eTesting_cypress__/cypress/e2e/home.cy.ts.ejs +6 -0
- package/dist/templates/__when_e2eTesting_cypress__/cypress.config.ts.ejs +8 -0
- package/dist/templates/__when_e2eTesting_playwright__/playwright.config.ts.ejs +16 -0
- package/dist/templates/__when_e2eTesting_playwright__/tests/e2e/home.spec.ts.ejs +6 -0
- package/dist/templates/__when_formatting_prettier__/.prettierrc +7 -0
- package/dist/templates/__when_formatting_stylelint__/.stylelintrc.json +3 -0
- package/dist/templates/__when_linting_biome__/biome.json +19 -0
- package/dist/templates/__when_linting_eslint__/eslint.config.js.ejs +42 -0
- package/dist/templates/__when_serverState_rtk-query__/src/store/postsApi.ts.ejs +19 -0
- package/dist/templates/__when_stateManagement_context__/src/store/context.tsx.ejs +57 -0
- package/dist/templates/__when_styling_tailwind__/postcss.config.js +6 -0
- package/dist/templates/__when_styling_tailwind__/tailwind.config.js.ejs +23 -0
- package/dist/templates/__when_unitTesting_jest__/jest.config.json.ejs +11 -0
- package/dist/templates/__when_unitTesting_jest__/tests/App.test.tsx.ejs +13 -0
- package/dist/templates/__when_unitTesting_jest__/tests/setup.ts.ejs +1 -0
- package/dist/templates/__when_unitTesting_jest__/tests/styleMock.ts.ejs +1 -0
- package/dist/templates/__when_unitTesting_rtl__/tests/App.test.tsx.ejs +13 -0
- package/dist/templates/__when_unitTesting_vitest__/tests/App.test.tsx.ejs +16 -0
- package/dist/templates/__when_unitTesting_vitest__/tests/setup.ts.ejs +1 -0
- package/dist/templates/__when_visualTesting_storybook__/.storybook/main.js.ejs +11 -0
- package/dist/templates/__when_visualTesting_storybook__/.storybook/preview.js +8 -0
- package/dist/templates/__when_visualTesting_storybook__/src/components/ui/UiButton.stories.tsx.ejs +15 -0
- package/dist/templates/commitlint.config.js +3 -0
- package/dist/templates/docs/architecture.md.ejs +74 -0
- package/dist/templates/docs/best-practices.md.ejs +56 -0
- package/dist/templates/docs/examples/README.md.ejs +14 -0
- package/dist/templates/docs/examples/api.md.ejs +17 -0
- package/dist/templates/docs/examples/data-fetching.md.ejs +18 -0
- package/dist/templates/docs/examples/forms.md.ejs +18 -0
- package/dist/templates/docs/examples/state.md.ejs +17 -0
- package/dist/templates/docs/examples/styling.md.ejs +23 -0
- package/dist/templates/docs/examples/testing.md.ejs +27 -0
- package/dist/templates/docs/getting-started.md.ejs +55 -0
- package/dist/templates/docs/selected-stack.md.ejs +55 -0
- package/dist/templates/index.html.ejs +19 -0
- package/dist/templates/public/vite.svg +4 -0
- package/dist/templates/src/app/App.tsx.ejs +71 -0
- package/dist/templates/src/app/providers.tsx.ejs +93 -0
- package/dist/templates/src/assets/.gitkeep +0 -0
- package/dist/templates/src/components/StackBadge.tsx.ejs +15 -0
- package/dist/templates/src/components/layout/AppLayout.tsx.ejs +64 -0
- package/dist/templates/src/components/ui/Toast.tsx.ejs +61 -0
- package/dist/templates/src/components/ui/UiButton.tsx.ejs +168 -0
- package/dist/templates/src/config/env.ts.ejs +18 -0
- package/dist/templates/src/features/counter/CounterPanel.tsx.ejs +66 -0
- package/dist/templates/src/features/feedback/FeedbackForm.tsx.ejs +373 -0
- package/dist/templates/src/features/posts/PostsPanel.tsx.ejs +91 -0
- package/dist/templates/src/hooks/useMounted.ts.ejs +15 -0
- package/dist/templates/src/lib/cn.ts.ejs +12 -0
- package/dist/templates/src/lib/monitoring.ts.ejs +66 -0
- package/dist/templates/src/lib/validation.ts.ejs +81 -0
- package/dist/templates/src/main.tsx.ejs +22 -0
- package/dist/templates/src/routes/AboutPage.tsx.ejs +30 -0
- package/dist/templates/src/routes/HomePage.tsx.ejs +47 -0
- package/dist/templates/src/services/api.ts.ejs +84 -0
- package/dist/templates/src/store/index.ts.ejs +80 -0
- package/dist/templates/src/styles/global.css.ejs +457 -0
- package/dist/templates/src/types/index.ts.ejs +7 -0
- package/dist/templates/src/utils/format.ts.ejs +3 -0
- package/dist/types.d.ts +46 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/logger.d.ts +12 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +31 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/validate.d.ts +3 -0
- package/dist/utils/validate.d.ts.map +1 -0
- package/dist/utils/validate.js +24 -0
- package/dist/utils/validate.js.map +1 -0
- package/package.json +62 -0
- package/templates/.env.example.ejs +21 -0
- package/templates/.husky/commit-msg +2 -0
- package/templates/.husky/pre-commit +2 -0
- package/templates/README.md.ejs +169 -0
- package/templates/__js__/jsconfig.json +9 -0
- package/templates/__ts__/src/vite-env.d.ts +16 -0
- package/templates/__ts__/tsconfig.json +24 -0
- package/templates/__when_buildTool_esbuild__/scripts/esbuild.build.mjs.ejs +23 -0
- package/templates/__when_buildTool_esbuild__/scripts/esbuild.dev.mjs.ejs +48 -0
- package/templates/__when_buildTool_parcel__/.parcelrc +9 -0
- package/templates/__when_buildTool_rollup__/rollup.config.js.ejs +33 -0
- package/templates/__when_buildTool_rspack__/rspack.config.js.ejs +36 -0
- package/templates/__when_buildTool_vite__/vite.config.ts.ejs +37 -0
- package/templates/__when_buildTool_webpack__/webpack.config.js.ejs +68 -0
- package/templates/__when_e2eTesting_cypress__/cypress/e2e/home.cy.ts.ejs +6 -0
- package/templates/__when_e2eTesting_cypress__/cypress.config.ts.ejs +8 -0
- package/templates/__when_e2eTesting_playwright__/playwright.config.ts.ejs +16 -0
- package/templates/__when_e2eTesting_playwright__/tests/e2e/home.spec.ts.ejs +6 -0
- package/templates/__when_formatting_prettier__/.prettierrc +7 -0
- package/templates/__when_formatting_stylelint__/.stylelintrc.json +3 -0
- package/templates/__when_linting_biome__/biome.json +19 -0
- package/templates/__when_linting_eslint__/eslint.config.js.ejs +42 -0
- package/templates/__when_serverState_rtk-query__/src/store/postsApi.ts.ejs +19 -0
- package/templates/__when_stateManagement_context__/src/store/context.tsx.ejs +57 -0
- package/templates/__when_styling_tailwind__/postcss.config.js +6 -0
- package/templates/__when_styling_tailwind__/tailwind.config.js.ejs +23 -0
- package/templates/__when_unitTesting_jest__/jest.config.json.ejs +11 -0
- package/templates/__when_unitTesting_jest__/tests/App.test.tsx.ejs +13 -0
- package/templates/__when_unitTesting_jest__/tests/setup.ts.ejs +1 -0
- package/templates/__when_unitTesting_jest__/tests/styleMock.ts.ejs +1 -0
- package/templates/__when_unitTesting_rtl__/tests/App.test.tsx.ejs +13 -0
- package/templates/__when_unitTesting_vitest__/tests/App.test.tsx.ejs +16 -0
- package/templates/__when_unitTesting_vitest__/tests/setup.ts.ejs +1 -0
- package/templates/__when_visualTesting_storybook__/.storybook/main.js.ejs +11 -0
- package/templates/__when_visualTesting_storybook__/.storybook/preview.js +8 -0
- package/templates/__when_visualTesting_storybook__/src/components/ui/UiButton.stories.tsx.ejs +15 -0
- package/templates/commitlint.config.js +3 -0
- package/templates/docs/architecture.md.ejs +74 -0
- package/templates/docs/best-practices.md.ejs +56 -0
- package/templates/docs/examples/README.md.ejs +14 -0
- package/templates/docs/examples/api.md.ejs +17 -0
- package/templates/docs/examples/data-fetching.md.ejs +18 -0
- package/templates/docs/examples/forms.md.ejs +18 -0
- package/templates/docs/examples/state.md.ejs +17 -0
- package/templates/docs/examples/styling.md.ejs +23 -0
- package/templates/docs/examples/testing.md.ejs +27 -0
- package/templates/docs/getting-started.md.ejs +55 -0
- package/templates/docs/selected-stack.md.ejs +55 -0
- package/templates/index.html.ejs +19 -0
- package/templates/public/vite.svg +4 -0
- package/templates/src/app/App.tsx.ejs +71 -0
- package/templates/src/app/providers.tsx.ejs +93 -0
- package/templates/src/assets/.gitkeep +0 -0
- package/templates/src/components/StackBadge.tsx.ejs +15 -0
- package/templates/src/components/layout/AppLayout.tsx.ejs +64 -0
- package/templates/src/components/ui/Toast.tsx.ejs +61 -0
- package/templates/src/components/ui/UiButton.tsx.ejs +168 -0
- package/templates/src/config/env.ts.ejs +18 -0
- package/templates/src/features/counter/CounterPanel.tsx.ejs +66 -0
- package/templates/src/features/feedback/FeedbackForm.tsx.ejs +373 -0
- package/templates/src/features/posts/PostsPanel.tsx.ejs +91 -0
- package/templates/src/hooks/useMounted.ts.ejs +15 -0
- package/templates/src/lib/cn.ts.ejs +12 -0
- package/templates/src/lib/monitoring.ts.ejs +66 -0
- package/templates/src/lib/validation.ts.ejs +81 -0
- package/templates/src/main.tsx.ejs +22 -0
- package/templates/src/routes/AboutPage.tsx.ejs +30 -0
- package/templates/src/routes/HomePage.tsx.ejs +47 -0
- package/templates/src/services/api.ts.ejs +84 -0
- package/templates/src/store/index.ts.ejs +80 -0
- package/templates/src/styles/global.css.ejs +457 -0
- package/templates/src/types/index.ts.ejs +7 -0
- package/templates/src/utils/format.ts.ejs +3 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
<% if (isTypeScript) { -%>
|
|
2
|
+
import type { ButtonHTMLAttributes, ReactNode } from 'react';
|
|
3
|
+
<% } -%>
|
|
4
|
+
import { cn } from '@/lib/cn';
|
|
5
|
+
<% if (uiLibrary === 'mui') { -%>
|
|
6
|
+
import Button from '@mui/material/Button';
|
|
7
|
+
<% } else if (uiLibrary === 'antd') { -%>
|
|
8
|
+
import { Button as AntButton } from 'antd';
|
|
9
|
+
<% } else if (uiLibrary === 'chakra') { -%>
|
|
10
|
+
import { Button as ChakraButton } from '@chakra-ui/react';
|
|
11
|
+
<% } else if (uiLibrary === 'headless-ui') { -%>
|
|
12
|
+
import { Button as HeadlessButton } from '@headlessui/react';
|
|
13
|
+
<% } else if (uiLibrary === 'shadcn') { -%>
|
|
14
|
+
import { cva<% if (isTypeScript) { %>, type VariantProps<% } %> } from 'class-variance-authority';
|
|
15
|
+
<% } -%>
|
|
16
|
+
|
|
17
|
+
<% if (uiLibrary === 'shadcn') { -%>
|
|
18
|
+
const buttonVariants = cva(
|
|
19
|
+
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 disabled:opacity-50',
|
|
20
|
+
{
|
|
21
|
+
variants: {
|
|
22
|
+
variant: {
|
|
23
|
+
default: 'bg-brand-600 text-white hover:bg-brand-700',
|
|
24
|
+
secondary: 'bg-slate-100 text-slate-900 hover:bg-slate-200',
|
|
25
|
+
outline: 'border border-slate-300 bg-white hover:bg-slate-50',
|
|
26
|
+
},
|
|
27
|
+
size: {
|
|
28
|
+
default: 'h-10 px-4 py-2',
|
|
29
|
+
sm: 'h-8 px-3',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
defaultVariants: {
|
|
33
|
+
variant: 'default',
|
|
34
|
+
size: 'default',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
<% if (isTypeScript) { -%>
|
|
40
|
+
export interface UiButtonProps
|
|
41
|
+
extends ButtonHTMLAttributes<HTMLButtonElement>,
|
|
42
|
+
VariantProps<typeof buttonVariants> {
|
|
43
|
+
children: ReactNode;
|
|
44
|
+
}
|
|
45
|
+
<% } -%>
|
|
46
|
+
|
|
47
|
+
export function UiButton({
|
|
48
|
+
className,
|
|
49
|
+
variant,
|
|
50
|
+
size,
|
|
51
|
+
children,
|
|
52
|
+
...props
|
|
53
|
+
}<% if (isTypeScript) { %>: UiButtonProps<% } %>) {
|
|
54
|
+
return (
|
|
55
|
+
<button className={cn(buttonVariants({ variant, size }), className)} {...props}>
|
|
56
|
+
{children}
|
|
57
|
+
</button>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
<% } else if (uiLibrary === 'mui') { -%>
|
|
61
|
+
<% if (isTypeScript) { -%>
|
|
62
|
+
interface UiButtonProps {
|
|
63
|
+
children: ReactNode;
|
|
64
|
+
onClick?: () => void;
|
|
65
|
+
variant?: 'contained' | 'outlined' | 'text';
|
|
66
|
+
type?: 'button' | 'submit' | 'reset';
|
|
67
|
+
disabled?: boolean;
|
|
68
|
+
}
|
|
69
|
+
<% } -%>
|
|
70
|
+
|
|
71
|
+
export function UiButton({
|
|
72
|
+
children,
|
|
73
|
+
onClick,
|
|
74
|
+
variant = 'contained',
|
|
75
|
+
type = 'button',
|
|
76
|
+
disabled,
|
|
77
|
+
}<% if (isTypeScript) { %>: UiButtonProps<% } %>) {
|
|
78
|
+
return (
|
|
79
|
+
<Button variant={variant} onClick={onClick} type={type} disabled={disabled}>
|
|
80
|
+
{children}
|
|
81
|
+
</Button>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
<% } else if (uiLibrary === 'antd') { -%>
|
|
85
|
+
<% if (isTypeScript) { -%>
|
|
86
|
+
interface UiButtonProps {
|
|
87
|
+
children: ReactNode;
|
|
88
|
+
onClick?: () => void;
|
|
89
|
+
type?: 'default' | 'primary' | 'dashed' | 'link' | 'text';
|
|
90
|
+
htmlType?: 'button' | 'submit' | 'reset';
|
|
91
|
+
disabled?: boolean;
|
|
92
|
+
}
|
|
93
|
+
<% } -%>
|
|
94
|
+
|
|
95
|
+
export function UiButton({
|
|
96
|
+
children,
|
|
97
|
+
onClick,
|
|
98
|
+
type = 'primary',
|
|
99
|
+
htmlType = 'button',
|
|
100
|
+
disabled,
|
|
101
|
+
}<% if (isTypeScript) { %>: UiButtonProps<% } %>) {
|
|
102
|
+
return (
|
|
103
|
+
<AntButton type={type} onClick={onClick} htmlType={htmlType} disabled={disabled}>
|
|
104
|
+
{children}
|
|
105
|
+
</AntButton>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
<% } else if (uiLibrary === 'chakra') { -%>
|
|
109
|
+
<% if (isTypeScript) { -%>
|
|
110
|
+
interface UiButtonProps {
|
|
111
|
+
children: ReactNode;
|
|
112
|
+
onClick?: () => void;
|
|
113
|
+
type?: 'button' | 'submit' | 'reset';
|
|
114
|
+
disabled?: boolean;
|
|
115
|
+
}
|
|
116
|
+
<% } -%>
|
|
117
|
+
|
|
118
|
+
export function UiButton({ children, onClick, type = 'button', disabled }<% if (isTypeScript) { %>: UiButtonProps<% } %>) {
|
|
119
|
+
return (
|
|
120
|
+
<ChakraButton colorPalette="blue" onClick={onClick} type={type} disabled={disabled}>
|
|
121
|
+
{children}
|
|
122
|
+
</ChakraButton>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
<% } else if (uiLibrary === 'headless-ui') { -%>
|
|
126
|
+
<% if (isTypeScript) { -%>
|
|
127
|
+
interface UiButtonProps {
|
|
128
|
+
children: ReactNode;
|
|
129
|
+
onClick?: () => void;
|
|
130
|
+
type?: 'button' | 'submit' | 'reset';
|
|
131
|
+
disabled?: boolean;
|
|
132
|
+
className?: string;
|
|
133
|
+
}
|
|
134
|
+
<% } -%>
|
|
135
|
+
|
|
136
|
+
export function UiButton({
|
|
137
|
+
children,
|
|
138
|
+
onClick,
|
|
139
|
+
type = 'button',
|
|
140
|
+
disabled,
|
|
141
|
+
className,
|
|
142
|
+
}<% if (isTypeScript) { %>: UiButtonProps<% } %>) {
|
|
143
|
+
return (
|
|
144
|
+
<HeadlessButton
|
|
145
|
+
type={type}
|
|
146
|
+
onClick={onClick}
|
|
147
|
+
disabled={disabled}
|
|
148
|
+
className={cn('ui-button', className)}
|
|
149
|
+
>
|
|
150
|
+
{children}
|
|
151
|
+
</HeadlessButton>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
<% } else { -%>
|
|
155
|
+
<% if (isTypeScript) { -%>
|
|
156
|
+
interface UiButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
157
|
+
children: ReactNode;
|
|
158
|
+
}
|
|
159
|
+
<% } -%>
|
|
160
|
+
|
|
161
|
+
export function UiButton({ children, className, ...props }<% if (isTypeScript) { %>: UiButtonProps<% } %>) {
|
|
162
|
+
return (
|
|
163
|
+
<button className={cn('ui-button', className)} {...props}>
|
|
164
|
+
{children}
|
|
165
|
+
</button>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
<% } -%>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<% if (isTypeScript) { -%>
|
|
2
|
+
/**
|
|
3
|
+
* Central place for reading public env vars.
|
|
4
|
+
* Only expose values that are safe for the browser bundle.
|
|
5
|
+
*/
|
|
6
|
+
export const env = {
|
|
7
|
+
apiBaseUrl:
|
|
8
|
+
import.meta.env?.VITE_API_BASE_URL ?? 'https://jsonplaceholder.typicode.com',
|
|
9
|
+
mode: import.meta.env?.MODE ?? 'development',
|
|
10
|
+
} as const;
|
|
11
|
+
<% } else { -%>
|
|
12
|
+
/** Central place for reading public env vars. */
|
|
13
|
+
export const env = {
|
|
14
|
+
apiBaseUrl:
|
|
15
|
+
import.meta.env?.VITE_API_BASE_URL ?? 'https://jsonplaceholder.typicode.com',
|
|
16
|
+
mode: import.meta.env?.MODE ?? 'development',
|
|
17
|
+
};
|
|
18
|
+
<% } -%>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<% if (stateManagement === 'redux') { -%>
|
|
2
|
+
import { useDispatch, useSelector } from 'react-redux';
|
|
3
|
+
import { decrement, increment } from '@/store';
|
|
4
|
+
<% if (isTypeScript) { -%>
|
|
5
|
+
import type { RootState } from '@/store';
|
|
6
|
+
<% } -%>
|
|
7
|
+
<% } else if (stateManagement === 'zustand') { -%>
|
|
8
|
+
import { useCounterStore } from '@/store';
|
|
9
|
+
<% } else if (stateManagement === 'jotai') { -%>
|
|
10
|
+
import { useAtom } from 'jotai';
|
|
11
|
+
import { counterAtom } from '@/store';
|
|
12
|
+
<% } else if (stateManagement === 'context') { -%>
|
|
13
|
+
import { useAppState } from '@/store/context';
|
|
14
|
+
<% } -%>
|
|
15
|
+
import { UiButton } from '@/components/ui/UiButton';
|
|
16
|
+
import { useToast } from '@/components/ui/Toast';
|
|
17
|
+
|
|
18
|
+
export function CounterPanel() {
|
|
19
|
+
const { push } = useToast();
|
|
20
|
+
<% if (stateManagement === 'redux') { -%>
|
|
21
|
+
const value = useSelector((state<% if (isTypeScript) { %>: RootState<% } %>) => state.counter.value);
|
|
22
|
+
const dispatch = useDispatch();
|
|
23
|
+
const onInc = () => dispatch(increment());
|
|
24
|
+
const onDec = () => dispatch(decrement());
|
|
25
|
+
<% } else if (stateManagement === 'zustand') { -%>
|
|
26
|
+
const value = useCounterStore((s) => s.value);
|
|
27
|
+
const onInc = useCounterStore((s) => s.increment);
|
|
28
|
+
const onDec = useCounterStore((s) => s.decrement);
|
|
29
|
+
<% } else if (stateManagement === 'jotai') { -%>
|
|
30
|
+
const [value, setValue] = useAtom(counterAtom);
|
|
31
|
+
const onInc = () => setValue((v) => v + 1);
|
|
32
|
+
const onDec = () => setValue((v) => v - 1);
|
|
33
|
+
<% } else if (stateManagement === 'context') { -%>
|
|
34
|
+
const { state, dispatch } = useAppState();
|
|
35
|
+
const value = state.value;
|
|
36
|
+
const onInc = () => dispatch({ type: 'increment' });
|
|
37
|
+
const onDec = () => dispatch({ type: 'decrement' });
|
|
38
|
+
<% } else { -%>
|
|
39
|
+
const value = 0;
|
|
40
|
+
const onInc = () => undefined;
|
|
41
|
+
const onDec = () => undefined;
|
|
42
|
+
<% } -%>
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<article className="panel">
|
|
46
|
+
<header className="panel-header">
|
|
47
|
+
<h2>Client state</h2>
|
|
48
|
+
<p><%= stackLabels.stateManagement %> example — local interactive counter.</p>
|
|
49
|
+
</header>
|
|
50
|
+
<% if (stateManagement === 'none') { -%>
|
|
51
|
+
<p className="muted">Client state library was set to None. Add one later under <code>src/store</code>.</p>
|
|
52
|
+
<% } else { -%>
|
|
53
|
+
<div className="counter">
|
|
54
|
+
<UiButton onClick={() => { onDec(); push('Decremented', 'info'); }}>−</UiButton>
|
|
55
|
+
<strong className="counter-value" aria-live="polite">{value}</strong>
|
|
56
|
+
<UiButton onClick={() => { onInc(); push('Incremented', 'success'); }}>+</UiButton>
|
|
57
|
+
</div>
|
|
58
|
+
<% } -%>
|
|
59
|
+
<p className="panel-note">
|
|
60
|
+
<strong>When to use:</strong> shared UI state that is not server cache.
|
|
61
|
+
<br />
|
|
62
|
+
<strong>Best practice:</strong> keep slices/stores small and colocated with features.
|
|
63
|
+
</p>
|
|
64
|
+
</article>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
<% if (forms === 'react-hook-form') { -%>
|
|
2
|
+
import { useForm } from 'react-hook-form';
|
|
3
|
+
<% if (validation === 'zod') { -%>
|
|
4
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
5
|
+
import { feedbackSchema<% if (isTypeScript) { %>, type FeedbackValues<% } %> } from '@/lib/validation';
|
|
6
|
+
<% } else if (isTypeScript) { -%>
|
|
7
|
+
import type { FeedbackValues } from '@/lib/validation';
|
|
8
|
+
<% } -%>
|
|
9
|
+
<% } else if (forms === 'formik') { -%>
|
|
10
|
+
import { Formik, Form, Field, ErrorMessage } from 'formik';
|
|
11
|
+
<% if (validation === 'yup') { -%>
|
|
12
|
+
import { feedbackSchema } from '@/lib/validation';
|
|
13
|
+
<% } else if (validation === 'zod') { -%>
|
|
14
|
+
import { feedbackSchema } from '@/lib/validation';
|
|
15
|
+
<% } -%>
|
|
16
|
+
<% if (isTypeScript) { -%>
|
|
17
|
+
import type { FeedbackValues } from '@/lib/validation';
|
|
18
|
+
<% } -%>
|
|
19
|
+
<% } else if (forms === 'tanstack-form') { -%>
|
|
20
|
+
import { useForm } from '@tanstack/react-form';
|
|
21
|
+
<% if (validation !== 'none') { -%>
|
|
22
|
+
import { feedbackSchema } from '@/lib/validation';
|
|
23
|
+
<% } -%>
|
|
24
|
+
<% if (isTypeScript) { -%>
|
|
25
|
+
import type { FeedbackValues } from '@/lib/validation';
|
|
26
|
+
<% } -%>
|
|
27
|
+
<% } else { -%>
|
|
28
|
+
import { useState } from 'react';
|
|
29
|
+
<% if (validation === 'none') { -%>
|
|
30
|
+
import { validateFeedback<% if (isTypeScript) { %>, type FeedbackValues<% } %> } from '@/lib/validation';
|
|
31
|
+
<% } else { -%>
|
|
32
|
+
import { feedbackSchema<% if (isTypeScript) { %>, type FeedbackValues<% } %> } from '@/lib/validation';
|
|
33
|
+
<% } -%>
|
|
34
|
+
<% } -%>
|
|
35
|
+
import { createFeedback } from '@/services/api';
|
|
36
|
+
import { useToast } from '@/components/ui/Toast';
|
|
37
|
+
import { UiButton } from '@/components/ui/UiButton';
|
|
38
|
+
|
|
39
|
+
const defaults<% if (isTypeScript) { %>: FeedbackValues<% } %> = {
|
|
40
|
+
name: '',
|
|
41
|
+
email: '',
|
|
42
|
+
message: '',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export function FeedbackForm() {
|
|
46
|
+
const { push } = useToast();
|
|
47
|
+
|
|
48
|
+
<% if (forms === 'react-hook-form') { -%>
|
|
49
|
+
const {
|
|
50
|
+
register,
|
|
51
|
+
handleSubmit,
|
|
52
|
+
reset,
|
|
53
|
+
formState: { errors, isSubmitting },
|
|
54
|
+
} = useForm<% if (isTypeScript) { %><FeedbackValues><% } %>({
|
|
55
|
+
defaultValues: defaults,
|
|
56
|
+
<% if (validation === 'zod') { -%>
|
|
57
|
+
resolver: zodResolver(feedbackSchema),
|
|
58
|
+
<% } -%>
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const onSubmit = async (values<% if (isTypeScript) { %>: FeedbackValues<% } %>) => {
|
|
62
|
+
<% if (validation === 'yup') { -%>
|
|
63
|
+
await feedbackSchema.validate(values, { abortEarly: false });
|
|
64
|
+
<% } else if (validation === 'valibot') { -%>
|
|
65
|
+
const { parse } = await import('valibot');
|
|
66
|
+
parse(feedbackSchema, values);
|
|
67
|
+
<% } else if (validation === 'joi') { -%>
|
|
68
|
+
const result = feedbackSchema.validate(values, { abortEarly: false });
|
|
69
|
+
if (result.error) {
|
|
70
|
+
throw result.error;
|
|
71
|
+
}
|
|
72
|
+
<% } -%>
|
|
73
|
+
await createFeedback(values);
|
|
74
|
+
push('Feedback submitted (demo API)', 'success');
|
|
75
|
+
reset();
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<article className="panel">
|
|
80
|
+
<header className="panel-header">
|
|
81
|
+
<h2>Forms & validation</h2>
|
|
82
|
+
<p>
|
|
83
|
+
<%= stackLabels.forms %>
|
|
84
|
+
<% if (validation !== 'none') { -%>
|
|
85
|
+
{' '}
|
|
86
|
+
+ <%= stackLabels.validation %>
|
|
87
|
+
<% } -%>
|
|
88
|
+
</p>
|
|
89
|
+
</header>
|
|
90
|
+
<form className="feedback-form" onSubmit={handleSubmit(onSubmit)} noValidate>
|
|
91
|
+
<label>
|
|
92
|
+
Name
|
|
93
|
+
<input {...register('name')} placeholder="Ada Lovelace" />
|
|
94
|
+
{errors.name && <span className="field-error">{errors.name.message}</span>}
|
|
95
|
+
</label>
|
|
96
|
+
<label>
|
|
97
|
+
Email
|
|
98
|
+
<input type="email" {...register('email')} placeholder="ada@example.com" />
|
|
99
|
+
{errors.email && <span className="field-error">{errors.email.message}</span>}
|
|
100
|
+
</label>
|
|
101
|
+
<label>
|
|
102
|
+
Message
|
|
103
|
+
<textarea rows={4} {...register('message')} placeholder="What should we improve?" />
|
|
104
|
+
{errors.message && <span className="field-error">{errors.message.message}</span>}
|
|
105
|
+
</label>
|
|
106
|
+
<UiButton type="submit" disabled={isSubmitting}>
|
|
107
|
+
{isSubmitting ? 'Sending…' : 'Send feedback'}
|
|
108
|
+
</UiButton>
|
|
109
|
+
</form>
|
|
110
|
+
<p className="panel-note">
|
|
111
|
+
<strong>When to use:</strong> complex forms with validation and async submit.
|
|
112
|
+
<br />
|
|
113
|
+
<strong>Best practice:</strong> keep schemas in <code>src/lib</code> and reuse across client/server.
|
|
114
|
+
</p>
|
|
115
|
+
</article>
|
|
116
|
+
);
|
|
117
|
+
<% } else if (forms === 'formik') { -%>
|
|
118
|
+
return (
|
|
119
|
+
<article className="panel">
|
|
120
|
+
<header className="panel-header">
|
|
121
|
+
<h2>Forms & validation</h2>
|
|
122
|
+
<p>
|
|
123
|
+
<%= stackLabels.forms %>
|
|
124
|
+
<% if (validation !== 'none') { -%>
|
|
125
|
+
{' '}
|
|
126
|
+
+ <%= stackLabels.validation %>
|
|
127
|
+
<% } -%>
|
|
128
|
+
</p>
|
|
129
|
+
</header>
|
|
130
|
+
<Formik
|
|
131
|
+
initialValues={defaults}
|
|
132
|
+
<% if (validation === 'yup') { -%>
|
|
133
|
+
validationSchema={feedbackSchema}
|
|
134
|
+
<% } -%>
|
|
135
|
+
onSubmit={async (values, helpers) => {
|
|
136
|
+
<% if (validation === 'zod') { -%>
|
|
137
|
+
feedbackSchema.parse(values);
|
|
138
|
+
<% } -%>
|
|
139
|
+
await createFeedback(values);
|
|
140
|
+
push('Feedback submitted (demo API)', 'success');
|
|
141
|
+
helpers.resetForm();
|
|
142
|
+
}}
|
|
143
|
+
>
|
|
144
|
+
{({ isSubmitting }) => (
|
|
145
|
+
<Form className="feedback-form" noValidate>
|
|
146
|
+
<label>
|
|
147
|
+
Name
|
|
148
|
+
<Field name="name" placeholder="Ada Lovelace" />
|
|
149
|
+
<ErrorMessage name="name" component="span" className="field-error" />
|
|
150
|
+
</label>
|
|
151
|
+
<label>
|
|
152
|
+
Email
|
|
153
|
+
<Field name="email" type="email" placeholder="ada@example.com" />
|
|
154
|
+
<ErrorMessage name="email" component="span" className="field-error" />
|
|
155
|
+
</label>
|
|
156
|
+
<label>
|
|
157
|
+
Message
|
|
158
|
+
<Field as="textarea" rows={4} name="message" placeholder="What should we improve?" />
|
|
159
|
+
<ErrorMessage name="message" component="span" className="field-error" />
|
|
160
|
+
</label>
|
|
161
|
+
<UiButton type="submit" disabled={isSubmitting}>
|
|
162
|
+
{isSubmitting ? 'Sending…' : 'Send feedback'}
|
|
163
|
+
</UiButton>
|
|
164
|
+
</Form>
|
|
165
|
+
)}
|
|
166
|
+
</Formik>
|
|
167
|
+
<p className="panel-note">
|
|
168
|
+
<strong>When to use:</strong> form-heavy screens with Formik's render/props model.
|
|
169
|
+
<br />
|
|
170
|
+
<strong>Best practice:</strong> prefer Yup/Zod schemas over ad-hoc validators.
|
|
171
|
+
</p>
|
|
172
|
+
</article>
|
|
173
|
+
);
|
|
174
|
+
<% } else if (forms === 'tanstack-form') { -%>
|
|
175
|
+
const form = useForm({
|
|
176
|
+
defaultValues: defaults,
|
|
177
|
+
onSubmit: async ({ value }) => {
|
|
178
|
+
<% if (validation === 'zod') { -%>
|
|
179
|
+
feedbackSchema.parse(value);
|
|
180
|
+
<% } else if (validation === 'yup') { -%>
|
|
181
|
+
await feedbackSchema.validate(value);
|
|
182
|
+
<% } else if (validation === 'valibot') { -%>
|
|
183
|
+
const { parse } = await import('valibot');
|
|
184
|
+
parse(feedbackSchema, value);
|
|
185
|
+
<% } else if (validation === 'joi') { -%>
|
|
186
|
+
const result = feedbackSchema.validate(value);
|
|
187
|
+
if (result.error) throw result.error;
|
|
188
|
+
<% } -%>
|
|
189
|
+
await createFeedback(value);
|
|
190
|
+
push('Feedback submitted (demo API)', 'success');
|
|
191
|
+
form.reset();
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<article className="panel">
|
|
197
|
+
<header className="panel-header">
|
|
198
|
+
<h2>Forms & validation</h2>
|
|
199
|
+
<p>
|
|
200
|
+
<%= stackLabels.forms %>
|
|
201
|
+
<% if (validation !== 'none') { -%>
|
|
202
|
+
{' '}
|
|
203
|
+
+ <%= stackLabels.validation %>
|
|
204
|
+
<% } -%>
|
|
205
|
+
</p>
|
|
206
|
+
</header>
|
|
207
|
+
<form
|
|
208
|
+
className="feedback-form"
|
|
209
|
+
onSubmit={(e) => {
|
|
210
|
+
e.preventDefault();
|
|
211
|
+
e.stopPropagation();
|
|
212
|
+
void form.handleSubmit();
|
|
213
|
+
}}
|
|
214
|
+
>
|
|
215
|
+
<form.Field name="name">
|
|
216
|
+
{(field) => (
|
|
217
|
+
<label>
|
|
218
|
+
Name
|
|
219
|
+
<input
|
|
220
|
+
value={field.state.value}
|
|
221
|
+
onBlur={field.handleBlur}
|
|
222
|
+
onChange={(e) => field.handleChange(e.target.value)}
|
|
223
|
+
placeholder="Ada Lovelace"
|
|
224
|
+
/>
|
|
225
|
+
</label>
|
|
226
|
+
)}
|
|
227
|
+
</form.Field>
|
|
228
|
+
<form.Field name="email">
|
|
229
|
+
{(field) => (
|
|
230
|
+
<label>
|
|
231
|
+
Email
|
|
232
|
+
<input
|
|
233
|
+
type="email"
|
|
234
|
+
value={field.state.value}
|
|
235
|
+
onBlur={field.handleBlur}
|
|
236
|
+
onChange={(e) => field.handleChange(e.target.value)}
|
|
237
|
+
placeholder="ada@example.com"
|
|
238
|
+
/>
|
|
239
|
+
</label>
|
|
240
|
+
)}
|
|
241
|
+
</form.Field>
|
|
242
|
+
<form.Field name="message">
|
|
243
|
+
{(field) => (
|
|
244
|
+
<label>
|
|
245
|
+
Message
|
|
246
|
+
<textarea
|
|
247
|
+
rows={4}
|
|
248
|
+
value={field.state.value}
|
|
249
|
+
onBlur={field.handleBlur}
|
|
250
|
+
onChange={(e) => field.handleChange(e.target.value)}
|
|
251
|
+
placeholder="What should we improve?"
|
|
252
|
+
/>
|
|
253
|
+
</label>
|
|
254
|
+
)}
|
|
255
|
+
</form.Field>
|
|
256
|
+
<UiButton type="submit">Send feedback</UiButton>
|
|
257
|
+
</form>
|
|
258
|
+
<p className="panel-note">
|
|
259
|
+
<strong>When to use:</strong> headless, type-safe forms with fine-grained reactivity.
|
|
260
|
+
<br />
|
|
261
|
+
<strong>Best practice:</strong> validate at field and form boundaries with a shared schema.
|
|
262
|
+
</p>
|
|
263
|
+
</article>
|
|
264
|
+
);
|
|
265
|
+
<% } else { -%>
|
|
266
|
+
const [values, setValues] = useState<% if (isTypeScript) { %><FeedbackValues><% } %>(defaults);
|
|
267
|
+
const [errors, setErrors] = useState<% if (isTypeScript) { %><Partial<Record<keyof FeedbackValues, string>>><% } %>({});
|
|
268
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
269
|
+
|
|
270
|
+
const onSubmit = async (event<% if (isTypeScript) { %>: React.FormEvent<% } %>) => {
|
|
271
|
+
event.preventDefault();
|
|
272
|
+
<% if (validation === 'none') { -%>
|
|
273
|
+
const nextErrors = validateFeedback(values);
|
|
274
|
+
<% } else if (validation === 'zod') { -%>
|
|
275
|
+
const parsed = feedbackSchema.safeParse(values);
|
|
276
|
+
const nextErrors = parsed.success
|
|
277
|
+
? {}
|
|
278
|
+
: Object.fromEntries(
|
|
279
|
+
parsed.error.issues.map((issue) => [String(issue.path[0]), issue.message]),
|
|
280
|
+
);
|
|
281
|
+
<% } else if (validation === 'yup') { -%>
|
|
282
|
+
let nextErrors<% if (isTypeScript) { %>: Partial<Record<keyof FeedbackValues, string>><% } %> = {};
|
|
283
|
+
try {
|
|
284
|
+
await feedbackSchema.validate(values, { abortEarly: false });
|
|
285
|
+
} catch (err) {
|
|
286
|
+
if (err && typeof err === 'object' && 'inner' in err) {
|
|
287
|
+
nextErrors = Object.fromEntries(
|
|
288
|
+
(err<% if (isTypeScript) { %> as { inner: Array<{ path?: string; message: string }> }<% } %>).inner
|
|
289
|
+
.filter((e) => e.path)
|
|
290
|
+
.map((e) => [e.path, e.message]),
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
<% } else if (validation === 'joi') { -%>
|
|
295
|
+
const result = feedbackSchema.validate(values, { abortEarly: false });
|
|
296
|
+
const nextErrors = result.error
|
|
297
|
+
? Object.fromEntries(
|
|
298
|
+
result.error.details.map((d<% if (isTypeScript) { %>: { path: (string | number)[]; message: string }<% } %>) => [
|
|
299
|
+
String(d.path[0]),
|
|
300
|
+
d.message,
|
|
301
|
+
]),
|
|
302
|
+
)
|
|
303
|
+
: {};
|
|
304
|
+
<% } else { -%>
|
|
305
|
+
let nextErrors<% if (isTypeScript) { %>: Partial<Record<keyof FeedbackValues, string>><% } %> = {};
|
|
306
|
+
try {
|
|
307
|
+
const { parse } = await import('valibot');
|
|
308
|
+
parse(feedbackSchema, values);
|
|
309
|
+
} catch (err) {
|
|
310
|
+
nextErrors = { message: err instanceof Error ? err.message : 'Invalid form' };
|
|
311
|
+
}
|
|
312
|
+
<% } -%>
|
|
313
|
+
setErrors(nextErrors);
|
|
314
|
+
if (Object.keys(nextErrors).length) return;
|
|
315
|
+
setIsSubmitting(true);
|
|
316
|
+
try {
|
|
317
|
+
await createFeedback(values);
|
|
318
|
+
push('Feedback submitted (demo API)', 'success');
|
|
319
|
+
setValues(defaults);
|
|
320
|
+
} finally {
|
|
321
|
+
setIsSubmitting(false);
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
return (
|
|
326
|
+
<article className="panel">
|
|
327
|
+
<header className="panel-header">
|
|
328
|
+
<h2>Forms & validation</h2>
|
|
329
|
+
<p>Native form controls<% if (validation !== 'none') { %> + <%= stackLabels.validation %><% } %></p>
|
|
330
|
+
</header>
|
|
331
|
+
<form className="feedback-form" onSubmit={onSubmit} noValidate>
|
|
332
|
+
<label>
|
|
333
|
+
Name
|
|
334
|
+
<input
|
|
335
|
+
value={values.name}
|
|
336
|
+
onChange={(e) => setValues((v) => ({ ...v, name: e.target.value }))}
|
|
337
|
+
placeholder="Ada Lovelace"
|
|
338
|
+
/>
|
|
339
|
+
{errors.name && <span className="field-error">{errors.name}</span>}
|
|
340
|
+
</label>
|
|
341
|
+
<label>
|
|
342
|
+
Email
|
|
343
|
+
<input
|
|
344
|
+
type="email"
|
|
345
|
+
value={values.email}
|
|
346
|
+
onChange={(e) => setValues((v) => ({ ...v, email: e.target.value }))}
|
|
347
|
+
placeholder="ada@example.com"
|
|
348
|
+
/>
|
|
349
|
+
{errors.email && <span className="field-error">{errors.email}</span>}
|
|
350
|
+
</label>
|
|
351
|
+
<label>
|
|
352
|
+
Message
|
|
353
|
+
<textarea
|
|
354
|
+
rows={4}
|
|
355
|
+
value={values.message}
|
|
356
|
+
onChange={(e) => setValues((v) => ({ ...v, message: e.target.value }))}
|
|
357
|
+
placeholder="What should we improve?"
|
|
358
|
+
/>
|
|
359
|
+
{errors.message && <span className="field-error">{errors.message}</span>}
|
|
360
|
+
</label>
|
|
361
|
+
<UiButton type="submit" disabled={isSubmitting}>
|
|
362
|
+
{isSubmitting ? 'Sending…' : 'Send feedback'}
|
|
363
|
+
</UiButton>
|
|
364
|
+
</form>
|
|
365
|
+
<p className="panel-note">
|
|
366
|
+
<strong>When to use:</strong> simple forms without a dedicated form library.
|
|
367
|
+
<br />
|
|
368
|
+
<strong>Best practice:</strong> graduate to a form library once fields or validation grow.
|
|
369
|
+
</p>
|
|
370
|
+
</article>
|
|
371
|
+
);
|
|
372
|
+
<% } -%>
|
|
373
|
+
}
|