sprygen 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.
Files changed (56) hide show
  1. package/README.md +80 -0
  2. package/dist/cli.js +55 -0
  3. package/package.json +53 -0
  4. package/templates/auth/AuthController.java.ejs +40 -0
  5. package/templates/auth/JwtAuthFilter.java.ejs +62 -0
  6. package/templates/auth/JwtService.java.ejs +81 -0
  7. package/templates/auth/SecurityConfig.java.ejs +65 -0
  8. package/templates/auth/UserDetailsServiceImpl.java.ejs +24 -0
  9. package/templates/entity/Entity.java.ejs +40 -0
  10. package/templates/entity/EntityController.java.ejs +92 -0
  11. package/templates/entity/EntityControllerTest.java.ejs +24 -0
  12. package/templates/entity/EntityDto.java.ejs +32 -0
  13. package/templates/entity/EntityRepository.java.ejs +9 -0
  14. package/templates/entity/EntityService.java.ejs +32 -0
  15. package/templates/project/java/config/CorsConfig.java.ejs +24 -0
  16. package/templates/project/java/config/SecurityConfig.java.ejs +76 -0
  17. package/templates/project/java/config/SecurityConfigSession.java.ejs +73 -0
  18. package/templates/project/java/config/SwaggerConfig.java.ejs +31 -0
  19. package/templates/project/java/controller/AdminController.java.ejs +82 -0
  20. package/templates/project/java/controller/AuthController.java.ejs +86 -0
  21. package/templates/project/java/controller/HomeController.java.ejs +63 -0
  22. package/templates/project/java/controller/ProfileController.java.ejs +65 -0
  23. package/templates/project/java/controller/UserController.java.ejs +35 -0
  24. package/templates/project/java/dto/AuthRequest.java.ejs +15 -0
  25. package/templates/project/java/dto/AuthResponse.java.ejs +18 -0
  26. package/templates/project/java/dto/ProfileUpdateRequest.java.ejs +20 -0
  27. package/templates/project/java/dto/RegisterRequest.java.ejs +30 -0
  28. package/templates/project/java/dto/UserDto.java.ejs +17 -0
  29. package/templates/project/java/entity/Role.java.ejs +6 -0
  30. package/templates/project/java/entity/User.java.ejs +97 -0
  31. package/templates/project/java/repository/UserRepository.java.ejs +11 -0
  32. package/templates/project/java/security/JwtAuthFilter.java.ejs +62 -0
  33. package/templates/project/java/security/UserDetailsServiceImpl.java.ejs +21 -0
  34. package/templates/project/java/service/JwtService.java.ejs +81 -0
  35. package/templates/project/java/service/UserService.java.ejs +32 -0
  36. package/templates/project/resources/application.yml.ejs +50 -0
  37. package/templates/project/resources/logback-spring.xml.ejs +41 -0
  38. package/templates/project/static/admin.html.ejs +163 -0
  39. package/templates/project/static/assets/app.js.ejs +340 -0
  40. package/templates/project/static/assets/style.css +533 -0
  41. package/templates/project/static/css/style.css +595 -0
  42. package/templates/project/static/dashboard.html.ejs +119 -0
  43. package/templates/project/static/index.html.ejs +96 -0
  44. package/templates/project/static/js/api.js +30 -0
  45. package/templates/project/static/js/auth.js +44 -0
  46. package/templates/project/static/js/nav.js.ejs +82 -0
  47. package/templates/project/static/js/ui.js +57 -0
  48. package/templates/project/static/login.html.ejs +71 -0
  49. package/templates/project/static/profile.html.ejs +163 -0
  50. package/templates/project/static/register.html.ejs +82 -0
  51. package/templates/project/thymeleaf/admin/users.html.ejs +111 -0
  52. package/templates/project/thymeleaf/dashboard.html.ejs +109 -0
  53. package/templates/project/thymeleaf/layout.html.ejs +75 -0
  54. package/templates/project/thymeleaf/login.html.ejs +56 -0
  55. package/templates/project/thymeleaf/profile.html.ejs +133 -0
  56. package/templates/project/thymeleaf/register.html.ejs +56 -0
@@ -0,0 +1,595 @@
1
+ /* ============================================================
2
+ Sprygen — stylesheet
3
+ Theme: Dark / Green · Minimal · Professional
4
+ ============================================================ */
5
+
6
+ @import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,300..700;1,14..32,300..700&family=JetBrains+Mono:wght@400;500&display=swap');
7
+
8
+ /* ---------- Tokens ---------- */
9
+ :root {
10
+ --bg: #0d1117;
11
+ --bg-2: #161b22;
12
+ --bg-3: #1c2128;
13
+ --border: #30363d;
14
+ --border-dim: #21262d;
15
+
16
+ --green: #3fb950;
17
+ --green-low: #2da44e;
18
+ --green-dim: rgba(63, 185, 80, 0.12);
19
+ --green-glow: rgba(63, 185, 80, 0.22);
20
+
21
+ --text: #e6edf3;
22
+ --text-muted: #8b949e;
23
+ --text-dim: #484f58;
24
+
25
+ --red: #f85149;
26
+ --red-dim: rgba(248, 81, 73, 0.12);
27
+ --amber: #d29922;
28
+ --blue: #79c0ff;
29
+
30
+ --sidebar-w: 220px;
31
+ --r: 6px;
32
+
33
+ font-size: 15px;
34
+ }
35
+
36
+ /* ---------- Reset ---------- */
37
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
38
+ html, body { height: 100%; }
39
+ body {
40
+ font-family: 'Inter', -apple-system, sans-serif;
41
+ background: var(--bg);
42
+ color: var(--text);
43
+ line-height: 1.5;
44
+ -webkit-font-smoothing: antialiased;
45
+ }
46
+
47
+ /* ---------- App shell ---------- */
48
+ .app-shell {
49
+ display: flex;
50
+ min-height: 100vh;
51
+ }
52
+
53
+ /* ---------- Sidebar ---------- */
54
+ .sidebar {
55
+ width: var(--sidebar-w);
56
+ background: var(--bg-2);
57
+ border-right: 1px solid var(--border);
58
+ display: flex;
59
+ flex-direction: column;
60
+ position: fixed;
61
+ inset-block: 0;
62
+ left: 0;
63
+ z-index: 100;
64
+ }
65
+
66
+ .sidebar-brand {
67
+ padding: 14px 16px;
68
+ border-bottom: 1px solid var(--border);
69
+ display: flex;
70
+ align-items: center;
71
+ gap: 9px;
72
+ }
73
+
74
+ .brand-mark {
75
+ width: 26px; height: 26px;
76
+ background: var(--green-low);
77
+ border-radius: 4px;
78
+ display: flex; align-items: center; justify-content: center;
79
+ font-size: 0.7rem;
80
+ font-weight: 800;
81
+ color: #fff;
82
+ letter-spacing: -0.5px;
83
+ flex-shrink: 0;
84
+ }
85
+
86
+ .brand-name { font-weight: 700; font-size: 0.9rem; }
87
+
88
+ .sidebar-nav {
89
+ flex: 1;
90
+ padding: 6px 8px;
91
+ overflow-y: auto;
92
+ }
93
+
94
+ .sidebar-nav ul { list-style: none; }
95
+
96
+ .nav-section-label {
97
+ display: block;
98
+ font-size: 0.68rem;
99
+ font-weight: 600;
100
+ letter-spacing: 0.7px;
101
+ text-transform: uppercase;
102
+ color: var(--text-dim);
103
+ padding: 14px 8px 5px;
104
+ }
105
+
106
+ .nav-link {
107
+ display: flex;
108
+ align-items: center;
109
+ gap: 8px;
110
+ padding: 6px 10px;
111
+ border-radius: var(--r);
112
+ color: var(--text-muted);
113
+ text-decoration: none;
114
+ font-size: 0.855rem;
115
+ font-weight: 500;
116
+ transition: background 0.12s, color 0.12s;
117
+ position: relative;
118
+ }
119
+ .nav-link:hover { background: rgba(177,186,196,0.07); color: var(--text); }
120
+ .nav-link.active { background: var(--green-dim); color: var(--green); font-weight: 600; }
121
+
122
+ .nav-icon {
123
+ font-style: normal;
124
+ font-family: 'JetBrains Mono', monospace;
125
+ font-size: 0.72rem;
126
+ width: 18px;
127
+ text-align: center;
128
+ opacity: 0.6;
129
+ }
130
+ .nav-link.active .nav-icon { opacity: 1; }
131
+
132
+ .nav-badge {
133
+ margin-left: auto;
134
+ font-size: 0.6rem;
135
+ font-weight: 700;
136
+ letter-spacing: 0.5px;
137
+ padding: 1px 6px;
138
+ border-radius: 99px;
139
+ background: var(--green-dim);
140
+ color: var(--green);
141
+ border: 1px solid var(--green-glow);
142
+ }
143
+
144
+ .sidebar-footer {
145
+ padding: 10px;
146
+ border-top: 1px solid var(--border);
147
+ }
148
+
149
+ .user-chip {
150
+ display: flex;
151
+ align-items: center;
152
+ gap: 9px;
153
+ padding: 8px;
154
+ border-radius: var(--r);
155
+ margin-bottom: 6px;
156
+ }
157
+ .user-chip-info { min-width: 0; }
158
+ .user-chip-name { font-size: 0.82rem; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
159
+ .user-chip-role { font-size: 0.7rem; color: var(--text-muted); }
160
+
161
+ /* ---------- Main / topbar ---------- */
162
+ .main {
163
+ margin-left: var(--sidebar-w);
164
+ flex: 1;
165
+ display: flex;
166
+ flex-direction: column;
167
+ }
168
+
169
+ .topbar {
170
+ padding: 12px 24px;
171
+ border-bottom: 1px solid var(--border);
172
+ background: var(--bg-2);
173
+ display: flex;
174
+ align-items: center;
175
+ justify-content: space-between;
176
+ position: sticky;
177
+ top: 0;
178
+ z-index: 50;
179
+ }
180
+ .topbar-title { font-size: 0.9rem; font-weight: 600; }
181
+ .topbar-meta { font-size: 0.78rem; color: var(--text-muted); }
182
+
183
+ .page { padding: 24px; }
184
+ .page-header { margin-bottom: 20px; }
185
+ .page-header h1 { font-size: 1.2rem; font-weight: 700; margin-bottom: 3px; }
186
+
187
+ /* ---------- Avatars ---------- */
188
+ .avatar {
189
+ width: 32px; height: 32px;
190
+ border-radius: 50%;
191
+ background: var(--green-low);
192
+ display: flex; align-items: center; justify-content: center;
193
+ font-size: 0.75rem;
194
+ font-weight: 700;
195
+ color: #fff;
196
+ flex-shrink: 0;
197
+ font-family: 'JetBrains Mono', monospace;
198
+ }
199
+ .avatar-lg {
200
+ width: 56px; height: 56px;
201
+ font-size: 1.1rem;
202
+ border: 2px solid var(--border);
203
+ }
204
+
205
+ /* ---------- Cards ---------- */
206
+ .card {
207
+ background: var(--bg-2);
208
+ border: 1px solid var(--border);
209
+ border-radius: var(--r);
210
+ margin-bottom: 16px;
211
+ }
212
+ .card-header {
213
+ padding: 14px 18px 0;
214
+ display: flex;
215
+ align-items: center;
216
+ justify-content: space-between;
217
+ margin-bottom: 14px;
218
+ }
219
+ .card-title { font-size: 0.875rem; font-weight: 600; }
220
+ .card-body { padding: 0 18px 18px; }
221
+
222
+ /* ---------- Stats ---------- */
223
+ .stats-row {
224
+ display: grid;
225
+ grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
226
+ gap: 12px;
227
+ margin-bottom: 20px;
228
+ }
229
+ .stat-card {
230
+ background: var(--bg-2);
231
+ border: 1px solid var(--border);
232
+ border-radius: var(--r);
233
+ padding: 16px;
234
+ }
235
+ .stat-label { font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-dim); margin-bottom: 8px; }
236
+ .stat-value { font-size: 1.9rem; font-weight: 700; line-height: 1; font-family: 'JetBrains Mono', monospace; }
237
+ .stat-sub { font-size: 0.72rem; color: var(--text-muted); margin-top: 4px; }
238
+
239
+ /* ---------- Info strip ---------- */
240
+ .info-strip {
241
+ display: flex;
242
+ align-items: center;
243
+ gap: 0;
244
+ background: var(--bg-2);
245
+ border: 1px solid var(--border);
246
+ border-radius: var(--r);
247
+ padding: 12px 18px;
248
+ margin-bottom: 16px;
249
+ flex-wrap: wrap;
250
+ gap: 14px;
251
+ }
252
+ .info-strip-item {
253
+ display: flex;
254
+ align-items: center;
255
+ gap: 8px;
256
+ }
257
+ .info-strip-label {
258
+ font-size: 0.72rem;
259
+ font-weight: 600;
260
+ text-transform: uppercase;
261
+ letter-spacing: 0.4px;
262
+ color: var(--text-dim);
263
+ }
264
+ .info-strip-value {
265
+ font-size: 0.84rem;
266
+ color: var(--text-muted);
267
+ font-family: 'JetBrains Mono', monospace;
268
+ }
269
+ .info-strip-sep {
270
+ width: 1px;
271
+ height: 16px;
272
+ background: var(--border);
273
+ flex-shrink: 0;
274
+ }
275
+
276
+
277
+ /* ---------- Tables ---------- */
278
+ .table-wrap { overflow-x: auto; }
279
+ table { width: 100%; border-collapse: collapse; }
280
+ thead th {
281
+ padding: 9px 14px;
282
+ font-size: 0.72rem;
283
+ font-weight: 600;
284
+ text-transform: uppercase;
285
+ letter-spacing: 0.4px;
286
+ color: var(--text-muted);
287
+ text-align: left;
288
+ border-bottom: 1px solid var(--border);
289
+ white-space: nowrap;
290
+ }
291
+ tbody tr { border-bottom: 1px solid var(--border-dim); transition: background 0.1s; }
292
+ tbody tr:last-child { border-bottom: none; }
293
+ tbody tr:hover { background: rgba(177,186,196,0.03); }
294
+ tbody td { padding: 11px 14px; font-size: 0.855rem; vertical-align: middle; }
295
+ .table-empty { text-align: center; padding: 32px !important; color: var(--text-muted); }
296
+
297
+ /* HTTP method codes */
298
+ code {
299
+ font-family: 'JetBrains Mono', monospace;
300
+ font-size: 0.8rem;
301
+ background: var(--bg-3);
302
+ padding: 1px 5px;
303
+ border-radius: 3px;
304
+ }
305
+ .http-get { color: var(--blue); background: transparent; padding: 0; }
306
+ .http-post { color: var(--green); background: transparent; padding: 0; }
307
+ .http-put { color: var(--amber); background: transparent; padding: 0; }
308
+ .http-delete { color: var(--red); background: transparent; padding: 0; }
309
+
310
+ /* ---------- Badges ---------- */
311
+ .badge {
312
+ display: inline-flex;
313
+ align-items: center;
314
+ padding: 2px 8px;
315
+ border-radius: 99px;
316
+ font-size: 0.68rem;
317
+ font-weight: 600;
318
+ letter-spacing: 0.3px;
319
+ border: 1px solid transparent;
320
+ }
321
+ .badge-open { background: var(--green-dim); color: var(--green); border-color: var(--green-glow); }
322
+ .badge-auth { background: rgba(121,192,255,0.1); color: var(--blue); border-color: rgba(121,192,255,0.2); }
323
+ .badge-admin { background: rgba(210,153,34,0.1); color: var(--amber); border-color: rgba(210,153,34,0.25); }
324
+
325
+ /* ---------- Forms ---------- */
326
+ .form-group { margin-bottom: 14px; }
327
+ label {
328
+ display: block;
329
+ font-size: 0.78rem;
330
+ font-weight: 500;
331
+ color: var(--text-muted);
332
+ margin-bottom: 5px;
333
+ }
334
+ .label-hint { font-size: 0.68rem; color: var(--text-dim); font-weight: 400; margin-left: 6px; }
335
+ input, textarea, select {
336
+ width: 100%;
337
+ background: var(--bg);
338
+ border: 1px solid var(--border);
339
+ border-radius: var(--r);
340
+ padding: 7px 11px;
341
+ color: var(--text);
342
+ font-family: inherit;
343
+ font-size: 0.855rem;
344
+ transition: border-color 0.12s, box-shadow 0.12s;
345
+ }
346
+ input:focus, textarea:focus, select:focus {
347
+ outline: none;
348
+ border-color: var(--green-low);
349
+ box-shadow: 0 0 0 3px var(--green-glow);
350
+ }
351
+ input::placeholder, textarea::placeholder { color: var(--text-dim); }
352
+
353
+ .row-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
354
+
355
+ /* ---------- Buttons ---------- */
356
+ .btn {
357
+ display: inline-flex;
358
+ align-items: center;
359
+ justify-content: center;
360
+ gap: 6px;
361
+ padding: 7px 14px;
362
+ border-radius: var(--r);
363
+ font-family: inherit;
364
+ font-size: 0.84rem;
365
+ font-weight: 600;
366
+ cursor: pointer;
367
+ border: 1px solid transparent;
368
+ text-decoration: none;
369
+ white-space: nowrap;
370
+ transition: background 0.12s, border-color 0.12s, opacity 0.12s;
371
+ }
372
+ .btn-primary {
373
+ background: var(--green-low);
374
+ border-color: var(--green-low);
375
+ color: #fff;
376
+ }
377
+ .btn-primary:hover { background: #26913d; border-color: #26913d; }
378
+
379
+ .btn-outline {
380
+ background: transparent;
381
+ border-color: var(--border);
382
+ color: var(--text-muted);
383
+ }
384
+ .btn-outline:hover { border-color: #8b949e; color: var(--text); }
385
+
386
+ .btn-ghost {
387
+ background: transparent;
388
+ border-color: transparent;
389
+ color: var(--text-muted);
390
+ }
391
+ .btn-ghost:hover { background: rgba(177,186,196,0.08); color: var(--text); }
392
+
393
+ .btn-danger-ghost {
394
+ background: transparent;
395
+ border-color: transparent;
396
+ color: var(--red);
397
+ }
398
+ .btn-danger-ghost:hover { background: var(--red-dim); }
399
+
400
+ .btn-sm { padding: 5px 10px; font-size: 0.78rem; }
401
+ .btn-xs { padding: 3px 8px; font-size: 0.72rem; }
402
+ .btn-full { width: 100%; }
403
+ .btn:disabled { opacity: 0.45; cursor: not-allowed; }
404
+
405
+ /* ---------- Profile hero ---------- */
406
+ .profile-hero {
407
+ display: flex;
408
+ align-items: center;
409
+ gap: 16px;
410
+ padding: 18px;
411
+ background: var(--bg-2);
412
+ border: 1px solid var(--border);
413
+ border-radius: var(--r);
414
+ margin-bottom: 18px;
415
+ }
416
+ .profile-hero h2 { font-size: 1.05rem; font-weight: 700; margin-bottom: 2px; }
417
+ .profile-hero p { font-size: 0.82rem; }
418
+
419
+ /* ---------- Two-column grid ---------- */
420
+ .two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
421
+
422
+ /* ---------- Actions ---------- */
423
+ .actions { display: flex; gap: 6px; align-items: center; }
424
+
425
+ /* ---------- Alerts ---------- */
426
+ .alert {
427
+ padding: 9px 13px;
428
+ border-radius: var(--r);
429
+ font-size: 0.82rem;
430
+ margin-bottom: 12px;
431
+ border: 1px solid transparent;
432
+ display: none;
433
+ }
434
+ .alert.visible { display: block; }
435
+ .alert-error { background: var(--red-dim); border-color: rgba(248,81,73,0.3); color: #ffa198; }
436
+ .alert-success { background: var(--green-dim); border-color: var(--green-glow); color: var(--green); }
437
+
438
+ /* ---------- Toast ---------- */
439
+ .toast {
440
+ position: fixed;
441
+ bottom: 20px; right: 20px;
442
+ padding: 10px 16px;
443
+ border-radius: var(--r);
444
+ font-size: 0.84rem;
445
+ font-weight: 500;
446
+ border: 1px solid transparent;
447
+ z-index: 9999;
448
+ transform: translateY(60px);
449
+ opacity: 0;
450
+ transition: transform 0.22s ease, opacity 0.22s ease;
451
+ max-width: 300px;
452
+ }
453
+ .toast.show { transform: translateY(0); opacity: 1; }
454
+ .toast-success { background: #1c3226; border-color: rgba(63,185,80,0.35); color: var(--green); }
455
+ .toast-error { background: #3d1f1f; border-color: rgba(248,81,73,0.35); color: #ffa198; }
456
+
457
+ /* ---------- Auth pages ---------- */
458
+ .auth-body { display: flex; align-items: center; justify-content: center; min-height: 100vh; }
459
+ .auth-wrap { width: 100%; max-width: 380px; padding: 20px; }
460
+ .auth-card {
461
+ background: var(--bg-2);
462
+ border: 1px solid var(--border);
463
+ border-radius: 8px;
464
+ padding: 24px;
465
+ }
466
+ .auth-header { text-align: center; margin-bottom: 20px; }
467
+ .brand-mark-lg {
468
+ width: 40px; height: 40px;
469
+ margin: 0 auto 10px;
470
+ background: var(--green-low);
471
+ border-radius: 6px;
472
+ display: flex; align-items: center; justify-content: center;
473
+ font-size: 0.8rem;
474
+ font-weight: 800;
475
+ color: #fff;
476
+ letter-spacing: -0.5px;
477
+ }
478
+ .auth-header h1 { font-size: 1.1rem; font-weight: 700; margin-bottom: 3px; }
479
+ .auth-switch {
480
+ text-align: center;
481
+ font-size: 0.8rem;
482
+ color: var(--text-muted);
483
+ margin-top: 10px;
484
+ }
485
+ .auth-switch a { color: var(--green); text-decoration: none; font-weight: 600; }
486
+ .auth-switch a:hover { text-decoration: underline; }
487
+
488
+ /* ---------- Landing ---------- */
489
+ .landing { min-height: 100vh; display: flex; flex-direction: column; }
490
+
491
+ .landing-nav {
492
+ padding: 14px 32px;
493
+ display: flex;
494
+ align-items: center;
495
+ justify-content: space-between;
496
+ border-bottom: 1px solid var(--border);
497
+ background: var(--bg-2);
498
+ }
499
+ .landing-brand { display: flex; align-items: center; gap: 9px; }
500
+ .landing-nav-actions { display: flex; gap: 8px; }
501
+
502
+ .landing-hero-section { flex: 1; padding: 80px 32px 60px; }
503
+ .landing-container { max-width: 900px; margin: 0 auto; }
504
+
505
+ .status-pill {
506
+ display: inline-flex;
507
+ align-items: center;
508
+ gap: 7px;
509
+ padding: 4px 12px;
510
+ border-radius: 99px;
511
+ background: var(--green-dim);
512
+ border: 1px solid var(--green-glow);
513
+ font-size: 0.75rem;
514
+ color: var(--green);
515
+ font-weight: 500;
516
+ margin-bottom: 20px;
517
+ }
518
+ .status-dot {
519
+ width: 6px; height: 6px;
520
+ border-radius: 50%;
521
+ background: var(--green);
522
+ animation: pulse 2s infinite;
523
+ }
524
+ @keyframes pulse {
525
+ 0%, 100% { opacity: 1; }
526
+ 50% { opacity: 0.4; }
527
+ }
528
+
529
+ .hero-title {
530
+ font-size: 2.4rem;
531
+ font-weight: 700;
532
+ line-height: 1.2;
533
+ margin-bottom: 14px;
534
+ }
535
+ .hero-sub {
536
+ font-size: 0.975rem;
537
+ color: var(--text-muted);
538
+ max-width: 540px;
539
+ line-height: 1.7;
540
+ margin-bottom: 28px;
541
+ }
542
+ .hero-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 28px; }
543
+
544
+ .tech-pills { display: flex; gap: 6px; flex-wrap: wrap; }
545
+ .tech-pill {
546
+ padding: 3px 9px;
547
+ border-radius: 99px;
548
+ font-size: 0.7rem;
549
+ font-weight: 500;
550
+ background: var(--bg-3);
551
+ border: 1px solid var(--border);
552
+ color: var(--text-muted);
553
+ font-family: 'JetBrains Mono', monospace;
554
+ }
555
+
556
+ .landing-section { padding: 0 32px 60px; }
557
+ .section-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
558
+ .section-heading { font-size: 1rem; font-weight: 700; }
559
+
560
+ .landing-footer {
561
+ padding: 14px 32px;
562
+ border-top: 1px solid var(--border);
563
+ display: flex;
564
+ justify-content: space-between;
565
+ font-size: 0.75rem;
566
+ color: var(--text-dim);
567
+ }
568
+
569
+ /* ---------- Utilities ---------- */
570
+ .hidden { display: none !important; }
571
+ .text-muted { color: var(--text-muted); }
572
+ .text-dim { color: var(--text-dim); }
573
+ .text-green { color: var(--green); }
574
+ .text-danger { color: var(--red); }
575
+ .link-green { color: var(--green); text-decoration: none; font-size: 0.82rem; font-weight: 500; }
576
+ .link-green:hover { text-decoration: underline; }
577
+
578
+ /* ---------- Scrollbar ---------- */
579
+ ::-webkit-scrollbar { width: 5px; height: 5px; }
580
+ ::-webkit-scrollbar-track { background: transparent; }
581
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
582
+
583
+ /* ---------- Responsive ---------- */
584
+ @media (max-width: 768px) {
585
+ .sidebar { display: none; }
586
+ .main { margin-left: 0; }
587
+ .page { padding: 14px; }
588
+ .two-col { grid-template-columns: 1fr; }
589
+ .row-2 { grid-template-columns: 1fr; }
590
+ .stats-row { grid-template-columns: 1fr 1fr; }
591
+ .hero-title { font-size: 1.7rem; }
592
+ .landing-hero-section { padding: 40px 16px; }
593
+ .landing-section { padding: 0 16px 40px; }
594
+ .landing-nav { padding: 12px 16px; }
595
+ }
@@ -0,0 +1,119 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8"/>
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
+ <title><%= projectName %> — Dashboard</title>
7
+ <link rel="stylesheet" href="/css/style.css"/>
8
+ </head>
9
+ <body>
10
+ <div class="app-shell">
11
+ <aside id="sidebar" class="sidebar"></aside>
12
+ <div class="main">
13
+ <header class="topbar">
14
+ <span class="topbar-title">Dashboard</span>
15
+ <span id="topbar-date" class="topbar-meta"></span>
16
+ </header>
17
+
18
+ <div class="page">
19
+ <div class="page-header">
20
+ <h1 id="welcome-msg">Welcome back</h1>
21
+ <p class="text-muted">Account overview and API reference.</p>
22
+ </div>
23
+
24
+ <!-- User info strip (role + email) -->
25
+ <div class="info-strip">
26
+ <div class="info-strip-item">
27
+ <span class="info-strip-label">Signed in as</span>
28
+ <span class="info-strip-value" id="strip-email"></span>
29
+ </div>
30
+ <div class="info-strip-sep"></div>
31
+ <div class="info-strip-item">
32
+ <span class="info-strip-label">Role</span>
33
+ <span id="strip-role-badge"></span>
34
+ </div>
35
+ </div>
36
+
37
+ <!-- Admin numeric stats (only visible to admins) -->
38
+ <div class="stats-row admin-zone hidden">
39
+ <div class="stat-card">
40
+ <div class="stat-label">Total Users</div>
41
+ <div class="stat-value text-green" id="stat-total">—</div>
42
+ </div>
43
+ <div class="stat-card">
44
+ <div class="stat-label">Administrators</div>
45
+ <div class="stat-value" id="stat-admins">—</div>
46
+ </div>
47
+ <div class="stat-card">
48
+ <div class="stat-label">Members</div>
49
+ <div class="stat-value" id="stat-members">—</div>
50
+ </div>
51
+ </div>
52
+
53
+ <!-- API Reference -->
54
+ <div class="card">
55
+ <div class="card-header">
56
+ <span class="card-title">API Reference</span>
57
+ <a href="/swagger-ui/index.html" target="_blank" class="link-green">Swagger UI &rarr;</a>
58
+ </div>
59
+ <div class="table-wrap">
60
+ <table>
61
+ <thead>
62
+ <tr><th>Method</th><th>Endpoint</th><th>Description</th><th>Access</th></tr>
63
+ </thead>
64
+ <tbody>
65
+ <tr><td><code class="http-post">POST</code></td><td><code>/api/v1/auth/register</code></td><td>Register account</td><td><span class="badge badge-open">Public</span></td></tr>
66
+ <tr><td><code class="http-post">POST</code></td><td><code>/api/v1/auth/login</code></td><td>Login, get token</td><td><span class="badge badge-open">Public</span></td></tr>
67
+ <tr><td><code class="http-get">GET</code></td><td><code>/api/v1/profile</code></td><td>Get own profile</td><td><span class="badge badge-auth">Auth</span></td></tr>
68
+ <tr><td><code class="http-put">PUT</code></td><td><code>/api/v1/profile</code></td><td>Update profile</td><td><span class="badge badge-auth">Auth</span></td></tr>
69
+ <tr><td><code class="http-put">PUT</code></td><td><code>/api/v1/profile/password</code></td><td>Change password</td><td><span class="badge badge-auth">Auth</span></td></tr>
70
+ <tr><td><code class="http-get">GET</code></td><td><code>/api/v1/admin/users</code></td><td>List all users</td><td><span class="badge badge-admin">Admin</span></td></tr>
71
+ <tr><td><code class="http-put">PUT</code></td><td><code>/api/v1/admin/users/{id}/role</code></td><td>Assign role</td><td><span class="badge badge-admin">Admin</span></td></tr>
72
+ <tr><td><code class="http-delete">DELETE</code></td><td><code>/api/v1/admin/users/{id}</code></td><td>Delete user</td><td><span class="badge badge-admin">Admin</span></td></tr>
73
+ </tbody>
74
+ </table>
75
+ </div>
76
+ </div>
77
+ </div>
78
+ </div>
79
+ </div>
80
+ <div id="toast" class="toast"></div>
81
+ <script src="/js/auth.js"></script>
82
+ <script src="/js/ui.js"></script>
83
+ <script src="/js/nav.js"></script>
84
+ <script>
85
+ if (!guardAuth()) throw new Error('redirect');
86
+
87
+ const user = Auth.user();
88
+ renderNav('dashboard');
89
+
90
+ document.getElementById('topbar-date').textContent =
91
+ new Date().toLocaleDateString('en-US', { weekday:'long', month:'long', day:'numeric' });
92
+ document.getElementById('welcome-msg').textContent =
93
+ 'Welcome back, ' + user.firstName;
94
+
95
+ // Info strip — role as badge, email as text
96
+ document.getElementById('strip-email').textContent = user.email;
97
+ const badge = document.getElementById('strip-role-badge');
98
+ badge.innerHTML = Auth.isAdmin()
99
+ ? '<span class="badge badge-admin">Administrator</span>'
100
+ : '<span class="badge badge-auth">Member</span>';
101
+
102
+ // Admin-only: show stat cards + fetch live counts
103
+ if (Auth.isAdmin()) {
104
+ document.querySelectorAll('.admin-zone').forEach(el => el.classList.remove('hidden'));
105
+
106
+ fetch('/api/v1/admin/stats', {
107
+ headers: { Authorization: 'Bearer ' + Auth.token() }
108
+ })
109
+ .then(r => r.json())
110
+ .then(stats => {
111
+ animateCount(document.getElementById('stat-total'), stats.totalUsers || 0);
112
+ animateCount(document.getElementById('stat-admins'), stats.admins || 0);
113
+ animateCount(document.getElementById('stat-members'), stats.regularUsers || 0);
114
+ })
115
+ .catch(() => showToast('Could not load stats', 'error'));
116
+ }
117
+ </script>
118
+ </body>
119
+ </html>