flarecms 0.1.0 → 0.1.2

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 (113) hide show
  1. package/LICENSE +21 -0
  2. package/dist/auth/index.js +201 -1
  3. package/dist/cli/commands.js +5554 -55
  4. package/dist/cli/index.js +5554 -55
  5. package/dist/cli/mcp.js +30 -0
  6. package/dist/client/index.js +23576 -0
  7. package/dist/db/index.js +10392 -25
  8. package/dist/index.js +56776 -7582
  9. package/dist/server/index.js +43280 -0
  10. package/dist/style.css +5536 -0
  11. package/package.json +33 -30
  12. package/scripts/fix-api-paths.mjs +0 -32
  13. package/scripts/fix-imports.mjs +0 -38
  14. package/scripts/prefix-css.mjs +0 -45
  15. package/src/api/lib/cache.ts +0 -45
  16. package/src/api/lib/response.ts +0 -40
  17. package/src/api/middlewares/auth.ts +0 -186
  18. package/src/api/middlewares/cors.ts +0 -10
  19. package/src/api/middlewares/rbac.ts +0 -85
  20. package/src/api/routes/auth.ts +0 -377
  21. package/src/api/routes/collections.ts +0 -205
  22. package/src/api/routes/content.ts +0 -175
  23. package/src/api/routes/device.ts +0 -160
  24. package/src/api/routes/magic.ts +0 -150
  25. package/src/api/routes/mcp.ts +0 -273
  26. package/src/api/routes/oauth.ts +0 -160
  27. package/src/api/routes/settings.ts +0 -43
  28. package/src/api/routes/setup.ts +0 -307
  29. package/src/api/routes/tokens.ts +0 -80
  30. package/src/api/schemas/auth.ts +0 -15
  31. package/src/api/schemas/index.ts +0 -51
  32. package/src/api/schemas/tokens.ts +0 -24
  33. package/src/auth/index.ts +0 -28
  34. package/src/cli/commands.ts +0 -217
  35. package/src/cli/index.ts +0 -21
  36. package/src/cli/mcp.ts +0 -210
  37. package/src/cli/tests/cli.test.ts +0 -40
  38. package/src/cli/tests/create.test.ts +0 -87
  39. package/src/client/FlareAdminRouter.tsx +0 -47
  40. package/src/client/app.tsx +0 -175
  41. package/src/client/components/app-sidebar.tsx +0 -227
  42. package/src/client/components/collection-modal.tsx +0 -215
  43. package/src/client/components/content-list.tsx +0 -247
  44. package/src/client/components/dynamic-form.tsx +0 -190
  45. package/src/client/components/field-modal.tsx +0 -221
  46. package/src/client/components/settings/api-token-section.tsx +0 -400
  47. package/src/client/components/settings/general-section.tsx +0 -224
  48. package/src/client/components/settings/security-section.tsx +0 -154
  49. package/src/client/components/settings/seo-section.tsx +0 -200
  50. package/src/client/components/settings/signup-section.tsx +0 -257
  51. package/src/client/components/ui/accordion.tsx +0 -78
  52. package/src/client/components/ui/avatar.tsx +0 -107
  53. package/src/client/components/ui/badge.tsx +0 -52
  54. package/src/client/components/ui/button.tsx +0 -60
  55. package/src/client/components/ui/card.tsx +0 -103
  56. package/src/client/components/ui/checkbox.tsx +0 -27
  57. package/src/client/components/ui/collapsible.tsx +0 -19
  58. package/src/client/components/ui/dialog.tsx +0 -162
  59. package/src/client/components/ui/icon-picker.tsx +0 -485
  60. package/src/client/components/ui/icons-data.ts +0 -8476
  61. package/src/client/components/ui/input.tsx +0 -20
  62. package/src/client/components/ui/label.tsx +0 -20
  63. package/src/client/components/ui/popover.tsx +0 -91
  64. package/src/client/components/ui/select.tsx +0 -204
  65. package/src/client/components/ui/separator.tsx +0 -23
  66. package/src/client/components/ui/sheet.tsx +0 -141
  67. package/src/client/components/ui/sidebar.tsx +0 -722
  68. package/src/client/components/ui/skeleton.tsx +0 -13
  69. package/src/client/components/ui/sonner.tsx +0 -47
  70. package/src/client/components/ui/switch.tsx +0 -30
  71. package/src/client/components/ui/table.tsx +0 -116
  72. package/src/client/components/ui/tabs.tsx +0 -80
  73. package/src/client/components/ui/textarea.tsx +0 -18
  74. package/src/client/components/ui/tooltip.tsx +0 -68
  75. package/src/client/hooks/use-mobile.ts +0 -19
  76. package/src/client/index.css +0 -149
  77. package/src/client/index.ts +0 -7
  78. package/src/client/layouts/admin-layout.tsx +0 -93
  79. package/src/client/layouts/settings-layout.tsx +0 -104
  80. package/src/client/lib/api.ts +0 -72
  81. package/src/client/lib/utils.ts +0 -6
  82. package/src/client/main.tsx +0 -10
  83. package/src/client/pages/collection-detail.tsx +0 -634
  84. package/src/client/pages/collections.tsx +0 -180
  85. package/src/client/pages/dashboard.tsx +0 -133
  86. package/src/client/pages/device.tsx +0 -66
  87. package/src/client/pages/document-detail-page.tsx +0 -139
  88. package/src/client/pages/documents-page.tsx +0 -103
  89. package/src/client/pages/login.tsx +0 -345
  90. package/src/client/pages/settings.tsx +0 -65
  91. package/src/client/pages/setup.tsx +0 -129
  92. package/src/client/pages/signup.tsx +0 -188
  93. package/src/client/store/auth.ts +0 -30
  94. package/src/client/store/collections.ts +0 -13
  95. package/src/client/store/config.ts +0 -12
  96. package/src/client/store/fetcher.ts +0 -30
  97. package/src/client/store/router.ts +0 -95
  98. package/src/client/store/schema.ts +0 -39
  99. package/src/client/store/settings.ts +0 -31
  100. package/src/client/types.ts +0 -34
  101. package/src/db/dynamic.ts +0 -70
  102. package/src/db/index.ts +0 -16
  103. package/src/db/migrations/001_initial_schema.ts +0 -57
  104. package/src/db/migrations/002_auth_tables.ts +0 -84
  105. package/src/db/migrator.ts +0 -61
  106. package/src/db/schema.ts +0 -142
  107. package/src/index.ts +0 -12
  108. package/src/server/index.ts +0 -66
  109. package/src/types.ts +0 -20
  110. package/tests/css.test.ts +0 -21
  111. package/tests/modular.test.ts +0 -29
  112. package/tsconfig.json +0 -10
  113. /package/{style.css.d.ts → dist/style.css.d.ts} +0 -0
@@ -1,247 +0,0 @@
1
- import { useEffect, useState } from 'react';
2
- import { apiFetch } from '../lib/api';
3
- import { useStore } from '@nanostores/react';
4
- import { $schema } from '../store/schema';
5
- import { $router, navigate } from '../store/router';
6
- import {
7
- FileEdit as FileEditIcon,
8
- Trash2 as Trash2Icon,
9
- Loader2 as Loader2Icon,
10
- Search as SearchIcon,
11
- ChevronDown as ChevronDownIcon,
12
- Filter as FilterIcon,
13
- LayoutGrid as LayoutGridIcon,
14
- List as ListIcon,
15
- History as HistoryIcon,
16
- } from 'lucide-react';
17
-
18
- import {
19
- Table,
20
- TableBody,
21
- TableCell,
22
- TableHead,
23
- TableHeader,
24
- TableRow,
25
- } from './ui/table';
26
- import { Button } from './ui/button';
27
- import { Card, CardHeader, CardTitle, CardContent } from './ui/card';
28
- import { Input } from './ui/input';
29
-
30
- export function ContentList({ slug }: { slug: string }) {
31
- const [data, setData] = useState<any[]>([]);
32
- const [meta, setMeta] = useState<any>(null);
33
- const [page, setPage] = useState(1);
34
- const [loading, setLoading] = useState(true);
35
- const { data: schema } = useStore($schema);
36
-
37
- const fetchContent = async () => {
38
- setLoading(true);
39
- try {
40
- const response = await apiFetch(`/content/${slug}?page=${page}&limit=20`);
41
- const json = await response.json();
42
- setData(json.data || []);
43
- setMeta(json.meta || null);
44
- } catch (err) {
45
- console.error(err);
46
- } finally {
47
- setLoading(false);
48
- }
49
- };
50
-
51
- useEffect(() => {
52
- fetchContent();
53
- }, [slug, page]);
54
-
55
- const handleDelete = async (id: string | number) => {
56
- if (!confirm('Are you certain? This action is irreversible.')) return;
57
- try {
58
- await apiFetch(`/content/${slug}/${id}`, { method: 'DELETE' });
59
- fetchContent();
60
- } catch (err) {
61
- console.error(err);
62
- }
63
- };
64
-
65
- if (loading) {
66
- return (
67
- <div className="flex flex-col items-center justify-center p-20 text-muted-foreground gap-6 bg-background border rounded-lg shadow-sm">
68
- <Loader2Icon className="size-8 animate-spin text-primary" />
69
- <p className="font-semibold text-[10px] uppercase tracking-[0.2em]">
70
- Synchronizing Data...
71
- </p>
72
- </div>
73
- );
74
- }
75
-
76
- return (
77
- <div className="space-y-6">
78
- {/* Table Actions Bar */}
79
- <div className="flex flex-col md:flex-row gap-4 items-center justify-between">
80
- <div className="flex items-center gap-2 w-full md:w-auto">
81
- <div className="relative w-full md:w-64 group">
82
- <SearchIcon className="absolute left-3 top-1/2 -translate-y-1/2 size-3.5 text-muted-foreground transition-colors" />
83
- <Input
84
- placeholder="Search entries..."
85
- className="pl-9 h-9 text-xs"
86
- />
87
- </div>
88
- <Button
89
- variant="outline"
90
- size="sm"
91
- className="h-9 gap-2 text-xs font-semibold"
92
- >
93
- <FilterIcon className="size-3.5" />
94
- Filter
95
- </Button>
96
- </div>
97
-
98
- <div className="flex items-center gap-1 border bg-muted/50 rounded-md p-0.5 border-border shrink-0">
99
- <Button
100
- variant="ghost"
101
- size="icon"
102
- className="h-7 w-7 bg-background shadow-sm text-primary"
103
- >
104
- <ListIcon className="size-3.5" />
105
- </Button>
106
- <Button
107
- variant="ghost"
108
- size="icon"
109
- className="h-7 w-7 text-muted-foreground/50 hover:text-foreground"
110
- >
111
- <LayoutGridIcon className="size-3.5" />
112
- </Button>
113
- </div>
114
- </div>
115
-
116
- <Card className="py-0 shadow-none border-border overflow-hidden">
117
- <CardHeader className="bg-muted/30 border-b py-3 px-6">
118
- <div className="flex items-center justify-between">
119
- <div className="flex items-center gap-2 text-[10px] font-semibold text-muted-foreground uppercase tracking-wider leading-none">
120
- <HistoryIcon className="size-3.5 opacity-50" />
121
- Synchronized Ledger
122
- </div>
123
- <div className="flex items-center gap-2 text-[10px] text-muted-foreground font-semibold uppercase tracking-wider">
124
- <span className="size-1.5 bg-primary rounded-full" />
125
- <span>{meta?.total || data.length} Entries</span>
126
- </div>
127
- </div>
128
- </CardHeader>
129
- <CardContent className="p-0">
130
- <Table>
131
- <TableHeader>
132
- <TableRow className="hover:bg-transparent text-muted-foreground uppercase text-[10px] font-bold tracking-wider bg-muted/20">
133
- <TableHead className="pl-6 py-3">
134
- Document Identification
135
- </TableHead>
136
- {schema?.fields?.slice(0, 2).map((field: any) => (
137
- <TableHead key={field.id}>{field.label}</TableHead>
138
- ))}
139
- <TableHead>Created</TableHead>
140
- <TableHead className="text-right pr-6">Actions</TableHead>
141
- </TableRow>
142
- </TableHeader>
143
- <TableBody>
144
- {data.map((item) => (
145
- <TableRow
146
- key={item.id}
147
- className="group transition-colors border-border/50"
148
- >
149
- <TableCell className="pl-6 py-4">
150
- <div className="flex flex-col gap-0.5">
151
- <span
152
- className="font-semibold text-primary hover:underline cursor-pointer text-sm leading-tight"
153
- onClick={() => navigate('document_edit', { slug, id: item.id })}
154
- >
155
- {item.slug || `Entry #${item.id}`}
156
- </span>
157
- <span className="text-[10px] text-muted-foreground font-medium font-mono uppercase tracking-tighter opacity-70">
158
- ID: {item.id}
159
- </span>
160
- </div>
161
- </TableCell>
162
-
163
- {schema?.fields?.slice(0, 2).map((field: any) => (
164
- <TableCell
165
- key={field.id}
166
- className="text-xs font-medium text-foreground/80"
167
- >
168
- {String(item[field.slug] || '—')}
169
- </TableCell>
170
- ))}
171
-
172
- <TableCell className="text-[11px] text-muted-foreground font-medium">
173
- {item.created_at
174
- ? new Date(item.created_at).toLocaleDateString()
175
- : '—'}
176
- </TableCell>
177
-
178
- <TableCell className="text-right pr-6">
179
- <div className="flex justify-end gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
180
- <Button
181
- variant="ghost"
182
- size="icon"
183
- className="size-7 text-muted-foreground hover:text-primary"
184
- onClick={() => navigate('document_edit', { slug, id: item.id })}
185
- >
186
- <FileEditIcon className="size-3.5" />
187
- </Button>
188
- <Button
189
- variant="ghost"
190
- size="icon"
191
- className="size-7 text-muted-foreground hover:text-destructive"
192
- onClick={() => handleDelete(item.id)}
193
- >
194
- <Trash2Icon className="size-3.5" />
195
- </Button>
196
- </div>
197
- </TableCell>
198
- </TableRow>
199
- ))}
200
- {data.length === 0 && (
201
- <TableRow>
202
- <TableCell
203
- colSpan={10}
204
- className="h-64 text-center text-muted-foreground bg-muted/5"
205
- >
206
- <div className="flex flex-col items-center justify-center gap-4">
207
- <HistoryIcon className="size-12 opacity-10" />
208
- <p className="text-sm font-semibold text-foreground/50">
209
- No Data Available
210
- </p>
211
- <p className="text-[10px] uppercase font-semibold tracking-wider opacity-40">
212
- Add your first entry to begin
213
- </p>
214
- </div>
215
- </TableCell>
216
- </TableRow>
217
- )}
218
- </TableBody>
219
- </Table>
220
- </CardContent>
221
- </Card>
222
-
223
- {/* Footer Info */}
224
- <div className="px-1 flex justify-between items-center text-[10px] font-semibold text-muted-foreground uppercase tracking-wider">
225
- <p>
226
- Showing {meta ? (meta.page - 1) * meta.limit + 1 : 1} — {meta ? Math.min(meta.page * meta.limit, meta.total) : data.length} of {meta?.total || data.length}
227
- </p>
228
- <div className="flex gap-4">
229
- <button
230
- disabled={!meta?.hasPrevPage}
231
- onClick={() => setPage(p => p - 1)}
232
- className="hover:text-primary disabled:opacity-30 disabled:hover:text-muted-foreground cursor-pointer transition-colors"
233
- >
234
- Previous
235
- </button>
236
- <button
237
- disabled={!meta?.hasNextPage}
238
- onClick={() => setPage(p => p + 1)}
239
- className="hover:text-primary disabled:opacity-30 disabled:hover:text-muted-foreground cursor-pointer transition-colors"
240
- >
241
- Next
242
- </button>
243
- </div>
244
- </div>
245
- </div>
246
- );
247
- }
@@ -1,190 +0,0 @@
1
- import React, { useState } from 'react';
2
- import { useStore } from '@nanostores/react';
3
- import { $schema } from '../store/schema';
4
- import { Loader2Icon, CheckIcon, TypeIcon } from 'lucide-react';
5
- import type { Field } from '../types';
6
- import { Button } from './ui/button';
7
-
8
- interface DynamicFormProps {
9
- slug: string;
10
- onSubmit: (data: Record<string, any>) => void;
11
- onCancel: () => void;
12
- initialData?: Record<string, any> | null;
13
- isSubmitting?: boolean;
14
- }
15
-
16
- export function DynamicForm({
17
- slug,
18
- onSubmit,
19
- onCancel,
20
- initialData,
21
- isSubmitting,
22
- }: DynamicFormProps) {
23
- const { data: schema, loading } = useStore($schema);
24
- const [formData, setFormData] = useState<Record<string, any>>(
25
- initialData || {},
26
- );
27
-
28
- if (loading)
29
- return (
30
- <div className="flex flex-col items-center gap-4 py-20 justify-center text-muted-foreground">
31
- <Loader2Icon className="size-8 animate-spin text-primary/40" />
32
- <p className="text-[10px] font-bold uppercase tracking-widest">
33
- Acquiring Structure...
34
- </p>
35
- </div>
36
- );
37
-
38
- const handleSubmit = (e: React.FormEvent) => {
39
- e.preventDefault();
40
- if (isSubmitting) return;
41
- onSubmit(formData);
42
- };
43
-
44
- const renderField = (field: Field) => {
45
- const value = formData[field.slug] ?? '';
46
- const onChange = (val: any) =>
47
- setFormData({ ...formData, [field.slug]: val });
48
-
49
- switch (field.type) {
50
- case 'boolean':
51
- return (
52
- <label className="flex items-center gap-4 cursor-pointer p-5 bg-muted/20 rounded-md border border-border/50 hover:bg-muted/30 transition-all border-dashed group opacity-100 disabled:opacity-50">
53
- <div className="relative flex items-center">
54
- <input
55
- type="checkbox"
56
- className="peer sr-only"
57
- checked={!!value}
58
- disabled={isSubmitting}
59
- onChange={(e) => onChange(e.target.checked)}
60
- />
61
- <div className="w-10 h-5 bg-muted-foreground/20 rounded-full peer peer-checked:bg-primary transition-colors after:content-[''] after:absolute after:top-1 after:left-1 after:bg-white after:rounded-full after:h-3 after:w-3 after:transition-transform peer-checked:after:translate-x-5" />
62
- </div>
63
- <div className="flex flex-col">
64
- <span className="text-xs font-bold text-foreground group-hover:text-primary transition-colors">
65
- {field.label}
66
- </span>
67
- <span className="text-[10px] font-medium text-muted-foreground uppercase tracking-wider opacity-60">
68
- Binary State Toggle
69
- </span>
70
- </div>
71
- </label>
72
- );
73
-
74
- case 'richtext':
75
- return (
76
- <div className="space-y-2">
77
- <label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground/60 pl-1">
78
- {field.label}
79
- </label>
80
- <textarea
81
- className="w-full bg-muted/20 border border-border rounded-md px-4 py-3 text-sm focus:bg-background transition-all min-h-[180px] outline-none focus:ring-1 focus:ring-primary/20 disabled:opacity-50"
82
- value={value}
83
- disabled={isSubmitting}
84
- onChange={(e) => onChange(e.target.value)}
85
- placeholder={`Enter content for ${field.label.toLowerCase()}...`}
86
- />
87
- </div>
88
- );
89
-
90
- case 'number':
91
- return (
92
- <div className="space-y-2">
93
- <label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground/60 pl-1">
94
- {field.label}
95
- </label>
96
- <input
97
- type="number"
98
- className="w-full h-10 bg-muted/20 border border-border rounded-md px-4 text-sm focus:bg-background transition-all outline-none focus:ring-1 focus:ring-primary/20 disabled:opacity-50"
99
- value={value}
100
- disabled={isSubmitting}
101
- onChange={(e) => onChange(Number(e.target.value))}
102
- />
103
- </div>
104
- );
105
-
106
- case 'date':
107
- return (
108
- <div className="space-y-2">
109
- <label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground/60 pl-1">
110
- {field.label}
111
- </label>
112
- <input
113
- type="date"
114
- className="w-full h-10 bg-muted/20 border border-border rounded-md px-4 text-sm focus:bg-background transition-all outline-none focus:ring-1 focus:ring-primary/20 disabled:opacity-50"
115
- value={value}
116
- disabled={isSubmitting}
117
- onChange={(e) => onChange(e.target.value)}
118
- />
119
- </div>
120
- );
121
-
122
- default:
123
- return (
124
- <div className="space-y-2">
125
- <label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground/60 pl-1">
126
- {field.label}
127
- </label>
128
- <input
129
- type="text"
130
- className="w-full h-10 bg-muted/20 border border-border rounded-md px-4 text-sm focus:bg-background transition-all outline-none focus:ring-1 focus:ring-primary/20 disabled:opacity-50"
131
- value={value}
132
- disabled={isSubmitting}
133
- onChange={(e) => onChange(e.target.value)}
134
- />
135
- </div>
136
- );
137
- }
138
- };
139
-
140
- return (
141
- <form onSubmit={handleSubmit} className="space-y-10">
142
- <fieldset disabled={isSubmitting} className="space-y-8 contents">
143
- {schema?.fields?.map((field: Field) => (
144
- <div key={field.id}>{renderField(field)}</div>
145
- ))}
146
- {(!schema?.fields || schema.fields.length === 0) && (
147
- <div className="p-12 text-center bg-muted/10 rounded-lg border border-dashed flex flex-col items-center gap-4">
148
- <TypeIcon className="size-8 opacity-10" />
149
- <div className="space-y-1">
150
- <p className="text-sm font-bold uppercase tracking-wider text-foreground/50">
151
- Model Void
152
- </p>
153
- <p className="text-[10px] uppercase font-semibold text-muted-foreground tracking-widest opacity-40">
154
- Define fields in the structure engine to begin entry
155
- </p>
156
- </div>
157
- </div>
158
- )}
159
- </fieldset>
160
-
161
- <div className="flex justify-end gap-3 pt-8 border-t border-dashed">
162
- <Button
163
- type="button"
164
- variant="ghost"
165
- disabled={isSubmitting}
166
- onClick={onCancel}
167
- className="text-xs font-semibold h-10 px-6 text-muted-foreground hover:text-foreground"
168
- >
169
- Discard Changes
170
- </Button>
171
- <Button
172
- type="submit"
173
- disabled={isSubmitting}
174
- className="font-bold h-10 px-8 text-xs tracking-tight min-w-32"
175
- >
176
- {isSubmitting ? (
177
- <Loader2Icon className="size-4 mr-2 animate-spin" />
178
- ) : (
179
- <CheckIcon className="size-4 mr-2" />
180
- )}
181
- {isSubmitting
182
- ? 'Processing...'
183
- : initialData?.id
184
- ? 'Update Document'
185
- : 'Create Document'}
186
- </Button>
187
- </div>
188
- </form>
189
- );
190
- }
@@ -1,221 +0,0 @@
1
- import React, { useState } from 'react';
2
- import { useStore } from '@nanostores/react';
3
- import {
4
- PlusIcon,
5
- Loader2Icon,
6
- TypeIcon,
7
- HashIcon,
8
- CheckSquareIcon,
9
- CalendarIcon,
10
- AlignLeftIcon,
11
- } from 'lucide-react';
12
- import { $addField } from '../store/schema';
13
- import type { Field } from '../types';
14
-
15
- import {
16
- Dialog,
17
- DialogContent,
18
- DialogDescription,
19
- DialogHeader,
20
- DialogTitle,
21
- DialogFooter,
22
- DialogTrigger,
23
- } from './ui/dialog';
24
- import { Button } from './ui/button';
25
- import { Input } from './ui/input';
26
- import { Label } from './ui/label';
27
-
28
- interface FieldModalProps {
29
- children: React.ReactElement;
30
- collectionId: string;
31
- collectionSlug: string;
32
- }
33
-
34
- const FIELD_TYPES = [
35
- { id: 'text', label: 'Plain Text', icon: <TypeIcon className="w-4 h-4" /> },
36
- {
37
- id: 'richtext',
38
- label: 'Rich Text',
39
- icon: <AlignLeftIcon className="w-4 h-4" />,
40
- },
41
- { id: 'number', label: 'Number', icon: <HashIcon className="w-4 h-4" /> },
42
- {
43
- id: 'boolean',
44
- label: 'Boolean',
45
- icon: <CheckSquareIcon className="w-4 h-4" />,
46
- },
47
- { id: 'date', label: 'Date', icon: <CalendarIcon className="w-4 h-4" /> },
48
- ] as const;
49
-
50
- export function FieldModal({
51
- children,
52
- collectionId,
53
- collectionSlug,
54
- }: FieldModalProps) {
55
- const [isOpen, setIsOpen] = useState(false);
56
- const [formData, setFormData] = useState<Partial<Field>>({
57
- label: '',
58
- slug: '',
59
- type: 'text',
60
- required: false,
61
- });
62
-
63
- const { mutate, loading } = useStore($addField);
64
-
65
- const handleSubmit = async (e: React.FormEvent) => {
66
- e.preventDefault();
67
- try {
68
- await mutate({
69
- ...formData,
70
- collectionId,
71
- collectionSlug,
72
- });
73
- setIsOpen(false);
74
- setFormData({ label: '', slug: '', type: 'text', required: false });
75
- } catch (err) {
76
- console.error(err);
77
- }
78
- };
79
-
80
- return (
81
- <Dialog open={isOpen} onOpenChange={setIsOpen}>
82
- <DialogTrigger render={children} />
83
- <DialogContent className="sm:max-w-[540px] p-0 overflow-hidden border-border bg-background rounded-md shadow-lg">
84
- <DialogHeader className="px-8 py-8 border-b bg-muted/30">
85
- <div className="flex items-center gap-4">
86
- <div className="p-2.5 bg-background border rounded-md shadow-sm">
87
- <PlusIcon className="size-5 text-primary" />
88
- </div>
89
- <div>
90
- <DialogTitle className="text-lg font-bold tracking-tight">
91
- New Structure Field
92
- </DialogTitle>
93
- <DialogDescription className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground mt-0.5">
94
- Define Attribute Parameter
95
- </DialogDescription>
96
- </div>
97
- </div>
98
- </DialogHeader>
99
-
100
- <form onSubmit={handleSubmit}>
101
- <div className="p-8 space-y-8">
102
- <div className="grid grid-cols-2 gap-6">
103
- <div className="space-y-2">
104
- <Label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground/60 pl-1">
105
- Field Label
106
- </Label>
107
- <Input
108
- required
109
- placeholder="e.g. Featured Toggle"
110
- className="h-10 bg-muted/20 focus:bg-background transition-all border-border rounded-md px-4 text-sm font-medium"
111
- value={formData.label}
112
- onChange={(e) => {
113
- const label = e.target.value;
114
- const slug = label
115
- .toLowerCase()
116
- .replace(/\s+/g, '_')
117
- .replace(/[^a-z0-9_]/g, '');
118
- setFormData({ ...formData, label, slug });
119
- }}
120
- />
121
- </div>
122
- <div className="space-y-2">
123
- <Label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground/60 pl-1">
124
- Database Key
125
- </Label>
126
- <Input
127
- required
128
- className="h-10 bg-muted/20 focus:bg-background transition-all border-border rounded-md font-mono text-[11px] font-semibold text-muted-foreground"
129
- value={formData.slug}
130
- onChange={(e) =>
131
- setFormData({ ...formData, slug: e.target.value })
132
- }
133
- />
134
- </div>
135
- </div>
136
-
137
- <div className="space-y-4">
138
- <Label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground/60 pl-1">
139
- Field Type
140
- </Label>
141
- <div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
142
- {FIELD_TYPES.map((type) => (
143
- <button
144
- key={type.id}
145
- type="button"
146
- onClick={() =>
147
- setFormData({ ...formData, type: type.id as any })
148
- }
149
- className={`flex items-center gap-3 p-3.5 rounded-md border transition-all text-left group ${
150
- formData.type === type.id
151
- ? 'bg-primary/5 border-primary text-primary font-bold shadow-sm'
152
- : 'bg-background border-border text-muted-foreground hover:border-muted-foreground/30 hover:text-foreground'
153
- }`}
154
- >
155
- <div
156
- className={
157
- formData.type === type.id
158
- ? 'text-primary'
159
- : 'text-muted-foreground/40 group-hover:text-muted-foreground/60 transition-colors'
160
- }
161
- >
162
- {type.icon}
163
- </div>
164
- <span className="text-[11px] uppercase tracking-tight leading-none font-semibold">
165
- {type.label}
166
- </span>
167
- </button>
168
- ))}
169
- </div>
170
- </div>
171
-
172
- <label className="flex items-center gap-4 cursor-pointer group p-5 bg-muted/20 border border-border/50 rounded-md hover:bg-muted/30 transition-all border-dashed">
173
- <div className="relative flex items-center">
174
- <input
175
- type="checkbox"
176
- className="peer sr-only"
177
- checked={formData.required}
178
- onChange={(e) =>
179
- setFormData({ ...formData, required: e.target.checked })
180
- }
181
- />
182
- <div className="w-10 h-5 bg-muted-foreground/20 rounded-full peer peer-checked:bg-primary transition-colors after:content-[''] after:absolute after:top-1 after:left-1 after:bg-white after:rounded-full after:h-3 after:w-3 after:transition-transform peer-checked:after:translate-x-5" />
183
- </div>
184
- <div className="flex-1">
185
- <p className="text-xs font-bold text-foreground">
186
- Required Property
187
- </p>
188
- <p className="text-[10px] font-medium text-muted-foreground uppercase tracking-widest opacity-60">
189
- Enforce data integrity
190
- </p>
191
- </div>
192
- </label>
193
- </div>
194
-
195
- <DialogFooter className="bg-muted/10 border-t p-6 px-8 gap-3 sm:gap-2">
196
- <Button
197
- type="button"
198
- variant="ghost"
199
- onClick={() => setIsOpen(false)}
200
- className="text-xs font-semibold text-muted-foreground hover:text-foreground h-10 px-6"
201
- >
202
- Discard
203
- </Button>
204
- <Button
205
- type="submit"
206
- disabled={loading}
207
- className="font-bold px-8 rounded-md h-10 text-xs tracking-tight"
208
- >
209
- {loading ? (
210
- <Loader2Icon className="size-4 animate-spin mr-2.5" />
211
- ) : (
212
- <PlusIcon className="size-4 mr-2.5" />
213
- )}
214
- Add Property
215
- </Button>
216
- </DialogFooter>
217
- </form>
218
- </DialogContent>
219
- </Dialog>
220
- );
221
- }