create-modern-react 2.3.6 → 2.3.7
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/lib/setup.js +50 -0
- package/package.json +1 -1
- package/templates/optional/antd-replacements/components/layout/error-boundary.tsx +63 -0
- package/templates/optional/antd-replacements/redux/provider.tsx +33 -0
- package/templates/optional/antd-replacements/routes/index.tsx +40 -0
- package/templates/optional/antd-replacements/screens/home/index.tsx +227 -0
- package/templates/optional/antd-replacements/screens/not-found/index.tsx +30 -0
package/lib/setup.js
CHANGED
|
@@ -38,6 +38,8 @@ async function setupProject(config) {
|
|
|
38
38
|
await fs.remove(path.join(projectPath, 'components.json'));
|
|
39
39
|
await copyOptionalTemplate('antd', projectPath);
|
|
40
40
|
await updateProvidersForAntd(projectPath);
|
|
41
|
+
// Replace components that use ~/components/ui with antd-compatible versions
|
|
42
|
+
await replaceComponentsForAntd(projectPath);
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
// Step 4: Copy optional feature templates
|
|
@@ -45,6 +47,10 @@ async function setupProject(config) {
|
|
|
45
47
|
console.log(chalk.gray(' Adding Redux Toolkit + Redux Persist...'));
|
|
46
48
|
await copyOptionalTemplate('redux', projectPath);
|
|
47
49
|
await updateProvidersForRedux(projectPath, config.useAntd);
|
|
50
|
+
// If antd is selected, replace redux provider with antd-compatible version
|
|
51
|
+
if (config.useAntd) {
|
|
52
|
+
await replaceReduxProviderForAntd(projectPath);
|
|
53
|
+
}
|
|
48
54
|
}
|
|
49
55
|
|
|
50
56
|
if (config.useForms) {
|
|
@@ -210,6 +216,50 @@ export { ThemeProvider, useTheme } from './theme-provider';
|
|
|
210
216
|
await fs.ensureDir(path.join(projectPath, 'src/styles'));
|
|
211
217
|
}
|
|
212
218
|
|
|
219
|
+
/**
|
|
220
|
+
* Replace components that import from ~/components/ui with antd-compatible versions
|
|
221
|
+
* These files use shadcn Button, Card, Skeleton which are removed when antd is selected
|
|
222
|
+
*/
|
|
223
|
+
async function replaceComponentsForAntd(projectPath) {
|
|
224
|
+
const replacementsPath = path.join(
|
|
225
|
+
__dirname,
|
|
226
|
+
'../templates/optional/antd-replacements'
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
// Files to replace: [source in antd-replacements, target in project]
|
|
230
|
+
const filesToReplace = [
|
|
231
|
+
['components/layout/error-boundary.tsx', 'src/components/layout/error-boundary.tsx'],
|
|
232
|
+
['routes/index.tsx', 'src/routes/index.tsx'],
|
|
233
|
+
['screens/home/index.tsx', 'src/screens/home/index.tsx'],
|
|
234
|
+
['screens/not-found/index.tsx', 'src/screens/not-found/index.tsx'],
|
|
235
|
+
];
|
|
236
|
+
|
|
237
|
+
for (const [source, target] of filesToReplace) {
|
|
238
|
+
const sourcePath = path.join(replacementsPath, source);
|
|
239
|
+
const targetPath = path.join(projectPath, target);
|
|
240
|
+
|
|
241
|
+
if (await fs.pathExists(sourcePath)) {
|
|
242
|
+
await fs.copy(sourcePath, targetPath);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Replace Redux provider with antd-compatible version
|
|
249
|
+
* The base provider imports Skeleton from ~/components/ui which is removed for antd
|
|
250
|
+
*/
|
|
251
|
+
async function replaceReduxProviderForAntd(projectPath) {
|
|
252
|
+
const antdProviderPath = path.join(
|
|
253
|
+
__dirname,
|
|
254
|
+
'../templates/optional/antd-replacements/redux/provider.tsx'
|
|
255
|
+
);
|
|
256
|
+
const targetPath = path.join(projectPath, 'src/redux/provider.tsx');
|
|
257
|
+
|
|
258
|
+
if (await fs.pathExists(antdProviderPath)) {
|
|
259
|
+
await fs.copy(antdProviderPath, targetPath);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
213
263
|
/**
|
|
214
264
|
* Update providers/index.tsx to include Redux Provider
|
|
215
265
|
*/
|
package/package.json
CHANGED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Component, type ErrorInfo, type ReactNode } from 'react';
|
|
2
|
+
import { AlertTriangle, RefreshCw } from 'lucide-react';
|
|
3
|
+
import { Button } from 'antd';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
fallback?: ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface State {
|
|
11
|
+
hasError: boolean;
|
|
12
|
+
error: Error | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class ErrorBoundary extends Component<Props, State> {
|
|
16
|
+
public state: State = {
|
|
17
|
+
hasError: false,
|
|
18
|
+
error: null,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
public static getDerivedStateFromError(error: Error): State {
|
|
22
|
+
return { hasError: true, error };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
|
26
|
+
console.error('ErrorBoundary caught an error:', error, errorInfo);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private handleRetry = () => {
|
|
30
|
+
this.setState({ hasError: false, error: null });
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
public render() {
|
|
34
|
+
if (this.state.hasError) {
|
|
35
|
+
if (this.props.fallback) {
|
|
36
|
+
return this.props.fallback;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className="flex min-h-[400px] flex-col items-center justify-center gap-4 p-8">
|
|
41
|
+
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-red-50 dark:bg-red-950">
|
|
42
|
+
<AlertTriangle className="h-8 w-8 text-red-500" />
|
|
43
|
+
</div>
|
|
44
|
+
<div className="text-center">
|
|
45
|
+
<h2 className="text-xl font-semibold">Something went wrong</h2>
|
|
46
|
+
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
|
47
|
+
{this.state.error?.message || 'An unexpected error occurred'}
|
|
48
|
+
</p>
|
|
49
|
+
</div>
|
|
50
|
+
<Button
|
|
51
|
+
onClick={this.handleRetry}
|
|
52
|
+
icon={<RefreshCw className="h-4 w-4" />}
|
|
53
|
+
className="mt-4"
|
|
54
|
+
>
|
|
55
|
+
Try again
|
|
56
|
+
</Button>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return this.props.children;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Provider } from 'react-redux';
|
|
2
|
+
import { PersistGate } from 'redux-persist/integration/react';
|
|
3
|
+
import { store, persistor } from './store';
|
|
4
|
+
import { Skeleton } from 'antd';
|
|
5
|
+
|
|
6
|
+
interface ReduxProviderProps {
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function LoadingFallback() {
|
|
11
|
+
return (
|
|
12
|
+
<div className="flex min-h-screen items-center justify-center">
|
|
13
|
+
<div className="flex flex-col items-center gap-4">
|
|
14
|
+
<Skeleton.Avatar active size={48} shape="circle" />
|
|
15
|
+
<Skeleton.Input active size="small" style={{ width: 128 }} />
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Redux Provider with persistence
|
|
23
|
+
* Wraps the application with Redux store and persist gate
|
|
24
|
+
*/
|
|
25
|
+
export function ReduxProvider({ children }: ReduxProviderProps) {
|
|
26
|
+
return (
|
|
27
|
+
<Provider store={store}>
|
|
28
|
+
<PersistGate loading={<LoadingFallback />} persistor={persistor}>
|
|
29
|
+
{children}
|
|
30
|
+
</PersistGate>
|
|
31
|
+
</Provider>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Suspense } from 'react';
|
|
2
|
+
import { Route, Switch } from 'wouter';
|
|
3
|
+
import { routes } from './routes';
|
|
4
|
+
import { Skeleton } from 'antd';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Loading fallback component
|
|
8
|
+
*/
|
|
9
|
+
function RouteLoading() {
|
|
10
|
+
return (
|
|
11
|
+
<div className="flex min-h-screen items-center justify-center">
|
|
12
|
+
<div className="flex flex-col items-center gap-4">
|
|
13
|
+
<Skeleton.Avatar active size={48} shape="circle" />
|
|
14
|
+
<Skeleton.Input active size="small" style={{ width: 128 }} />
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Application router using Wouter
|
|
22
|
+
* - Lightweight (2KB) alternative to React Router
|
|
23
|
+
* - Supports lazy loading with React.Suspense
|
|
24
|
+
* - Simple API with <Route> and <Switch>
|
|
25
|
+
*/
|
|
26
|
+
export function AppRouter() {
|
|
27
|
+
return (
|
|
28
|
+
<Suspense fallback={<RouteLoading />}>
|
|
29
|
+
<Switch>
|
|
30
|
+
{routes.map(({ path, component: Component }) => (
|
|
31
|
+
<Route key={path} path={path}>
|
|
32
|
+
<Component />
|
|
33
|
+
</Route>
|
|
34
|
+
))}
|
|
35
|
+
</Switch>
|
|
36
|
+
</Suspense>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export { routes } from './routes';
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { Moon, Sun, Github, Zap, Bot, ExternalLink } from 'lucide-react';
|
|
3
|
+
import { Button, Card } from 'antd';
|
|
4
|
+
import { useTheme } from '~/providers';
|
|
5
|
+
|
|
6
|
+
export default function Home() {
|
|
7
|
+
const { theme, setTheme, resolvedTheme } = useTheme();
|
|
8
|
+
const [count, setCount] = useState(0);
|
|
9
|
+
|
|
10
|
+
const toggleTheme = () => {
|
|
11
|
+
setTheme(resolvedTheme === 'dark' ? 'light' : 'dark');
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div className="flex min-h-screen flex-col items-center justify-center p-8">
|
|
16
|
+
<div className="w-full max-w-2xl space-y-8">
|
|
17
|
+
{/* Header */}
|
|
18
|
+
<div className="text-center">
|
|
19
|
+
<div className="mb-4 flex items-center justify-between">
|
|
20
|
+
<div className="w-40" />
|
|
21
|
+
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-primary text-primary-foreground">
|
|
22
|
+
<Zap className="h-8 w-8" />
|
|
23
|
+
</div>
|
|
24
|
+
<Button
|
|
25
|
+
type="default"
|
|
26
|
+
href="https://github.com/abhay-rana/create-modern-react"
|
|
27
|
+
target="_blank"
|
|
28
|
+
rel="noopener noreferrer"
|
|
29
|
+
icon={<Github className="h-4 w-4" />}
|
|
30
|
+
>
|
|
31
|
+
View CLI on GitHub
|
|
32
|
+
</Button>
|
|
33
|
+
</div>
|
|
34
|
+
<h1 className="text-4xl font-bold tracking-tight">
|
|
35
|
+
create-modern-react
|
|
36
|
+
</h1>
|
|
37
|
+
<p className="mt-2 text-muted-foreground">
|
|
38
|
+
Production-ready React + TypeScript + Tailwind in seconds
|
|
39
|
+
</p>
|
|
40
|
+
<div className="mt-4">
|
|
41
|
+
<code className="rounded-md bg-muted px-3 py-1.5 font-mono text-sm">
|
|
42
|
+
npx create-modern-react my-app
|
|
43
|
+
</code>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
{/* Counter Card */}
|
|
48
|
+
<Card
|
|
49
|
+
title={
|
|
50
|
+
<div className="flex items-center justify-between">
|
|
51
|
+
<span>Interactive Counter</span>
|
|
52
|
+
<Button
|
|
53
|
+
type="text"
|
|
54
|
+
shape="circle"
|
|
55
|
+
onClick={toggleTheme}
|
|
56
|
+
aria-label="Toggle theme"
|
|
57
|
+
icon={
|
|
58
|
+
resolvedTheme === 'dark' ? (
|
|
59
|
+
<Sun className="h-5 w-5" />
|
|
60
|
+
) : (
|
|
61
|
+
<Moon className="h-5 w-5" />
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
/>
|
|
65
|
+
</div>
|
|
66
|
+
}
|
|
67
|
+
>
|
|
68
|
+
<div className="space-y-4">
|
|
69
|
+
<div className="flex items-center justify-center gap-4">
|
|
70
|
+
<Button size="large" onClick={() => setCount((c) => c - 1)}>
|
|
71
|
+
-
|
|
72
|
+
</Button>
|
|
73
|
+
<span className="min-w-[4rem] text-center text-4xl font-bold tabular-nums">
|
|
74
|
+
{count}
|
|
75
|
+
</span>
|
|
76
|
+
<Button size="large" onClick={() => setCount((c) => c + 1)}>
|
|
77
|
+
+
|
|
78
|
+
</Button>
|
|
79
|
+
</div>
|
|
80
|
+
<p className="text-center text-sm text-muted-foreground">
|
|
81
|
+
Click the buttons to update the count
|
|
82
|
+
</p>
|
|
83
|
+
</div>
|
|
84
|
+
</Card>
|
|
85
|
+
|
|
86
|
+
{/* Features */}
|
|
87
|
+
<div className="grid gap-4 sm:grid-cols-2">
|
|
88
|
+
<FeatureCard
|
|
89
|
+
title="Vite + SWC"
|
|
90
|
+
description="Lightning fast builds with Hot Module Replacement"
|
|
91
|
+
/>
|
|
92
|
+
<FeatureCard
|
|
93
|
+
title="TypeScript"
|
|
94
|
+
description="Full type safety with strict mode enabled"
|
|
95
|
+
/>
|
|
96
|
+
<FeatureCard
|
|
97
|
+
title="Tailwind CSS"
|
|
98
|
+
description="Utility-first CSS with dark mode support"
|
|
99
|
+
/>
|
|
100
|
+
<FeatureCard
|
|
101
|
+
title="Ant Design"
|
|
102
|
+
description="Enterprise-class UI components"
|
|
103
|
+
/>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
{/* AI Skills Highlight */}
|
|
107
|
+
<div className="rounded-lg border bg-gradient-to-r from-primary/5 to-primary/10 p-4">
|
|
108
|
+
<div className="flex items-start gap-3">
|
|
109
|
+
<Bot className="h-5 w-5 text-primary mt-0.5" />
|
|
110
|
+
<div>
|
|
111
|
+
<h3 className="font-semibold">Claude Code AI Skills</h3>
|
|
112
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
113
|
+
8 pre-configured skills for React best practices, UI/UX design, browser testing, and spec refinement
|
|
114
|
+
</p>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
{/* Projects Showcase */}
|
|
120
|
+
<div className="space-y-4">
|
|
121
|
+
<h2 className="text-center text-2xl font-bold">
|
|
122
|
+
Production Projects created using this boilerplate
|
|
123
|
+
</h2>
|
|
124
|
+
<div className="grid gap-6 sm:grid-cols-2">
|
|
125
|
+
<ProjectCard
|
|
126
|
+
category="AI/Career"
|
|
127
|
+
title="ResumeFreePro"
|
|
128
|
+
description="AI-Powered Resume Builder"
|
|
129
|
+
designStyle="Modern + Glassmorphism"
|
|
130
|
+
url="https://resumefreepro.com?utm_source=starter-template&utm_medium=landing-page&utm_campaign=create-modern-react-demo"
|
|
131
|
+
previewUrl="https://storage.resumefreepro.com/media-files/resumefreepro.webp"
|
|
132
|
+
/>
|
|
133
|
+
<ProjectCard
|
|
134
|
+
category="E-Pharmacy"
|
|
135
|
+
title="HealthMug"
|
|
136
|
+
description="Online Pharmacy Platform"
|
|
137
|
+
designStyle="Clean + Professional"
|
|
138
|
+
url="https://healthmug.com?utm_source=starter-template&utm_medium=landing-page&utm_campaign=create-modern-react-demo"
|
|
139
|
+
previewUrl="https://storage.resumefreepro.com/media-files/healthmug.webp"
|
|
140
|
+
/>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<p className="text-center text-xs text-muted-foreground">
|
|
145
|
+
Current theme: {theme} (resolved: {resolvedTheme})
|
|
146
|
+
</p>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function FeatureCard({
|
|
153
|
+
title,
|
|
154
|
+
description,
|
|
155
|
+
}: {
|
|
156
|
+
title: string;
|
|
157
|
+
description: string;
|
|
158
|
+
}) {
|
|
159
|
+
return (
|
|
160
|
+
<div className="rounded-lg border bg-card p-4 text-card-foreground">
|
|
161
|
+
<h3 className="font-semibold">{title}</h3>
|
|
162
|
+
<p className="mt-1 text-sm text-muted-foreground">{description}</p>
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function ProjectCard({
|
|
168
|
+
category,
|
|
169
|
+
title,
|
|
170
|
+
description,
|
|
171
|
+
designStyle,
|
|
172
|
+
url,
|
|
173
|
+
previewUrl,
|
|
174
|
+
}: {
|
|
175
|
+
category: string;
|
|
176
|
+
title: string;
|
|
177
|
+
description: string;
|
|
178
|
+
designStyle: string;
|
|
179
|
+
url: string;
|
|
180
|
+
previewUrl: string;
|
|
181
|
+
}) {
|
|
182
|
+
return (
|
|
183
|
+
<a
|
|
184
|
+
href={url}
|
|
185
|
+
target="_blank"
|
|
186
|
+
rel="noopener noreferrer"
|
|
187
|
+
className="group block"
|
|
188
|
+
>
|
|
189
|
+
<Card
|
|
190
|
+
hoverable
|
|
191
|
+
className="overflow-hidden"
|
|
192
|
+
cover={
|
|
193
|
+
<div className="relative h-48 overflow-hidden bg-muted">
|
|
194
|
+
<img
|
|
195
|
+
src={previewUrl}
|
|
196
|
+
alt={`${title} preview`}
|
|
197
|
+
className="h-full w-full object-cover object-top transition-transform duration-300 group-hover:scale-105"
|
|
198
|
+
/>
|
|
199
|
+
{/* Overlay on hover */}
|
|
200
|
+
<div className="absolute inset-0 flex items-center justify-center bg-black/60 opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
|
201
|
+
<div className="text-center text-white">
|
|
202
|
+
<ExternalLink className="mx-auto h-8 w-8" />
|
|
203
|
+
<p className="mt-2 text-sm font-medium">View Demo</p>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
}
|
|
208
|
+
>
|
|
209
|
+
<div className="space-y-3">
|
|
210
|
+
{/* Category Badge */}
|
|
211
|
+
<span className="inline-block rounded-md bg-primary/10 px-2 py-1 text-xs font-medium text-primary">
|
|
212
|
+
{category}
|
|
213
|
+
</span>
|
|
214
|
+
|
|
215
|
+
{/* Title & Description */}
|
|
216
|
+
<div>
|
|
217
|
+
<h3 className="font-bold">{title}</h3>
|
|
218
|
+
<p className="mt-1 text-sm text-muted-foreground">{description}</p>
|
|
219
|
+
</div>
|
|
220
|
+
|
|
221
|
+
{/* Design Style */}
|
|
222
|
+
<p className="text-xs text-muted-foreground">{designStyle}</p>
|
|
223
|
+
</div>
|
|
224
|
+
</Card>
|
|
225
|
+
</a>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Link } from 'wouter';
|
|
2
|
+
import { Home, ArrowLeft } from 'lucide-react';
|
|
3
|
+
import { Button } from 'antd';
|
|
4
|
+
|
|
5
|
+
export default function NotFound() {
|
|
6
|
+
return (
|
|
7
|
+
<div className="flex min-h-screen flex-col items-center justify-center p-8">
|
|
8
|
+
<div className="text-center">
|
|
9
|
+
<h1 className="text-9xl font-bold text-muted-foreground/20">404</h1>
|
|
10
|
+
<h2 className="mt-4 text-2xl font-semibold">Page not found</h2>
|
|
11
|
+
<p className="mt-2 text-muted-foreground">
|
|
12
|
+
Sorry, we couldn't find the page you're looking for.
|
|
13
|
+
</p>
|
|
14
|
+
<div className="mt-8 flex flex-col items-center gap-4 sm:flex-row sm:justify-center">
|
|
15
|
+
<Link href="/">
|
|
16
|
+
<Button type="primary" icon={<Home className="h-4 w-4" />}>
|
|
17
|
+
Go home
|
|
18
|
+
</Button>
|
|
19
|
+
</Link>
|
|
20
|
+
<Button
|
|
21
|
+
icon={<ArrowLeft className="h-4 w-4" />}
|
|
22
|
+
onClick={() => window.history.back()}
|
|
23
|
+
>
|
|
24
|
+
Go back
|
|
25
|
+
</Button>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|