create-mantiq 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/kits/react.ts +73 -65
- package/src/kits/svelte.ts +109 -63
- package/src/kits/vue.ts +99 -82
- package/src/templates.ts +121 -3
package/package.json
CHANGED
package/src/kits/react.ts
CHANGED
|
@@ -297,12 +297,41 @@ export default function Dashboard({ appName = '${ctx.name}', currentUser, users:
|
|
|
297
297
|
}
|
|
298
298
|
|
|
299
299
|
return (
|
|
300
|
-
<div className="min-h-screen bg-gray-50 dark:bg-gray-950
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
300
|
+
<div className="min-h-screen flex bg-gray-50 dark:bg-gray-950">
|
|
301
|
+
{/* Sidebar */}
|
|
302
|
+
<aside className="w-60 fixed inset-y-0 left-0 bg-white dark:bg-gray-900 border-r border-gray-200 dark:border-gray-800 flex flex-col z-30">
|
|
303
|
+
<div className="h-14 flex items-center px-5 border-b border-gray-200 dark:border-gray-800">
|
|
304
|
+
<span className="text-sm font-semibold text-gray-900 dark:text-white">{appName}</span>
|
|
305
|
+
</div>
|
|
306
|
+
<nav className="flex-1 px-3 py-3 space-y-0.5">
|
|
307
|
+
<a href="/dashboard" className="flex items-center gap-2.5 px-2.5 py-2 rounded-lg text-sm font-medium bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-white">
|
|
308
|
+
<svg className="w-4 h-4 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" /></svg>
|
|
309
|
+
Dashboard
|
|
310
|
+
</a>
|
|
311
|
+
<a href="#users-section" onClick={(e) => { e.preventDefault(); document.getElementById('users-section')?.scrollIntoView({ behavior: 'smooth' }) }} className="flex items-center gap-2.5 px-2.5 py-2 rounded-lg text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors">
|
|
312
|
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" /></svg>
|
|
313
|
+
Users
|
|
314
|
+
</a>
|
|
315
|
+
</nav>
|
|
316
|
+
<div className="px-3 py-3 mt-auto border-t border-gray-200 dark:border-gray-800 space-y-0.5">
|
|
317
|
+
<a href="/_heartbeat" className="flex items-center gap-2.5 px-2.5 py-2 rounded-lg text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors">
|
|
318
|
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064" /></svg>
|
|
319
|
+
Heartbeat
|
|
320
|
+
</a>
|
|
321
|
+
<a href="/api/ping" className="flex items-center gap-2.5 px-2.5 py-2 rounded-lg text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors">
|
|
322
|
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
|
|
323
|
+
API Ping
|
|
324
|
+
</a>
|
|
325
|
+
</div>
|
|
326
|
+
</aside>
|
|
327
|
+
|
|
328
|
+
{/* Main */}
|
|
329
|
+
<div className="flex-1 ml-60">
|
|
330
|
+
{/* Top bar */}
|
|
331
|
+
<header className="h-14 border-b border-gray-200 dark:border-gray-800 bg-white/90 dark:bg-gray-950/90 backdrop-blur-md sticky top-0 z-20 flex items-center justify-between px-6">
|
|
332
|
+
<h1 className="text-sm font-medium text-gray-900 dark:text-gray-200">Dashboard</h1>
|
|
304
333
|
<div className="flex items-center gap-3">
|
|
305
|
-
<button onClick={toggleTheme} className="
|
|
334
|
+
<button onClick={toggleTheme} className="p-1.5 rounded-lg text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors" title="Toggle theme">
|
|
306
335
|
{isDark ? (
|
|
307
336
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
308
337
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
|
@@ -314,73 +343,52 @@ export default function Dashboard({ appName = '${ctx.name}', currentUser, users:
|
|
|
314
343
|
)}
|
|
315
344
|
</button>
|
|
316
345
|
<span className="text-xs text-gray-500 dark:text-gray-400">{currentUser?.name}</span>
|
|
317
|
-
<button onClick={handleLogout}
|
|
318
|
-
className="text-xs text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white bg-gray-100 dark:bg-gray-900 hover:bg-gray-200 dark:hover:bg-gray-800 border border-gray-200 dark:border-gray-800 rounded-lg px-3 py-1.5 transition-colors">
|
|
346
|
+
<button onClick={handleLogout} className="text-xs text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg px-3 py-1.5 transition-colors">
|
|
319
347
|
Logout
|
|
320
348
|
</button>
|
|
321
349
|
</div>
|
|
322
|
-
</
|
|
323
|
-
</nav>
|
|
350
|
+
</header>
|
|
324
351
|
|
|
325
|
-
|
|
326
|
-
<
|
|
327
|
-
<
|
|
328
|
-
|
|
329
|
-
|
|
352
|
+
{/* Content */}
|
|
353
|
+
<main className="p-6 space-y-6 animate-fade-up">
|
|
354
|
+
<div className="bg-white dark:bg-gray-900/50 rounded-xl border border-gray-200 dark:border-gray-800 p-6">
|
|
355
|
+
<h2 className="text-lg font-bold text-gray-900 dark:text-white">Welcome back, {currentUser?.name}</h2>
|
|
356
|
+
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">Here's what's happening with your application.</p>
|
|
357
|
+
</div>
|
|
330
358
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
<
|
|
335
|
-
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-200">Heartbeat Dashboard</h3>
|
|
336
|
-
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">Monitor application health</p>
|
|
337
|
-
</div>
|
|
338
|
-
<span className="text-emerald-600 dark:text-emerald-400 text-sm group-hover:translate-x-0.5 transition-transform">→</span>
|
|
359
|
+
<div id="users-section" className="bg-white dark:bg-gray-900/50 rounded-xl border border-gray-200 dark:border-gray-800 overflow-hidden">
|
|
360
|
+
<div className="px-5 py-4 border-b border-gray-200 dark:border-gray-800 flex items-center justify-between">
|
|
361
|
+
<h2 className="text-sm font-bold text-gray-900 dark:text-gray-200">Users</h2>
|
|
362
|
+
<span className="text-xs text-gray-500 dark:text-gray-400">{loading ? 'Loading...' : \`\${users.length} total\`}</span>
|
|
339
363
|
</div>
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
</div>
|
|
347
|
-
<span className="text-emerald-600 dark:text-emerald-400 text-sm group-hover:translate-x-0.5 transition-transform">→</span>
|
|
348
|
-
</div>
|
|
349
|
-
</a>
|
|
350
|
-
</div>
|
|
351
|
-
|
|
352
|
-
<div className="bg-white dark:bg-gray-900/50 rounded-xl border border-gray-200 dark:border-gray-800 overflow-hidden">
|
|
353
|
-
<div className="px-5 py-4 border-b border-gray-200 dark:border-gray-800 flex items-center justify-between">
|
|
354
|
-
<h2 className="text-sm font-bold text-gray-900 dark:text-gray-200">Users</h2>
|
|
355
|
-
<span className="text-xs text-gray-500 dark:text-gray-400">{loading ? 'Loading...' : \`\${users.length} total\`}</span>
|
|
356
|
-
</div>
|
|
357
|
-
<table className="w-full text-sm">
|
|
358
|
-
<thead>
|
|
359
|
-
<tr className="border-b border-gray-100 dark:border-gray-800 text-left text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
360
|
-
<th className="px-5 py-3 font-medium">Name</th>
|
|
361
|
-
<th className="px-5 py-3 font-medium">Email</th>
|
|
362
|
-
<th className="px-5 py-3 font-medium">Role</th>
|
|
363
|
-
</tr>
|
|
364
|
-
</thead>
|
|
365
|
-
<tbody className="divide-y divide-gray-100 dark:divide-gray-800/60">
|
|
366
|
-
{users.map((u) => (
|
|
367
|
-
<tr key={u.id} className="hover:bg-gray-50 dark:hover:bg-gray-900/50 transition-colors">
|
|
368
|
-
<td className="px-5 py-3 text-gray-900 dark:text-gray-200">{u.name}</td>
|
|
369
|
-
<td className="px-5 py-3 text-gray-500 dark:text-gray-400">{u.email}</td>
|
|
370
|
-
<td className="px-5 py-3">
|
|
371
|
-
<span className={\`text-[10px] px-2 py-0.5 rounded-full font-medium \${
|
|
372
|
-
u.role === 'admin' ? 'bg-emerald-100 dark:bg-emerald-500/15 text-emerald-700 dark:text-emerald-300 border border-emerald-200 dark:border-emerald-500/20' : 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 border border-gray-200 dark:border-gray-700'
|
|
373
|
-
}\`}>{u.role}</span>
|
|
374
|
-
</td>
|
|
364
|
+
<table className="w-full text-sm">
|
|
365
|
+
<thead>
|
|
366
|
+
<tr className="border-b border-gray-100 dark:border-gray-800 text-left text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
367
|
+
<th className="px-5 py-3 font-medium">Name</th>
|
|
368
|
+
<th className="px-5 py-3 font-medium">Email</th>
|
|
369
|
+
<th className="px-5 py-3 font-medium">Role</th>
|
|
375
370
|
</tr>
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
371
|
+
</thead>
|
|
372
|
+
<tbody className="divide-y divide-gray-100 dark:divide-gray-800/60">
|
|
373
|
+
{users.map((u) => (
|
|
374
|
+
<tr key={u.id} className="hover:bg-gray-50 dark:hover:bg-gray-900/50 transition-colors">
|
|
375
|
+
<td className="px-5 py-3 text-gray-900 dark:text-gray-200">{u.name}</td>
|
|
376
|
+
<td className="px-5 py-3 text-gray-500 dark:text-gray-400">{u.email}</td>
|
|
377
|
+
<td className="px-5 py-3">
|
|
378
|
+
<span className={\`text-[10px] px-2 py-0.5 rounded-full font-medium \${
|
|
379
|
+
u.role === 'admin' ? 'bg-emerald-100 dark:bg-emerald-500/15 text-emerald-700 dark:text-emerald-300 border border-emerald-200 dark:border-emerald-500/20' : 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 border border-gray-200 dark:border-gray-700'
|
|
380
|
+
}\`}>{u.role}</span>
|
|
381
|
+
</td>
|
|
382
|
+
</tr>
|
|
383
|
+
))}
|
|
384
|
+
{users.length === 0 && !loading && (
|
|
385
|
+
<tr><td colSpan={3} className="px-5 py-8 text-center text-gray-400 dark:text-gray-600">No users found</td></tr>
|
|
386
|
+
)}
|
|
387
|
+
</tbody>
|
|
388
|
+
</table>
|
|
389
|
+
</div>
|
|
390
|
+
</main>
|
|
391
|
+
</div>
|
|
384
392
|
</div>
|
|
385
393
|
)
|
|
386
394
|
}
|
package/src/kits/svelte.ts
CHANGED
|
@@ -96,6 +96,13 @@ export function render(_url: string, data?: Record<string, any>) {
|
|
|
96
96
|
|
|
97
97
|
$: PageComponent = pages[currentPage] ?? null
|
|
98
98
|
|
|
99
|
+
// Initialize theme immediately to prevent flash
|
|
100
|
+
if (typeof window !== 'undefined') {
|
|
101
|
+
const theme = localStorage.getItem('theme') ||
|
|
102
|
+
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
|
|
103
|
+
document.documentElement.classList.toggle('dark', theme === 'dark')
|
|
104
|
+
}
|
|
105
|
+
|
|
99
106
|
async function navigate(href: string) {
|
|
100
107
|
const res = await fetch(href, {
|
|
101
108
|
headers: { 'X-Mantiq': 'true', Accept: 'application/json' },
|
|
@@ -272,7 +279,7 @@ export function render(_url: string, data?: Record<string, any>) {
|
|
|
272
279
|
`,
|
|
273
280
|
|
|
274
281
|
'src/pages/Dashboard.svelte': `<script lang="ts">
|
|
275
|
-
import { onMount } from 'svelte'
|
|
282
|
+
import { onMount, getContext } from 'svelte'
|
|
276
283
|
import { api, post } from '../lib/api.ts'
|
|
277
284
|
|
|
278
285
|
export let appName: string = '${ctx.name}'
|
|
@@ -280,10 +287,21 @@ export function render(_url: string, data?: Record<string, any>) {
|
|
|
280
287
|
export let users: any[] = []
|
|
281
288
|
export let navigate: (href: string) => void
|
|
282
289
|
|
|
290
|
+
const ctxNavigate = getContext<(href: string) => void>('navigate')
|
|
291
|
+
|
|
283
292
|
let localUsers: any[] = users ?? []
|
|
284
293
|
let loading = !users?.length
|
|
285
294
|
let isDark = true
|
|
286
295
|
|
|
296
|
+
const navItems = [
|
|
297
|
+
{ label: 'Dashboard', icon: 'grid', href: '/dashboard', active: true },
|
|
298
|
+
]
|
|
299
|
+
|
|
300
|
+
const bottomLinks = [
|
|
301
|
+
{ label: 'Heartbeat', icon: 'heart', href: '/heartbeat' },
|
|
302
|
+
{ label: 'API Ping', icon: 'zap', href: '/api/ping' },
|
|
303
|
+
]
|
|
304
|
+
|
|
287
305
|
async function fetchUsers() {
|
|
288
306
|
loading = true
|
|
289
307
|
const { ok, data } = await api('/api/users')
|
|
@@ -293,7 +311,7 @@ export function render(_url: string, data?: Record<string, any>) {
|
|
|
293
311
|
|
|
294
312
|
async function handleLogout() {
|
|
295
313
|
await post('/logout', {})
|
|
296
|
-
navigate('/login')
|
|
314
|
+
;(navigate ?? ctxNavigate)('/login')
|
|
297
315
|
}
|
|
298
316
|
|
|
299
317
|
function toggleTheme() {
|
|
@@ -310,10 +328,56 @@ export function render(_url: string, data?: Record<string, any>) {
|
|
|
310
328
|
})
|
|
311
329
|
</script>
|
|
312
330
|
|
|
313
|
-
<div class="min-h-screen bg-gray-50 dark:bg-gray-950 text-gray-900 dark:text-gray-100 transition-colors">
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
331
|
+
<div class="min-h-screen flex bg-gray-50 dark:bg-gray-950 text-gray-900 dark:text-gray-100 transition-colors">
|
|
332
|
+
<!-- Sidebar -->
|
|
333
|
+
<aside class="w-60 flex-shrink-0 bg-white dark:bg-gray-900 border-r border-gray-200 dark:border-gray-800 flex flex-col fixed inset-y-0 left-0 z-30">
|
|
334
|
+
<!-- App name -->
|
|
335
|
+
<div class="h-14 flex items-center px-5 border-b border-gray-200 dark:border-gray-800">
|
|
336
|
+
<div class="flex items-center gap-2.5">
|
|
337
|
+
<div class="w-7 h-7 rounded-lg bg-emerald-600 flex items-center justify-center">
|
|
338
|
+
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
|
|
339
|
+
</div>
|
|
340
|
+
<span class="text-sm font-bold text-gray-900 dark:text-white">{appName}</span>
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
343
|
+
|
|
344
|
+
<!-- Nav items -->
|
|
345
|
+
<nav class="flex-1 px-3 py-4 space-y-1 overflow-y-auto">
|
|
346
|
+
{#each navItems as item}
|
|
347
|
+
<a href={item.href}
|
|
348
|
+
class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors
|
|
349
|
+
{item.active
|
|
350
|
+
? 'bg-emerald-50 dark:bg-emerald-500/10 text-emerald-700 dark:text-emerald-300'
|
|
351
|
+
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800'}">
|
|
352
|
+
{#if item.icon === 'grid'}
|
|
353
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" /></svg>
|
|
354
|
+
{/if}
|
|
355
|
+
{item.label}
|
|
356
|
+
</a>
|
|
357
|
+
{/each}
|
|
358
|
+
</nav>
|
|
359
|
+
|
|
360
|
+
<!-- Bottom links -->
|
|
361
|
+
<div class="px-3 py-4 border-t border-gray-200 dark:border-gray-800 space-y-1">
|
|
362
|
+
{#each bottomLinks as link}
|
|
363
|
+
<a href={link.href}
|
|
364
|
+
class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors">
|
|
365
|
+
{#if link.icon === 'heart'}
|
|
366
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" /></svg>
|
|
367
|
+
{:else if link.icon === 'zap'}
|
|
368
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
|
|
369
|
+
{/if}
|
|
370
|
+
{link.label}
|
|
371
|
+
</a>
|
|
372
|
+
{/each}
|
|
373
|
+
</div>
|
|
374
|
+
</aside>
|
|
375
|
+
|
|
376
|
+
<!-- Main content -->
|
|
377
|
+
<div class="flex-1 ml-60 flex flex-col min-h-screen">
|
|
378
|
+
<!-- Header -->
|
|
379
|
+
<header class="h-14 flex items-center justify-between px-6 border-b border-gray-200 dark:border-gray-800 bg-white/80 dark:bg-gray-950/90 backdrop-blur-md sticky top-0 z-20">
|
|
380
|
+
<h1 class="text-sm font-semibold text-gray-900 dark:text-white">Dashboard</h1>
|
|
317
381
|
<div class="flex items-center gap-3">
|
|
318
382
|
<button on:click={toggleTheme} class="p-2 rounded-lg text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors" aria-label="Toggle theme">
|
|
319
383
|
{#if isDark}
|
|
@@ -325,66 +389,48 @@ export function render(_url: string, data?: Record<string, any>) {
|
|
|
325
389
|
<span class="text-xs text-gray-500 dark:text-gray-400">{currentUser?.name}</span>
|
|
326
390
|
<button on:click={handleLogout} class="text-xs text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white bg-gray-100 dark:bg-gray-900 hover:bg-gray-200 dark:hover:bg-gray-800 border border-gray-200 dark:border-gray-800 rounded-lg px-3 py-1.5 transition-colors">Logout</button>
|
|
327
391
|
</div>
|
|
328
|
-
</
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
<
|
|
334
|
-
|
|
335
|
-
|
|
392
|
+
</header>
|
|
393
|
+
|
|
394
|
+
<!-- Page content -->
|
|
395
|
+
<main class="flex-1 px-6 py-8 space-y-6 animate-fade-up">
|
|
396
|
+
<!-- Welcome card -->
|
|
397
|
+
<div class="bg-white dark:bg-gray-900/50 rounded-xl border border-gray-200 dark:border-gray-800 p-6">
|
|
398
|
+
<h2 class="text-lg font-bold text-gray-900 dark:text-white">Welcome back, {currentUser?.name}</h2>
|
|
399
|
+
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">Here's what's happening with your application.</p>
|
|
400
|
+
</div>
|
|
336
401
|
|
|
337
|
-
|
|
338
|
-
<
|
|
339
|
-
<div class="flex items-center justify-between">
|
|
340
|
-
<
|
|
341
|
-
|
|
342
|
-
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Monitor application health</p>
|
|
343
|
-
</div>
|
|
344
|
-
<span class="text-emerald-600 dark:text-emerald-400 group-hover:translate-x-0.5 transition-transform">→</span>
|
|
345
|
-
</div>
|
|
346
|
-
</a>
|
|
347
|
-
<a href="/api/ping" class="group bg-white dark:bg-gray-900/50 rounded-xl border border-gray-200 dark:border-gray-800 p-5 hover:border-emerald-300 dark:hover:border-emerald-500/40 transition-colors">
|
|
348
|
-
<div class="flex items-center justify-between">
|
|
349
|
-
<div>
|
|
350
|
-
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">API Ping</h3>
|
|
351
|
-
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Test API connectivity</p>
|
|
352
|
-
</div>
|
|
353
|
-
<span class="text-emerald-600 dark:text-emerald-400 group-hover:translate-x-0.5 transition-transform">→</span>
|
|
402
|
+
<!-- Users table -->
|
|
403
|
+
<div class="bg-white dark:bg-gray-900/50 rounded-xl border border-gray-200 dark:border-gray-800 overflow-hidden">
|
|
404
|
+
<div class="px-5 py-4 border-b border-gray-200 dark:border-gray-800 flex items-center justify-between">
|
|
405
|
+
<h2 class="text-sm font-bold text-gray-900 dark:text-gray-200">Users</h2>
|
|
406
|
+
<span class="text-xs text-gray-500 dark:text-gray-400">{loading ? 'Loading...' : localUsers.length + ' total'}</span>
|
|
354
407
|
</div>
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
<span class="text-xs text-gray-500 dark:text-gray-400">{loading ? 'Loading...' : localUsers.length + ' total'}</span>
|
|
362
|
-
</div>
|
|
363
|
-
<table class="w-full text-sm">
|
|
364
|
-
<thead>
|
|
365
|
-
<tr class="border-b border-gray-200 dark:border-gray-800 text-left text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
366
|
-
<th class="px-5 py-3 font-medium">Name</th>
|
|
367
|
-
<th class="px-5 py-3 font-medium">Email</th>
|
|
368
|
-
<th class="px-5 py-3 font-medium">Role</th>
|
|
369
|
-
</tr>
|
|
370
|
-
</thead>
|
|
371
|
-
<tbody class="divide-y divide-gray-100 dark:divide-gray-800/60">
|
|
372
|
-
{#each localUsers as u (u.id)}
|
|
373
|
-
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/30 transition-colors">
|
|
374
|
-
<td class="px-5 py-3 text-gray-900 dark:text-gray-200">{u.name}</td>
|
|
375
|
-
<td class="px-5 py-3 text-gray-500 dark:text-gray-400">{u.email}</td>
|
|
376
|
-
<td class="px-5 py-3">
|
|
377
|
-
<span class="text-[10px] px-2 py-0.5 rounded-full font-medium border {u.role === 'admin' ? 'bg-emerald-50 dark:bg-emerald-500/15 text-emerald-700 dark:text-emerald-300 border-emerald-200 dark:border-emerald-500/20' : 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-700'}">{u.role}</span>
|
|
378
|
-
</td>
|
|
408
|
+
<table class="w-full text-sm">
|
|
409
|
+
<thead>
|
|
410
|
+
<tr class="border-b border-gray-200 dark:border-gray-800 text-left text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
411
|
+
<th class="px-5 py-3 font-medium">Name</th>
|
|
412
|
+
<th class="px-5 py-3 font-medium">Email</th>
|
|
413
|
+
<th class="px-5 py-3 font-medium">Role</th>
|
|
379
414
|
</tr>
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
415
|
+
</thead>
|
|
416
|
+
<tbody class="divide-y divide-gray-100 dark:divide-gray-800/60">
|
|
417
|
+
{#each localUsers as u (u.id)}
|
|
418
|
+
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/30 transition-colors">
|
|
419
|
+
<td class="px-5 py-3 text-gray-900 dark:text-gray-200">{u.name}</td>
|
|
420
|
+
<td class="px-5 py-3 text-gray-500 dark:text-gray-400">{u.email}</td>
|
|
421
|
+
<td class="px-5 py-3">
|
|
422
|
+
<span class="text-[10px] px-2 py-0.5 rounded-full font-medium border {u.role === 'admin' ? 'bg-emerald-50 dark:bg-emerald-500/15 text-emerald-700 dark:text-emerald-300 border-emerald-200 dark:border-emerald-500/20' : 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-700'}">{u.role}</span>
|
|
423
|
+
</td>
|
|
424
|
+
</tr>
|
|
425
|
+
{/each}
|
|
426
|
+
{#if localUsers.length === 0 && !loading}
|
|
427
|
+
<tr><td colspan="3" class="px-5 py-8 text-center text-gray-400 dark:text-gray-600">No users found</td></tr>
|
|
428
|
+
{/if}
|
|
429
|
+
</tbody>
|
|
430
|
+
</table>
|
|
431
|
+
</div>
|
|
432
|
+
</main>
|
|
433
|
+
</div>
|
|
388
434
|
</div>
|
|
389
435
|
`,
|
|
390
436
|
}
|
package/src/kits/vue.ts
CHANGED
|
@@ -120,7 +120,7 @@ onMounted(() => {
|
|
|
120
120
|
|
|
121
121
|
// Initialize theme: localStorage > system preference > dark default
|
|
122
122
|
const theme = localStorage.getItem('theme') ||
|
|
123
|
-
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : '
|
|
123
|
+
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
|
|
124
124
|
document.documentElement.classList.toggle('dark', theme === 'dark')
|
|
125
125
|
})
|
|
126
126
|
|
|
@@ -327,7 +327,7 @@ async function handleSubmit() {
|
|
|
327
327
|
`,
|
|
328
328
|
|
|
329
329
|
'src/pages/Dashboard.vue': `<script setup lang="ts">
|
|
330
|
-
import { ref, onMounted } from 'vue'
|
|
330
|
+
import { ref, onMounted, inject } from 'vue'
|
|
331
331
|
import { api, post } from '../lib/api.ts'
|
|
332
332
|
|
|
333
333
|
const props = defineProps<{
|
|
@@ -341,6 +341,7 @@ const appName = props.appName ?? '${ctx.name}'
|
|
|
341
341
|
const users = ref(props.users ?? [])
|
|
342
342
|
const loading = ref(!props.users?.length)
|
|
343
343
|
const isDark = ref(true)
|
|
344
|
+
const nav = inject<(href: string) => void>('navigate', props.navigate)
|
|
344
345
|
|
|
345
346
|
function toggleTheme() {
|
|
346
347
|
const dark = document.documentElement.classList.toggle('dark')
|
|
@@ -361,21 +362,64 @@ async function handleLogout() {
|
|
|
361
362
|
}
|
|
362
363
|
|
|
363
364
|
onMounted(() => {
|
|
364
|
-
|
|
365
|
-
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'dark')
|
|
366
|
-
document.documentElement.classList.toggle('dark', theme === 'dark')
|
|
367
|
-
isDark.value = theme === 'dark'
|
|
368
|
-
|
|
365
|
+
isDark.value = document.documentElement.classList.contains('dark')
|
|
369
366
|
if (!props.users?.length) fetchUsers()
|
|
370
367
|
})
|
|
371
368
|
</script>
|
|
372
369
|
|
|
373
370
|
<template>
|
|
374
|
-
<div class="min-h-screen bg-gray-50 dark:bg-gray-950 text-gray-900 dark:text-gray-100 transition-colors">
|
|
375
|
-
<!--
|
|
376
|
-
<
|
|
377
|
-
|
|
371
|
+
<div class="min-h-screen flex bg-gray-50 dark:bg-gray-950 text-gray-900 dark:text-gray-100 transition-colors">
|
|
372
|
+
<!-- Sidebar -->
|
|
373
|
+
<aside class="w-60 flex-shrink-0 bg-white dark:bg-gray-900 border-r border-gray-200 dark:border-gray-800 flex flex-col fixed inset-y-0 left-0 z-30">
|
|
374
|
+
<!-- App name -->
|
|
375
|
+
<div class="h-14 flex items-center px-5 border-b border-gray-200 dark:border-gray-800">
|
|
378
376
|
<span class="text-sm font-bold text-gray-900 dark:text-white">{{ appName }}</span>
|
|
377
|
+
</div>
|
|
378
|
+
|
|
379
|
+
<!-- Navigation -->
|
|
380
|
+
<nav class="flex-1 px-3 py-4 space-y-1">
|
|
381
|
+
<a
|
|
382
|
+
href="/dashboard"
|
|
383
|
+
class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium bg-emerald-50 dark:bg-emerald-500/10 text-emerald-700 dark:text-emerald-300"
|
|
384
|
+
>
|
|
385
|
+
<!-- Dashboard icon -->
|
|
386
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
387
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
|
|
388
|
+
</svg>
|
|
389
|
+
Dashboard
|
|
390
|
+
</a>
|
|
391
|
+
</nav>
|
|
392
|
+
|
|
393
|
+
<!-- Bottom links -->
|
|
394
|
+
<div class="px-3 py-4 border-t border-gray-200 dark:border-gray-800 space-y-1">
|
|
395
|
+
<a
|
|
396
|
+
href="/heartbeat"
|
|
397
|
+
class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
|
398
|
+
>
|
|
399
|
+
<!-- Heart/pulse icon -->
|
|
400
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
401
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
|
|
402
|
+
</svg>
|
|
403
|
+
Heartbeat
|
|
404
|
+
</a>
|
|
405
|
+
<a
|
|
406
|
+
href="/api/ping"
|
|
407
|
+
class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
|
408
|
+
>
|
|
409
|
+
<!-- Signal/wifi icon -->
|
|
410
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
411
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.858 15.355-5.858 21.213 0" />
|
|
412
|
+
</svg>
|
|
413
|
+
API Ping
|
|
414
|
+
</a>
|
|
415
|
+
</div>
|
|
416
|
+
</aside>
|
|
417
|
+
|
|
418
|
+
<!-- Main area -->
|
|
419
|
+
<div class="flex-1 ml-60 flex flex-col min-h-screen">
|
|
420
|
+
<!-- Header -->
|
|
421
|
+
<header class="h-14 flex items-center justify-between px-6 border-b border-gray-200 dark:border-gray-800 bg-white/80 dark:bg-gray-950/90 backdrop-blur-md sticky top-0 z-20">
|
|
422
|
+
<h1 class="text-sm font-semibold text-gray-900 dark:text-white">Dashboard</h1>
|
|
379
423
|
<div class="flex items-center gap-3">
|
|
380
424
|
<!-- Dark/Light toggle -->
|
|
381
425
|
<button
|
|
@@ -400,80 +444,53 @@ onMounted(() => {
|
|
|
400
444
|
Logout
|
|
401
445
|
</button>
|
|
402
446
|
</div>
|
|
403
|
-
</
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
447
|
+
</header>
|
|
448
|
+
|
|
449
|
+
<!-- Content -->
|
|
450
|
+
<main class="flex-1 px-6 py-8 space-y-6">
|
|
451
|
+
<!-- Welcome card -->
|
|
452
|
+
<div class="bg-white dark:bg-gray-900/50 rounded-xl border border-gray-200 dark:border-gray-800 p-6 animate-fade-up">
|
|
453
|
+
<h2 class="text-lg font-bold text-gray-900 dark:text-white">Welcome back, {{ currentUser?.name }}</h2>
|
|
454
|
+
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">Here's what's happening with your application.</p>
|
|
455
|
+
</div>
|
|
412
456
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
>
|
|
419
|
-
<div class="flex items-center justify-between">
|
|
420
|
-
<div>
|
|
421
|
-
<h3 class="text-sm font-semibold text-gray-900 dark:text-gray-100">Heartbeat Dashboard</h3>
|
|
422
|
-
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Monitor application health</p>
|
|
423
|
-
</div>
|
|
424
|
-
<span class="text-emerald-600 dark:text-emerald-400 group-hover:translate-x-0.5 transition-transform">→</span>
|
|
457
|
+
<!-- Users table -->
|
|
458
|
+
<div class="bg-white dark:bg-gray-900/50 rounded-xl border border-gray-200 dark:border-gray-800 overflow-hidden animate-fade-up">
|
|
459
|
+
<div class="px-5 py-4 border-b border-gray-200 dark:border-gray-800 flex items-center justify-between">
|
|
460
|
+
<h2 class="text-sm font-bold text-gray-900 dark:text-gray-200">Users</h2>
|
|
461
|
+
<span class="text-xs text-gray-500 dark:text-gray-400">{{ loading ? 'Loading...' : users.length + ' total' }}</span>
|
|
425
462
|
</div>
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
463
|
+
<table class="w-full text-sm">
|
|
464
|
+
<thead>
|
|
465
|
+
<tr class="border-b border-gray-200 dark:border-gray-800 text-left text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
466
|
+
<th class="px-5 py-3 font-medium">Name</th>
|
|
467
|
+
<th class="px-5 py-3 font-medium">Email</th>
|
|
468
|
+
<th class="px-5 py-3 font-medium">Role</th>
|
|
469
|
+
</tr>
|
|
470
|
+
</thead>
|
|
471
|
+
<tbody class="divide-y divide-gray-100 dark:divide-gray-800/60">
|
|
472
|
+
<tr v-for="u in users" :key="u.id" class="hover:bg-gray-50 dark:hover:bg-gray-900/50 transition-colors">
|
|
473
|
+
<td class="px-5 py-3 text-gray-900 dark:text-gray-200">{{ u.name }}</td>
|
|
474
|
+
<td class="px-5 py-3 text-gray-500 dark:text-gray-400">{{ u.email }}</td>
|
|
475
|
+
<td class="px-5 py-3">
|
|
476
|
+
<span
|
|
477
|
+
:class="u.role === 'admin'
|
|
478
|
+
? 'bg-emerald-50 dark:bg-emerald-500/15 text-emerald-700 dark:text-emerald-300 border-emerald-200 dark:border-emerald-500/20'
|
|
479
|
+
: 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-700'"
|
|
480
|
+
class="text-[10px] px-2 py-0.5 rounded-full font-medium border"
|
|
481
|
+
>
|
|
482
|
+
{{ u.role }}
|
|
483
|
+
</span>
|
|
484
|
+
</td>
|
|
485
|
+
</tr>
|
|
486
|
+
<tr v-if="users.length === 0 && !loading">
|
|
487
|
+
<td colspan="3" class="px-5 py-8 text-center text-gray-400 dark:text-gray-600">No users found</td>
|
|
488
|
+
</tr>
|
|
489
|
+
</tbody>
|
|
490
|
+
</table>
|
|
446
491
|
</div>
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
<tr class="border-b border-gray-200 dark:border-gray-800 text-left text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
450
|
-
<th class="px-5 py-3 font-medium">Name</th>
|
|
451
|
-
<th class="px-5 py-3 font-medium">Email</th>
|
|
452
|
-
<th class="px-5 py-3 font-medium">Role</th>
|
|
453
|
-
</tr>
|
|
454
|
-
</thead>
|
|
455
|
-
<tbody class="divide-y divide-gray-100 dark:divide-gray-800/60">
|
|
456
|
-
<tr v-for="u in users" :key="u.id" class="hover:bg-gray-50 dark:hover:bg-gray-900/50 transition-colors">
|
|
457
|
-
<td class="px-5 py-3 text-gray-800 dark:text-gray-200">{{ u.name }}</td>
|
|
458
|
-
<td class="px-5 py-3 text-gray-500 dark:text-gray-400">{{ u.email }}</td>
|
|
459
|
-
<td class="px-5 py-3">
|
|
460
|
-
<span
|
|
461
|
-
:class="u.role === 'admin'
|
|
462
|
-
? 'bg-emerald-50 dark:bg-emerald-500/15 text-emerald-700 dark:text-emerald-300 border-emerald-200 dark:border-emerald-500/20'
|
|
463
|
-
: 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-700'"
|
|
464
|
-
class="text-[10px] px-2 py-0.5 rounded-full font-medium border"
|
|
465
|
-
>
|
|
466
|
-
{{ u.role }}
|
|
467
|
-
</span>
|
|
468
|
-
</td>
|
|
469
|
-
</tr>
|
|
470
|
-
<tr v-if="users.length === 0 && !loading">
|
|
471
|
-
<td colspan="3" class="px-5 py-8 text-center text-gray-400 dark:text-gray-600">No users found</td>
|
|
472
|
-
</tr>
|
|
473
|
-
</tbody>
|
|
474
|
-
</table>
|
|
475
|
-
</div>
|
|
476
|
-
</main>
|
|
492
|
+
</main>
|
|
493
|
+
</div>
|
|
477
494
|
</div>
|
|
478
495
|
</template>
|
|
479
496
|
`,
|
package/src/templates.ts
CHANGED
|
@@ -869,13 +869,13 @@ export default {
|
|
|
869
869
|
|
|
870
870
|
// ── routes/web.ts ───────────────────────────────────────────────────────
|
|
871
871
|
templates['routes/web.ts'] = `import type { Router } from '@mantiq/core'
|
|
872
|
-
import {
|
|
872
|
+
import { HomeController } from '../app/Http/Controllers/HomeController.ts'
|
|
873
873
|
import { PageController } from '../app/Http/Controllers/PageController.ts'
|
|
874
874
|
import { AuthController } from '../app/Http/Controllers/AuthController.ts'
|
|
875
875
|
|
|
876
876
|
export default function (router: Router) {
|
|
877
|
-
//
|
|
878
|
-
router.get('/',
|
|
877
|
+
// Welcome page — shows auth-aware buttons
|
|
878
|
+
router.get('/', [HomeController, 'index'])
|
|
879
879
|
|
|
880
880
|
// Page routes — each returns HTML (first load) or JSON (client navigation)
|
|
881
881
|
router.get('/dashboard', [PageController, 'dashboard']).middleware('auth')
|
|
@@ -904,6 +904,124 @@ export default function (router: Router) {
|
|
|
904
904
|
return MantiqResponse.json({ data: users.map((u: any) => u.toObject()) })
|
|
905
905
|
}).middleware('auth')
|
|
906
906
|
}
|
|
907
|
+
`
|
|
908
|
+
|
|
909
|
+
// ── HomeController (welcome page, auth-aware) ─────────────────────────
|
|
910
|
+
templates['app/Http/Controllers/HomeController.ts'] = `import type { MantiqRequest } from '@mantiq/core'
|
|
911
|
+
import { config } from '@mantiq/core'
|
|
912
|
+
import { auth } from '@mantiq/auth'
|
|
913
|
+
|
|
914
|
+
export class HomeController {
|
|
915
|
+
async index(request: MantiqRequest): Promise<Response> {
|
|
916
|
+
const appName = config('app.name') ?? 'MantiqJS'
|
|
917
|
+
const appEnv = config('app.env') ?? 'production'
|
|
918
|
+
const debug = config('app.debug') ? 'Enabled' : 'Disabled'
|
|
919
|
+
const bunVersion = typeof Bun !== 'undefined' ? Bun.version : 'unknown'
|
|
920
|
+
|
|
921
|
+
let mantiqVersion = '0.0.0'
|
|
922
|
+
try {
|
|
923
|
+
const pkg = await Bun.file(require.resolve('@mantiq/core/package.json')).json()
|
|
924
|
+
mantiqVersion = pkg.version
|
|
925
|
+
} catch { /* fallback */ }
|
|
926
|
+
|
|
927
|
+
// Check auth state
|
|
928
|
+
let isLoggedIn = false
|
|
929
|
+
let userName = ''
|
|
930
|
+
try {
|
|
931
|
+
const manager = auth()
|
|
932
|
+
manager.setRequest(request)
|
|
933
|
+
const user = await manager.user()
|
|
934
|
+
if (user) {
|
|
935
|
+
isLoggedIn = true
|
|
936
|
+
userName = (user as any).getAttribute?.('name') ?? ''
|
|
937
|
+
}
|
|
938
|
+
} catch { /* not authenticated or migration not run */ }
|
|
939
|
+
|
|
940
|
+
// Check if migration has been run (users table exists)
|
|
941
|
+
let migrated = false
|
|
942
|
+
try {
|
|
943
|
+
const { User } = await import('../../Models/User.ts')
|
|
944
|
+
await User.count()
|
|
945
|
+
migrated = true
|
|
946
|
+
} catch { /* table doesn't exist yet */ }
|
|
947
|
+
|
|
948
|
+
const authButtons = isLoggedIn
|
|
949
|
+
? \\\`<a class="l l-accent" href="/dashboard">Dashboard<span class="a">→</span></a>
|
|
950
|
+
<a class="l" href="/_heartbeat">Heartbeat<span class="a">→</span></a>\\\`
|
|
951
|
+
: migrated
|
|
952
|
+
? \\\`<a class="l l-accent" href="/login">Sign in<span class="a">→</span></a>
|
|
953
|
+
<a class="l" href="/register">Register<span class="a">→</span></a>\\\`
|
|
954
|
+
: \\\`<a class="l" href="/_heartbeat">Heartbeat<span class="a">→</span></a>
|
|
955
|
+
<a class="l" href="/api/ping">API Ping<span class="a">→</span></a>\\\`
|
|
956
|
+
|
|
957
|
+
const html = \\\`<!DOCTYPE html>
|
|
958
|
+
<html lang="en">
|
|
959
|
+
<head>
|
|
960
|
+
<meta charset="UTF-8">
|
|
961
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
962
|
+
<title>\\\${appName}</title>
|
|
963
|
+
<style>
|
|
964
|
+
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
965
|
+
body{
|
|
966
|
+
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;
|
|
967
|
+
background:#0a0a0b;color:#fafafa;min-height:100vh;
|
|
968
|
+
display:flex;align-items:center;justify-content:center;
|
|
969
|
+
-webkit-font-smoothing:antialiased;
|
|
970
|
+
}
|
|
971
|
+
.c{width:100%;max-width:460px;padding:32px;animation:up .5s ease}
|
|
972
|
+
@keyframes up{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}
|
|
973
|
+
.w{font-size:28px;font-weight:600;letter-spacing:-0.04em;color:#fafafa}
|
|
974
|
+
.w .d{color:#10b981}
|
|
975
|
+
.v{font-family:'SF Mono',ui-monospace,monospace;font-size:12px;color:#52525b;margin-top:6px}
|
|
976
|
+
hr{border:none;border-top:1px solid #1e1e1e;margin:24px 0}
|
|
977
|
+
.g{display:grid;grid-template-columns:1fr 1fr;gap:8px}
|
|
978
|
+
.l{
|
|
979
|
+
background:#111113;border:1px solid #1e1e1e;border-radius:8px;
|
|
980
|
+
padding:14px 16px;text-decoration:none;color:#a1a1aa;font-size:13px;
|
|
981
|
+
display:flex;align-items:center;justify-content:space-between;
|
|
982
|
+
transition:border-color .15s,color .15s;
|
|
983
|
+
}
|
|
984
|
+
.l:hover{border-color:#27272a;color:#34d399}
|
|
985
|
+
.l-accent{border-color:#10b981;color:#fafafa}
|
|
986
|
+
.l-accent:hover{background:#10b981;color:#0a0a0b}
|
|
987
|
+
.l .a{color:#52525b;font-size:11px;transition:color .15s}
|
|
988
|
+
.l:hover .a{color:#34d399}
|
|
989
|
+
.l-accent .a{color:#10b981}
|
|
990
|
+
.l-accent:hover .a{color:#0a0a0b}
|
|
991
|
+
.e{
|
|
992
|
+
margin-top:24px;font-family:'SF Mono',ui-monospace,monospace;
|
|
993
|
+
font-size:11px;color:#3f3f46;line-height:2;
|
|
994
|
+
}
|
|
995
|
+
.e span{color:#52525b}
|
|
996
|
+
.g2{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:8px}
|
|
997
|
+
</style>
|
|
998
|
+
</head>
|
|
999
|
+
<body>
|
|
1000
|
+
<div class="c">
|
|
1001
|
+
<div class="w"><span class="d">.</span>mantiq</div>
|
|
1002
|
+
<div class="v">v\\\${mantiqVersion} — \\\${appName}</div>
|
|
1003
|
+
<hr>
|
|
1004
|
+
<div class="g">
|
|
1005
|
+
\\\${authButtons}
|
|
1006
|
+
</div>
|
|
1007
|
+
<div class="g2">
|
|
1008
|
+
<a class="l" href="https://github.com/mantiqjs/mantiq" target="_blank" rel="noopener">GitHub<span class="a">↗</span></a>
|
|
1009
|
+
<a class="l" href="https://www.npmjs.com/org/mantiq" target="_blank" rel="noopener">npm<span class="a">↗</span></a>
|
|
1010
|
+
</div>
|
|
1011
|
+
<div class="e">
|
|
1012
|
+
<span>Runtime</span> Bun \\\${bunVersion}<br>
|
|
1013
|
+
<span>Environment</span> \\\${appEnv}<br>
|
|
1014
|
+
<span>Debug</span> \\\${debug}
|
|
1015
|
+
</div>
|
|
1016
|
+
</div>
|
|
1017
|
+
</body>
|
|
1018
|
+
</html>\\\`
|
|
1019
|
+
|
|
1020
|
+
return new Response(html, {
|
|
1021
|
+
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
|
1022
|
+
})
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
907
1025
|
`
|
|
908
1026
|
|
|
909
1027
|
// ── PageController (SSR + universal routing) ────────────────────────────
|