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.
Files changed (182) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +119 -0
  3. package/bin/create-react-starter-kit.js +7 -0
  4. package/dist/cli-args.d.ts +15 -0
  5. package/dist/cli-args.d.ts.map +1 -0
  6. package/dist/cli-args.js +78 -0
  7. package/dist/cli-args.js.map +1 -0
  8. package/dist/generator/dependencies.d.ts +8 -0
  9. package/dist/generator/dependencies.d.ts.map +1 -0
  10. package/dist/generator/dependencies.js +356 -0
  11. package/dist/generator/dependencies.js.map +1 -0
  12. package/dist/generator/index.d.ts +5 -0
  13. package/dist/generator/index.d.ts.map +1 -0
  14. package/dist/generator/index.js +358 -0
  15. package/dist/generator/index.js.map +1 -0
  16. package/dist/index.d.ts +3 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +38 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/prompts/index.d.ts +3 -0
  21. package/dist/prompts/index.d.ts.map +1 -0
  22. package/dist/prompts/index.js +267 -0
  23. package/dist/prompts/index.js.map +1 -0
  24. package/dist/templates/.env.example.ejs +21 -0
  25. package/dist/templates/.husky/commit-msg +2 -0
  26. package/dist/templates/.husky/pre-commit +2 -0
  27. package/dist/templates/README.md.ejs +169 -0
  28. package/dist/templates/__js__/jsconfig.json +9 -0
  29. package/dist/templates/__ts__/src/vite-env.d.ts +16 -0
  30. package/dist/templates/__ts__/tsconfig.json +24 -0
  31. package/dist/templates/__when_buildTool_esbuild__/scripts/esbuild.build.mjs.ejs +23 -0
  32. package/dist/templates/__when_buildTool_esbuild__/scripts/esbuild.dev.mjs.ejs +48 -0
  33. package/dist/templates/__when_buildTool_parcel__/.parcelrc +9 -0
  34. package/dist/templates/__when_buildTool_rollup__/rollup.config.js.ejs +33 -0
  35. package/dist/templates/__when_buildTool_rspack__/rspack.config.js.ejs +36 -0
  36. package/dist/templates/__when_buildTool_vite__/vite.config.ts.ejs +37 -0
  37. package/dist/templates/__when_buildTool_webpack__/webpack.config.js.ejs +68 -0
  38. package/dist/templates/__when_e2eTesting_cypress__/cypress/e2e/home.cy.ts.ejs +6 -0
  39. package/dist/templates/__when_e2eTesting_cypress__/cypress.config.ts.ejs +8 -0
  40. package/dist/templates/__when_e2eTesting_playwright__/playwright.config.ts.ejs +16 -0
  41. package/dist/templates/__when_e2eTesting_playwright__/tests/e2e/home.spec.ts.ejs +6 -0
  42. package/dist/templates/__when_formatting_prettier__/.prettierrc +7 -0
  43. package/dist/templates/__when_formatting_stylelint__/.stylelintrc.json +3 -0
  44. package/dist/templates/__when_linting_biome__/biome.json +19 -0
  45. package/dist/templates/__when_linting_eslint__/eslint.config.js.ejs +42 -0
  46. package/dist/templates/__when_serverState_rtk-query__/src/store/postsApi.ts.ejs +19 -0
  47. package/dist/templates/__when_stateManagement_context__/src/store/context.tsx.ejs +57 -0
  48. package/dist/templates/__when_styling_tailwind__/postcss.config.js +6 -0
  49. package/dist/templates/__when_styling_tailwind__/tailwind.config.js.ejs +23 -0
  50. package/dist/templates/__when_unitTesting_jest__/jest.config.json.ejs +11 -0
  51. package/dist/templates/__when_unitTesting_jest__/tests/App.test.tsx.ejs +13 -0
  52. package/dist/templates/__when_unitTesting_jest__/tests/setup.ts.ejs +1 -0
  53. package/dist/templates/__when_unitTesting_jest__/tests/styleMock.ts.ejs +1 -0
  54. package/dist/templates/__when_unitTesting_rtl__/tests/App.test.tsx.ejs +13 -0
  55. package/dist/templates/__when_unitTesting_vitest__/tests/App.test.tsx.ejs +16 -0
  56. package/dist/templates/__when_unitTesting_vitest__/tests/setup.ts.ejs +1 -0
  57. package/dist/templates/__when_visualTesting_storybook__/.storybook/main.js.ejs +11 -0
  58. package/dist/templates/__when_visualTesting_storybook__/.storybook/preview.js +8 -0
  59. package/dist/templates/__when_visualTesting_storybook__/src/components/ui/UiButton.stories.tsx.ejs +15 -0
  60. package/dist/templates/commitlint.config.js +3 -0
  61. package/dist/templates/docs/architecture.md.ejs +74 -0
  62. package/dist/templates/docs/best-practices.md.ejs +56 -0
  63. package/dist/templates/docs/examples/README.md.ejs +14 -0
  64. package/dist/templates/docs/examples/api.md.ejs +17 -0
  65. package/dist/templates/docs/examples/data-fetching.md.ejs +18 -0
  66. package/dist/templates/docs/examples/forms.md.ejs +18 -0
  67. package/dist/templates/docs/examples/state.md.ejs +17 -0
  68. package/dist/templates/docs/examples/styling.md.ejs +23 -0
  69. package/dist/templates/docs/examples/testing.md.ejs +27 -0
  70. package/dist/templates/docs/getting-started.md.ejs +55 -0
  71. package/dist/templates/docs/selected-stack.md.ejs +55 -0
  72. package/dist/templates/index.html.ejs +19 -0
  73. package/dist/templates/public/vite.svg +4 -0
  74. package/dist/templates/src/app/App.tsx.ejs +71 -0
  75. package/dist/templates/src/app/providers.tsx.ejs +93 -0
  76. package/dist/templates/src/assets/.gitkeep +0 -0
  77. package/dist/templates/src/components/StackBadge.tsx.ejs +15 -0
  78. package/dist/templates/src/components/layout/AppLayout.tsx.ejs +64 -0
  79. package/dist/templates/src/components/ui/Toast.tsx.ejs +61 -0
  80. package/dist/templates/src/components/ui/UiButton.tsx.ejs +168 -0
  81. package/dist/templates/src/config/env.ts.ejs +18 -0
  82. package/dist/templates/src/features/counter/CounterPanel.tsx.ejs +66 -0
  83. package/dist/templates/src/features/feedback/FeedbackForm.tsx.ejs +373 -0
  84. package/dist/templates/src/features/posts/PostsPanel.tsx.ejs +91 -0
  85. package/dist/templates/src/hooks/useMounted.ts.ejs +15 -0
  86. package/dist/templates/src/lib/cn.ts.ejs +12 -0
  87. package/dist/templates/src/lib/monitoring.ts.ejs +66 -0
  88. package/dist/templates/src/lib/validation.ts.ejs +81 -0
  89. package/dist/templates/src/main.tsx.ejs +22 -0
  90. package/dist/templates/src/routes/AboutPage.tsx.ejs +30 -0
  91. package/dist/templates/src/routes/HomePage.tsx.ejs +47 -0
  92. package/dist/templates/src/services/api.ts.ejs +84 -0
  93. package/dist/templates/src/store/index.ts.ejs +80 -0
  94. package/dist/templates/src/styles/global.css.ejs +457 -0
  95. package/dist/templates/src/types/index.ts.ejs +7 -0
  96. package/dist/templates/src/utils/format.ts.ejs +3 -0
  97. package/dist/types.d.ts +46 -0
  98. package/dist/types.d.ts.map +1 -0
  99. package/dist/types.js +2 -0
  100. package/dist/types.js.map +1 -0
  101. package/dist/utils/logger.d.ts +12 -0
  102. package/dist/utils/logger.d.ts.map +1 -0
  103. package/dist/utils/logger.js +31 -0
  104. package/dist/utils/logger.js.map +1 -0
  105. package/dist/utils/validate.d.ts +3 -0
  106. package/dist/utils/validate.d.ts.map +1 -0
  107. package/dist/utils/validate.js +24 -0
  108. package/dist/utils/validate.js.map +1 -0
  109. package/package.json +62 -0
  110. package/templates/.env.example.ejs +21 -0
  111. package/templates/.husky/commit-msg +2 -0
  112. package/templates/.husky/pre-commit +2 -0
  113. package/templates/README.md.ejs +169 -0
  114. package/templates/__js__/jsconfig.json +9 -0
  115. package/templates/__ts__/src/vite-env.d.ts +16 -0
  116. package/templates/__ts__/tsconfig.json +24 -0
  117. package/templates/__when_buildTool_esbuild__/scripts/esbuild.build.mjs.ejs +23 -0
  118. package/templates/__when_buildTool_esbuild__/scripts/esbuild.dev.mjs.ejs +48 -0
  119. package/templates/__when_buildTool_parcel__/.parcelrc +9 -0
  120. package/templates/__when_buildTool_rollup__/rollup.config.js.ejs +33 -0
  121. package/templates/__when_buildTool_rspack__/rspack.config.js.ejs +36 -0
  122. package/templates/__when_buildTool_vite__/vite.config.ts.ejs +37 -0
  123. package/templates/__when_buildTool_webpack__/webpack.config.js.ejs +68 -0
  124. package/templates/__when_e2eTesting_cypress__/cypress/e2e/home.cy.ts.ejs +6 -0
  125. package/templates/__when_e2eTesting_cypress__/cypress.config.ts.ejs +8 -0
  126. package/templates/__when_e2eTesting_playwright__/playwright.config.ts.ejs +16 -0
  127. package/templates/__when_e2eTesting_playwright__/tests/e2e/home.spec.ts.ejs +6 -0
  128. package/templates/__when_formatting_prettier__/.prettierrc +7 -0
  129. package/templates/__when_formatting_stylelint__/.stylelintrc.json +3 -0
  130. package/templates/__when_linting_biome__/biome.json +19 -0
  131. package/templates/__when_linting_eslint__/eslint.config.js.ejs +42 -0
  132. package/templates/__when_serverState_rtk-query__/src/store/postsApi.ts.ejs +19 -0
  133. package/templates/__when_stateManagement_context__/src/store/context.tsx.ejs +57 -0
  134. package/templates/__when_styling_tailwind__/postcss.config.js +6 -0
  135. package/templates/__when_styling_tailwind__/tailwind.config.js.ejs +23 -0
  136. package/templates/__when_unitTesting_jest__/jest.config.json.ejs +11 -0
  137. package/templates/__when_unitTesting_jest__/tests/App.test.tsx.ejs +13 -0
  138. package/templates/__when_unitTesting_jest__/tests/setup.ts.ejs +1 -0
  139. package/templates/__when_unitTesting_jest__/tests/styleMock.ts.ejs +1 -0
  140. package/templates/__when_unitTesting_rtl__/tests/App.test.tsx.ejs +13 -0
  141. package/templates/__when_unitTesting_vitest__/tests/App.test.tsx.ejs +16 -0
  142. package/templates/__when_unitTesting_vitest__/tests/setup.ts.ejs +1 -0
  143. package/templates/__when_visualTesting_storybook__/.storybook/main.js.ejs +11 -0
  144. package/templates/__when_visualTesting_storybook__/.storybook/preview.js +8 -0
  145. package/templates/__when_visualTesting_storybook__/src/components/ui/UiButton.stories.tsx.ejs +15 -0
  146. package/templates/commitlint.config.js +3 -0
  147. package/templates/docs/architecture.md.ejs +74 -0
  148. package/templates/docs/best-practices.md.ejs +56 -0
  149. package/templates/docs/examples/README.md.ejs +14 -0
  150. package/templates/docs/examples/api.md.ejs +17 -0
  151. package/templates/docs/examples/data-fetching.md.ejs +18 -0
  152. package/templates/docs/examples/forms.md.ejs +18 -0
  153. package/templates/docs/examples/state.md.ejs +17 -0
  154. package/templates/docs/examples/styling.md.ejs +23 -0
  155. package/templates/docs/examples/testing.md.ejs +27 -0
  156. package/templates/docs/getting-started.md.ejs +55 -0
  157. package/templates/docs/selected-stack.md.ejs +55 -0
  158. package/templates/index.html.ejs +19 -0
  159. package/templates/public/vite.svg +4 -0
  160. package/templates/src/app/App.tsx.ejs +71 -0
  161. package/templates/src/app/providers.tsx.ejs +93 -0
  162. package/templates/src/assets/.gitkeep +0 -0
  163. package/templates/src/components/StackBadge.tsx.ejs +15 -0
  164. package/templates/src/components/layout/AppLayout.tsx.ejs +64 -0
  165. package/templates/src/components/ui/Toast.tsx.ejs +61 -0
  166. package/templates/src/components/ui/UiButton.tsx.ejs +168 -0
  167. package/templates/src/config/env.ts.ejs +18 -0
  168. package/templates/src/features/counter/CounterPanel.tsx.ejs +66 -0
  169. package/templates/src/features/feedback/FeedbackForm.tsx.ejs +373 -0
  170. package/templates/src/features/posts/PostsPanel.tsx.ejs +91 -0
  171. package/templates/src/hooks/useMounted.ts.ejs +15 -0
  172. package/templates/src/lib/cn.ts.ejs +12 -0
  173. package/templates/src/lib/monitoring.ts.ejs +66 -0
  174. package/templates/src/lib/validation.ts.ejs +81 -0
  175. package/templates/src/main.tsx.ejs +22 -0
  176. package/templates/src/routes/AboutPage.tsx.ejs +30 -0
  177. package/templates/src/routes/HomePage.tsx.ejs +47 -0
  178. package/templates/src/services/api.ts.ejs +84 -0
  179. package/templates/src/store/index.ts.ejs +80 -0
  180. package/templates/src/styles/global.css.ejs +457 -0
  181. package/templates/src/types/index.ts.ejs +7 -0
  182. 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
+ }