flarecms 0.1.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 (110) hide show
  1. package/README.md +73 -0
  2. package/dist/auth/index.js +40 -0
  3. package/dist/cli/commands.js +389 -0
  4. package/dist/cli/index.js +403 -0
  5. package/dist/cli/mcp.js +209 -0
  6. package/dist/db/index.js +164 -0
  7. package/dist/index.js +17626 -0
  8. package/package.json +105 -0
  9. package/scripts/fix-api-paths.mjs +32 -0
  10. package/scripts/fix-imports.mjs +38 -0
  11. package/scripts/prefix-css.mjs +45 -0
  12. package/src/api/lib/cache.ts +45 -0
  13. package/src/api/lib/response.ts +40 -0
  14. package/src/api/middlewares/auth.ts +186 -0
  15. package/src/api/middlewares/cors.ts +10 -0
  16. package/src/api/middlewares/rbac.ts +85 -0
  17. package/src/api/routes/auth.ts +377 -0
  18. package/src/api/routes/collections.ts +205 -0
  19. package/src/api/routes/content.ts +175 -0
  20. package/src/api/routes/device.ts +160 -0
  21. package/src/api/routes/magic.ts +150 -0
  22. package/src/api/routes/mcp.ts +273 -0
  23. package/src/api/routes/oauth.ts +160 -0
  24. package/src/api/routes/settings.ts +43 -0
  25. package/src/api/routes/setup.ts +307 -0
  26. package/src/api/routes/tokens.ts +80 -0
  27. package/src/api/schemas/auth.ts +15 -0
  28. package/src/api/schemas/index.ts +51 -0
  29. package/src/api/schemas/tokens.ts +24 -0
  30. package/src/auth/index.ts +28 -0
  31. package/src/cli/commands.ts +217 -0
  32. package/src/cli/index.ts +21 -0
  33. package/src/cli/mcp.ts +210 -0
  34. package/src/cli/tests/cli.test.ts +40 -0
  35. package/src/cli/tests/create.test.ts +87 -0
  36. package/src/client/FlareAdminRouter.tsx +47 -0
  37. package/src/client/app.tsx +175 -0
  38. package/src/client/components/app-sidebar.tsx +227 -0
  39. package/src/client/components/collection-modal.tsx +215 -0
  40. package/src/client/components/content-list.tsx +247 -0
  41. package/src/client/components/dynamic-form.tsx +190 -0
  42. package/src/client/components/field-modal.tsx +221 -0
  43. package/src/client/components/settings/api-token-section.tsx +400 -0
  44. package/src/client/components/settings/general-section.tsx +224 -0
  45. package/src/client/components/settings/security-section.tsx +154 -0
  46. package/src/client/components/settings/seo-section.tsx +200 -0
  47. package/src/client/components/settings/signup-section.tsx +257 -0
  48. package/src/client/components/ui/accordion.tsx +78 -0
  49. package/src/client/components/ui/avatar.tsx +107 -0
  50. package/src/client/components/ui/badge.tsx +52 -0
  51. package/src/client/components/ui/button.tsx +60 -0
  52. package/src/client/components/ui/card.tsx +103 -0
  53. package/src/client/components/ui/checkbox.tsx +27 -0
  54. package/src/client/components/ui/collapsible.tsx +19 -0
  55. package/src/client/components/ui/dialog.tsx +162 -0
  56. package/src/client/components/ui/icon-picker.tsx +485 -0
  57. package/src/client/components/ui/icons-data.ts +8476 -0
  58. package/src/client/components/ui/input.tsx +20 -0
  59. package/src/client/components/ui/label.tsx +20 -0
  60. package/src/client/components/ui/popover.tsx +91 -0
  61. package/src/client/components/ui/select.tsx +204 -0
  62. package/src/client/components/ui/separator.tsx +23 -0
  63. package/src/client/components/ui/sheet.tsx +141 -0
  64. package/src/client/components/ui/sidebar.tsx +722 -0
  65. package/src/client/components/ui/skeleton.tsx +13 -0
  66. package/src/client/components/ui/sonner.tsx +47 -0
  67. package/src/client/components/ui/switch.tsx +30 -0
  68. package/src/client/components/ui/table.tsx +116 -0
  69. package/src/client/components/ui/tabs.tsx +80 -0
  70. package/src/client/components/ui/textarea.tsx +18 -0
  71. package/src/client/components/ui/tooltip.tsx +68 -0
  72. package/src/client/hooks/use-mobile.ts +19 -0
  73. package/src/client/index.css +149 -0
  74. package/src/client/index.ts +7 -0
  75. package/src/client/layouts/admin-layout.tsx +93 -0
  76. package/src/client/layouts/settings-layout.tsx +104 -0
  77. package/src/client/lib/api.ts +72 -0
  78. package/src/client/lib/utils.ts +6 -0
  79. package/src/client/main.tsx +10 -0
  80. package/src/client/pages/collection-detail.tsx +634 -0
  81. package/src/client/pages/collections.tsx +180 -0
  82. package/src/client/pages/dashboard.tsx +133 -0
  83. package/src/client/pages/device.tsx +66 -0
  84. package/src/client/pages/document-detail-page.tsx +139 -0
  85. package/src/client/pages/documents-page.tsx +103 -0
  86. package/src/client/pages/login.tsx +345 -0
  87. package/src/client/pages/settings.tsx +65 -0
  88. package/src/client/pages/setup.tsx +129 -0
  89. package/src/client/pages/signup.tsx +188 -0
  90. package/src/client/store/auth.ts +30 -0
  91. package/src/client/store/collections.ts +13 -0
  92. package/src/client/store/config.ts +12 -0
  93. package/src/client/store/fetcher.ts +30 -0
  94. package/src/client/store/router.ts +95 -0
  95. package/src/client/store/schema.ts +39 -0
  96. package/src/client/store/settings.ts +31 -0
  97. package/src/client/types.ts +34 -0
  98. package/src/db/dynamic.ts +70 -0
  99. package/src/db/index.ts +16 -0
  100. package/src/db/migrations/001_initial_schema.ts +57 -0
  101. package/src/db/migrations/002_auth_tables.ts +84 -0
  102. package/src/db/migrator.ts +61 -0
  103. package/src/db/schema.ts +142 -0
  104. package/src/index.ts +12 -0
  105. package/src/server/index.ts +66 -0
  106. package/src/types.ts +20 -0
  107. package/style.css.d.ts +8 -0
  108. package/tests/css.test.ts +21 -0
  109. package/tests/modular.test.ts +29 -0
  110. package/tsconfig.json +10 -0
@@ -0,0 +1,345 @@
1
+ import { apiFetch } from '../lib/api';
2
+ import { $auth } from '../store/auth';
3
+ import { $router, navigate } from '../store/router';
4
+ import { $basePath } from '../store/config';
5
+ import { startAuthentication } from '@simplewebauthn/browser';
6
+ import {
7
+ Command,
8
+ FingerprintIcon,
9
+ KeyIcon,
10
+ Loader2Icon,
11
+ LockIcon,
12
+ MailIcon,
13
+ ShieldCheckIcon,
14
+ } from 'lucide-react';
15
+ import React, { useEffect, useState } from 'react';
16
+
17
+ import { Button } from '../components/ui/button';
18
+ import { Card, CardContent, CardFooter } from '../components/ui/card';
19
+ import { Input } from '../components/ui/input';
20
+ import { Label } from '../components/ui/label';
21
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '../components/ui/tabs';
22
+
23
+ export function LoginPage() {
24
+ const [email, setEmail] = useState('');
25
+ const [password, setPassword] = useState('');
26
+ const [loading, setLoading] = useState(false);
27
+ const [passkeyLoading, setPasskeyLoading] = useState(false);
28
+ const [error, setError] = useState('');
29
+ const [signupEnabled, setSignupEnabled] = useState(false);
30
+ const [activeTab, setActiveTab] = useState('credentials');
31
+
32
+ useEffect(() => {
33
+ async function checkSignup() {
34
+ try {
35
+ const res = await apiFetch('/auth/registration-settings');
36
+ const { data } = await res.json();
37
+ setSignupEnabled(data.enabled === 'true');
38
+ } catch (err) {
39
+ setSignupEnabled(false);
40
+ }
41
+ }
42
+ checkSignup();
43
+ }, []);
44
+
45
+ const handleLogin = async (e: React.FormEvent) => {
46
+ e.preventDefault();
47
+ setLoading(true);
48
+ setError('');
49
+
50
+ try {
51
+ const response = await apiFetch('/auth/login', {
52
+ method: 'POST',
53
+ headers: { 'Content-Type': 'application/json' },
54
+ body: JSON.stringify({ email, password }),
55
+ });
56
+
57
+ const data = await response.json();
58
+
59
+ if (response.ok) {
60
+ $auth.set({
61
+ token: data.data.token || 'cookie',
62
+ user: data.data.user || { email, role: 'admin' },
63
+ });
64
+ } else {
65
+ if (data.code === 'SETUP_REQUIRED') {
66
+ window.location.href = `${$basePath.get()}/setup`;
67
+ return;
68
+ }
69
+ setError(data.error || 'Invalid credentials');
70
+ }
71
+ } catch (err) {
72
+ setError('Connection failed. Is the API running?');
73
+ } finally {
74
+ setLoading(false);
75
+ }
76
+ };
77
+
78
+ const handlePasskeyLogin = async () => {
79
+ if (!email) {
80
+ setError('Enter your email to use Passkey login');
81
+ return;
82
+ }
83
+
84
+ setPasskeyLoading(true);
85
+ setError('');
86
+
87
+ try {
88
+ const optionsResponse = await apiFetch('/auth/passkey/options', {
89
+ method: 'POST',
90
+ headers: { 'Content-Type': 'application/json' },
91
+ body: JSON.stringify({ email }),
92
+ });
93
+
94
+ if (!optionsResponse.ok) {
95
+ const err = await optionsResponse.json();
96
+ throw new Error(err.error || 'Failed to get passkey options');
97
+ }
98
+
99
+ const data = await optionsResponse.json();
100
+ const asseResp = await startAuthentication({ optionsJSON: data.data });
101
+
102
+ const verifyResponse = await apiFetch('/auth/passkey/verify', {
103
+ method: 'POST',
104
+ headers: { 'Content-Type': 'application/json' },
105
+ body: JSON.stringify({
106
+ email,
107
+ response: asseResp,
108
+ }),
109
+ });
110
+
111
+ if (verifyResponse.ok) {
112
+ const data = await verifyResponse.json();
113
+ $auth.set({
114
+ token: 'cookie',
115
+ user: data.data.user || { email, role: 'admin' },
116
+ });
117
+ } else {
118
+ const err = await verifyResponse.json();
119
+ setError(err.error || 'Passkey verification failed');
120
+ }
121
+ } catch (err: any) {
122
+ console.error(err);
123
+ setError(err.message || 'Passkey authentication failed');
124
+ } finally {
125
+ setPasskeyLoading(false);
126
+ }
127
+ };
128
+
129
+ return (
130
+ <div className="min-h-screen bg-[oklch(0.985_0_0)] flex flex-col items-center justify-center p-6 antialiased selection:bg-primary/10">
131
+ <div className="w-full max-w-[400px] space-y-6">
132
+ <div className="flex flex-col items-center gap-4 mb-2">
133
+ <div className="size-12 rounded-2xl bg-primary text-primary-foreground flex items-center justify-center shadow-2xl shadow-primary/20">
134
+ <ShieldCheckIcon className="size-6" />
135
+ </div>
136
+ <div className="text-center">
137
+ <h1 className="text-xl font-bold tracking-tight">Access Flare</h1>
138
+ <p className="text-[10px] text-muted-foreground uppercase font-bold tracking-widest mt-1 opacity-60">
139
+ Nexus Administrative Portal
140
+ </p>
141
+ </div>
142
+ </div>
143
+
144
+ <Card className="py-0 shadow-xl border-border/50 overflow-hidden bg-background/80 backdrop-blur-xl">
145
+ <Tabs
146
+ value={activeTab}
147
+ onValueChange={setActiveTab}
148
+ className="w-full"
149
+ >
150
+ <div className="px-8 pt-8">
151
+ <TabsList className="grid grid-cols-2 w-full h-11 bg-muted/30 p-1 rounded-xl">
152
+ <TabsTrigger
153
+ value="credentials"
154
+ className="text-[10px] font-bold uppercase tracking-wider gap-2"
155
+ >
156
+ <KeyIcon className="size-3" />
157
+ Credentials
158
+ </TabsTrigger>
159
+ <TabsTrigger
160
+ value="passkey"
161
+ className="text-[10px] font-bold uppercase tracking-wider gap-2"
162
+ >
163
+ <FingerprintIcon className="size-3" />
164
+ Secure Key
165
+ </TabsTrigger>
166
+ </TabsList>
167
+ </div>
168
+
169
+ <CardContent className="px-8 pt-6 pb-8 min-h-[220px]">
170
+ <TabsContent
171
+ value="credentials"
172
+ className="space-y-4 focus-visible:outline-none"
173
+ >
174
+ <form onSubmit={handleLogin} className="space-y-4">
175
+ <div className="space-y-2">
176
+ <Label
177
+ htmlFor="email-cred"
178
+ className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground/70 ml-1"
179
+ >
180
+ Gateway Email
181
+ </Label>
182
+ <div className="relative group">
183
+ <MailIcon className="absolute left-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground/30 group-focus-within:text-primary transition-colors" />
184
+ <Input
185
+ id="email-cred"
186
+ type="email"
187
+ placeholder="name@nexus.io"
188
+ value={email}
189
+ onChange={(e) => setEmail(e.target.value)}
190
+ className="pl-10 h-11 text-sm bg-muted/5 border-muted-foreground/10 focus:bg-background transition-all"
191
+ required
192
+ />
193
+ </div>
194
+ </div>
195
+ <div className="space-y-2">
196
+ <div className="flex items-center justify-between ml-1">
197
+ <Label
198
+ htmlFor="password"
199
+ className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground/70"
200
+ >
201
+ Authorization Key
202
+ </Label>
203
+ <button
204
+ type="button"
205
+ className="text-[9px] text-primary font-bold uppercase tracking-widest opacity-40 hover:opacity-100 transition-opacity"
206
+ >
207
+ Lost Access?
208
+ </button>
209
+ </div>
210
+ <div className="relative group">
211
+ <LockIcon className="absolute left-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground/30 group-focus-within:text-primary transition-colors" />
212
+ <Input
213
+ id="password"
214
+ type="password"
215
+ placeholder="••••••••"
216
+ value={password}
217
+ onChange={(e) => setPassword(e.target.value)}
218
+ className="pl-10 h-11 text-sm bg-muted/5 border-muted-foreground/10 focus:bg-background transition-all"
219
+ required
220
+ />
221
+ </div>
222
+ </div>
223
+
224
+ {error && activeTab === 'credentials' && (
225
+ <div className="p-3 rounded-lg bg-destructive/5 border border-destructive/10 text-destructive text-[10px] font-bold text-center uppercase tracking-widest">
226
+ {error}
227
+ </div>
228
+ )}
229
+
230
+ <Button
231
+ type="submit"
232
+ className="w-full h-11 font-black text-[10px] uppercase tracking-[0.2em] shadow-lg shadow-primary/20"
233
+ disabled={loading}
234
+ >
235
+ {loading ? (
236
+ <Loader2Icon className="size-4 animate-spin" />
237
+ ) : (
238
+ 'Authorize Session'
239
+ )}
240
+ </Button>
241
+ </form>
242
+ </TabsContent>
243
+
244
+ <TabsContent
245
+ value="passkey"
246
+ className="space-y-6 focus-visible:outline-none"
247
+ >
248
+ <div className="space-y-4">
249
+ <div className="space-y-2 text-center pb-2">
250
+ <p className="text-[11px] text-muted-foreground font-medium leading-relaxed">
251
+ Authenticate instantly using your device's biometric
252
+ sensors or security key.
253
+ </p>
254
+ </div>
255
+
256
+ <div className="space-y-2">
257
+ <Label
258
+ htmlFor="email-pass"
259
+ className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground/70 ml-1"
260
+ >
261
+ Registered Email
262
+ </Label>
263
+ <div className="relative group">
264
+ <MailIcon className="absolute left-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground/30 group-focus-within:text-primary transition-colors" />
265
+ <Input
266
+ id="email-pass"
267
+ type="email"
268
+ placeholder="name@nexus.io"
269
+ value={email}
270
+ onChange={(e) => setEmail(e.target.value)}
271
+ className="pl-10 h-11 text-sm bg-muted/5 border-muted-foreground/10 focus:bg-background transition-all"
272
+ />
273
+ </div>
274
+ </div>
275
+
276
+ {error && activeTab === 'passkey' && (
277
+ <div className="p-3 rounded-lg bg-destructive/5 border border-destructive/10 text-destructive text-[10px] font-bold text-center uppercase tracking-widest">
278
+ {error}
279
+ </div>
280
+ )}
281
+
282
+ <div className="grid grid-cols-1 gap-3 pt-2">
283
+ <Button
284
+ type="button"
285
+ className="w-full h-14 font-black text-[11px] uppercase tracking-[0.2em] shadow-lg shadow-primary/10 flex flex-col gap-1 items-center justify-center py-8"
286
+ onClick={handlePasskeyLogin}
287
+ disabled={passkeyLoading}
288
+ >
289
+ {passkeyLoading ? (
290
+ <Loader2Icon className="size-5 animate-spin" />
291
+ ) : (
292
+ <>
293
+ <FingerprintIcon className="size-6" />
294
+ <span>Identify with Passkey</span>
295
+ </>
296
+ )}
297
+ </Button>
298
+ </div>
299
+ </div>
300
+ </TabsContent>
301
+ </CardContent>
302
+ </Tabs>
303
+
304
+ <CardFooter className="p-0 flex flex-col gap-6 px-8 pb-8 bg-muted/5 border-t border-muted-foreground/5">
305
+ <div className="relative w-full">
306
+ <div className="absolute inset-0 flex items-center">
307
+ <span className="w-full border-t border-muted-foreground/10" />
308
+ </div>
309
+ <div className="relative flex justify-center text-[9px] uppercase tracking-[0.3em]">
310
+ <span className="bg-background px-4 text-muted-foreground/40 font-bold">
311
+ External Auth
312
+ </span>
313
+ </div>
314
+ </div>
315
+
316
+ <Button
317
+ type="button"
318
+ variant="outline"
319
+ className="w-full h-11 text-[10px] font-bold uppercase tracking-widest flex gap-3 border-muted-foreground/20 hover:bg-background shadow-xs"
320
+ onClick={() => (window.location.href = '/api/oauth/github/login')}
321
+ >
322
+ <Command className="size-3.5" />
323
+ Continue with GitHub
324
+ </Button>
325
+
326
+ {signupEnabled && (
327
+ <div className="flex flex-col items-center gap-3 w-full">
328
+ <p className="text-[9px] text-muted-foreground/40 font-bold uppercase tracking-[0.2em]">
329
+ New Administrator?
330
+ </p>
331
+ <Button
332
+ variant="ghost"
333
+ className="w-full h-11 text-[10px] font-black uppercase tracking-[0.2em] text-primary hover:bg-primary/5 border border-primary/20 border-dashed"
334
+ onClick={() => navigate('signup')}
335
+ >
336
+ Deploy New Identity
337
+ </Button>
338
+ </div>
339
+ )}
340
+ </CardFooter>
341
+ </Card>
342
+ </div>
343
+ </div>
344
+ );
345
+ }
@@ -0,0 +1,65 @@
1
+ import { useStore } from '@nanostores/react';
2
+ import { $router } from '../store/router';
3
+ import { SettingsLayout } from '../layouts/settings-layout';
4
+ import { GeneralSection } from '../components/settings/general-section';
5
+ import { SEOSection } from '../components/settings/seo-section';
6
+ import { SecuritySection } from '../components/settings/security-section';
7
+ import { SignupSection } from '../components/settings/signup-section';
8
+ import { APITokenSection } from '../components/settings/api-token-section';
9
+
10
+ export function SettingsPage() {
11
+ const page = useStore($router);
12
+
13
+ if (!page) return null;
14
+
15
+ // Map routes to sections
16
+ switch (page.route) {
17
+ case 'settings_general':
18
+ return (
19
+ <SettingsLayout
20
+ title="General Configuration"
21
+ subtitle="Manage your platform's core identity and reading preferences."
22
+ >
23
+ <GeneralSection />
24
+ </SettingsLayout>
25
+ );
26
+ case 'settings_seo':
27
+ return (
28
+ <SettingsLayout
29
+ title="Search Engine Optimization"
30
+ subtitle="Optimize how your platform communicates with global indexers."
31
+ >
32
+ <SEOSection />
33
+ </SettingsLayout>
34
+ );
35
+ case 'settings_security':
36
+ return (
37
+ <SettingsLayout
38
+ title="Security & Access"
39
+ subtitle="Protect your administrative environment with modern credentials."
40
+ >
41
+ <SecuritySection />
42
+ </SettingsLayout>
43
+ );
44
+ case 'settings_signup':
45
+ return (
46
+ <SettingsLayout
47
+ title="Registration Policy"
48
+ subtitle="Control automated user onboarding and domain domain policies."
49
+ >
50
+ <SignupSection />
51
+ </SettingsLayout>
52
+ );
53
+ case 'settings':
54
+ case 'settings_api':
55
+ default:
56
+ return (
57
+ <SettingsLayout
58
+ title="Administrative API"
59
+ subtitle="Manage Personal Access Tokens for secure programmatic integration."
60
+ >
61
+ <APITokenSection />
62
+ </SettingsLayout>
63
+ );
64
+ }
65
+ }
@@ -0,0 +1,129 @@
1
+ import { useState } from 'react';
2
+ import type { FormEvent } from 'react';
3
+ import { apiFetch } from '../lib/api';
4
+ import { $router } from '../store/router';
5
+ import { $auth } from '../store/auth';
6
+ import { $basePath } from '../store/config';
7
+
8
+ export function SetupPage() {
9
+ const [loading, setLoading] = useState(false);
10
+ const [error, setError] = useState<string | null>(null);
11
+
12
+ const handleSetup = async (e: React.FormEvent<HTMLFormElement>) => {
13
+ e.preventDefault();
14
+ setLoading(true);
15
+ setError(null);
16
+
17
+ const formData = new FormData(e.currentTarget);
18
+ const title = formData.get('title');
19
+ const email = formData.get('email');
20
+ const password = formData.get('password');
21
+
22
+ try {
23
+ // 1. Run Setup (includes Migrations + Admin Creation + Auto-Login via Cookie)
24
+ const res = await apiFetch('/setup', {
25
+ method: 'POST',
26
+ body: JSON.stringify({ title, email, password }),
27
+ headers: { 'Content-Type': 'application/json' },
28
+ });
29
+
30
+ const data = await res.json();
31
+
32
+ if (!res.ok) {
33
+ throw new Error(data.error || 'Failed to complete setup');
34
+ }
35
+
36
+ // 2. Auth store update (The API already sets the 'session' cookie)
37
+ // We set the email in store for UI purposes
38
+ $auth.set({
39
+ token: data.data.token || 'cookie',
40
+ user: data.data.user,
41
+ });
42
+
43
+ // 3. Mandatory Refresh
44
+ // Using window.location instead of $router.open to ensure a clean application boot
45
+ // after the database and settings have been initialized.
46
+ window.location.href = $basePath.get();
47
+ } catch (err: any) {
48
+ setError(err.message || 'An error occurred during setup');
49
+ } finally {
50
+ setLoading(false);
51
+ }
52
+ };
53
+
54
+ return (
55
+ <div className="min-h-screen flex items-center justify-center bg-background p-6">
56
+ <div className="max-w-md w-full border border-border/40 shadow-sm rounded-lg p-8 bg-card relative overflow-hidden">
57
+ {/* Subtle decorative grid/line background */}
58
+ <div className="absolute inset-0 bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:14px_24px] pointer-events-none opacity-30"></div>
59
+
60
+ <div className="relative z-10">
61
+ <div className="text-center mb-8">
62
+ <h1 className="text-xl font-bold tracking-tight text-foreground">
63
+ Initial Setup
64
+ </h1>
65
+ <p className="text-[11px] font-semibold tracking-widest text-muted-foreground uppercase mt-2">
66
+ Configure FlareCMS Admin
67
+ </p>
68
+ </div>
69
+
70
+ {error && (
71
+ <div className="mb-6 p-3 bg-destructive/10 border border-destructive/20 rounded-md text-destructive text-[11px] uppercase tracking-wider font-semibold text-center">
72
+ {error}
73
+ </div>
74
+ )}
75
+
76
+ <form onSubmit={handleSetup} className="space-y-5">
77
+ <div className="space-y-1.5">
78
+ <label className="text-[10px] uppercase tracking-widest text-muted-foreground font-semibold">
79
+ Site Title
80
+ </label>
81
+ <input
82
+ name="title"
83
+ type="text"
84
+ required
85
+ className="w-full bg-background border border-border/50 text-sm px-3 py-2 rounded-md focus:outline-none focus:border-primary/50 focus:ring-1 focus:ring-primary/50 transition-all font-mono"
86
+ placeholder="My CMS"
87
+ />
88
+ </div>
89
+
90
+ <div className="space-y-1.5">
91
+ <label className="text-[10px] uppercase tracking-widest text-muted-foreground font-semibold">
92
+ Admin Email
93
+ </label>
94
+ <input
95
+ name="email"
96
+ type="email"
97
+ required
98
+ className="w-full bg-background border border-border/50 text-sm px-3 py-2 rounded-md focus:outline-none focus:border-primary/50 focus:ring-1 focus:ring-primary/50 transition-all font-mono"
99
+ placeholder="admin@example.com"
100
+ />
101
+ </div>
102
+
103
+ <div className="space-y-1.5">
104
+ <label className="text-[10px] uppercase tracking-widest text-muted-foreground font-semibold">
105
+ Admin Password
106
+ </label>
107
+ <input
108
+ name="password"
109
+ type="password"
110
+ required
111
+ minLength={6}
112
+ className="w-full bg-background border border-border/50 text-sm px-3 py-2 rounded-md focus:outline-none focus:border-primary/50 focus:ring-1 focus:ring-primary/50 transition-all font-mono"
113
+ placeholder="Min 6 characters"
114
+ />
115
+ </div>
116
+
117
+ <button
118
+ type="submit"
119
+ disabled={loading}
120
+ className="w-full bg-primary text-primary-foreground font-semibold py-2.5 px-4 rounded-md text-xs tracking-wider uppercase hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:ring-offset-2 focus:ring-offset-background transition-all disabled:opacity-50 disabled:cursor-not-allowed mt-2"
121
+ >
122
+ {loading ? 'Configuring...' : 'Complete Setup'}
123
+ </button>
124
+ </form>
125
+ </div>
126
+ </div>
127
+ </div>
128
+ );
129
+ }