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.
@@ -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>&copy; {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
+ }