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,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
|
+
);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export function AboutPage() {
|
|
2
|
+
return (
|
|
3
|
+
<div className="page about-page">
|
|
4
|
+
<h1>About this starter</h1>
|
|
5
|
+
<p>
|
|
6
|
+
Generated with <strong>create-react-starter-kit</strong> using a scalable{' '}
|
|
7
|
+
<strong>feature-based</strong> layout (domain modules under <code>src/features</code>) —
|
|
8
|
+
the same organizational approach used across large production React codebases.
|
|
9
|
+
</p>
|
|
10
|
+
<ul className="about-list">
|
|
11
|
+
<li>
|
|
12
|
+
<strong>Extend features</strong> under <code>src/features/<name>/</code> — keep
|
|
13
|
+
domain logic close to the UI that owns it.
|
|
14
|
+
</li>
|
|
15
|
+
<li>
|
|
16
|
+
<strong>Shared primitives</strong> live in <code>src/components/</code> and{' '}
|
|
17
|
+
<code>src/lib/</code>. Prefer imports via the <code>@/</code> path alias.
|
|
18
|
+
</li>
|
|
19
|
+
<li>
|
|
20
|
+
<strong>Docs</strong> in <code>docs/</code> explain architecture, testing, and production
|
|
21
|
+
habits for this stack.
|
|
22
|
+
</li>
|
|
23
|
+
</ul>
|
|
24
|
+
<p>
|
|
25
|
+
Prefer reading <code>docs/getting-started.md</code> next, then{' '}
|
|
26
|
+
<code>docs/selected-stack.md</code> for library-specific guidance.
|
|
27
|
+
</p>
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { CounterPanel } from '@/features/counter/CounterPanel';
|
|
2
|
+
import { PostsPanel } from '@/features/posts/PostsPanel';
|
|
3
|
+
import { FeedbackForm } from '@/features/feedback/FeedbackForm';
|
|
4
|
+
import { StackBadge } from '@/components/StackBadge';
|
|
5
|
+
import { useToast } from '@/components/ui/Toast';
|
|
6
|
+
import { UiButton } from '@/components/ui/UiButton';
|
|
7
|
+
|
|
8
|
+
export function HomePage() {
|
|
9
|
+
const { push } = useToast();
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<div className="page home-page">
|
|
13
|
+
<section className="hero">
|
|
14
|
+
<p className="eyebrow">Starter workspace</p>
|
|
15
|
+
<h1><%= projectName %></h1>
|
|
16
|
+
<p className="lede">
|
|
17
|
+
A minimal, production-shaped React app wired for your selected stack. Explore the panels
|
|
18
|
+
below — each one is a real usage example, not a placeholder.
|
|
19
|
+
</p>
|
|
20
|
+
<div className="hero-actions">
|
|
21
|
+
<UiButton onClick={() => push('Toast system is ready', 'success')}>
|
|
22
|
+
Show toast
|
|
23
|
+
</UiButton>
|
|
24
|
+
<a className="text-link" href="/about">
|
|
25
|
+
Read about the stack →
|
|
26
|
+
</a>
|
|
27
|
+
</div>
|
|
28
|
+
<div className="stack-row">
|
|
29
|
+
<StackBadge label="Build" value="<%= stackLabels.buildTool %>" />
|
|
30
|
+
<StackBadge label="Language" value="<%= stackLabels.language %>" />
|
|
31
|
+
<StackBadge label="Router" value="<%= stackLabels.router %>" />
|
|
32
|
+
<StackBadge label="State" value="<%= stackLabels.stateManagement %>" />
|
|
33
|
+
<StackBadge label="Data" value="<%= stackLabels.serverState %>" />
|
|
34
|
+
<StackBadge label="UI" value="<%= stackLabels.uiLibrary %>" />
|
|
35
|
+
</div>
|
|
36
|
+
</section>
|
|
37
|
+
|
|
38
|
+
<section className="panels">
|
|
39
|
+
<% if (stateManagement !== 'none') { -%>
|
|
40
|
+
<CounterPanel />
|
|
41
|
+
<% } -%>
|
|
42
|
+
<PostsPanel />
|
|
43
|
+
<FeedbackForm />
|
|
44
|
+
</section>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<% if (apiLayer === 'axios') { -%>
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
import { env } from '@/config/env';
|
|
4
|
+
|
|
5
|
+
const baseURL = env.apiBaseUrl;
|
|
6
|
+
|
|
7
|
+
export const apiClient = axios.create({
|
|
8
|
+
baseURL,
|
|
9
|
+
headers: { 'Content-Type': 'application/json' },
|
|
10
|
+
timeout: 10_000,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
apiClient.interceptors.response.use(
|
|
14
|
+
(response) => response,
|
|
15
|
+
(error) => {
|
|
16
|
+
console.error('[api]', error);
|
|
17
|
+
return Promise.reject(error);
|
|
18
|
+
},
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
export async function apiGet<% if (isTypeScript) { %><T = unknown><% } %>(path<% if (isTypeScript) { %>: string<% } %>)<% if (isTypeScript) { %>: Promise<T><% } %> {
|
|
22
|
+
const { data } = await apiClient.get<% if (isTypeScript) { %><T><% } %>(path);
|
|
23
|
+
return data;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function apiPost<% if (isTypeScript) { %><T = unknown, B = unknown><% } %>(
|
|
27
|
+
path<% if (isTypeScript) { %>: string<% } %>,
|
|
28
|
+
body<% if (isTypeScript) { %>: B<% } %>,
|
|
29
|
+
)<% if (isTypeScript) { %>: Promise<T><% } %> {
|
|
30
|
+
const { data } = await apiClient.post<% if (isTypeScript) { %><T><% } %>(path, body);
|
|
31
|
+
return data;
|
|
32
|
+
}
|
|
33
|
+
<% } else { -%>
|
|
34
|
+
import { env } from '@/config/env';
|
|
35
|
+
|
|
36
|
+
const baseURL = env.apiBaseUrl;
|
|
37
|
+
|
|
38
|
+
async function request<% if (isTypeScript) { %><T = unknown><% } %>(
|
|
39
|
+
path<% if (isTypeScript) { %>: string<% } %>,
|
|
40
|
+
init<% if (isTypeScript) { %>?: RequestInit<% } %>,
|
|
41
|
+
)<% if (isTypeScript) { %>: Promise<T><% } %> {
|
|
42
|
+
const response = await fetch(`${baseURL}${path}`, {
|
|
43
|
+
headers: { 'Content-Type': 'application/json', ...(init?.headers ?? {}) },
|
|
44
|
+
...init,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
throw new Error(`Request failed: ${response.status}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return response.json();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function apiGet<% if (isTypeScript) { %><T = unknown><% } %>(path<% if (isTypeScript) { %>: string<% } %>)<% if (isTypeScript) { %>: Promise<T><% } %> {
|
|
55
|
+
return request<% if (isTypeScript) { %><T><% } %>(path);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function apiPost<% if (isTypeScript) { %><T = unknown, B = unknown><% } %>(
|
|
59
|
+
path<% if (isTypeScript) { %>: string<% } %>,
|
|
60
|
+
body<% if (isTypeScript) { %>: B<% } %>,
|
|
61
|
+
)<% if (isTypeScript) { %>: Promise<T><% } %> {
|
|
62
|
+
return request<% if (isTypeScript) { %><T><% } %>(path, {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
body: JSON.stringify(body),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
<% } -%>
|
|
68
|
+
|
|
69
|
+
<% if (isTypeScript) { -%>
|
|
70
|
+
export interface Post {
|
|
71
|
+
userId: number;
|
|
72
|
+
id: number;
|
|
73
|
+
title: string;
|
|
74
|
+
body: string;
|
|
75
|
+
}
|
|
76
|
+
<% } -%>
|
|
77
|
+
|
|
78
|
+
export async function fetchPosts() {
|
|
79
|
+
return apiGet<% if (isTypeScript) { %><Post[]><% } %>('/posts?_limit=5');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function createFeedback(payload<% if (isTypeScript) { %>: { name: string; email: string; message: string }<% } %>) {
|
|
83
|
+
return apiPost('/posts', payload);
|
|
84
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
<% if (stateManagement === 'redux') { -%>
|
|
2
|
+
import { configureStore, createSlice } from '@reduxjs/toolkit';
|
|
3
|
+
<% if (isTypeScript) { -%>
|
|
4
|
+
import type { PayloadAction } from '@reduxjs/toolkit';
|
|
5
|
+
<% } -%>
|
|
6
|
+
<% if (serverState === 'rtk-query') { -%>
|
|
7
|
+
import { postsApi } from './postsApi';
|
|
8
|
+
<% } -%>
|
|
9
|
+
|
|
10
|
+
<% if (isTypeScript) { -%>
|
|
11
|
+
interface CounterState {
|
|
12
|
+
value: number;
|
|
13
|
+
}
|
|
14
|
+
<% } -%>
|
|
15
|
+
|
|
16
|
+
const initialState<% if (isTypeScript) { %>: CounterState<% } %> = { value: 0 };
|
|
17
|
+
|
|
18
|
+
const counterSlice = createSlice({
|
|
19
|
+
name: 'counter',
|
|
20
|
+
initialState,
|
|
21
|
+
reducers: {
|
|
22
|
+
increment(state) {
|
|
23
|
+
state.value += 1;
|
|
24
|
+
},
|
|
25
|
+
decrement(state) {
|
|
26
|
+
state.value -= 1;
|
|
27
|
+
},
|
|
28
|
+
setValue(state, action<% if (isTypeScript) { %>: PayloadAction<number><% } %>) {
|
|
29
|
+
state.value = action.payload;
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export const { increment, decrement, setValue } = counterSlice.actions;
|
|
35
|
+
|
|
36
|
+
export const store = configureStore({
|
|
37
|
+
reducer: {
|
|
38
|
+
counter: counterSlice.reducer,
|
|
39
|
+
<% if (serverState === 'rtk-query') { -%>
|
|
40
|
+
[postsApi.reducerPath]: postsApi.reducer,
|
|
41
|
+
<% } -%>
|
|
42
|
+
},
|
|
43
|
+
<% if (serverState === 'rtk-query') { -%>
|
|
44
|
+
middleware: (getDefaultMiddleware) =>
|
|
45
|
+
getDefaultMiddleware().concat(postsApi.middleware),
|
|
46
|
+
<% } -%>
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
<% if (isTypeScript) { -%>
|
|
50
|
+
export type RootState = ReturnType<typeof store.getState>;
|
|
51
|
+
export type AppDispatch = typeof store.dispatch;
|
|
52
|
+
<% } -%>
|
|
53
|
+
<% } else if (stateManagement === 'zustand') { -%>
|
|
54
|
+
import { create } from 'zustand';
|
|
55
|
+
|
|
56
|
+
<% if (isTypeScript) { -%>
|
|
57
|
+
interface CounterStore {
|
|
58
|
+
value: number;
|
|
59
|
+
increment: () => void;
|
|
60
|
+
decrement: () => void;
|
|
61
|
+
setValue: (value: number) => void;
|
|
62
|
+
}
|
|
63
|
+
<% } -%>
|
|
64
|
+
|
|
65
|
+
export const useCounterStore = create<% if (isTypeScript) { %><CounterStore><% } %>((set) => ({
|
|
66
|
+
value: 0,
|
|
67
|
+
increment: () => set((state) => ({ value: state.value + 1 })),
|
|
68
|
+
decrement: () => set((state) => ({ value: state.value - 1 })),
|
|
69
|
+
setValue: (value) => set({ value }),
|
|
70
|
+
}));
|
|
71
|
+
<% } else if (stateManagement === 'jotai') { -%>
|
|
72
|
+
import { atom } from 'jotai';
|
|
73
|
+
|
|
74
|
+
export const counterAtom = atom(0);
|
|
75
|
+
<% } else if (stateManagement === 'context') { -%>
|
|
76
|
+
export { AppStateProvider, useAppState } from './context';
|
|
77
|
+
<% } else { -%>
|
|
78
|
+
// Client state management was set to "None".
|
|
79
|
+
export {};
|
|
80
|
+
<% } -%>
|