create-mantiq 0.1.2 → 0.1.4
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/index.ts +23 -33
- package/src/kits/react.ts +114 -93
- package/src/kits/svelte.ts +133 -79
- package/src/kits/vue.ts +216 -81
- package/src/templates.ts +67 -10
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -5,11 +5,12 @@ import { randomBytes } from 'node:crypto'
|
|
|
5
5
|
import { getTemplates } from './templates.ts'
|
|
6
6
|
|
|
7
7
|
// ── ANSI helpers ─────────────────────────────────────────────────────────────
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
8
|
+
const R = '\x1b[0m'
|
|
9
|
+
const bold = (s: string) => `\x1b[1m${s}${R}`
|
|
10
|
+
const dim = (s: string) => `\x1b[2m${s}${R}`
|
|
11
|
+
const red = (s: string) => `\x1b[31m${s}${R}`
|
|
12
|
+
const emerald = (s: string) => `\x1b[38;2;52;211;153m${s}${R}`
|
|
13
|
+
const gray = (s: string) => `\x1b[90m${s}${R}`
|
|
13
14
|
|
|
14
15
|
// ── Parse args ───────────────────────────────────────────────────────────────
|
|
15
16
|
const rawArgs = process.argv.slice(2)
|
|
@@ -38,13 +39,13 @@ type Kit = typeof validKits[number]
|
|
|
38
39
|
|
|
39
40
|
if (!projectName) {
|
|
40
41
|
console.log(`
|
|
41
|
-
${
|
|
42
|
+
${emerald('mantiq')} ${dim('framework')}
|
|
42
43
|
|
|
43
44
|
${bold('Usage:')}
|
|
44
|
-
bun create mantiq ${
|
|
45
|
+
bun create mantiq ${emerald('<project-name>')} [options]
|
|
45
46
|
|
|
46
47
|
${bold('Options:')}
|
|
47
|
-
--kit=${
|
|
48
|
+
--kit=${emerald('react|vue|svelte')} Add a frontend starter kit
|
|
48
49
|
--no-git Skip git initialization
|
|
49
50
|
|
|
50
51
|
${bold('Examples:')}
|
|
@@ -69,8 +70,8 @@ if (existsSync(projectDir)) {
|
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
// ── Generate ─────────────────────────────────────────────────────────────────
|
|
72
|
-
const kitLabel = kit ? ` with ${bold(kit)}
|
|
73
|
-
console.log(`\n ${
|
|
73
|
+
const kitLabel = kit ? ` ${dim('with')} ${bold(kit)}` : ''
|
|
74
|
+
console.log(`\n ${emerald('mantiq')} Creating ${bold(projectName)}${kitLabel}\n`)
|
|
74
75
|
|
|
75
76
|
const appKey = `base64:${randomBytes(32).toString('base64')}`
|
|
76
77
|
const templates = getTemplates({ name: projectName, appKey, kit: kit as Kit | undefined })
|
|
@@ -82,8 +83,8 @@ for (const relativePath of files) {
|
|
|
82
83
|
mkdirSync(dirname(fullPath), { recursive: true })
|
|
83
84
|
await Bun.write(fullPath, templates[relativePath]!)
|
|
84
85
|
|
|
85
|
-
const display = relativePath.endsWith('.gitkeep') ? dim(relativePath) :
|
|
86
|
-
console.log(` ${
|
|
86
|
+
const display = relativePath.endsWith('.gitkeep') ? dim(relativePath) : relativePath
|
|
87
|
+
console.log(` ${emerald('+')} ${display}`)
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
// ── Install dependencies ─────────────────────────────────────────────────────
|
|
@@ -100,7 +101,6 @@ await install.exited
|
|
|
100
101
|
if (kit) {
|
|
101
102
|
console.log(`\n ${bold('Building frontend assets...')}\n`)
|
|
102
103
|
|
|
103
|
-
// Client build
|
|
104
104
|
const viteBuild = Bun.spawn(['npx', 'vite', 'build'], {
|
|
105
105
|
cwd: projectDir,
|
|
106
106
|
stdout: 'inherit',
|
|
@@ -108,7 +108,6 @@ if (kit) {
|
|
|
108
108
|
})
|
|
109
109
|
await viteBuild.exited
|
|
110
110
|
|
|
111
|
-
// SSR build
|
|
112
111
|
const ssrEntry = kit === 'react' ? 'src/ssr.tsx' : 'src/ssr.ts'
|
|
113
112
|
console.log(`\n ${bold('Building SSR bundle...')}\n`)
|
|
114
113
|
|
|
@@ -148,32 +147,23 @@ if (!noGit) {
|
|
|
148
147
|
|
|
149
148
|
// ── Done ─────────────────────────────────────────────────────────────────────
|
|
150
149
|
const frontendSteps = kit
|
|
151
|
-
? ` ${
|
|
150
|
+
? ` ${emerald('bun run')} dev:frontend ${dim('# start Vite dev server')}\n`
|
|
152
151
|
: ''
|
|
153
152
|
|
|
154
153
|
console.log(`
|
|
155
|
-
${
|
|
154
|
+
${emerald('mantiq')} ${bold(projectName)} ready.
|
|
156
155
|
|
|
157
156
|
${bold('Next steps:')}
|
|
158
157
|
|
|
159
|
-
${
|
|
160
|
-
${
|
|
161
|
-
${
|
|
162
|
-
${frontendSteps}
|
|
163
|
-
|
|
164
|
-
${bold('Included packages:')}
|
|
165
|
-
|
|
166
|
-
${dim('core · database · auth · validation · filesystem · logging')}
|
|
167
|
-
${dim('events · queue · realtime · heartbeat · helpers · cli')}
|
|
168
|
-
|
|
158
|
+
${emerald('cd')} ${projectName}
|
|
159
|
+
${emerald('bun mantiq')} migrate ${dim('# run database migrations')}
|
|
160
|
+
${emerald('bun run')} dev ${dim('# start development server')}
|
|
161
|
+
${frontendSteps}
|
|
169
162
|
${bold('Useful commands:')}
|
|
170
163
|
|
|
171
|
-
${
|
|
172
|
-
${
|
|
173
|
-
${
|
|
174
|
-
${cyan('bun mantiq')} make:job ${dim('# generate a new job class')}
|
|
175
|
-
|
|
176
|
-
${dim('Dashboard: http://localhost:3000/_heartbeat')}
|
|
164
|
+
${emerald('bun mantiq')} route:list ${dim('# list all registered routes')}
|
|
165
|
+
${emerald('bun mantiq')} make:model ${dim('# generate a new model')}
|
|
166
|
+
${emerald('bun mantiq')} about ${dim('# framework environment info')}
|
|
177
167
|
|
|
178
|
-
${dim('
|
|
168
|
+
${dim('Dashboard http://localhost:3000/_heartbeat')}
|
|
179
169
|
`)
|
package/src/kits/react.ts
CHANGED
|
@@ -21,6 +21,12 @@ export default defineConfig({
|
|
|
21
21
|
`,
|
|
22
22
|
|
|
23
23
|
'src/style.css': `@import "tailwindcss";
|
|
24
|
+
|
|
25
|
+
@keyframes fadeUp {
|
|
26
|
+
from { opacity: 0; transform: translateY(8px); }
|
|
27
|
+
to { opacity: 1; transform: translateY(0); }
|
|
28
|
+
}
|
|
29
|
+
.animate-fade-up { animation: fadeUp 0.4s ease-out; }
|
|
24
30
|
`,
|
|
25
31
|
|
|
26
32
|
'src/pages.ts': `import Login from './pages/Login.tsx'
|
|
@@ -73,6 +79,15 @@ interface MantiqAppProps {
|
|
|
73
79
|
initialData?: Record<string, any>
|
|
74
80
|
}
|
|
75
81
|
|
|
82
|
+
function initTheme() {
|
|
83
|
+
if (typeof window === 'undefined') return
|
|
84
|
+
const theme = localStorage.getItem('theme') ||
|
|
85
|
+
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
|
|
86
|
+
document.documentElement.classList.toggle('dark', theme === 'dark')
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
initTheme()
|
|
90
|
+
|
|
76
91
|
export function MantiqApp({ pages, initialData }: MantiqAppProps) {
|
|
77
92
|
const windowData = typeof window !== 'undefined' ? (window as any).__MANTIQ_DATA__ : {}
|
|
78
93
|
const initial = initialData ?? windowData
|
|
@@ -135,49 +150,35 @@ export default function Login({ appName = '${ctx.name}', navigate }: LoginProps)
|
|
|
135
150
|
}
|
|
136
151
|
|
|
137
152
|
return (
|
|
138
|
-
<div className="min-h-screen bg-gray-950 flex">
|
|
139
|
-
<div className="
|
|
140
|
-
<div className="
|
|
141
|
-
|
|
142
|
-
<div className="flex items-center gap-3">
|
|
143
|
-
<div className="w-12 h-12 rounded-xl bg-indigo-600/20 border border-indigo-500/30 flex items-center justify-center">
|
|
144
|
-
<svg className="w-6 h-6 text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
145
|
-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
146
|
-
</svg>
|
|
147
|
-
</div>
|
|
148
|
-
<span className="text-2xl font-bold text-white">{appName}</span>
|
|
149
|
-
</div>
|
|
150
|
-
<h2 className="text-4xl font-bold text-white leading-tight">Build something<br />amazing.</h2>
|
|
151
|
-
<p className="text-gray-400 text-lg leading-relaxed">
|
|
152
|
-
Session auth, encrypted cookies, CSRF protection — all wired up and ready to go.
|
|
153
|
-
</p>
|
|
153
|
+
<div className="min-h-screen bg-gray-50 dark:bg-gray-950 flex items-center justify-center p-4">
|
|
154
|
+
<div className="animate-fade-up w-full max-w-sm">
|
|
155
|
+
<div className="text-center mb-8">
|
|
156
|
+
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">{appName}</h2>
|
|
154
157
|
</div>
|
|
155
|
-
|
|
156
|
-
<div className="flex-1 flex items-center justify-center p-8">
|
|
157
|
-
<div className="bg-gray-900 rounded-xl border border-gray-800 w-full max-w-md p-8 space-y-6">
|
|
158
|
+
<div className="bg-white dark:bg-gray-900/50 rounded-xl border border-gray-200 dark:border-gray-800 p-8 space-y-6 shadow-sm">
|
|
158
159
|
<div>
|
|
159
|
-
<h1 className="text-xl font-bold text-white">Welcome back</h1>
|
|
160
|
-
<p className="text-sm text-gray-500 mt-1">Sign in to your account</p>
|
|
160
|
+
<h1 className="text-xl font-bold text-gray-900 dark:text-white">Welcome back</h1>
|
|
161
|
+
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">Sign in to your account</p>
|
|
161
162
|
</div>
|
|
162
|
-
{error && <div className="bg-red-500/10 border border-red-500/30 text-red-400 rounded-lg px-3.5 py-2.5 text-sm">{error}</div>}
|
|
163
|
+
{error && <div className="bg-red-50 dark:bg-red-500/10 border border-red-200 dark:border-red-500/30 text-red-600 dark:text-red-400 rounded-lg px-3.5 py-2.5 text-sm">{error}</div>}
|
|
163
164
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
164
|
-
<div className="space-y-1">
|
|
165
|
-
<label className="block text-sm font-medium text-gray-
|
|
165
|
+
<div className="space-y-1.5">
|
|
166
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">Email</label>
|
|
166
167
|
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required
|
|
167
|
-
className="w-full bg-gray-900 border border-gray-
|
|
168
|
+
className="w-full bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-lg px-3.5 py-2.5 text-sm text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-600 focus:outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500/25 transition-all" />
|
|
168
169
|
</div>
|
|
169
|
-
<div className="space-y-1">
|
|
170
|
-
<label className="block text-sm font-medium text-gray-
|
|
170
|
+
<div className="space-y-1.5">
|
|
171
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">Password</label>
|
|
171
172
|
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} required
|
|
172
|
-
className="w-full bg-gray-900 border border-gray-
|
|
173
|
+
className="w-full bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-lg px-3.5 py-2.5 text-sm text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-600 focus:outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500/25 transition-all" />
|
|
173
174
|
</div>
|
|
174
175
|
<button type="submit" disabled={loading}
|
|
175
|
-
className="w-full bg-
|
|
176
|
-
Sign in
|
|
176
|
+
className="w-full bg-emerald-600 hover:bg-emerald-500 disabled:opacity-50 disabled:cursor-not-allowed text-white font-semibold text-sm py-2.5 rounded-lg transition-colors">
|
|
177
|
+
{loading ? 'Signing in...' : 'Sign in'}
|
|
177
178
|
</button>
|
|
178
179
|
</form>
|
|
179
|
-
<p className="text-sm text-gray-500 text-center">
|
|
180
|
-
Don't have an account? <a href="/register" className="text-
|
|
180
|
+
<p className="text-sm text-gray-500 dark:text-gray-400 text-center">
|
|
181
|
+
Don't have an account? <a href="/register" className="text-emerald-600 dark:text-emerald-400 hover:text-emerald-500 dark:hover:text-emerald-300 font-medium">Register</a>
|
|
181
182
|
</p>
|
|
182
183
|
</div>
|
|
183
184
|
</div>
|
|
@@ -211,54 +212,40 @@ export default function Register({ appName = '${ctx.name}', navigate }: Register
|
|
|
211
212
|
}
|
|
212
213
|
|
|
213
214
|
return (
|
|
214
|
-
<div className="min-h-screen bg-gray-950 flex">
|
|
215
|
-
<div className="
|
|
216
|
-
<div className="
|
|
217
|
-
|
|
218
|
-
<div className="flex items-center gap-3">
|
|
219
|
-
<div className="w-12 h-12 rounded-xl bg-indigo-600/20 border border-indigo-500/30 flex items-center justify-center">
|
|
220
|
-
<svg className="w-6 h-6 text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
221
|
-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
222
|
-
</svg>
|
|
223
|
-
</div>
|
|
224
|
-
<span className="text-2xl font-bold text-white">{appName}</span>
|
|
225
|
-
</div>
|
|
226
|
-
<h2 className="text-4xl font-bold text-white leading-tight">Build something<br />amazing.</h2>
|
|
227
|
-
<p className="text-gray-400 text-lg leading-relaxed">
|
|
228
|
-
Session auth, encrypted cookies, CSRF protection — all wired up and ready to go.
|
|
229
|
-
</p>
|
|
215
|
+
<div className="min-h-screen bg-gray-50 dark:bg-gray-950 flex items-center justify-center p-4">
|
|
216
|
+
<div className="animate-fade-up w-full max-w-sm">
|
|
217
|
+
<div className="text-center mb-8">
|
|
218
|
+
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">{appName}</h2>
|
|
230
219
|
</div>
|
|
231
|
-
|
|
232
|
-
<div className="flex-1 flex items-center justify-center p-8">
|
|
233
|
-
<div className="bg-gray-900 rounded-xl border border-gray-800 w-full max-w-md p-8 space-y-6">
|
|
220
|
+
<div className="bg-white dark:bg-gray-900/50 rounded-xl border border-gray-200 dark:border-gray-800 p-8 space-y-6 shadow-sm">
|
|
234
221
|
<div>
|
|
235
|
-
<h1 className="text-xl font-bold text-white">Create an account</h1>
|
|
236
|
-
<p className="text-sm text-gray-500 mt-1">Get started with {appName}</p>
|
|
222
|
+
<h1 className="text-xl font-bold text-gray-900 dark:text-white">Create an account</h1>
|
|
223
|
+
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">Get started with {appName}</p>
|
|
237
224
|
</div>
|
|
238
|
-
{error && <div className="bg-red-500/10 border border-red-500/30 text-red-400 rounded-lg px-3.5 py-2.5 text-sm">{error}</div>}
|
|
225
|
+
{error && <div className="bg-red-50 dark:bg-red-500/10 border border-red-200 dark:border-red-500/30 text-red-600 dark:text-red-400 rounded-lg px-3.5 py-2.5 text-sm">{error}</div>}
|
|
239
226
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
240
|
-
<div className="space-y-1">
|
|
241
|
-
<label className="block text-sm font-medium text-gray-
|
|
227
|
+
<div className="space-y-1.5">
|
|
228
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">Name</label>
|
|
242
229
|
<input value={name} onChange={(e) => setName(e.target.value)} required
|
|
243
|
-
className="w-full bg-gray-900 border border-gray-
|
|
230
|
+
className="w-full bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-lg px-3.5 py-2.5 text-sm text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-600 focus:outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500/25 transition-all" />
|
|
244
231
|
</div>
|
|
245
|
-
<div className="space-y-1">
|
|
246
|
-
<label className="block text-sm font-medium text-gray-
|
|
232
|
+
<div className="space-y-1.5">
|
|
233
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">Email</label>
|
|
247
234
|
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required
|
|
248
|
-
className="w-full bg-gray-900 border border-gray-
|
|
235
|
+
className="w-full bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-lg px-3.5 py-2.5 text-sm text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-600 focus:outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500/25 transition-all" />
|
|
249
236
|
</div>
|
|
250
|
-
<div className="space-y-1">
|
|
251
|
-
<label className="block text-sm font-medium text-gray-
|
|
237
|
+
<div className="space-y-1.5">
|
|
238
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">Password</label>
|
|
252
239
|
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} required
|
|
253
|
-
className="w-full bg-gray-900 border border-gray-
|
|
240
|
+
className="w-full bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-lg px-3.5 py-2.5 text-sm text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-600 focus:outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500/25 transition-all" />
|
|
254
241
|
</div>
|
|
255
242
|
<button type="submit" disabled={loading}
|
|
256
|
-
className="w-full bg-
|
|
257
|
-
Create account
|
|
243
|
+
className="w-full bg-emerald-600 hover:bg-emerald-500 disabled:opacity-50 disabled:cursor-not-allowed text-white font-semibold text-sm py-2.5 rounded-lg transition-colors">
|
|
244
|
+
{loading ? 'Creating account...' : 'Create account'}
|
|
258
245
|
</button>
|
|
259
246
|
</form>
|
|
260
|
-
<p className="text-sm text-gray-500 text-center">
|
|
261
|
-
Already have an account? <a href="/login" className="text-
|
|
247
|
+
<p className="text-sm text-gray-500 dark:text-gray-400 text-center">
|
|
248
|
+
Already have an account? <a href="/login" className="text-emerald-600 dark:text-emerald-400 hover:text-emerald-500 dark:hover:text-emerald-300 font-medium">Sign in</a>
|
|
262
249
|
</p>
|
|
263
250
|
</div>
|
|
264
251
|
</div>
|
|
@@ -283,6 +270,15 @@ interface DashboardProps {
|
|
|
283
270
|
export default function Dashboard({ appName = '${ctx.name}', currentUser, users: initialUsers, navigate }: DashboardProps) {
|
|
284
271
|
const [users, setUsers] = useState<User[]>(initialUsers ?? [])
|
|
285
272
|
const [loading, setLoading] = useState(!initialUsers?.length)
|
|
273
|
+
const [isDark, setIsDark] = useState(() =>
|
|
274
|
+
typeof document !== 'undefined' ? document.documentElement.classList.contains('dark') : true
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
const toggleTheme = () => {
|
|
278
|
+
const dark = document.documentElement.classList.toggle('dark')
|
|
279
|
+
localStorage.setItem('theme', dark ? 'dark' : 'light')
|
|
280
|
+
setIsDark(dark)
|
|
281
|
+
}
|
|
286
282
|
|
|
287
283
|
const fetchUsers = useCallback(async () => {
|
|
288
284
|
setLoading(true)
|
|
@@ -301,60 +297,85 @@ export default function Dashboard({ appName = '${ctx.name}', currentUser, users:
|
|
|
301
297
|
}
|
|
302
298
|
|
|
303
299
|
return (
|
|
304
|
-
<div className="min-h-screen bg-gray-950 text-gray-100">
|
|
305
|
-
<nav className="border-b border-gray-800/80 bg-gray-950/90 backdrop-blur-md sticky top-0 z-20">
|
|
300
|
+
<div className="min-h-screen bg-gray-50 dark:bg-gray-950 text-gray-900 dark:text-gray-100">
|
|
301
|
+
<nav className="border-b border-gray-200 dark:border-gray-800/80 bg-white/90 dark:bg-gray-950/90 backdrop-blur-md sticky top-0 z-20">
|
|
306
302
|
<div className="max-w-5xl mx-auto px-6 h-14 flex items-center justify-between">
|
|
307
|
-
<
|
|
308
|
-
<div className="w-7 h-7 rounded-lg bg-indigo-600/20 border border-indigo-500/30 flex items-center justify-center">
|
|
309
|
-
<svg className="w-3.5 h-3.5 text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
310
|
-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
311
|
-
</svg>
|
|
312
|
-
</div>
|
|
313
|
-
<span className="text-sm font-bold text-white">{appName}</span>
|
|
314
|
-
</div>
|
|
303
|
+
<span className="text-sm font-bold text-gray-900 dark:text-white">{appName}</span>
|
|
315
304
|
<div className="flex items-center gap-3">
|
|
316
|
-
<
|
|
305
|
+
<button onClick={toggleTheme} className="text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white p-1.5 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors" title="Toggle theme">
|
|
306
|
+
{isDark ? (
|
|
307
|
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
308
|
+
<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" />
|
|
309
|
+
</svg>
|
|
310
|
+
) : (
|
|
311
|
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
312
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
|
313
|
+
</svg>
|
|
314
|
+
)}
|
|
315
|
+
</button>
|
|
316
|
+
<span className="text-xs text-gray-500 dark:text-gray-400">{currentUser?.name}</span>
|
|
317
317
|
<button onClick={handleLogout}
|
|
318
|
-
className="text-xs text-gray-
|
|
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">
|
|
319
319
|
Logout
|
|
320
320
|
</button>
|
|
321
321
|
</div>
|
|
322
322
|
</div>
|
|
323
323
|
</nav>
|
|
324
324
|
|
|
325
|
-
<main className="max-w-5xl mx-auto px-6 py-8 space-y-6">
|
|
325
|
+
<main className="max-w-5xl mx-auto px-6 py-8 space-y-6 animate-fade-up">
|
|
326
326
|
<div>
|
|
327
|
-
<h1 className="text-xl font-bold text-white">
|
|
328
|
-
<p className="text-sm text-gray-500 mt-1">
|
|
327
|
+
<h1 className="text-xl font-bold text-gray-900 dark:text-white">Welcome back, {currentUser?.name}</h1>
|
|
328
|
+
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">Here's what's happening with your application.</p>
|
|
329
|
+
</div>
|
|
330
|
+
|
|
331
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
332
|
+
<a href="/heartbeat" className="group bg-white dark:bg-gray-900/50 rounded-xl border border-gray-200 dark:border-gray-800 p-5 hover:border-emerald-500/40 transition-colors">
|
|
333
|
+
<div className="flex items-center justify-between">
|
|
334
|
+
<div>
|
|
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>
|
|
339
|
+
</div>
|
|
340
|
+
</a>
|
|
341
|
+
<a href="/api/ping" className="group bg-white dark:bg-gray-900/50 rounded-xl border border-gray-200 dark:border-gray-800 p-5 hover:border-emerald-500/40 transition-colors">
|
|
342
|
+
<div className="flex items-center justify-between">
|
|
343
|
+
<div>
|
|
344
|
+
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-200">API Ping</h3>
|
|
345
|
+
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">Test API connectivity</p>
|
|
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>
|
|
329
350
|
</div>
|
|
330
351
|
|
|
331
|
-
<div className="bg-gray-900 rounded-xl border border-gray-800 overflow-hidden">
|
|
332
|
-
<div className="px-5 py-4 border-b border-gray-800 flex items-center justify-between">
|
|
333
|
-
<h2 className="text-sm font-bold text-gray-200">Users</h2>
|
|
334
|
-
<span className="text-xs text-gray-500">{loading ? 'Loading...' : \`\${users.length} total\`}</span>
|
|
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>
|
|
335
356
|
</div>
|
|
336
357
|
<table className="w-full text-sm">
|
|
337
358
|
<thead>
|
|
338
|
-
<tr className="border-b border-gray-800 text-left text-xs text-gray-500 uppercase tracking-wider">
|
|
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">
|
|
339
360
|
<th className="px-5 py-3 font-medium">Name</th>
|
|
340
361
|
<th className="px-5 py-3 font-medium">Email</th>
|
|
341
362
|
<th className="px-5 py-3 font-medium">Role</th>
|
|
342
363
|
</tr>
|
|
343
364
|
</thead>
|
|
344
|
-
<tbody className="divide-y divide-gray-800/60">
|
|
365
|
+
<tbody className="divide-y divide-gray-100 dark:divide-gray-800/60">
|
|
345
366
|
{users.map((u) => (
|
|
346
|
-
<tr key={u.id} className="hover:bg-gray-900/50 transition-colors">
|
|
347
|
-
<td className="px-5 py-3 text-gray-200">{u.name}</td>
|
|
348
|
-
<td className="px-5 py-3 text-gray-400">{u.email}</td>
|
|
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>
|
|
349
370
|
<td className="px-5 py-3">
|
|
350
371
|
<span className={\`text-[10px] px-2 py-0.5 rounded-full font-medium \${
|
|
351
|
-
u.role === 'admin' ? 'bg-
|
|
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'
|
|
352
373
|
}\`}>{u.role}</span>
|
|
353
374
|
</td>
|
|
354
375
|
</tr>
|
|
355
376
|
))}
|
|
356
377
|
{users.length === 0 && !loading && (
|
|
357
|
-
<tr><td colSpan={3} className="px-5 py-8 text-center text-gray-600">No users found</td></tr>
|
|
378
|
+
<tr><td colSpan={3} className="px-5 py-8 text-center text-gray-400 dark:text-gray-600">No users found</td></tr>
|
|
358
379
|
)}
|
|
359
380
|
</tbody>
|
|
360
381
|
</table>
|