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,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
|
+
);
|