create-flex-stack 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 +57 -0
- package/dist/index.js +330 -0
- package/dist/templates/cleanTemplate.js +697 -0
- package/dist/templates/frontendTemplate.js +855 -0
- package/dist/templates/hexagonalTemplate.js +855 -0
- package/dist/templates/layeredTemplate.js +745 -0
- package/dist/templates/modularTemplate.js +691 -0
- package/dist/templates/sharedTemplate.js +654 -0
- package/dist/templates/vmcTemplate.js +26 -0
- package/dist/types.js +1 -0
- package/dist/utils/generator.js +1120 -0
- package/package.json +46 -0
|
@@ -0,0 +1,855 @@
|
|
|
1
|
+
function toPackageName(name) {
|
|
2
|
+
const normalized = name
|
|
3
|
+
.toLowerCase()
|
|
4
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
5
|
+
.replace(/^[._-]+/, '')
|
|
6
|
+
.replace(/[._-]+$/, '');
|
|
7
|
+
return normalized || 'app';
|
|
8
|
+
}
|
|
9
|
+
export function getFrontendFiles(options) {
|
|
10
|
+
if (!options.auth) {
|
|
11
|
+
return getPublicFrontendFiles(options);
|
|
12
|
+
}
|
|
13
|
+
const files = {};
|
|
14
|
+
const packageName = toPackageName(options.projectName);
|
|
15
|
+
// 1. package.json
|
|
16
|
+
files['package.json'] = `{
|
|
17
|
+
"name": "${packageName}-frontend",
|
|
18
|
+
"version": "1.0.0",
|
|
19
|
+
"type": "module",
|
|
20
|
+
"scripts": {
|
|
21
|
+
"dev": "vite",
|
|
22
|
+
"build": "tsc && vite build",
|
|
23
|
+
"preview": "vite preview"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"react": "^18.3.1",
|
|
27
|
+
"react-dom": "^18.3.1",
|
|
28
|
+
"axios": "^1.6.8",
|
|
29
|
+
"zustand": "^4.5.2",
|
|
30
|
+
"lucide-react": "^0.372.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@vitejs/plugin-react": "^4.2.1",
|
|
34
|
+
"vite": "^5.2.10",
|
|
35
|
+
"typescript": "^5.4.5",
|
|
36
|
+
"@types/react": "^18.3.1",
|
|
37
|
+
"@types/react-dom": "^18.3.1",
|
|
38
|
+
"tailwindcss": "^3.4.3",
|
|
39
|
+
"postcss": "^8.4.38",
|
|
40
|
+
"autoprefixer": "^10.4.19"
|
|
41
|
+
}
|
|
42
|
+
}`;
|
|
43
|
+
// 2. tsconfig.json
|
|
44
|
+
files['tsconfig.json'] = `{
|
|
45
|
+
"compilerOptions": {
|
|
46
|
+
"target": "ES2020",
|
|
47
|
+
"useDefineForClassFields": true,
|
|
48
|
+
"lib": ["DOM", "DOM.Iterable", "ES2020"],
|
|
49
|
+
"module": "ESNext",
|
|
50
|
+
"skipLibCheck": true,
|
|
51
|
+
|
|
52
|
+
/* Bundler mode */
|
|
53
|
+
"moduleResolution": "bundler",
|
|
54
|
+
"allowImportingTsExtensions": true,
|
|
55
|
+
"resolveJsonModule": true,
|
|
56
|
+
"isolatedModules": true,
|
|
57
|
+
"noEmit": true,
|
|
58
|
+
"jsx": "react-jsx",
|
|
59
|
+
|
|
60
|
+
/* Linting */
|
|
61
|
+
"strict": true,
|
|
62
|
+
"noUnusedLocals": true,
|
|
63
|
+
"noUnusedParameters": true,
|
|
64
|
+
"noFallthroughCasesInSwitch": true
|
|
65
|
+
},
|
|
66
|
+
"include": ["src"]
|
|
67
|
+
}`;
|
|
68
|
+
// 3. vite.config.ts
|
|
69
|
+
files['vite.config.ts'] = `import { defineConfig } from 'vite';
|
|
70
|
+
import react from '@vitejs/plugin-react';
|
|
71
|
+
|
|
72
|
+
export default defineConfig({
|
|
73
|
+
plugins: [react()],
|
|
74
|
+
server: {
|
|
75
|
+
port: 5173,
|
|
76
|
+
proxy: {
|
|
77
|
+
'/api': 'http://localhost:3000'
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
`;
|
|
82
|
+
// 4. tailwind.config.js & postcss.config.js
|
|
83
|
+
files['tailwind.config.js'] = `/** @type {import('tailwindcss').Config} */
|
|
84
|
+
export default {
|
|
85
|
+
content: [
|
|
86
|
+
"./index.html",
|
|
87
|
+
"./src/**/*.{js,ts,jsx,tsx}",
|
|
88
|
+
],
|
|
89
|
+
theme: {
|
|
90
|
+
extend: {},
|
|
91
|
+
},
|
|
92
|
+
plugins: [],
|
|
93
|
+
}
|
|
94
|
+
`;
|
|
95
|
+
files['postcss.config.js'] = `export default {
|
|
96
|
+
plugins: {
|
|
97
|
+
tailwindcss: {},
|
|
98
|
+
autoprefixer: {},
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
`;
|
|
102
|
+
// 5. HTML entry point
|
|
103
|
+
files['index.html'] = `<!doctype html>
|
|
104
|
+
<html lang="en">
|
|
105
|
+
<head>
|
|
106
|
+
<meta charset="UTF-8" />
|
|
107
|
+
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🚀</text></svg>" />
|
|
108
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
109
|
+
<title>${options.projectName}</title>
|
|
110
|
+
</head>
|
|
111
|
+
<body className="bg-slate-950 text-slate-100 min-h-screen">
|
|
112
|
+
<div id="root"></div>
|
|
113
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
114
|
+
</body>
|
|
115
|
+
</html>
|
|
116
|
+
`;
|
|
117
|
+
// 6. Source Entry Points
|
|
118
|
+
files['src/main.tsx'] = `import React from 'react'
|
|
119
|
+
import ReactDOM from 'react-dom/client'
|
|
120
|
+
import App from './App.tsx'
|
|
121
|
+
import './index.css'
|
|
122
|
+
|
|
123
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
124
|
+
<React.StrictMode>
|
|
125
|
+
<App />
|
|
126
|
+
</React.StrictMode>,
|
|
127
|
+
)
|
|
128
|
+
`;
|
|
129
|
+
files['src/index.css'] = `@tailwind base;
|
|
130
|
+
@tailwind components;
|
|
131
|
+
@tailwind utilities;
|
|
132
|
+
|
|
133
|
+
@layer base {
|
|
134
|
+
body {
|
|
135
|
+
@apply antialiased selection:bg-cyan-500/30 selection:text-cyan-200;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
`;
|
|
139
|
+
// 7. API client (Axios with Auto JWT Refresh Interceptor)
|
|
140
|
+
files['src/api/client.ts'] = `import axios from 'axios';
|
|
141
|
+
import { useAuthStore } from '../store/authStore.ts';
|
|
142
|
+
|
|
143
|
+
export const apiClient = axios.create({
|
|
144
|
+
baseURL: '/api',
|
|
145
|
+
headers: {
|
|
146
|
+
'Content-Type': 'application/json',
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Request Interceptor: Attach access token
|
|
151
|
+
apiClient.interceptors.request.use(
|
|
152
|
+
(config) => {
|
|
153
|
+
const token = useAuthStore.getState().accessToken;
|
|
154
|
+
if (token) {
|
|
155
|
+
config.headers.Authorization = \`Bearer \${token}\`;
|
|
156
|
+
}
|
|
157
|
+
return config;
|
|
158
|
+
},
|
|
159
|
+
(error) => Promise.reject(error)
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
// Response Interceptor: Auto-refresh JWT tokens on 401
|
|
163
|
+
let isRefreshing = false;
|
|
164
|
+
let failedQueue: any[] = [];
|
|
165
|
+
|
|
166
|
+
const processQueue = (error: any, token: string | null = null) => {
|
|
167
|
+
failedQueue.forEach((prom) => {
|
|
168
|
+
if (error) {
|
|
169
|
+
prom.reject(error);
|
|
170
|
+
} else {
|
|
171
|
+
prom.resolve(token);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
failedQueue = [];
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
apiClient.interceptors.response.use(
|
|
178
|
+
(response) => response,
|
|
179
|
+
async (error) => {
|
|
180
|
+
const originalRequest = error.config;
|
|
181
|
+
|
|
182
|
+
if (error.response?.status === 401 && !originalRequest._retry) {
|
|
183
|
+
if (isRefreshing) {
|
|
184
|
+
return new Promise((resolve, reject) => {
|
|
185
|
+
failedQueue.push({ resolve, reject });
|
|
186
|
+
})
|
|
187
|
+
.then((token) => {
|
|
188
|
+
originalRequest.headers.Authorization = \`Bearer \${token}\`;
|
|
189
|
+
return apiClient(originalRequest);
|
|
190
|
+
})
|
|
191
|
+
.catch((err) => Promise.reject(err));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
originalRequest._retry = true;
|
|
195
|
+
isRefreshing = true;
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
const refreshToken = useAuthStore.getState().refreshToken;
|
|
199
|
+
if (!refreshToken) {
|
|
200
|
+
throw new Error('No refresh token available');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const res = await axios.post('/api/users/refresh', { refreshToken });
|
|
204
|
+
const { accessToken, refreshToken: newRefreshToken } = res.data.data;
|
|
205
|
+
|
|
206
|
+
useAuthStore.getState().setTokens(accessToken, newRefreshToken);
|
|
207
|
+
|
|
208
|
+
processQueue(null, accessToken);
|
|
209
|
+
isRefreshing = false;
|
|
210
|
+
|
|
211
|
+
originalRequest.headers.Authorization = \`Bearer \${accessToken}\`;
|
|
212
|
+
return apiClient(originalRequest);
|
|
213
|
+
} catch (refreshError) {
|
|
214
|
+
processQueue(refreshError, null);
|
|
215
|
+
isRefreshing = false;
|
|
216
|
+
useAuthStore.getState().logout();
|
|
217
|
+
return Promise.reject(refreshError);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return Promise.reject(error);
|
|
222
|
+
}
|
|
223
|
+
);
|
|
224
|
+
`;
|
|
225
|
+
// 8. Zustand Auth Store
|
|
226
|
+
files['src/store/authStore.ts'] = `import { create } from 'zustand';
|
|
227
|
+
|
|
228
|
+
interface User {
|
|
229
|
+
id: string;
|
|
230
|
+
email: string;
|
|
231
|
+
name: string | null;
|
|
232
|
+
role: string;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
interface AuthState {
|
|
236
|
+
user: User | null;
|
|
237
|
+
accessToken: string | null;
|
|
238
|
+
refreshToken: string | null;
|
|
239
|
+
isAuthenticated: boolean;
|
|
240
|
+
login: (user: User, accessToken: string, refreshToken: string) => void;
|
|
241
|
+
setTokens: (accessToken: string, refreshToken: string) => void;
|
|
242
|
+
logout: () => void;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export const useAuthStore = create<AuthState>((set) => {
|
|
246
|
+
// Load initial state from LocalStorage
|
|
247
|
+
const cachedUser = localStorage.getItem('user');
|
|
248
|
+
const cachedAccess = localStorage.getItem('accessToken');
|
|
249
|
+
const cachedRefresh = localStorage.getItem('refreshToken');
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
user: cachedUser ? JSON.parse(cachedUser) : null,
|
|
253
|
+
accessToken: cachedAccess || null,
|
|
254
|
+
refreshToken: cachedRefresh || null,
|
|
255
|
+
isAuthenticated: !!cachedAccess,
|
|
256
|
+
|
|
257
|
+
login: (user, accessToken, refreshToken) => {
|
|
258
|
+
localStorage.setItem('user', JSON.stringify(user));
|
|
259
|
+
localStorage.setItem('accessToken', accessToken);
|
|
260
|
+
localStorage.setItem('refreshToken', refreshToken);
|
|
261
|
+
set({ user, accessToken, refreshToken, isAuthenticated: true });
|
|
262
|
+
},
|
|
263
|
+
|
|
264
|
+
setTokens: (accessToken, refreshToken) => {
|
|
265
|
+
localStorage.setItem('accessToken', accessToken);
|
|
266
|
+
localStorage.setItem('refreshToken', refreshToken);
|
|
267
|
+
set({ accessToken, refreshToken });
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
logout: () => {
|
|
271
|
+
localStorage.removeItem('user');
|
|
272
|
+
localStorage.removeItem('accessToken');
|
|
273
|
+
localStorage.removeItem('refreshToken');
|
|
274
|
+
set({ user: null, accessToken: null, refreshToken: null, isAuthenticated: false });
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
});
|
|
278
|
+
`;
|
|
279
|
+
// 9. React Views & Components
|
|
280
|
+
// App Router & View Controller
|
|
281
|
+
files['src/App.tsx'] = `import { useState } from 'react';
|
|
282
|
+
import { useAuthStore } from './store/authStore.ts';
|
|
283
|
+
import { LoginView } from './views/Login.tsx';
|
|
284
|
+
import { RegisterView } from './views/Register.tsx';
|
|
285
|
+
import { DashboardView } from './views/Dashboard.tsx';
|
|
286
|
+
import { Navbar } from './components/Navbar.tsx';
|
|
287
|
+
|
|
288
|
+
function App() {
|
|
289
|
+
const { isAuthenticated } = useAuthStore();
|
|
290
|
+
const [view, setView] = useState<'login' | 'register'>('login');
|
|
291
|
+
|
|
292
|
+
return (
|
|
293
|
+
<div className="flex flex-col min-h-screen bg-slate-950 text-slate-100 font-sans">
|
|
294
|
+
<Navbar setView={setView} />
|
|
295
|
+
|
|
296
|
+
<main className="flex-grow flex items-center justify-center p-6">
|
|
297
|
+
{isAuthenticated ? (
|
|
298
|
+
<DashboardView />
|
|
299
|
+
) : view === 'login' ? (
|
|
300
|
+
<LoginView setView={setView} />
|
|
301
|
+
) : (
|
|
302
|
+
<RegisterView setView={setView} />
|
|
303
|
+
)}
|
|
304
|
+
</main>
|
|
305
|
+
|
|
306
|
+
<footer className="border-t border-slate-900 py-6 text-center text-slate-500 text-xs">
|
|
307
|
+
<p>© {new Date().getFullYear()} ${options.projectName}. Generated with create-flex-stack.</p>
|
|
308
|
+
</footer>
|
|
309
|
+
</div>
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export default App;
|
|
314
|
+
`;
|
|
315
|
+
// Components - Navbar
|
|
316
|
+
files['src/components/Navbar.tsx'] = `import React from 'react';
|
|
317
|
+
import { useAuthStore } from '../store/authStore.ts';
|
|
318
|
+
import { LogOut, LogIn, UserPlus } from 'lucide-react';
|
|
319
|
+
|
|
320
|
+
interface NavbarProps {
|
|
321
|
+
setView: (view: 'login' | 'register') => void;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export const Navbar: React.FC<NavbarProps> = ({ setView }) => {
|
|
325
|
+
const { isAuthenticated, user, logout } = useAuthStore();
|
|
326
|
+
|
|
327
|
+
return (
|
|
328
|
+
<header className="border-b border-slate-900 bg-slate-950/80 backdrop-blur-md sticky top-0 z-50">
|
|
329
|
+
<div className="max-w-7xl mx-auto px-4 h-16 flex items-center justify-between">
|
|
330
|
+
<div className="flex items-center gap-2">
|
|
331
|
+
<span className="text-xl font-bold bg-gradient-to-r from-cyan-400 to-indigo-500 bg-clip-text text-transparent">
|
|
332
|
+
🚀 UltimateStack
|
|
333
|
+
</span>
|
|
334
|
+
</div>
|
|
335
|
+
|
|
336
|
+
<nav className="flex items-center gap-4">
|
|
337
|
+
{isAuthenticated ? (
|
|
338
|
+
<div className="flex items-center gap-4">
|
|
339
|
+
<span className="text-sm text-slate-400">
|
|
340
|
+
Welcome, <strong className="text-slate-200">{user?.name || user?.email}</strong>
|
|
341
|
+
<span className="ml-2 px-2 py-0.5 text-xs bg-slate-800 text-cyan-400 rounded-full font-mono uppercase">
|
|
342
|
+
{user?.role}
|
|
343
|
+
</span>
|
|
344
|
+
</span>
|
|
345
|
+
<button
|
|
346
|
+
onClick={logout}
|
|
347
|
+
className="flex items-center gap-1.5 px-3.5 py-1.5 bg-rose-600 hover:bg-rose-500 text-sm font-medium rounded-lg transition-colors"
|
|
348
|
+
>
|
|
349
|
+
<LogOut className="w-4 h-4" /> Logout
|
|
350
|
+
</button>
|
|
351
|
+
</div>
|
|
352
|
+
) : (
|
|
353
|
+
<div className="flex gap-2">
|
|
354
|
+
<button
|
|
355
|
+
onClick={() => setView('login')}
|
|
356
|
+
className="flex items-center gap-1.5 px-3 py-1.5 border border-slate-800 hover:bg-slate-900 text-sm font-medium rounded-lg transition-colors"
|
|
357
|
+
>
|
|
358
|
+
<LogIn className="w-4 h-4" /> Sign In
|
|
359
|
+
</button>
|
|
360
|
+
<button
|
|
361
|
+
onClick={() => setView('register')}
|
|
362
|
+
className="flex items-center gap-1.5 px-3 py-1.5 bg-cyan-600 hover:bg-cyan-500 text-sm font-medium rounded-lg transition-colors"
|
|
363
|
+
>
|
|
364
|
+
<UserPlus className="w-4 h-4" /> Sign Up
|
|
365
|
+
</button>
|
|
366
|
+
</div>
|
|
367
|
+
)}
|
|
368
|
+
</nav>
|
|
369
|
+
</div>
|
|
370
|
+
</header>
|
|
371
|
+
);
|
|
372
|
+
};
|
|
373
|
+
`;
|
|
374
|
+
// Views - Login
|
|
375
|
+
files['src/views/Login.tsx'] = `import React, { useState } from 'react';
|
|
376
|
+
import { useAuthStore } from '../store/authStore.ts';
|
|
377
|
+
import { apiClient } from '../api/client.ts';
|
|
378
|
+
import { LogIn, Key, Mail, AlertTriangle } from 'lucide-react';
|
|
379
|
+
|
|
380
|
+
interface LoginViewProps {
|
|
381
|
+
setView: (view: 'login' | 'register') => void;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export const LoginView: React.FC<LoginViewProps> = ({ setView }) => {
|
|
385
|
+
const { login } = useAuthStore();
|
|
386
|
+
const [email, setEmail] = useState('');
|
|
387
|
+
const [password, setPassword] = useState('');
|
|
388
|
+
const [error, setError] = useState<string | null>(null);
|
|
389
|
+
const [loading, setLoading] = useState(false);
|
|
390
|
+
|
|
391
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
392
|
+
e.preventDefault();
|
|
393
|
+
setError(null);
|
|
394
|
+
setLoading(true);
|
|
395
|
+
|
|
396
|
+
try {
|
|
397
|
+
const res = await apiClient.post('/users/login', { email, password });
|
|
398
|
+
const { user, accessToken, refreshToken } = res.data.data;
|
|
399
|
+
login(user, accessToken, refreshToken);
|
|
400
|
+
} catch (err: any) {
|
|
401
|
+
setError(err.response?.data?.message || 'Failed to authenticate user.');
|
|
402
|
+
} finally {
|
|
403
|
+
setLoading(false);
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
return (
|
|
408
|
+
<div className="w-full max-w-md bg-slate-900/40 border border-slate-900 p-8 rounded-2xl backdrop-blur-lg">
|
|
409
|
+
<div className="text-center mb-6">
|
|
410
|
+
<h2 className="text-2xl font-bold tracking-tight">Welcome Back</h2>
|
|
411
|
+
<p className="text-slate-400 text-sm mt-1">Please sign in to access the secure dashboard.</p>
|
|
412
|
+
</div>
|
|
413
|
+
|
|
414
|
+
{error && (
|
|
415
|
+
<div className="flex items-center gap-2 mb-4 p-3.5 bg-rose-500/10 border border-rose-500/20 text-rose-300 text-sm rounded-lg">
|
|
416
|
+
<AlertTriangle className="w-4 h-4 shrink-0" />
|
|
417
|
+
<p>{error}</p>
|
|
418
|
+
</div>
|
|
419
|
+
)}
|
|
420
|
+
|
|
421
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
422
|
+
<div>
|
|
423
|
+
<label className="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1.5">Email Address</label>
|
|
424
|
+
<div className="relative">
|
|
425
|
+
<Mail className="absolute left-3 top-3 w-4.5 h-4.5 text-slate-500" />
|
|
426
|
+
<input
|
|
427
|
+
type="email"
|
|
428
|
+
value={email}
|
|
429
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
430
|
+
required
|
|
431
|
+
className="w-full pl-10 pr-4 py-2.5 bg-slate-950/60 border border-slate-800 focus:border-cyan-500 rounded-lg text-slate-200 outline-none text-sm transition-colors"
|
|
432
|
+
placeholder="name@example.com"
|
|
433
|
+
/>
|
|
434
|
+
</div>
|
|
435
|
+
</div>
|
|
436
|
+
|
|
437
|
+
<div>
|
|
438
|
+
<label className="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1.5">Password</label>
|
|
439
|
+
<div className="relative">
|
|
440
|
+
<Key className="absolute left-3 top-3 w-4.5 h-4.5 text-slate-500" />
|
|
441
|
+
<input
|
|
442
|
+
type="password"
|
|
443
|
+
value={password}
|
|
444
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
445
|
+
required
|
|
446
|
+
className="w-full pl-10 pr-4 py-2.5 bg-slate-950/60 border border-slate-800 focus:border-cyan-500 rounded-lg text-slate-200 outline-none text-sm transition-colors"
|
|
447
|
+
placeholder="••••••••"
|
|
448
|
+
/>
|
|
449
|
+
</div>
|
|
450
|
+
</div>
|
|
451
|
+
|
|
452
|
+
<button
|
|
453
|
+
type="submit"
|
|
454
|
+
disabled={loading}
|
|
455
|
+
className="w-full flex items-center justify-center gap-2 py-2.5 bg-cyan-600 hover:bg-cyan-500 disabled:bg-cyan-700 text-sm font-semibold rounded-lg transition-colors mt-2"
|
|
456
|
+
>
|
|
457
|
+
{loading ? 'Authenticating...' : (
|
|
458
|
+
<>
|
|
459
|
+
<LogIn className="w-4 h-4" /> Sign In
|
|
460
|
+
</>
|
|
461
|
+
)}
|
|
462
|
+
</button>
|
|
463
|
+
</form>
|
|
464
|
+
|
|
465
|
+
<div className="text-center text-sm text-slate-400 mt-6">
|
|
466
|
+
Don't have an account?{' '}
|
|
467
|
+
<button onClick={() => setView('register')} className="text-cyan-400 hover:underline">
|
|
468
|
+
Sign up
|
|
469
|
+
</button>
|
|
470
|
+
</div>
|
|
471
|
+
</div>
|
|
472
|
+
);
|
|
473
|
+
};
|
|
474
|
+
`;
|
|
475
|
+
// Views - Register
|
|
476
|
+
files['src/views/Register.tsx'] = `import React, { useState } from 'react';
|
|
477
|
+
import { useAuthStore } from '../store/authStore.ts';
|
|
478
|
+
import { apiClient } from '../api/client.ts';
|
|
479
|
+
import { UserPlus, User as UserIcon, Mail, Key, AlertTriangle } from 'lucide-react';
|
|
480
|
+
|
|
481
|
+
interface RegisterViewProps {
|
|
482
|
+
setView: (view: 'login' | 'register') => void;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
export const RegisterView: React.FC<RegisterViewProps> = ({ setView }) => {
|
|
486
|
+
const { login } = useAuthStore();
|
|
487
|
+
const [name, setName] = useState('');
|
|
488
|
+
const [email, setEmail] = useState('');
|
|
489
|
+
const [password, setPassword] = useState('');
|
|
490
|
+
const [error, setError] = useState<string | null>(null);
|
|
491
|
+
const [loading, setLoading] = useState(false);
|
|
492
|
+
|
|
493
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
494
|
+
e.preventDefault();
|
|
495
|
+
setError(null);
|
|
496
|
+
setLoading(true);
|
|
497
|
+
|
|
498
|
+
try {
|
|
499
|
+
const res = await apiClient.post('/users/register', { name, email, password });
|
|
500
|
+
const { user, accessToken, refreshToken } = res.data.data;
|
|
501
|
+
login(user, accessToken, refreshToken);
|
|
502
|
+
} catch (err: any) {
|
|
503
|
+
setError(err.response?.data?.message || 'Failed to register account.');
|
|
504
|
+
} finally {
|
|
505
|
+
setLoading(false);
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
return (
|
|
510
|
+
<div className="w-full max-w-md bg-slate-900/40 border border-slate-900 p-8 rounded-2xl backdrop-blur-lg">
|
|
511
|
+
<div className="text-center mb-6">
|
|
512
|
+
<h2 className="text-2xl font-bold tracking-tight">Create Account</h2>
|
|
513
|
+
<p className="text-slate-400 text-sm mt-1">Get started with your fresh boilerplate workspace.</p>
|
|
514
|
+
</div>
|
|
515
|
+
|
|
516
|
+
{error && (
|
|
517
|
+
<div className="flex items-center gap-2 mb-4 p-3.5 bg-rose-500/10 border border-rose-500/20 text-rose-300 text-sm rounded-lg">
|
|
518
|
+
<AlertTriangle className="w-4 h-4 shrink-0" />
|
|
519
|
+
<p>{error}</p>
|
|
520
|
+
</div>
|
|
521
|
+
)}
|
|
522
|
+
|
|
523
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
524
|
+
<div>
|
|
525
|
+
<label className="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1.5">Full Name</label>
|
|
526
|
+
<div className="relative">
|
|
527
|
+
<UserIcon className="absolute left-3 top-3 w-4.5 h-4.5 text-slate-500" />
|
|
528
|
+
<input
|
|
529
|
+
type="text"
|
|
530
|
+
value={name}
|
|
531
|
+
onChange={(e) => setName(e.target.value)}
|
|
532
|
+
required
|
|
533
|
+
className="w-full pl-10 pr-4 py-2.5 bg-slate-950/60 border border-slate-800 focus:border-cyan-500 rounded-lg text-slate-200 outline-none text-sm transition-colors"
|
|
534
|
+
placeholder="John Doe"
|
|
535
|
+
/>
|
|
536
|
+
</div>
|
|
537
|
+
</div>
|
|
538
|
+
|
|
539
|
+
<div>
|
|
540
|
+
<label className="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1.5">Email Address</label>
|
|
541
|
+
<div className="relative">
|
|
542
|
+
<Mail className="absolute left-3 top-3 w-4.5 h-4.5 text-slate-500" />
|
|
543
|
+
<input
|
|
544
|
+
type="email"
|
|
545
|
+
value={email}
|
|
546
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
547
|
+
required
|
|
548
|
+
className="w-full pl-10 pr-4 py-2.5 bg-slate-950/60 border border-slate-800 focus:border-cyan-500 rounded-lg text-slate-200 outline-none text-sm transition-colors"
|
|
549
|
+
placeholder="name@example.com"
|
|
550
|
+
/>
|
|
551
|
+
</div>
|
|
552
|
+
</div>
|
|
553
|
+
|
|
554
|
+
<div>
|
|
555
|
+
<label className="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1.5">Password</label>
|
|
556
|
+
<div className="relative">
|
|
557
|
+
<Key className="absolute left-3 top-3 w-4.5 h-4.5 text-slate-500" />
|
|
558
|
+
<input
|
|
559
|
+
type="password"
|
|
560
|
+
value={password}
|
|
561
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
562
|
+
required
|
|
563
|
+
className="w-full pl-10 pr-4 py-2.5 bg-slate-950/60 border border-slate-800 focus:border-cyan-500 rounded-lg text-slate-200 outline-none text-sm transition-colors"
|
|
564
|
+
placeholder="••••••••"
|
|
565
|
+
/>
|
|
566
|
+
</div>
|
|
567
|
+
</div>
|
|
568
|
+
|
|
569
|
+
<button
|
|
570
|
+
type="submit"
|
|
571
|
+
disabled={loading}
|
|
572
|
+
className="w-full flex items-center justify-center gap-2 py-2.5 bg-cyan-600 hover:bg-cyan-500 disabled:bg-cyan-700 text-sm font-semibold rounded-lg transition-colors mt-2"
|
|
573
|
+
>
|
|
574
|
+
{loading ? 'Creating Account...' : (
|
|
575
|
+
<>
|
|
576
|
+
<UserPlus className="w-4 h-4" /> Sign Up
|
|
577
|
+
</>
|
|
578
|
+
)}
|
|
579
|
+
</button>
|
|
580
|
+
</form>
|
|
581
|
+
|
|
582
|
+
<div className="text-center text-sm text-slate-400 mt-6">
|
|
583
|
+
Already have an account?{' '}
|
|
584
|
+
<button onClick={() => setView('login')} className="text-cyan-400 hover:underline">
|
|
585
|
+
Sign in
|
|
586
|
+
</button>
|
|
587
|
+
</div>
|
|
588
|
+
</div>
|
|
589
|
+
);
|
|
590
|
+
};
|
|
591
|
+
`;
|
|
592
|
+
// Views - Dashboard
|
|
593
|
+
files['src/views/Dashboard.tsx'] = `import React, { useState, useEffect } from 'react';
|
|
594
|
+
import { useAuthStore } from '../store/authStore.ts';
|
|
595
|
+
import { apiClient } from '../api/client.ts';
|
|
596
|
+
import { Users, Shield, Server, CheckCircle, Database } from 'lucide-react';
|
|
597
|
+
|
|
598
|
+
export const DashboardView: React.FC = () => {
|
|
599
|
+
const { user } = useAuthStore();
|
|
600
|
+
const [usersList, setUsersList] = useState<any[]>([]);
|
|
601
|
+
const [loading, setLoading] = useState(true);
|
|
602
|
+
const [error, setError] = useState<string | null>(null);
|
|
603
|
+
|
|
604
|
+
useEffect(() => {
|
|
605
|
+
const fetchUsers = async () => {
|
|
606
|
+
try {
|
|
607
|
+
const res = await apiClient.get('/users');
|
|
608
|
+
setUsersList(res.data.data.users || []);
|
|
609
|
+
} catch (err: any) {
|
|
610
|
+
setError(err.response?.data?.message || 'Access restricted. Administrator privileges required.');
|
|
611
|
+
} finally {
|
|
612
|
+
setLoading(false);
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
fetchUsers();
|
|
616
|
+
}, []);
|
|
617
|
+
|
|
618
|
+
return (
|
|
619
|
+
<div className="w-full max-w-5xl space-y-6">
|
|
620
|
+
{/* Welcome Card */}
|
|
621
|
+
<div className="p-8 bg-slate-900/40 border border-slate-900 rounded-2xl backdrop-blur-lg flex flex-col md:flex-row md:items-center justify-between gap-6">
|
|
622
|
+
<div>
|
|
623
|
+
<h1 className="text-3xl font-bold tracking-tight">Active Boilerplate Workspace</h1>
|
|
624
|
+
<p className="text-slate-400 text-sm mt-1">
|
|
625
|
+
You are authenticated as <strong className="text-slate-200">{user?.email}</strong>.
|
|
626
|
+
All client requests are dynamically authorized.
|
|
627
|
+
</p>
|
|
628
|
+
</div>
|
|
629
|
+
<div className="flex items-center gap-2 text-emerald-400 bg-emerald-500/10 px-4 py-2 border border-emerald-500/20 text-sm rounded-lg font-medium self-start md:self-auto">
|
|
630
|
+
<CheckCircle className="w-4 h-4" /> Connected to Express
|
|
631
|
+
</div>
|
|
632
|
+
</div>
|
|
633
|
+
|
|
634
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
635
|
+
{/* Stat Cards */}
|
|
636
|
+
<div className="p-6 bg-slate-900/40 border border-slate-900 rounded-xl flex items-center gap-4">
|
|
637
|
+
<div className="p-3 bg-cyan-500/10 text-cyan-400 rounded-lg"><Server className="w-6 h-6" /></div>
|
|
638
|
+
<div>
|
|
639
|
+
<span className="text-slate-400 text-xs font-semibold uppercase tracking-wider block">Backend Framework</span>
|
|
640
|
+
<strong className="text-lg text-slate-200">ExpressJS + TypeScript</strong>
|
|
641
|
+
</div>
|
|
642
|
+
</div>
|
|
643
|
+
|
|
644
|
+
<div className="p-6 bg-slate-900/40 border border-slate-900 rounded-xl flex items-center gap-4">
|
|
645
|
+
<div className="p-3 bg-indigo-500/10 text-indigo-400 rounded-lg"><Database className="w-6 h-6" /></div>
|
|
646
|
+
<div>
|
|
647
|
+
<span className="text-slate-400 text-xs font-semibold uppercase tracking-wider block">Architecture Style</span>
|
|
648
|
+
<strong className="text-lg text-slate-200">${options.architecture.toUpperCase()}</strong>
|
|
649
|
+
</div>
|
|
650
|
+
</div>
|
|
651
|
+
|
|
652
|
+
<div className="p-6 bg-slate-900/40 border border-slate-900 rounded-xl flex items-center gap-4">
|
|
653
|
+
<div className="p-3 bg-cyan-500/10 text-cyan-400 rounded-lg"><Shield className="w-6 h-6" /></div>
|
|
654
|
+
<div>
|
|
655
|
+
<span className="text-slate-400 text-xs font-semibold uppercase tracking-wider block">Auth Middleware</span>
|
|
656
|
+
<strong className="text-lg text-slate-200">JWT Token Rotation</strong>
|
|
657
|
+
</div>
|
|
658
|
+
</div>
|
|
659
|
+
</div>
|
|
660
|
+
|
|
661
|
+
{/* Users Table */}
|
|
662
|
+
<div className="bg-slate-900/40 border border-slate-900 rounded-2xl p-6">
|
|
663
|
+
<div className="flex items-center gap-2 mb-4">
|
|
664
|
+
<Users className="w-5 h-5 text-cyan-400" />
|
|
665
|
+
<h3 className="text-lg font-semibold text-slate-200">Registered Accounts</h3>
|
|
666
|
+
</div>
|
|
667
|
+
|
|
668
|
+
{loading ? (
|
|
669
|
+
<div className="text-center py-8 text-slate-400 text-sm">Loading users from backend...</div>
|
|
670
|
+
) : error ? (
|
|
671
|
+
<div className="text-center py-8 text-rose-400 text-sm">{error} (RBAC protected: Only admin role can view this list)</div>
|
|
672
|
+
) : (
|
|
673
|
+
<div className="overflow-x-auto">
|
|
674
|
+
<table className="w-full text-left text-sm border-collapse">
|
|
675
|
+
<thead>
|
|
676
|
+
<tr className="border-b border-slate-800 text-slate-400 font-semibold uppercase tracking-wider text-xs">
|
|
677
|
+
<th className="pb-3 px-4">Name</th>
|
|
678
|
+
<th className="pb-3 px-4">Email</th>
|
|
679
|
+
<th className="pb-3 px-4">Role</th>
|
|
680
|
+
<th className="pb-3 px-4">Created At</th>
|
|
681
|
+
</tr>
|
|
682
|
+
</thead>
|
|
683
|
+
<tbody>
|
|
684
|
+
{usersList.map((usr: any) => (
|
|
685
|
+
<tr key={usr.id} className="border-b border-slate-900 hover:bg-slate-900/20 text-slate-300">
|
|
686
|
+
<td className="py-3 px-4">{usr.name || 'Anonymous'}</td>
|
|
687
|
+
<td className="py-3 px-4 font-mono">{usr.email}</td>
|
|
688
|
+
<td className="py-3 px-4">
|
|
689
|
+
<span className="px-2 py-0.5 text-xs bg-slate-800 text-cyan-400 rounded-full font-mono uppercase">
|
|
690
|
+
{usr.role}
|
|
691
|
+
</span>
|
|
692
|
+
</td>
|
|
693
|
+
<td className="py-3 px-4 text-slate-500 text-xs">{new Date(usr.createdAt).toLocaleDateString()}</td>
|
|
694
|
+
</tr>
|
|
695
|
+
))}
|
|
696
|
+
</tbody>
|
|
697
|
+
</table>
|
|
698
|
+
</div>
|
|
699
|
+
)}
|
|
700
|
+
</div>
|
|
701
|
+
</div>
|
|
702
|
+
);
|
|
703
|
+
};
|
|
704
|
+
`;
|
|
705
|
+
return files;
|
|
706
|
+
}
|
|
707
|
+
function getPublicFrontendFiles(options) {
|
|
708
|
+
const packageName = toPackageName(options.projectName);
|
|
709
|
+
return {
|
|
710
|
+
'package.json': `{
|
|
711
|
+
"name": "${packageName}-frontend",
|
|
712
|
+
"version": "1.0.0",
|
|
713
|
+
"type": "module",
|
|
714
|
+
"scripts": {
|
|
715
|
+
"dev": "vite",
|
|
716
|
+
"build": "tsc && vite build",
|
|
717
|
+
"preview": "vite preview"
|
|
718
|
+
},
|
|
719
|
+
"dependencies": {
|
|
720
|
+
"react": "^18.3.1",
|
|
721
|
+
"react-dom": "^18.3.1",
|
|
722
|
+
"lucide-react": "^0.372.0"
|
|
723
|
+
},
|
|
724
|
+
"devDependencies": {
|
|
725
|
+
"@vitejs/plugin-react": "^4.2.1",
|
|
726
|
+
"vite": "^5.2.10",
|
|
727
|
+
"typescript": "^5.4.5",
|
|
728
|
+
"@types/react": "^18.3.1",
|
|
729
|
+
"@types/react-dom": "^18.3.1",
|
|
730
|
+
"tailwindcss": "^3.4.3",
|
|
731
|
+
"postcss": "^8.4.38",
|
|
732
|
+
"autoprefixer": "^10.4.19"
|
|
733
|
+
}
|
|
734
|
+
}`,
|
|
735
|
+
'tsconfig.json': `{
|
|
736
|
+
"compilerOptions": {
|
|
737
|
+
"target": "ES2020",
|
|
738
|
+
"useDefineForClassFields": true,
|
|
739
|
+
"lib": ["DOM", "DOM.Iterable", "ES2020"],
|
|
740
|
+
"module": "ESNext",
|
|
741
|
+
"skipLibCheck": true,
|
|
742
|
+
"moduleResolution": "bundler",
|
|
743
|
+
"allowImportingTsExtensions": true,
|
|
744
|
+
"resolveJsonModule": true,
|
|
745
|
+
"isolatedModules": true,
|
|
746
|
+
"noEmit": true,
|
|
747
|
+
"jsx": "react-jsx",
|
|
748
|
+
"strict": true,
|
|
749
|
+
"noUnusedLocals": true,
|
|
750
|
+
"noUnusedParameters": true,
|
|
751
|
+
"noFallthroughCasesInSwitch": true
|
|
752
|
+
},
|
|
753
|
+
"include": ["src"]
|
|
754
|
+
}`,
|
|
755
|
+
'vite.config.ts': `import { defineConfig } from 'vite';
|
|
756
|
+
import react from '@vitejs/plugin-react';
|
|
757
|
+
|
|
758
|
+
export default defineConfig({
|
|
759
|
+
plugins: [react()],
|
|
760
|
+
server: {
|
|
761
|
+
port: 5173,
|
|
762
|
+
proxy: {
|
|
763
|
+
'/api': 'http://localhost:3000'
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
});
|
|
767
|
+
`,
|
|
768
|
+
'tailwind.config.js': `/** @type {import('tailwindcss').Config} */
|
|
769
|
+
export default {
|
|
770
|
+
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
|
771
|
+
theme: { extend: {} },
|
|
772
|
+
plugins: [],
|
|
773
|
+
}
|
|
774
|
+
`,
|
|
775
|
+
'postcss.config.js': `export default {
|
|
776
|
+
plugins: {
|
|
777
|
+
tailwindcss: {},
|
|
778
|
+
autoprefixer: {},
|
|
779
|
+
},
|
|
780
|
+
}
|
|
781
|
+
`,
|
|
782
|
+
'index.html': `<!doctype html>
|
|
783
|
+
<html lang="en">
|
|
784
|
+
<head>
|
|
785
|
+
<meta charset="UTF-8" />
|
|
786
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
787
|
+
<title>${options.projectName}</title>
|
|
788
|
+
</head>
|
|
789
|
+
<body class="bg-zinc-950 text-zinc-100 min-h-screen">
|
|
790
|
+
<div id="root"></div>
|
|
791
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
792
|
+
</body>
|
|
793
|
+
</html>
|
|
794
|
+
`,
|
|
795
|
+
'src/main.tsx': `import React from 'react';
|
|
796
|
+
import ReactDOM from 'react-dom/client';
|
|
797
|
+
import App from './App.tsx';
|
|
798
|
+
import './index.css';
|
|
799
|
+
|
|
800
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
801
|
+
<React.StrictMode>
|
|
802
|
+
<App />
|
|
803
|
+
</React.StrictMode>,
|
|
804
|
+
);
|
|
805
|
+
`,
|
|
806
|
+
'src/index.css': `@tailwind base;
|
|
807
|
+
@tailwind components;
|
|
808
|
+
@tailwind utilities;
|
|
809
|
+
|
|
810
|
+
@layer base {
|
|
811
|
+
body {
|
|
812
|
+
@apply antialiased selection:bg-emerald-500/30 selection:text-emerald-100;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
`,
|
|
816
|
+
'src/App.tsx': `import { Database, Server, Boxes } from 'lucide-react';
|
|
817
|
+
|
|
818
|
+
function App() {
|
|
819
|
+
return (
|
|
820
|
+
<main className="min-h-screen bg-zinc-950 text-zinc-100">
|
|
821
|
+
<section className="mx-auto flex min-h-screen w-full max-w-6xl flex-col justify-center gap-8 px-6 py-10">
|
|
822
|
+
<div className="space-y-3">
|
|
823
|
+
<p className="text-sm font-semibold uppercase tracking-wide text-emerald-300">${options.architecture} architecture</p>
|
|
824
|
+
<h1 className="max-w-3xl text-4xl font-bold tracking-normal text-zinc-50 md:text-6xl">${options.projectName}</h1>
|
|
825
|
+
<p className="max-w-2xl text-base leading-7 text-zinc-400">
|
|
826
|
+
Public full-stack starter generated with open API routes.
|
|
827
|
+
</p>
|
|
828
|
+
</div>
|
|
829
|
+
|
|
830
|
+
<div className="grid gap-4 md:grid-cols-3">
|
|
831
|
+
<div className="rounded-lg border border-zinc-800 bg-zinc-900/50 p-5">
|
|
832
|
+
<Server className="mb-4 h-6 w-6 text-emerald-300" />
|
|
833
|
+
<h2 className="text-lg font-semibold">Express API</h2>
|
|
834
|
+
<p className="mt-2 text-sm text-zinc-400">Backend endpoints are available under /api.</p>
|
|
835
|
+
</div>
|
|
836
|
+
<div className="rounded-lg border border-zinc-800 bg-zinc-900/50 p-5">
|
|
837
|
+
<Database className="mb-4 h-6 w-6 text-sky-300" />
|
|
838
|
+
<h2 className="text-lg font-semibold">${options.database}</h2>
|
|
839
|
+
<p className="mt-2 text-sm text-zinc-400">Configured with ${options.orm} for persistence.</p>
|
|
840
|
+
</div>
|
|
841
|
+
<div className="rounded-lg border border-zinc-800 bg-zinc-900/50 p-5">
|
|
842
|
+
<Boxes className="mb-4 h-6 w-6 text-amber-300" />
|
|
843
|
+
<h2 className="text-lg font-semibold">Public UI</h2>
|
|
844
|
+
<p className="mt-2 text-sm text-zinc-400">A lightweight interface for open project workflows.</p>
|
|
845
|
+
</div>
|
|
846
|
+
</div>
|
|
847
|
+
</section>
|
|
848
|
+
</main>
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
export default App;
|
|
853
|
+
`,
|
|
854
|
+
};
|
|
855
|
+
}
|