create-lumina-project 1.2.0 → 2.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 (37) hide show
  1. package/package.json +3 -3
  2. package/template/.env.example +2 -1
  3. package/template/nodemon.json +1 -1
  4. package/template/package.json +26 -15
  5. package/template/resources/css/app.css +491 -0
  6. package/template/resources/js/Pages/Errors/404.tsx +32 -0
  7. package/template/resources/js/Pages/Maintenance.tsx +34 -0
  8. package/template/resources/js/Pages/Status.tsx +103 -0
  9. package/template/resources/js/Pages/Welcome.tsx +82 -0
  10. package/template/resources/js/app.tsx +43 -0
  11. package/template/resources/js/tsconfig.json +20 -0
  12. package/template/resources/js/vite-env.d.ts +1 -0
  13. package/template/resources/views/app.html +14 -0
  14. package/template/src/controllers/UserController.ts +5 -1
  15. package/template/src/controllers/WebController.ts +94 -0
  16. package/template/src/database/migrations/20260420050855-create_refresh_tokens.js +5 -0
  17. package/template/src/exceptions/Handler.ts +4 -4
  18. package/template/src/middlewares/ApiAuth.ts +67 -0
  19. package/template/src/middlewares/Csrf.ts +5 -4
  20. package/template/src/middlewares/InertiaMiddleware.ts +95 -0
  21. package/template/src/middlewares/Maintenance.ts +5 -5
  22. package/template/src/middlewares/RoleGuard.ts +20 -0
  23. package/template/src/middlewares/Validator.ts +17 -1
  24. package/template/src/middlewares/WebAuth.ts +122 -0
  25. package/template/src/models/BaseModel.ts +24 -0
  26. package/template/src/models/RefreshToken.ts +6 -5
  27. package/template/src/models/User.ts +3 -5
  28. package/template/src/routes/api.ts +16 -8
  29. package/template/src/routes/web.ts +10 -43
  30. package/template/src/server.ts +115 -0
  31. package/template/src/types/express/index.d.ts +8 -1
  32. package/template/src/types/express-inertia.d.ts +28 -0
  33. package/template/vite.config.ts +20 -0
  34. package/template/views/404.html +0 -233
  35. package/template/views/maintenance.html +0 -464
  36. package/template/views/status.html +0 -545
  37. package/template/views/welcome.html +0 -495
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-lumina-project",
3
- "version": "1.2.0",
3
+ "version": "2.0.0",
4
4
  "author": "Glenson Ansin",
5
5
  "description": "Scaffold a modern Node.js API with Lumina architecture.",
6
6
  "type": "module",
@@ -12,9 +12,9 @@
12
12
  "template"
13
13
  ],
14
14
  "dependencies": {
15
- "fs-extra": "^11.3.4",
15
+ "fs-extra": "^11.3.6",
16
16
  "kleur": "^4.1.5",
17
- "ora": "^9.3.0",
17
+ "ora": "^9.4.1",
18
18
  "prompts": "^2.4.2"
19
19
  },
20
20
  "license": "MIT",
@@ -12,7 +12,8 @@ DB_SSL=
12
12
 
13
13
  # JWT Configuration
14
14
  JWT_SECRET=your_super_secret_key_here
15
- JWT_EXPIRES_IN=1d
15
+ JWT_EXPIRES_IN=15m
16
+ JWT_REFRESH_EXPIRES_IN=7d
16
17
 
17
18
  # Application
18
19
  NODE_ENV=development
@@ -1,5 +1,5 @@
1
1
  {
2
- "watch": ["src"],
2
+ "watch": ["src", "server.ts"],
3
3
  "ext": "ts,json",
4
4
  "exec": "tsx src/server.ts"
5
5
  }
@@ -2,10 +2,13 @@
2
2
  "name": "lumina",
3
3
  "version": "1.0.0",
4
4
  "description": "",
5
- "main": "server.ts",
5
+ "main": "src/server.ts",
6
6
  "scripts": {
7
- "dev": "nodemon --exec tsx server.ts",
8
- "build": "tsc",
7
+ "dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"",
8
+ "dev:server": "nodemon --exec tsx src/server.ts",
9
+ "dev:client": "vite",
10
+ "build:client": "vite build",
11
+ "build": "tsc && npm run build:client",
9
12
  "start": "node dist/server.js",
10
13
  "test": "vitest run",
11
14
  "migrate": "tsx scripts/migrate.ts up",
@@ -24,38 +27,46 @@
24
27
  "private": true,
25
28
  "type": "module",
26
29
  "dependencies": {
30
+ "@inertiajs/react": "^3.6.0",
27
31
  "bcrypt": "^6.0.0",
28
- "bcryptjs": "^3.0.3",
29
32
  "compression": "^1.8.1",
30
33
  "cookie-parser": "^1.4.7",
31
34
  "cors": "^2.8.6",
32
35
  "dotenv": "^17.4.2",
33
36
  "express": "^5.2.1",
34
- "express-rate-limit": "^8.3.2",
35
- "helmet": "^8.1.0",
37
+ "express-rate-limit": "^8.5.2",
38
+ "helmet": "^8.2.0",
39
+ "inertia-node": "^2.0.0",
36
40
  "jsonwebtoken": "^9.0.3",
37
- "multer": "^2.1.1",
38
- "mysql2": "^3.22.1",
41
+ "multer": "^2.2.0",
42
+ "mysql2": "^3.22.5",
39
43
  "nodemon": "^3.1.14",
40
- "pg": "^8.20.0",
44
+ "pg": "^8.22.0",
41
45
  "pg-hstore": "^2.3.4",
46
+ "react": "^19.2.7",
47
+ "react-dom": "^19.2.7",
42
48
  "sequelize": "^6.37.8",
43
49
  "sequelize-cli": "^6.6.5",
44
- "umzug": "^3.8.2",
50
+ "umzug": "^3.8.3",
45
51
  "winston": "^3.19.0",
46
- "zod": "^4.3.6"
52
+ "zod": "^4.4.3"
47
53
  },
48
54
  "devDependencies": {
49
- "@faker-js/faker": "^10.4.0",
55
+ "@faker-js/faker": "^10.5.0",
50
56
  "@types/bcrypt": "^6.0.0",
51
57
  "@types/compression": "^1.8.1",
52
58
  "@types/cookie-parser": "^1.4.10",
53
59
  "@types/cors": "^2.8.19",
54
60
  "@types/express": "^5.0.6",
55
61
  "@types/jsonwebtoken": "^9.0.10",
56
- "@types/multer": "^2.1.0",
57
- "tsx": "^4.21.0",
62
+ "@types/multer": "^2.2.0",
63
+ "@types/react": "^19.2.17",
64
+ "@types/react-dom": "^19.2.3",
65
+ "@vitejs/plugin-react": "^6.0.3",
66
+ "concurrently": "^10.0.3",
67
+ "tsx": "^4.23.0",
58
68
  "typescript": "^6.0.3",
59
- "vitest": "^4.1.4"
69
+ "vite": "^8.1.3",
70
+ "vitest": "^4.1.10"
60
71
  }
61
72
  }
@@ -0,0 +1,491 @@
1
+ *, *::before, *::after {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ :root {
8
+ --indigo-100: #e0e7ff;
9
+ --indigo-200: #c7d2fe;
10
+ --indigo-400: #818cf8;
11
+ --indigo-500: #6366f1;
12
+ --indigo-600: #4f46e5;
13
+ --slate-100: #f1f5f9;
14
+ --slate-200: #e2e8f0;
15
+ --slate-300: #cbd5e1;
16
+ --slate-400: #94a3b8;
17
+ --slate-500: #64748b;
18
+ --slate-600: #475569;
19
+ --slate-800: #1e293b;
20
+ --slate-900: #0f172a;
21
+ --emerald-400: #34d399;
22
+ --red-400: #f87171;
23
+ }
24
+
25
+ body {
26
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
27
+ background-color: var(--slate-900);
28
+ color: var(--slate-100);
29
+ min-height: 100vh;
30
+ display: flex;
31
+ flex-direction: column;
32
+ justify-content: center;
33
+ align-items: center;
34
+ overflow: hidden;
35
+ position: relative;
36
+ }
37
+
38
+ .bg-gradient {
39
+ position: fixed;
40
+ inset: 0;
41
+ z-index: -1;
42
+ background:
43
+ radial-gradient(ellipse 80% 60% at 20% 20%, rgba(99, 102, 241, 0.15) 0%, transparent 60%),
44
+ radial-gradient(ellipse 60% 80% at 80% 80%, rgba(79, 70, 229, 0.12) 0%, transparent 60%);
45
+ animation: bgShift 12s ease-in-out infinite alternate;
46
+ }
47
+
48
+ @keyframes bgShift {
49
+ 0% { opacity: 0.7; transform: scale(1); }
50
+ 50% { opacity: 1; transform: scale(1.05); }
51
+ 100% { opacity: 0.8; transform: scale(1.02); }
52
+ }
53
+
54
+ .orb {
55
+ position: fixed;
56
+ border-radius: 50%;
57
+ filter: blur(80px);
58
+ z-index: -1;
59
+ animation: float 20s ease-in-out infinite;
60
+ pointer-events: none;
61
+ }
62
+
63
+ .orb-1 {
64
+ width: 350px;
65
+ height: 350px;
66
+ background: rgba(99, 102, 241, 0.1);
67
+ top: -10%;
68
+ left: -5%;
69
+ }
70
+
71
+ .orb-2 {
72
+ width: 250px;
73
+ height: 250px;
74
+ background: rgba(52, 211, 153, 0.08);
75
+ bottom: -5%;
76
+ right: -5%;
77
+ animation-delay: -7s;
78
+ }
79
+
80
+ .orb-3 {
81
+ width: 300px;
82
+ height: 300px;
83
+ background: rgba(167, 139, 250, 0.08);
84
+ top: 40%;
85
+ left: 60%;
86
+ animation-delay: -14s;
87
+ }
88
+
89
+ .orb-error {
90
+ background: rgba(248, 113, 113, 0.06) !important;
91
+ }
92
+
93
+ @keyframes float {
94
+ 0%, 100% { transform: translate(0, 0); }
95
+ 25% { transform: translate(30px, -40px); }
96
+ 50% { transform: translate(-20px, 20px); }
97
+ 75% { transform: translate(15px, 30px); }
98
+ }
99
+
100
+ .grid-pattern {
101
+ position: fixed;
102
+ inset: 0;
103
+ z-index: -1;
104
+ background-image:
105
+ linear-gradient(rgba(148, 163, 184, 0.03) 1px, transparent 1px),
106
+ linear-gradient(90deg, rgba(148, 163, 184, 0.03) 1px, transparent 1px);
107
+ background-size: 60px 60px;
108
+ mask-image: radial-gradient(ellipse 70% 70% at 50% 50%, black 30%, transparent 80%);
109
+ -webkit-mask-image: radial-gradient(ellipse 70% 70% at 50% 50%, black 30%, transparent 80%);
110
+ }
111
+
112
+ .main {
113
+ position: relative;
114
+ z-index: 1;
115
+ display: flex;
116
+ flex-direction: column;
117
+ align-items: center;
118
+ gap: 1.75rem;
119
+ padding: 2rem;
120
+ width: 100%;
121
+ max-width: 520px;
122
+ animation: fadeInUp 0.8s ease-out both;
123
+ }
124
+
125
+ @keyframes fadeInUp {
126
+ from { opacity: 0; transform: translateY(30px); }
127
+ to { opacity: 1; transform: translateY(0); }
128
+ }
129
+
130
+ .logo-wrapper {
131
+ position: relative;
132
+ width: 80px;
133
+ height: 80px;
134
+ display: flex;
135
+ align-items: center;
136
+ justify-content: center;
137
+ animation: pulse 4s ease-in-out infinite;
138
+ }
139
+
140
+ .logo-glow {
141
+ position: absolute;
142
+ inset: 0;
143
+ background: var(--indigo-500);
144
+ border-radius: 50%;
145
+ filter: blur(25px);
146
+ opacity: 0.5;
147
+ }
148
+
149
+ .logo {
150
+ position: relative;
151
+ width: 100%;
152
+ height: 100%;
153
+ object-fit: contain;
154
+ z-index: 1;
155
+ drop-shadow: 0 0 10px rgba(99, 102, 241, 0.5);
156
+ }
157
+
158
+ @keyframes pulse {
159
+ 0%, 100% { transform: scale(1); }
160
+ 50% { transform: scale(1.05); }
161
+ }
162
+
163
+ .header {
164
+ text-align: center;
165
+ }
166
+
167
+ .title {
168
+ font-size: 2.25rem;
169
+ font-weight: 800;
170
+ letter-spacing: -0.02em;
171
+ background: linear-gradient(135deg, #fff 0%, var(--slate-300) 100%);
172
+ -webkit-background-clip: text;
173
+ -webkit-text-fill-color: transparent;
174
+ background-clip: text;
175
+ margin-bottom: 0.6rem;
176
+ }
177
+
178
+ .subtitle {
179
+ font-size: 0.95rem;
180
+ color: var(--slate-400);
181
+ line-height: 1.6;
182
+ }
183
+
184
+ .card {
185
+ width: 100%;
186
+ background: rgba(30, 41, 59, 0.4);
187
+ backdrop-filter: blur(12px);
188
+ -webkit-backdrop-filter: blur(12px);
189
+ border: 1px solid rgba(148, 163, 184, 0.1);
190
+ border-radius: 16px;
191
+ padding: 1.5rem;
192
+ box-shadow: 0 10px 30px -10px rgba(0,0,0,0.3);
193
+ animation: fadeInUp 0.8s ease-out 0.2s both;
194
+ }
195
+
196
+ .card-title {
197
+ font-size: 0.85rem;
198
+ font-weight: 600;
199
+ color: var(--slate-400);
200
+ text-transform: uppercase;
201
+ letter-spacing: 0.05em;
202
+ margin-bottom: 1rem;
203
+ display: flex;
204
+ align-items: center;
205
+ gap: 0.5rem;
206
+ }
207
+
208
+ .features {
209
+ display: grid;
210
+ grid-template-columns: 1fr 1fr;
211
+ gap: 1.25rem;
212
+ }
213
+
214
+ .feature {
215
+ display: flex;
216
+ align-items: flex-start;
217
+ gap: 0.75rem;
218
+ }
219
+
220
+ .feature-icon {
221
+ width: 36px;
222
+ height: 36px;
223
+ border-radius: 10px;
224
+ background: rgba(148, 163, 184, 0.06);
225
+ display: flex;
226
+ align-items: center;
227
+ justify-content: center;
228
+ font-size: 1rem;
229
+ }
230
+
231
+ .feature-text h3 {
232
+ font-size: 0.8rem;
233
+ font-weight: 600;
234
+ color: var(--slate-100);
235
+ margin-bottom: 0.15rem;
236
+ }
237
+
238
+ .feature-text p {
239
+ font-size: 0.72rem;
240
+ color: var(--slate-400);
241
+ line-height: 1.4;
242
+ }
243
+
244
+ .status-bar {
245
+ width: 100%;
246
+ display: flex;
247
+ align-items: center;
248
+ justify-content: center;
249
+ gap: 1.5rem;
250
+ animation: fadeInUp 0.8s ease-out 0.35s both;
251
+ }
252
+
253
+ .status {
254
+ display: inline-flex;
255
+ align-items: center;
256
+ gap: 0.5rem;
257
+ padding: 0.6rem 1.25rem;
258
+ background: rgba(16, 185, 129, 0.08);
259
+ border: 1px solid rgba(16, 185, 129, 0.15);
260
+ border-radius: 999px;
261
+ font-size: 0.8rem;
262
+ font-weight: 600;
263
+ color: var(--emerald-400);
264
+ letter-spacing: 0.01em;
265
+ }
266
+
267
+ .status-error {
268
+ background: rgba(248, 113, 113, 0.08);
269
+ border-color: rgba(248, 113, 113, 0.15);
270
+ color: var(--red-400);
271
+ }
272
+
273
+ .status-dot {
274
+ width: 8px;
275
+ height: 8px;
276
+ border-radius: 50%;
277
+ background: var(--emerald-400);
278
+ box-shadow: 0 0 8px rgba(52, 211, 153, 0.5);
279
+ animation: blink 2s ease-in-out infinite;
280
+ }
281
+
282
+ .status-dot.error {
283
+ background: var(--red-400);
284
+ box-shadow: 0 0 8px rgba(248, 113, 113, 0.5);
285
+ }
286
+
287
+ @keyframes blink {
288
+ 0%, 100% { opacity: 1; }
289
+ 50% { opacity: 0.4; }
290
+ }
291
+
292
+ .actions {
293
+ display: flex;
294
+ align-items: center;
295
+ gap: 0.75rem;
296
+ animation: fadeInUp 0.8s ease-out 0.5s both;
297
+ }
298
+
299
+ .btn {
300
+ display: inline-flex;
301
+ align-items: center;
302
+ gap: 0.5rem;
303
+ padding: 0.7rem 1.5rem;
304
+ border-radius: 12px;
305
+ font-size: 0.85rem;
306
+ font-weight: 600;
307
+ text-decoration: none;
308
+ transition: all 0.25s ease;
309
+ cursor: pointer;
310
+ }
311
+
312
+ .btn-primary {
313
+ background: linear-gradient(135deg, var(--indigo-600), var(--indigo-500));
314
+ color: white;
315
+ box-shadow: 0 4px 16px -4px rgba(99, 102, 241, 0.4);
316
+ border: 1px solid rgba(129, 140, 248, 0.2);
317
+ }
318
+
319
+ .btn-primary:hover {
320
+ background: linear-gradient(135deg, var(--indigo-500), var(--indigo-400));
321
+ box-shadow: 0 6px 24px -4px rgba(99, 102, 241, 0.5);
322
+ transform: translateY(-2px);
323
+ }
324
+
325
+ .btn-secondary {
326
+ background: rgba(148, 163, 184, 0.06);
327
+ color: var(--slate-300);
328
+ border: 1px solid rgba(148, 163, 184, 0.1);
329
+ }
330
+
331
+ .btn-secondary:hover {
332
+ background: rgba(148, 163, 184, 0.1);
333
+ color: var(--slate-100);
334
+ border-color: rgba(148, 163, 184, 0.18);
335
+ transform: translateY(-2px);
336
+ }
337
+
338
+ .btn svg {
339
+ width: 16px;
340
+ height: 16px;
341
+ }
342
+
343
+ .footer {
344
+ margin-top: 0.5rem;
345
+ font-size: 0.75rem;
346
+ color: var(--slate-600);
347
+ animation: fadeInUp 0.8s ease-out 0.5s both;
348
+ letter-spacing: 0.02em;
349
+ }
350
+
351
+ .footer a {
352
+ color: var(--indigo-400);
353
+ text-decoration: none;
354
+ font-weight: 500;
355
+ transition: color 0.2s ease;
356
+ }
357
+
358
+ .footer a:hover {
359
+ color: var(--indigo-200);
360
+ }
361
+
362
+ .info-row {
363
+ display: flex;
364
+ justify-content: space-between;
365
+ align-items: center;
366
+ padding: 0.75rem 0;
367
+ border-bottom: 1px solid rgba(148, 163, 184, 0.06);
368
+ }
369
+
370
+ .info-row:last-child {
371
+ border-bottom: none;
372
+ padding-bottom: 0;
373
+ }
374
+
375
+ .info-label {
376
+ display: flex;
377
+ align-items: center;
378
+ gap: 0.6rem;
379
+ font-size: 0.82rem;
380
+ color: var(--slate-400);
381
+ font-weight: 500;
382
+ }
383
+
384
+ .info-icon {
385
+ width: 32px;
386
+ height: 32px;
387
+ border-radius: 8px;
388
+ background: rgba(148, 163, 184, 0.06);
389
+ display: flex;
390
+ align-items: center;
391
+ justify-content: center;
392
+ font-size: 0.85rem;
393
+ flex-shrink: 0;
394
+ }
395
+
396
+ .info-value {
397
+ font-family: 'JetBrains Mono', monospace, monospace;
398
+ font-size: 0.8rem;
399
+ font-weight: 500;
400
+ color: var(--slate-200);
401
+ background: rgba(148, 163, 184, 0.06);
402
+ padding: 0.3rem 0.7rem;
403
+ border-radius: 6px;
404
+ }
405
+
406
+ .info-value.env-badge {
407
+ background: rgba(99, 102, 241, 0.1);
408
+ color: var(--indigo-400);
409
+ border: 1px solid rgba(99, 102, 241, 0.15);
410
+ }
411
+
412
+ .info-value.status-up {
413
+ background: rgba(16, 185, 129, 0.1);
414
+ color: var(--emerald-400);
415
+ border: 1px solid rgba(16, 185, 129, 0.15);
416
+ }
417
+
418
+ .services-grid {
419
+ display: grid;
420
+ grid-template-columns: 1fr 1fr;
421
+ gap: 0.6rem;
422
+ }
423
+
424
+ .service-item {
425
+ display: flex;
426
+ align-items: center;
427
+ gap: 0.6rem;
428
+ padding: 0.7rem 0.85rem;
429
+ border-radius: 10px;
430
+ background: rgba(148, 163, 184, 0.03);
431
+ border: 1px solid rgba(148, 163, 184, 0.05);
432
+ transition: background 0.2s ease, border-color 0.2s ease;
433
+ }
434
+
435
+ .service-item:hover {
436
+ background: rgba(148, 163, 184, 0.06);
437
+ border-color: rgba(148, 163, 184, 0.1);
438
+ }
439
+
440
+ .service-dot {
441
+ width: 7px;
442
+ height: 7px;
443
+ border-radius: 50%;
444
+ flex-shrink: 0;
445
+ }
446
+
447
+ .service-dot.up {
448
+ background: var(--emerald-400);
449
+ box-shadow: 0 0 6px rgba(52, 211, 153, 0.4);
450
+ }
451
+
452
+ .service-name {
453
+ font-size: 0.78rem;
454
+ font-weight: 500;
455
+ color: var(--slate-300);
456
+ }
457
+
458
+ .error-code {
459
+ font-size: 7rem;
460
+ font-weight: 800;
461
+ letter-spacing: -0.04em;
462
+ background: linear-gradient(135deg, rgba(255,255,255,0.15) 0%, rgba(99, 102, 241, 0.2) 50%, rgba(248, 113, 113, 0.15) 100%);
463
+ -webkit-background-clip: text;
464
+ -webkit-text-fill-color: transparent;
465
+ background-clip: text;
466
+ line-height: 1;
467
+ }
468
+
469
+ .back-link {
470
+ display: inline-flex;
471
+ align-items: center;
472
+ gap: 0.5rem;
473
+ font-size: 0.8rem;
474
+ font-weight: 600;
475
+ color: var(--slate-400);
476
+ text-decoration: none;
477
+ transition: color 0.2s ease;
478
+ margin-bottom: 1rem;
479
+ }
480
+ .back-link:hover { color: var(--slate-200); }
481
+ .back-link svg { width: 14px; height: 14px; }
482
+
483
+ @media (max-width: 540px) {
484
+ .title { font-size: 2rem; }
485
+ .subtitle { font-size: 0.95rem; }
486
+ .features { grid-template-columns: 1fr; }
487
+ .actions { flex-direction: column; width: 100%; }
488
+ .btn { width: 100%; justify-content: center; }
489
+ .error-code { font-size: 5rem; }
490
+ .services-grid { grid-template-columns: 1fr; }
491
+ }
@@ -0,0 +1,32 @@
1
+ interface NotFoundProps {
2
+ url: string;
3
+ }
4
+
5
+ export default function NotFound({ url }: NotFoundProps) {
6
+ return (
7
+ <>
8
+ <div className="bg-gradient"></div>
9
+ <div className="orb orb-1"></div>
10
+ <div className="orb orb-2 orb-error"></div>
11
+ <div className="grid-pattern"></div>
12
+
13
+ <div className="main">
14
+ <div className="error-code">404</div>
15
+
16
+ <div className="header">
17
+ <h1 className="title">Page Not Found</h1>
18
+ <p className="subtitle">The page you're looking for doesn't exist or has been moved. Let's get you back on track.</p>
19
+ </div>
20
+
21
+ <a href="/" className="btn btn-secondary">
22
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="2.5" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" d="m2.25 12 8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" /></svg>
23
+ Back to Home
24
+ </a>
25
+
26
+ <div className="footer">
27
+ <a href="https://github.com/glensonansin/lumina" target="_blank">Lumina</a> — Built with Express & Sequelize
28
+ </div>
29
+ </div>
30
+ </>
31
+ );
32
+ }
@@ -0,0 +1,34 @@
1
+ interface MaintenanceProps {
2
+ message?: string;
3
+ }
4
+
5
+ export default function Maintenance({ message }: MaintenanceProps) {
6
+ return (
7
+ <>
8
+ <div className="bg-gradient"></div>
9
+ <div className="orb orb-1"></div>
10
+ <div className="orb orb-2 orb-error"></div>
11
+ <div className="grid-pattern"></div>
12
+
13
+ <div className="main">
14
+ <div className="error-code" style={{ fontSize: '5rem', marginBottom: '-1rem' }}>503</div>
15
+
16
+ <div className="header">
17
+ <h1 className="title">Maintenance Mode</h1>
18
+ <p className="subtitle">{message || "We're performing some scheduled maintenance. We'll be back online shortly!"}</p>
19
+ </div>
20
+
21
+ <div className="status-bar" style={{ animationDelay: '0s' }}>
22
+ <div className="status status-error">
23
+ <span className="status-dot error"></span>
24
+ System Offline
25
+ </div>
26
+ </div>
27
+
28
+ <div className="footer">
29
+ <a href="https://github.com/glensonansin/lumina" target="_blank">Lumina</a> — Built with Express & Sequelize
30
+ </div>
31
+ </div>
32
+ </>
33
+ );
34
+ }