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,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
+ }
@@ -0,0 +1,91 @@
1
+ <% if (serverState === 'tanstack-query') { -%>
2
+ import { useQuery } from '@tanstack/react-query';
3
+ import { fetchPosts } from '@/services/api';
4
+ <% } else if (serverState === 'swr') { -%>
5
+ import useSWR from 'swr';
6
+ <% } else if (serverState === 'rtk-query') { -%>
7
+ import { useGetPostsQuery } from '@/store/postsApi';
8
+ <% } else { -%>
9
+ import { useEffect, useState } from 'react';
10
+ import { fetchPosts } from '@/services/api';
11
+ <% } -%>
12
+
13
+ export function PostsPanel() {
14
+ <% if (serverState === 'tanstack-query') { -%>
15
+ const { data, isLoading, error, refetch, isFetching } = useQuery({
16
+ queryKey: ['posts'],
17
+ queryFn: fetchPosts,
18
+ });
19
+ <% } else if (serverState === 'swr') { -%>
20
+ const { data, error, isLoading, isValidating, mutate } = useSWR('/posts?_limit=5');
21
+ const isFetching = isValidating;
22
+ const refetch = () => mutate();
23
+ <% } else if (serverState === 'rtk-query') { -%>
24
+ const { data, isLoading, error, refetch, isFetching } = useGetPostsQuery();
25
+ <% } else { -%>
26
+ const [data, setData] = useState<% if (isTypeScript) { %><Awaited<ReturnType<typeof fetchPosts>> | null><% } %>(null);
27
+ const [isLoading, setIsLoading] = useState(true);
28
+ const [error, setError] = useState<% if (isTypeScript) { %><Error | null><% } %>(null);
29
+ const [isFetching, setIsFetching] = useState(false);
30
+
31
+ const load = async () => {
32
+ setIsFetching(true);
33
+ setError(null);
34
+ try {
35
+ const posts = await fetchPosts();
36
+ setData(posts);
37
+ } catch (err) {
38
+ setError(err instanceof Error ? err : new Error('Failed to load'));
39
+ } finally {
40
+ setIsLoading(false);
41
+ setIsFetching(false);
42
+ }
43
+ };
44
+
45
+ useEffect(() => {
46
+ void load();
47
+ }, []);
48
+
49
+ const refetch = () => void load();
50
+ <% } -%>
51
+
52
+ return (
53
+ <article className="panel">
54
+ <header className="panel-header">
55
+ <h2>Server state</h2>
56
+ <p>
57
+ <%= stackLabels.serverState === 'None' ? 'Manual fetch via ' + stackLabels.apiLayer : stackLabels.serverState %>{' '}
58
+ · JSONPlaceholder posts
59
+ </p>
60
+ </header>
61
+
62
+ <div className="panel-toolbar">
63
+ <button type="button" className="ui-button" onClick={() => void refetch()} disabled={isFetching}>
64
+ {isFetching ? 'Refreshing…' : 'Refresh'}
65
+ </button>
66
+ </div>
67
+
68
+ {isLoading && <p className="muted">Loading posts…</p>}
69
+ {error && (
70
+ <p className="error-text">
71
+ {error instanceof Error ? error.message : 'Request failed'}
72
+ </p>
73
+ )}
74
+ {data && (
75
+ <ul className="post-list">
76
+ {data.map((post<% if (isTypeScript) { %>: { id: number; title: string }<% } %>) => (
77
+ <li key={post.id}>
78
+ <strong>#{post.id}</strong> {post.title}
79
+ </li>
80
+ ))}
81
+ </ul>
82
+ )}
83
+
84
+ <p className="panel-note">
85
+ <strong>When to use:</strong> remote data with caching, retries, and refetch rules.
86
+ <br />
87
+ <strong>Best practice:</strong> treat server cache separately from UI/client state.
88
+ </p>
89
+ </article>
90
+ );
91
+ }
@@ -0,0 +1,15 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ /**
4
+ * Returns true after the component has mounted on the client.
5
+ * Useful for avoiding hydration mismatches with client-only UI.
6
+ */
7
+ export function useMounted() {
8
+ const [mounted, setMounted] = useState(false);
9
+
10
+ useEffect(() => {
11
+ setMounted(true);
12
+ }, []);
13
+
14
+ return mounted;
15
+ }
@@ -0,0 +1,12 @@
1
+ <% if (uiLibrary === 'shadcn') { -%>
2
+ import { clsx<% if (isTypeScript) { %>, type ClassValue<% } %> } from 'clsx';
3
+ import { twMerge } from 'tailwind-merge';
4
+
5
+ export function cn(...inputs<% if (isTypeScript) { %>: ClassValue[]<% } %>) {
6
+ return twMerge(clsx(inputs));
7
+ }
8
+ <% } else { -%>
9
+ export function cn(...parts<% if (isTypeScript) { %>: Array<string | false | null | undefined><% } %>) {
10
+ return parts.filter(Boolean).join(' ');
11
+ }
12
+ <% } -%>
@@ -0,0 +1,66 @@
1
+ <% if (errorTracking === 'sentry') { -%>
2
+ import * as Sentry from '@sentry/react';
3
+
4
+ export function initMonitoring() {
5
+ const dsn = import.meta.env?.VITE_SENTRY_DSN;
6
+ if (!dsn) {
7
+ return;
8
+ }
9
+
10
+ Sentry.init({
11
+ dsn,
12
+ tracesSampleRate: 0.1,
13
+ environment: import.meta.env?.MODE ?? 'development',
14
+ });
15
+ }
16
+ <% } else if (errorTracking === 'bugsnag') { -%>
17
+ import Bugsnag from '@bugsnag/js';
18
+ import BugsnagPluginReact from '@bugsnag/plugin-react';
19
+
20
+ export function initMonitoring() {
21
+ const apiKey = import.meta.env?.VITE_BUGSNAG_API_KEY;
22
+ if (!apiKey) {
23
+ return;
24
+ }
25
+
26
+ Bugsnag.start({
27
+ apiKey,
28
+ plugins: [new BugsnagPluginReact()],
29
+ });
30
+ }
31
+ <% } else if (errorTracking === 'logrocket') { -%>
32
+ import LogRocket from 'logrocket';
33
+
34
+ export function initMonitoring() {
35
+ const appId = import.meta.env?.VITE_LOGROCKET_APP_ID;
36
+ if (!appId) {
37
+ return;
38
+ }
39
+
40
+ LogRocket.init(appId);
41
+ }
42
+ <% } else if (errorTracking === 'datadog') { -%>
43
+ import { datadogRum } from '@datadog/browser-rum';
44
+
45
+ export function initMonitoring() {
46
+ const applicationId = import.meta.env?.VITE_DATADOG_APPLICATION_ID;
47
+ const clientToken = import.meta.env?.VITE_DATADOG_CLIENT_TOKEN;
48
+ if (!applicationId || !clientToken) {
49
+ return;
50
+ }
51
+
52
+ datadogRum.init({
53
+ applicationId,
54
+ clientToken,
55
+ site: import.meta.env?.VITE_DATADOG_SITE ?? 'datadoghq.com',
56
+ service: '<%= projectName %>',
57
+ sessionSampleRate: 100,
58
+ trackResources: true,
59
+ trackLongTasks: true,
60
+ });
61
+ }
62
+ <% } else { -%>
63
+ export function initMonitoring() {
64
+ // Error tracking disabled for this scaffold.
65
+ }
66
+ <% } -%>
@@ -0,0 +1,81 @@
1
+ <% if (validation === 'zod') { -%>
2
+ import { z } from 'zod';
3
+
4
+ export const feedbackSchema = z.object({
5
+ name: z.string().min(2, 'Name must be at least 2 characters'),
6
+ email: z.string().email('Enter a valid email'),
7
+ message: z.string().min(10, 'Message must be at least 10 characters'),
8
+ });
9
+
10
+ <% if (isTypeScript) { -%>
11
+ export type FeedbackValues = z.infer<typeof feedbackSchema>;
12
+ <% } -%>
13
+ <% } else if (validation === 'yup') { -%>
14
+ import * as yup from 'yup';
15
+
16
+ export const feedbackSchema = yup.object({
17
+ name: yup.string().min(2, 'Name must be at least 2 characters').required(),
18
+ email: yup.string().email('Enter a valid email').required(),
19
+ message: yup.string().min(10, 'Message must be at least 10 characters').required(),
20
+ });
21
+
22
+ <% if (isTypeScript) { -%>
23
+ export type FeedbackValues = yup.InferType<typeof feedbackSchema>;
24
+ <% } -%>
25
+ <% } else if (validation === 'valibot') { -%>
26
+ import * as v from 'valibot';
27
+
28
+ export const feedbackSchema = v.object({
29
+ name: v.pipe(v.string(), v.minLength(2, 'Name must be at least 2 characters')),
30
+ email: v.pipe(v.string(), v.email('Enter a valid email')),
31
+ message: v.pipe(v.string(), v.minLength(10, 'Message must be at least 10 characters')),
32
+ });
33
+
34
+ <% if (isTypeScript) { -%>
35
+ export type FeedbackValues = v.InferOutput<typeof feedbackSchema>;
36
+ <% } -%>
37
+ <% } else if (validation === 'joi') { -%>
38
+ import Joi from 'joi';
39
+
40
+ export const feedbackSchema = Joi.object({
41
+ name: Joi.string().min(2).required().messages({
42
+ 'string.min': 'Name must be at least 2 characters',
43
+ }),
44
+ email: Joi.string().email({ tlds: false }).required().messages({
45
+ 'string.email': 'Enter a valid email',
46
+ }),
47
+ message: Joi.string().min(10).required().messages({
48
+ 'string.min': 'Message must be at least 10 characters',
49
+ }),
50
+ });
51
+
52
+ <% if (isTypeScript) { -%>
53
+ export interface FeedbackValues {
54
+ name: string;
55
+ email: string;
56
+ message: string;
57
+ }
58
+ <% } -%>
59
+ <% } else { -%>
60
+ <% if (isTypeScript) { -%>
61
+ export interface FeedbackValues {
62
+ name: string;
63
+ email: string;
64
+ message: string;
65
+ }
66
+ <% } -%>
67
+
68
+ export function validateFeedback(values<% if (isTypeScript) { %>: FeedbackValues<% } %>) {
69
+ const errors<% if (isTypeScript) { %>: Partial<Record<keyof FeedbackValues, string>><% } %> = {};
70
+ if (!values.name || values.name.length < 2) {
71
+ errors.name = 'Name must be at least 2 characters';
72
+ }
73
+ if (!values.email || !values.email.includes('@')) {
74
+ errors.email = 'Enter a valid email';
75
+ }
76
+ if (!values.message || values.message.length < 10) {
77
+ errors.message = 'Message must be at least 10 characters';
78
+ }
79
+ return errors;
80
+ }
81
+ <% } -%>
@@ -0,0 +1,22 @@
1
+ import { StrictMode } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import { AppProviders } from '@/app/providers';
4
+ import { App } from '@/app/App';
5
+ import { initMonitoring } from '@/lib/monitoring';
6
+ import '@/styles/global.<%= styling === 'sass' && uiLibrary !== 'shadcn' ? 'scss' : 'css' %>';
7
+
8
+ initMonitoring();
9
+
10
+ const rootElement = document.getElementById('root');
11
+
12
+ if (!rootElement) {
13
+ throw new Error('Root element #root not found');
14
+ }
15
+
16
+ createRoot(rootElement).render(
17
+ <StrictMode>
18
+ <AppProviders>
19
+ <App />
20
+ </AppProviders>
21
+ </StrictMode>,
22
+ );