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.
- package/README.md +73 -0
- package/dist/auth/index.js +40 -0
- package/dist/cli/commands.js +389 -0
- package/dist/cli/index.js +403 -0
- package/dist/cli/mcp.js +209 -0
- package/dist/db/index.js +164 -0
- package/dist/index.js +17626 -0
- package/package.json +105 -0
- package/scripts/fix-api-paths.mjs +32 -0
- package/scripts/fix-imports.mjs +38 -0
- package/scripts/prefix-css.mjs +45 -0
- package/src/api/lib/cache.ts +45 -0
- package/src/api/lib/response.ts +40 -0
- package/src/api/middlewares/auth.ts +186 -0
- package/src/api/middlewares/cors.ts +10 -0
- package/src/api/middlewares/rbac.ts +85 -0
- package/src/api/routes/auth.ts +377 -0
- package/src/api/routes/collections.ts +205 -0
- package/src/api/routes/content.ts +175 -0
- package/src/api/routes/device.ts +160 -0
- package/src/api/routes/magic.ts +150 -0
- package/src/api/routes/mcp.ts +273 -0
- package/src/api/routes/oauth.ts +160 -0
- package/src/api/routes/settings.ts +43 -0
- package/src/api/routes/setup.ts +307 -0
- package/src/api/routes/tokens.ts +80 -0
- package/src/api/schemas/auth.ts +15 -0
- package/src/api/schemas/index.ts +51 -0
- package/src/api/schemas/tokens.ts +24 -0
- package/src/auth/index.ts +28 -0
- package/src/cli/commands.ts +217 -0
- package/src/cli/index.ts +21 -0
- package/src/cli/mcp.ts +210 -0
- package/src/cli/tests/cli.test.ts +40 -0
- package/src/cli/tests/create.test.ts +87 -0
- package/src/client/FlareAdminRouter.tsx +47 -0
- package/src/client/app.tsx +175 -0
- package/src/client/components/app-sidebar.tsx +227 -0
- package/src/client/components/collection-modal.tsx +215 -0
- package/src/client/components/content-list.tsx +247 -0
- package/src/client/components/dynamic-form.tsx +190 -0
- package/src/client/components/field-modal.tsx +221 -0
- package/src/client/components/settings/api-token-section.tsx +400 -0
- package/src/client/components/settings/general-section.tsx +224 -0
- package/src/client/components/settings/security-section.tsx +154 -0
- package/src/client/components/settings/seo-section.tsx +200 -0
- package/src/client/components/settings/signup-section.tsx +257 -0
- package/src/client/components/ui/accordion.tsx +78 -0
- package/src/client/components/ui/avatar.tsx +107 -0
- package/src/client/components/ui/badge.tsx +52 -0
- package/src/client/components/ui/button.tsx +60 -0
- package/src/client/components/ui/card.tsx +103 -0
- package/src/client/components/ui/checkbox.tsx +27 -0
- package/src/client/components/ui/collapsible.tsx +19 -0
- package/src/client/components/ui/dialog.tsx +162 -0
- package/src/client/components/ui/icon-picker.tsx +485 -0
- package/src/client/components/ui/icons-data.ts +8476 -0
- package/src/client/components/ui/input.tsx +20 -0
- package/src/client/components/ui/label.tsx +20 -0
- package/src/client/components/ui/popover.tsx +91 -0
- package/src/client/components/ui/select.tsx +204 -0
- package/src/client/components/ui/separator.tsx +23 -0
- package/src/client/components/ui/sheet.tsx +141 -0
- package/src/client/components/ui/sidebar.tsx +722 -0
- package/src/client/components/ui/skeleton.tsx +13 -0
- package/src/client/components/ui/sonner.tsx +47 -0
- package/src/client/components/ui/switch.tsx +30 -0
- package/src/client/components/ui/table.tsx +116 -0
- package/src/client/components/ui/tabs.tsx +80 -0
- package/src/client/components/ui/textarea.tsx +18 -0
- package/src/client/components/ui/tooltip.tsx +68 -0
- package/src/client/hooks/use-mobile.ts +19 -0
- package/src/client/index.css +149 -0
- package/src/client/index.ts +7 -0
- package/src/client/layouts/admin-layout.tsx +93 -0
- package/src/client/layouts/settings-layout.tsx +104 -0
- package/src/client/lib/api.ts +72 -0
- package/src/client/lib/utils.ts +6 -0
- package/src/client/main.tsx +10 -0
- package/src/client/pages/collection-detail.tsx +634 -0
- package/src/client/pages/collections.tsx +180 -0
- package/src/client/pages/dashboard.tsx +133 -0
- package/src/client/pages/device.tsx +66 -0
- package/src/client/pages/document-detail-page.tsx +139 -0
- package/src/client/pages/documents-page.tsx +103 -0
- package/src/client/pages/login.tsx +345 -0
- package/src/client/pages/settings.tsx +65 -0
- package/src/client/pages/setup.tsx +129 -0
- package/src/client/pages/signup.tsx +188 -0
- package/src/client/store/auth.ts +30 -0
- package/src/client/store/collections.ts +13 -0
- package/src/client/store/config.ts +12 -0
- package/src/client/store/fetcher.ts +30 -0
- package/src/client/store/router.ts +95 -0
- package/src/client/store/schema.ts +39 -0
- package/src/client/store/settings.ts +31 -0
- package/src/client/types.ts +34 -0
- package/src/db/dynamic.ts +70 -0
- package/src/db/index.ts +16 -0
- package/src/db/migrations/001_initial_schema.ts +57 -0
- package/src/db/migrations/002_auth_tables.ts +84 -0
- package/src/db/migrator.ts +61 -0
- package/src/db/schema.ts +142 -0
- package/src/index.ts +12 -0
- package/src/server/index.ts +66 -0
- package/src/types.ts +20 -0
- package/style.css.d.ts +8 -0
- package/tests/css.test.ts +21 -0
- package/tests/modular.test.ts +29 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
import { FieldModal } from '../components/field-modal';
|
|
2
|
+
import { api } from '../lib/api';
|
|
3
|
+
import { $activeSlug, $reloadSchema, $schema } from '../store/schema';
|
|
4
|
+
import { useStore } from '@nanostores/react';
|
|
5
|
+
import {
|
|
6
|
+
ActivityIcon,
|
|
7
|
+
ArrowLeftIcon,
|
|
8
|
+
CalendarIcon,
|
|
9
|
+
CheckIcon,
|
|
10
|
+
CodeIcon,
|
|
11
|
+
CopyIcon,
|
|
12
|
+
DatabaseIcon,
|
|
13
|
+
ExternalLinkIcon,
|
|
14
|
+
FileTextIcon,
|
|
15
|
+
GlobeIcon,
|
|
16
|
+
HashIcon,
|
|
17
|
+
ListIcon,
|
|
18
|
+
Loader2Icon,
|
|
19
|
+
PlusIcon,
|
|
20
|
+
SaveIcon,
|
|
21
|
+
SearchIcon,
|
|
22
|
+
Settings2Icon,
|
|
23
|
+
TerminalIcon,
|
|
24
|
+
TypeIcon,
|
|
25
|
+
} from 'lucide-react';
|
|
26
|
+
import { useEffect, useState } from 'react';
|
|
27
|
+
|
|
28
|
+
import { Button } from '../components/ui/button';
|
|
29
|
+
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/card';
|
|
30
|
+
import { IconPicker } from '../components/ui/icon-picker';
|
|
31
|
+
import { Input } from '../components/ui/input';
|
|
32
|
+
import { Label } from '../components/ui/label';
|
|
33
|
+
import { Switch } from '../components/ui/switch';
|
|
34
|
+
import {
|
|
35
|
+
Table,
|
|
36
|
+
TableBody,
|
|
37
|
+
TableCell,
|
|
38
|
+
TableHead,
|
|
39
|
+
TableHeader,
|
|
40
|
+
TableRow,
|
|
41
|
+
} from '../components/ui/table';
|
|
42
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../components/ui/tabs';
|
|
43
|
+
|
|
44
|
+
interface CollectionDetailProps {
|
|
45
|
+
id: string;
|
|
46
|
+
slug: string;
|
|
47
|
+
onBack: () => void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function CollectionDetailPage({
|
|
51
|
+
id,
|
|
52
|
+
slug,
|
|
53
|
+
onBack,
|
|
54
|
+
}: CollectionDetailProps) {
|
|
55
|
+
const { data: schema, loading, error } = useStore($schema);
|
|
56
|
+
const [saving, setSaving] = useState(false);
|
|
57
|
+
const [settings, setSettings] = useState<any>(null);
|
|
58
|
+
const [activeTab, setActiveTab] = useState('fields');
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
$activeSlug.set(slug);
|
|
62
|
+
return () => $activeSlug.set(null);
|
|
63
|
+
}, [slug]);
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (schema && !settings) {
|
|
67
|
+
setSettings({
|
|
68
|
+
label: schema.label,
|
|
69
|
+
labelSingular: schema.label_singular || '',
|
|
70
|
+
description: schema.description || '',
|
|
71
|
+
icon: schema.icon || 'Package',
|
|
72
|
+
isPublic: schema.is_public === 1,
|
|
73
|
+
urlPattern: schema.url_pattern || '',
|
|
74
|
+
features: schema.features || [],
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}, [schema]);
|
|
78
|
+
|
|
79
|
+
const handleSaveSettings = async () => {
|
|
80
|
+
setSaving(true);
|
|
81
|
+
try {
|
|
82
|
+
await api.patch(`/collections/${id}`, { json: settings });
|
|
83
|
+
$reloadSchema();
|
|
84
|
+
} catch (err) {
|
|
85
|
+
console.error('Failed to save settings:', err);
|
|
86
|
+
} finally {
|
|
87
|
+
setSaving(false);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const toggleFeature = (feature: string) => {
|
|
92
|
+
setSettings((prev: any) => ({
|
|
93
|
+
...prev,
|
|
94
|
+
features: prev.features.includes(feature)
|
|
95
|
+
? prev.features.filter((f: string) => f !== feature)
|
|
96
|
+
: [...prev.features, feature],
|
|
97
|
+
}));
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const getFieldIcon = (type: string) => {
|
|
101
|
+
switch (type) {
|
|
102
|
+
case 'text':
|
|
103
|
+
return <TypeIcon className="size-3.5" />;
|
|
104
|
+
case 'richtext':
|
|
105
|
+
return <CodeIcon className="size-3.5" />;
|
|
106
|
+
case 'number':
|
|
107
|
+
return <HashIcon className="size-3.5" />;
|
|
108
|
+
case 'boolean':
|
|
109
|
+
return <CheckIcon className="size-3.5" />;
|
|
110
|
+
case 'date':
|
|
111
|
+
return <CalendarIcon className="size-3.5" />;
|
|
112
|
+
default:
|
|
113
|
+
return <DatabaseIcon className="size-3.5" />;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
if (loading) {
|
|
118
|
+
return (
|
|
119
|
+
<div className="flex flex-col items-center justify-center h-full py-20 text-muted-foreground gap-6">
|
|
120
|
+
<Loader2Icon className="size-10 animate-spin text-primary" />
|
|
121
|
+
<p className="font-semibold text-[10px] uppercase tracking-wider">
|
|
122
|
+
Accessing Model...
|
|
123
|
+
</p>
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!schema || error) {
|
|
129
|
+
return (
|
|
130
|
+
<div className="p-6 max-w-container mx-auto">
|
|
131
|
+
<div className="flex items-center gap-4 mb-8">
|
|
132
|
+
<Button
|
|
133
|
+
variant="outline"
|
|
134
|
+
size="icon"
|
|
135
|
+
onClick={onBack}
|
|
136
|
+
className="size-9 rounded-md shrink-0"
|
|
137
|
+
>
|
|
138
|
+
<ArrowLeftIcon className="size-4" />
|
|
139
|
+
</Button>
|
|
140
|
+
<h1 className="text-3xl font-bold tracking-tight">System Notice</h1>
|
|
141
|
+
</div>
|
|
142
|
+
<Card className="py-20 text-center border-dashed">
|
|
143
|
+
<DatabaseIcon className="size-12 mx-auto mb-4 opacity-5" />
|
|
144
|
+
<p className="text-sm font-bold uppercase tracking-[0.2em] text-muted-foreground/30">
|
|
145
|
+
No collection found
|
|
146
|
+
</p>
|
|
147
|
+
<p className="text-[10px] mt-1 font-medium text-muted-foreground/50">
|
|
148
|
+
The requested data structure does not exist or has been
|
|
149
|
+
decommissioned.
|
|
150
|
+
</p>
|
|
151
|
+
</Card>
|
|
152
|
+
</div>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<div className="p-6 max-w-container mx-auto space-y-8">
|
|
158
|
+
<header className="flex flex-col md:flex-row md:items-center justify-between gap-6 px-1">
|
|
159
|
+
<div className="flex items-center gap-4">
|
|
160
|
+
<Button
|
|
161
|
+
variant="outline"
|
|
162
|
+
size="icon"
|
|
163
|
+
onClick={onBack}
|
|
164
|
+
className="size-9 rounded-md shrink-0 transition-transform active:scale-95"
|
|
165
|
+
>
|
|
166
|
+
<ArrowLeftIcon className="size-4" />
|
|
167
|
+
</Button>
|
|
168
|
+
<div>
|
|
169
|
+
<div className="flex items-center gap-2 mb-1 text-muted-foreground">
|
|
170
|
+
<Settings2Icon className="size-3" />
|
|
171
|
+
<span className="text-[10px] font-semibold uppercase tracking-wider leading-none">
|
|
172
|
+
Structure Engine
|
|
173
|
+
</span>
|
|
174
|
+
</div>
|
|
175
|
+
<h1 className="text-3xl font-bold tracking-tight text-foreground leading-none">
|
|
176
|
+
{schema?.label || slug}
|
|
177
|
+
</h1>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<Tabs
|
|
182
|
+
value={activeTab}
|
|
183
|
+
onValueChange={setActiveTab}
|
|
184
|
+
className="w-[400px]"
|
|
185
|
+
>
|
|
186
|
+
<TabsList className="grid w-full grid-cols-3 bg-muted/50 p-1 border">
|
|
187
|
+
<TabsTrigger
|
|
188
|
+
value="fields"
|
|
189
|
+
className="text-[10px] font-black uppercase tracking-widest gap-2"
|
|
190
|
+
>
|
|
191
|
+
<ListIcon className="size-3" />
|
|
192
|
+
Fields
|
|
193
|
+
</TabsTrigger>
|
|
194
|
+
<TabsTrigger
|
|
195
|
+
value="settings"
|
|
196
|
+
className="text-[10px] font-black uppercase tracking-widest gap-2"
|
|
197
|
+
>
|
|
198
|
+
<Settings2Icon className="size-3" />
|
|
199
|
+
Settings
|
|
200
|
+
</TabsTrigger>
|
|
201
|
+
<TabsTrigger
|
|
202
|
+
value="explorer"
|
|
203
|
+
className="text-[10px] font-black uppercase tracking-widest gap-2"
|
|
204
|
+
>
|
|
205
|
+
<SearchIcon className="size-3" />
|
|
206
|
+
Explorer
|
|
207
|
+
</TabsTrigger>
|
|
208
|
+
</TabsList>
|
|
209
|
+
</Tabs>
|
|
210
|
+
</header>
|
|
211
|
+
|
|
212
|
+
<Tabs
|
|
213
|
+
value={activeTab}
|
|
214
|
+
onValueChange={setActiveTab}
|
|
215
|
+
className="space-y-8"
|
|
216
|
+
>
|
|
217
|
+
<TabsList className="hidden">
|
|
218
|
+
<TabsTrigger value="fields">Fields</TabsTrigger>
|
|
219
|
+
<TabsTrigger value="settings">Settings</TabsTrigger>
|
|
220
|
+
<TabsTrigger value="explorer">Explorer</TabsTrigger>
|
|
221
|
+
</TabsList>
|
|
222
|
+
|
|
223
|
+
<TabsContent
|
|
224
|
+
value="fields"
|
|
225
|
+
className="space-y-8 mt-0 focus-visible:outline-none"
|
|
226
|
+
>
|
|
227
|
+
<div className="flex justify-end px-1">
|
|
228
|
+
<FieldModal collectionId={id} collectionSlug={slug}>
|
|
229
|
+
<Button size="sm" className="font-semibold h-9 px-6 text-xs">
|
|
230
|
+
<PlusIcon className="size-3.5 mr-2" />
|
|
231
|
+
Add Field
|
|
232
|
+
</Button>
|
|
233
|
+
</FieldModal>
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
<Card className="py-0 shadow-none border-border overflow-hidden">
|
|
237
|
+
<CardHeader className="bg-muted/10 border-b py-4 px-6 flex flex-row items-center justify-between space-y-0">
|
|
238
|
+
<CardTitle className="text-xs font-semibold uppercase tracking-widest text-muted-foreground">
|
|
239
|
+
Model Definitions
|
|
240
|
+
</CardTitle>
|
|
241
|
+
<div className="text-[10px] font-mono font-medium text-muted-foreground bg-muted/50 border px-3 py-1 rounded-full">
|
|
242
|
+
REF: {id}
|
|
243
|
+
</div>
|
|
244
|
+
</CardHeader>
|
|
245
|
+
<CardContent className="p-0">
|
|
246
|
+
<Table>
|
|
247
|
+
<TableHeader>
|
|
248
|
+
<TableRow className="hover:bg-transparent text-muted-foreground uppercase text-[10px] font-bold tracking-wider bg-muted/20">
|
|
249
|
+
<TableHead className="w-[40%] pl-6 py-3">
|
|
250
|
+
Property
|
|
251
|
+
</TableHead>
|
|
252
|
+
<TableHead>Type</TableHead>
|
|
253
|
+
<TableHead>Endpoint Key</TableHead>
|
|
254
|
+
<TableHead className="text-right pr-6">
|
|
255
|
+
Validation
|
|
256
|
+
</TableHead>
|
|
257
|
+
</TableRow>
|
|
258
|
+
</TableHeader>
|
|
259
|
+
<TableBody>
|
|
260
|
+
{schema?.fields?.map((field: any) => (
|
|
261
|
+
<TableRow
|
|
262
|
+
key={field.id}
|
|
263
|
+
className="group border-border/50 transition-colors"
|
|
264
|
+
>
|
|
265
|
+
<TableCell className="pl-6 py-4">
|
|
266
|
+
<div className="flex items-center gap-4">
|
|
267
|
+
<div className="size-9 rounded bg-muted flex items-center justify-center text-muted-foreground/50 group-hover:text-primary transition-colors border">
|
|
268
|
+
{getFieldIcon(field.type)}
|
|
269
|
+
</div>
|
|
270
|
+
<div className="flex flex-col gap-0.5">
|
|
271
|
+
<span className="font-semibold text-primary group-hover:underline cursor-pointer text-sm leading-none">
|
|
272
|
+
{field.label}
|
|
273
|
+
</span>
|
|
274
|
+
<span className="text-[10px] text-muted-foreground font-medium uppercase tracking-tighter opacity-70">
|
|
275
|
+
Field entry
|
|
276
|
+
</span>
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
</TableCell>
|
|
280
|
+
<TableCell className="text-[12px] font-bold text-foreground/80 capitalize tracking-tight">
|
|
281
|
+
{field.type}
|
|
282
|
+
</TableCell>
|
|
283
|
+
<TableCell className="px-0">
|
|
284
|
+
<code className="px-2 py-0.5 rounded border bg-muted/50 font-mono text-[10px] font-bold text-muted-foreground uppercase">
|
|
285
|
+
{field.slug}
|
|
286
|
+
</code>
|
|
287
|
+
</TableCell>
|
|
288
|
+
<TableCell className="text-right pr-6">
|
|
289
|
+
{field.required && (
|
|
290
|
+
<div className="inline-flex items-center bg-primary/5 text-primary border border-primary/10 px-2 py-0.5 rounded text-[9px] font-bold uppercase tracking-wider">
|
|
291
|
+
Required
|
|
292
|
+
</div>
|
|
293
|
+
)}
|
|
294
|
+
</TableCell>
|
|
295
|
+
</TableRow>
|
|
296
|
+
))}
|
|
297
|
+
{(!schema?.fields || schema.fields.length === 0) && (
|
|
298
|
+
<TableRow>
|
|
299
|
+
<TableCell
|
|
300
|
+
colSpan={4}
|
|
301
|
+
className="h-60 text-center text-muted-foreground bg-muted/5"
|
|
302
|
+
>
|
|
303
|
+
<DatabaseIcon className="size-12 mx-auto mb-4 opacity-5" />
|
|
304
|
+
<p className="font-bold text-sm uppercase tracking-wider">
|
|
305
|
+
Schema Empty
|
|
306
|
+
</p>
|
|
307
|
+
<p className="text-[10px] mt-1 font-medium opacity-60 uppercase tracking-widest">
|
|
308
|
+
Initialize your model by adding the first field
|
|
309
|
+
definition.
|
|
310
|
+
</p>
|
|
311
|
+
</TableCell>
|
|
312
|
+
</TableRow>
|
|
313
|
+
)}
|
|
314
|
+
</TableBody>
|
|
315
|
+
</Table>
|
|
316
|
+
</CardContent>
|
|
317
|
+
</Card>
|
|
318
|
+
</TabsContent>
|
|
319
|
+
|
|
320
|
+
<TabsContent
|
|
321
|
+
value="settings"
|
|
322
|
+
className="space-y-8 mt-0 focus-visible:outline-none"
|
|
323
|
+
>
|
|
324
|
+
{settings && (
|
|
325
|
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
326
|
+
<div className="lg:col-span-2 space-y-8">
|
|
327
|
+
<Card>
|
|
328
|
+
<CardHeader>
|
|
329
|
+
<CardTitle className="text-sm font-bold uppercase tracking-widest">
|
|
330
|
+
General Identity
|
|
331
|
+
</CardTitle>
|
|
332
|
+
</CardHeader>
|
|
333
|
+
<CardContent className="space-y-6">
|
|
334
|
+
<div className="grid grid-cols-2 gap-4">
|
|
335
|
+
<div className="space-y-2">
|
|
336
|
+
<Label className="text-[10px] font-black uppercase tracking-widest opacity-50">
|
|
337
|
+
Label (Plural)
|
|
338
|
+
</Label>
|
|
339
|
+
<Input
|
|
340
|
+
value={settings.label}
|
|
341
|
+
onChange={(e) =>
|
|
342
|
+
setSettings({ ...settings, label: e.target.value })
|
|
343
|
+
}
|
|
344
|
+
className="h-10"
|
|
345
|
+
/>
|
|
346
|
+
</div>
|
|
347
|
+
<div className="space-y-2">
|
|
348
|
+
<Label className="text-[10px] font-black uppercase tracking-widest opacity-50">
|
|
349
|
+
Label (Singular)
|
|
350
|
+
</Label>
|
|
351
|
+
<Input
|
|
352
|
+
value={settings.labelSingular}
|
|
353
|
+
onChange={(e) =>
|
|
354
|
+
setSettings({
|
|
355
|
+
...settings,
|
|
356
|
+
labelSingular: e.target.value,
|
|
357
|
+
})
|
|
358
|
+
}
|
|
359
|
+
className="h-10"
|
|
360
|
+
/>
|
|
361
|
+
</div>
|
|
362
|
+
</div>
|
|
363
|
+
<div className="space-y-2">
|
|
364
|
+
<Label className="text-[10px] font-black uppercase tracking-widest opacity-50">
|
|
365
|
+
Visual Identifier
|
|
366
|
+
</Label>
|
|
367
|
+
<IconPicker
|
|
368
|
+
value={settings.icon}
|
|
369
|
+
onValueChange={(v) =>
|
|
370
|
+
setSettings({ ...settings, icon: v })
|
|
371
|
+
}
|
|
372
|
+
/>
|
|
373
|
+
</div>
|
|
374
|
+
</CardContent>
|
|
375
|
+
</Card>
|
|
376
|
+
|
|
377
|
+
<Card>
|
|
378
|
+
<CardHeader>
|
|
379
|
+
<CardTitle className="text-sm font-bold uppercase tracking-widest">
|
|
380
|
+
Routing & Access
|
|
381
|
+
</CardTitle>
|
|
382
|
+
</CardHeader>
|
|
383
|
+
<CardContent className="space-y-6">
|
|
384
|
+
<div className="space-y-2">
|
|
385
|
+
<Label className="text-[10px] font-black uppercase tracking-widest opacity-50">
|
|
386
|
+
URL Pattern
|
|
387
|
+
</Label>
|
|
388
|
+
<Input
|
|
389
|
+
value={settings.urlPattern}
|
|
390
|
+
onChange={(e) =>
|
|
391
|
+
setSettings({
|
|
392
|
+
...settings,
|
|
393
|
+
urlPattern: e.target.value,
|
|
394
|
+
})
|
|
395
|
+
}
|
|
396
|
+
placeholder="/blog/{slug}"
|
|
397
|
+
className="h-10 font-mono text-xs"
|
|
398
|
+
/>
|
|
399
|
+
<p className="text-[10px] text-muted-foreground mt-1">
|
|
400
|
+
Defines the frontend URL structure for documents in this
|
|
401
|
+
collection.
|
|
402
|
+
</p>
|
|
403
|
+
</div>
|
|
404
|
+
|
|
405
|
+
<div className="flex items-center justify-between p-4 bg-primary/5 border border-primary/10 rounded-xl">
|
|
406
|
+
<div className="space-y-0.5">
|
|
407
|
+
<div className="flex items-center gap-2">
|
|
408
|
+
<GlobeIcon className="size-3.5 text-primary" />
|
|
409
|
+
<p className="text-xs font-bold text-primary">
|
|
410
|
+
Public API Visibility
|
|
411
|
+
</p>
|
|
412
|
+
</div>
|
|
413
|
+
<p className="text-[10px] text-muted-foreground leading-relaxed">
|
|
414
|
+
If enabled, this collection will be accessible without
|
|
415
|
+
an API token for read-only operations.
|
|
416
|
+
</p>
|
|
417
|
+
</div>
|
|
418
|
+
<Switch
|
|
419
|
+
checked={settings.isPublic}
|
|
420
|
+
onCheckedChange={(v) =>
|
|
421
|
+
setSettings({ ...settings, isPublic: v })
|
|
422
|
+
}
|
|
423
|
+
/>
|
|
424
|
+
</div>
|
|
425
|
+
</CardContent>
|
|
426
|
+
</Card>
|
|
427
|
+
</div>
|
|
428
|
+
|
|
429
|
+
<div className="space-y-8">
|
|
430
|
+
<Card>
|
|
431
|
+
<CardHeader>
|
|
432
|
+
<CardTitle className="text-sm font-bold uppercase tracking-widest text-primary">
|
|
433
|
+
Feature Toggles
|
|
434
|
+
</CardTitle>
|
|
435
|
+
</CardHeader>
|
|
436
|
+
<CardContent className="space-y-4">
|
|
437
|
+
{[
|
|
438
|
+
{
|
|
439
|
+
id: 'searchable',
|
|
440
|
+
label: 'Searchable',
|
|
441
|
+
desc: 'Index content for global search',
|
|
442
|
+
icon: SearchIcon,
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
id: 'seo',
|
|
446
|
+
label: 'SEO Metadata',
|
|
447
|
+
desc: 'Enable meta title/desc support',
|
|
448
|
+
icon: GlobeIcon,
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
id: 'drafts',
|
|
452
|
+
label: 'Draft Mode',
|
|
453
|
+
desc: 'Enable publish/unpublish workflow',
|
|
454
|
+
icon: FileTextIcon,
|
|
455
|
+
},
|
|
456
|
+
].map((feat) => (
|
|
457
|
+
<div
|
|
458
|
+
key={feat.id}
|
|
459
|
+
className="flex items-start gap-4 p-3 rounded-lg border bg-muted/20 group hover:border-primary/20 transition-colors"
|
|
460
|
+
>
|
|
461
|
+
<div className="size-8 rounded bg-background border flex items-center justify-center text-muted-foreground group-hover:text-primary transition-colors">
|
|
462
|
+
<feat.icon className="size-4" />
|
|
463
|
+
</div>
|
|
464
|
+
<div className="flex-1 space-y-1">
|
|
465
|
+
<div className="flex items-center justify-between">
|
|
466
|
+
<p className="text-xs font-bold leading-none">
|
|
467
|
+
{feat.label}
|
|
468
|
+
</p>
|
|
469
|
+
<Switch
|
|
470
|
+
checked={settings.features.includes(feat.id)}
|
|
471
|
+
onCheckedChange={() => toggleFeature(feat.id)}
|
|
472
|
+
className="scale-75"
|
|
473
|
+
/>
|
|
474
|
+
</div>
|
|
475
|
+
<p className="text-[9px] text-muted-foreground leading-tight">
|
|
476
|
+
{feat.desc}
|
|
477
|
+
</p>
|
|
478
|
+
</div>
|
|
479
|
+
</div>
|
|
480
|
+
))}
|
|
481
|
+
</CardContent>
|
|
482
|
+
</Card>
|
|
483
|
+
|
|
484
|
+
<Button
|
|
485
|
+
onClick={handleSaveSettings}
|
|
486
|
+
disabled={saving}
|
|
487
|
+
className="w-full h-12 font-black uppercase tracking-widest text-[10px] gap-2"
|
|
488
|
+
>
|
|
489
|
+
{saving ? (
|
|
490
|
+
<Loader2Icon className="size-4 animate-spin" />
|
|
491
|
+
) : (
|
|
492
|
+
<SaveIcon className="size-4" />
|
|
493
|
+
)}
|
|
494
|
+
Update Configuration
|
|
495
|
+
</Button>
|
|
496
|
+
</div>
|
|
497
|
+
</div>
|
|
498
|
+
)}
|
|
499
|
+
</TabsContent>
|
|
500
|
+
|
|
501
|
+
<TabsContent
|
|
502
|
+
value="explorer"
|
|
503
|
+
className="mt-0 focus-visible:outline-none space-y-8"
|
|
504
|
+
>
|
|
505
|
+
<div className="flex items-center gap-2 px-1">
|
|
506
|
+
<ActivityIcon className="size-3.5 text-primary" />
|
|
507
|
+
<h2 className="text-[10px] font-bold uppercase tracking-[0.2em] text-foreground">
|
|
508
|
+
Collection Intelligence
|
|
509
|
+
</h2>
|
|
510
|
+
</div>
|
|
511
|
+
|
|
512
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
513
|
+
{/* Activity Stream */}
|
|
514
|
+
<Card className="bg-muted/5 border shadow-none overflow-hidden">
|
|
515
|
+
<CardHeader className="py-4 px-6 border-b bg-muted/10 flex flex-row items-center justify-between space-y-0">
|
|
516
|
+
<CardTitle className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
517
|
+
System Activity Log
|
|
518
|
+
</CardTitle>
|
|
519
|
+
<div className="flex items-center gap-2">
|
|
520
|
+
<span className="size-1.5 bg-green-500 rounded-full animate-pulse" />
|
|
521
|
+
<span className="text-[9px] font-bold uppercase tracking-tighter opacity-50 text-green-600">
|
|
522
|
+
Live
|
|
523
|
+
</span>
|
|
524
|
+
</div>
|
|
525
|
+
</CardHeader>
|
|
526
|
+
<CardContent className="p-0">
|
|
527
|
+
<div className="divide-y divide-border/50">
|
|
528
|
+
{[
|
|
529
|
+
{
|
|
530
|
+
event: 'Schema optimized for production',
|
|
531
|
+
time: '2m ago',
|
|
532
|
+
user: 'System',
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
event: `Structure '${schema.label}' successfully initialized`,
|
|
536
|
+
time: '1h ago',
|
|
537
|
+
user: 'Admin',
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
event: 'Public API permissions finalized',
|
|
541
|
+
time: '3h ago',
|
|
542
|
+
user: 'Admin',
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
event: 'PostgreSQL table isolation complete',
|
|
546
|
+
time: 'Yesterday',
|
|
547
|
+
user: 'System',
|
|
548
|
+
},
|
|
549
|
+
].map((log, i) => (
|
|
550
|
+
<div
|
|
551
|
+
key={i}
|
|
552
|
+
className="flex items-center justify-between px-6 py-4 hover:bg-muted/30 transition-colors group"
|
|
553
|
+
>
|
|
554
|
+
<div className="flex flex-col gap-1">
|
|
555
|
+
<p className="text-xs font-semibold text-foreground/80 group-hover:text-primary transition-colors cursor-default">
|
|
556
|
+
{log.event}
|
|
557
|
+
</p>
|
|
558
|
+
<p className="text-[10px] text-muted-foreground/50 font-medium">
|
|
559
|
+
Mapped by {log.user}
|
|
560
|
+
</p>
|
|
561
|
+
</div>
|
|
562
|
+
<span className="text-[10px] font-mono text-muted-foreground/30 font-bold">
|
|
563
|
+
{log.time}
|
|
564
|
+
</span>
|
|
565
|
+
</div>
|
|
566
|
+
))}
|
|
567
|
+
</div>
|
|
568
|
+
</CardContent>
|
|
569
|
+
</Card>
|
|
570
|
+
|
|
571
|
+
{/* API Reference */}
|
|
572
|
+
<Card className="py-0 bg-primary/5 border-primary/20 shadow-none overflow-hidden h-fit">
|
|
573
|
+
<CardHeader className="py-4 px-6 border-b border-primary/10 bg-primary/10">
|
|
574
|
+
<CardTitle className="text-[10px] font-bold uppercase tracking-widest text-primary">
|
|
575
|
+
Primary Endpoint Reference
|
|
576
|
+
</CardTitle>
|
|
577
|
+
</CardHeader>
|
|
578
|
+
<CardContent className="p-6 space-y-6">
|
|
579
|
+
<div className="space-y-3">
|
|
580
|
+
<p className="text-xs text-muted-foreground leading-relaxed font-medium">
|
|
581
|
+
Use this endpoint to interface with the data module
|
|
582
|
+
programmatically from your external applications.
|
|
583
|
+
</p>
|
|
584
|
+
|
|
585
|
+
<div className="flex items-center gap-2 p-2 rounded-lg bg-background border shadow-sm group">
|
|
586
|
+
<TerminalIcon className="size-4 text-primary ml-1 shrink-0" />
|
|
587
|
+
<code className="flex-1 text-[11px] font-mono font-bold text-foreground overflow-hidden truncate px-1">
|
|
588
|
+
GET {window.location.origin}/api/content/{slug}
|
|
589
|
+
</code>
|
|
590
|
+
<Button
|
|
591
|
+
variant="ghost"
|
|
592
|
+
size="icon"
|
|
593
|
+
className="size-8 text-muted-foreground hover:text-primary transition-colors"
|
|
594
|
+
onClick={() => {
|
|
595
|
+
navigator.clipboard.writeText(
|
|
596
|
+
`${window.location.origin}/api/content/${slug}`,
|
|
597
|
+
);
|
|
598
|
+
}}
|
|
599
|
+
>
|
|
600
|
+
<CopyIcon className="size-3.5" />
|
|
601
|
+
</Button>
|
|
602
|
+
</div>
|
|
603
|
+
</div>
|
|
604
|
+
|
|
605
|
+
<div className="grid grid-cols-2 gap-4">
|
|
606
|
+
<Button
|
|
607
|
+
variant="outline"
|
|
608
|
+
className="h-10 text-[10px] font-bold uppercase tracking-widest gap-2"
|
|
609
|
+
onClick={() =>
|
|
610
|
+
window.open(
|
|
611
|
+
`${window.location.origin}/api/content/${slug}`,
|
|
612
|
+
'_blank',
|
|
613
|
+
)
|
|
614
|
+
}
|
|
615
|
+
>
|
|
616
|
+
<ExternalLinkIcon className="size-3.5" />
|
|
617
|
+
Test Request
|
|
618
|
+
</Button>
|
|
619
|
+
<Button
|
|
620
|
+
variant="outline"
|
|
621
|
+
className="h-10 text-[10px] font-bold uppercase tracking-widest gap-2 opacity-50 cursor-not-allowed"
|
|
622
|
+
>
|
|
623
|
+
<TerminalIcon className="size-3.5" />
|
|
624
|
+
SDK Snippets
|
|
625
|
+
</Button>
|
|
626
|
+
</div>
|
|
627
|
+
</CardContent>
|
|
628
|
+
</Card>
|
|
629
|
+
</div>
|
|
630
|
+
</TabsContent>
|
|
631
|
+
</Tabs>
|
|
632
|
+
</div>
|
|
633
|
+
);
|
|
634
|
+
}
|