@kyro-cms/admin 0.1.7 → 0.1.8
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 +5 -3
- package/src/components/Admin.tsx +1 -1
- package/src/components/AutoForm.tsx +966 -337
- package/src/components/CreateView.tsx +1 -1
- package/src/components/DetailView.tsx +1 -1
- package/src/components/EnhancedListView.tsx +156 -52
- package/src/components/ListView.tsx +1 -1
- package/src/components/Modal.tsx +65 -8
- package/src/components/Sidebar.astro +2 -2
- package/src/components/ThemeProvider.tsx +8 -2
- package/src/components/blocks/AccordionBlock.tsx +20 -52
- package/src/components/blocks/ArrayBlock.tsx +40 -31
- package/src/components/blocks/BlockEditModal.tsx +170 -581
- package/src/components/blocks/ButtonBlock.tsx +27 -128
- package/src/components/blocks/CodeBlock.tsx +88 -40
- package/src/components/blocks/ColumnsBlock.tsx +27 -85
- package/src/components/blocks/FileBlock.tsx +38 -39
- package/src/components/blocks/HeadingBlock.tsx +9 -31
- package/src/components/blocks/HeroBlock.tsx +42 -100
- package/src/components/blocks/ImageBlock.tsx +6 -7
- package/src/components/blocks/LinkBlock.tsx +27 -33
- package/src/components/blocks/ListBlock.tsx +47 -26
- package/src/components/blocks/RelationshipBlock.tsx +26 -233
- package/src/components/blocks/RichTextBlock.tsx +66 -0
- package/src/components/blocks/VStackBlock.tsx +23 -37
- package/src/components/blocks/VideoBlock.tsx +52 -32
- package/src/components/fields/AccordionField.tsx +213 -0
- package/src/components/fields/ArrayField.tsx +241 -0
- package/src/components/fields/BlocksField.tsx +5 -5
- package/src/components/fields/ButtonField.tsx +53 -0
- package/src/components/fields/CheckboxField.tsx +7 -3
- package/src/components/fields/ChildrenField.tsx +48 -0
- package/src/components/fields/CodeField.tsx +154 -94
- package/src/components/fields/ColumnsField.tsx +137 -0
- package/src/components/fields/DateField.tsx +9 -24
- package/src/components/fields/EditorClient.tsx +426 -160
- package/src/components/fields/HeadingField.tsx +31 -0
- package/src/components/fields/HeroField.tsx +101 -0
- package/src/components/fields/JSONField.tsx +7 -27
- package/src/components/fields/LinkField.tsx +81 -0
- package/src/components/fields/ListField.tsx +74 -0
- package/src/components/fields/MarkdownField.tsx +4 -26
- package/src/components/fields/NumberField.tsx +9 -27
- package/src/components/fields/PortableTextField.tsx +61 -49
- package/src/components/fields/RelationshipBlockField.tsx +233 -0
- package/src/components/fields/RelationshipField.tsx +59 -13
- package/src/components/fields/SelectField.tsx +6 -4
- package/src/components/fields/TextField.tsx +9 -24
- package/src/components/fields/UploadField.tsx +613 -0
- package/src/components/fields/VideoField.tsx +73 -0
- package/src/components/fields/extensions/blockComponents.tsx +11 -1
- package/src/components/fields/extensions/blocksStore.ts +1 -1
- package/src/components/fields/index.ts +12 -1
- package/src/components/layout/Layout.tsx +1 -1
- package/src/lib/api.ts +163 -0
- package/src/lib/config.ts +1 -1
- package/src/lib/dataStore.ts +87 -30
- package/src/lib/date-utils.ts +69 -0
- package/src/lib/db/version-adapter.ts +248 -0
- package/src/lib/i18n.tsx +353 -0
- package/src/lib/slugify.ts +15 -0
- package/src/lib/validation.ts +250 -0
- package/src/pages/api/[collection]/[id]/publish.ts +12 -4
- package/src/pages/api/[collection]/[id]/versions.ts +39 -9
- package/src/pages/api/[collection]/[id].ts +13 -1
- package/src/pages/api/[collection]/index.ts +5 -6
- package/src/styles/main.css +12 -2
- package/src/components/blocks/BlockEditModal.MARKER +0 -12
- package/src/components/fields/FileField.tsx +0 -390
- package/src/components/fields/HybridContentField.tsx +0 -109
- package/src/components/fields/ImageField.tsx +0 -429
|
@@ -1,279 +1,72 @@
|
|
|
1
|
-
import React
|
|
1
|
+
import React from "react";
|
|
2
2
|
import {
|
|
3
3
|
useBlockById,
|
|
4
4
|
useBlockActions,
|
|
5
5
|
} from "../fields/extensions/blocksStore";
|
|
6
|
-
import { ChevronRight, X
|
|
6
|
+
import { ChevronRight, X } from "lucide-react";
|
|
7
|
+
import { RelationshipBlockField } from "../fields/RelationshipBlockField";
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
block: any;
|
|
10
|
-
index: number;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export const RelationshipBlock: React.FC<RelationshipBlockProps> = ({
|
|
9
|
+
export const RelationshipBlock: React.FC<{ block: any; index: number }> = ({
|
|
14
10
|
block,
|
|
15
11
|
index,
|
|
16
12
|
}) => {
|
|
17
13
|
const blockData = useBlockById(block.id);
|
|
18
14
|
const { updateBlock, removeBlock, moveBlock } = useBlockActions();
|
|
19
|
-
const data = blockData?.data ?? block.data ?? {};
|
|
20
|
-
|
|
21
|
-
const [isOpen, setIsOpen] = useState(false);
|
|
22
|
-
const [search, setSearch] = useState("");
|
|
23
|
-
const [options, setOptions] = useState<any[]>([]);
|
|
24
|
-
const [loading, setLoading] = useState(false);
|
|
25
|
-
const [collections, setCollections] = useState<string[]>([]);
|
|
26
|
-
const [loadingCollections, setLoadingCollections] = useState(true);
|
|
27
|
-
|
|
28
|
-
useEffect(() => {
|
|
29
|
-
fetch("/api/collections", { credentials: "include" })
|
|
30
|
-
.then((res) => res.json())
|
|
31
|
-
.then((data) => {
|
|
32
|
-
setCollections(
|
|
33
|
-
(data.collections || []).map((c: any) => c.slug || c.name || c),
|
|
34
|
-
);
|
|
35
|
-
setLoadingCollections(false);
|
|
36
|
-
})
|
|
37
|
-
.catch(() => setLoadingCollections(false));
|
|
38
|
-
}, []);
|
|
39
|
-
|
|
40
|
-
const relationTo = data.relationTo || "pages";
|
|
41
|
-
const isMultiple = data.hasMany;
|
|
42
|
-
|
|
43
|
-
const fetchOptions = (query: string = "") => {
|
|
44
|
-
setLoading(true);
|
|
45
|
-
const url = query
|
|
46
|
-
? `/api/${relationTo}?where[title][contains]=${encodeURIComponent(query)}&limit=20`
|
|
47
|
-
: `/api/${relationTo}?limit=20`;
|
|
48
|
-
|
|
49
|
-
fetch(url, { credentials: "include" })
|
|
50
|
-
.then((res) => res.json())
|
|
51
|
-
.then((data) => {
|
|
52
|
-
setOptions(data.docs || []);
|
|
53
|
-
setLoading(false);
|
|
54
|
-
})
|
|
55
|
-
.catch(() => setLoading(false));
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
useEffect(() => {
|
|
59
|
-
if (isOpen) fetchOptions(search);
|
|
60
|
-
}, [isOpen, search]);
|
|
61
|
-
|
|
62
|
-
const getLabel = (opt: any) => {
|
|
63
|
-
return (
|
|
64
|
-
opt?.title ||
|
|
65
|
-
opt?.name ||
|
|
66
|
-
opt?.label ||
|
|
67
|
-
opt?.filename ||
|
|
68
|
-
opt?.slug ||
|
|
69
|
-
opt?.id ||
|
|
70
|
-
"Untitled"
|
|
71
|
-
);
|
|
72
|
-
};
|
|
73
15
|
|
|
74
|
-
const
|
|
75
|
-
? data.selectedIds
|
|
76
|
-
: data.selectedId
|
|
77
|
-
? [data.selectedId]
|
|
78
|
-
: [];
|
|
79
|
-
|
|
80
|
-
const handleSelect = (opt: any) => {
|
|
81
|
-
if (isMultiple) {
|
|
82
|
-
const current = selectedIds;
|
|
83
|
-
if (current.includes(opt.id)) {
|
|
84
|
-
updateBlock(block.id, {
|
|
85
|
-
data: {
|
|
86
|
-
...data,
|
|
87
|
-
selectedIds: current.filter((id: string) => id !== opt.id),
|
|
88
|
-
},
|
|
89
|
-
});
|
|
90
|
-
} else {
|
|
91
|
-
updateBlock(block.id, {
|
|
92
|
-
data: { ...data, selectedIds: [...current, opt.id] },
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
} else {
|
|
96
|
-
updateBlock(block.id, {
|
|
97
|
-
data: { ...data, selectedId: opt.id, selectedIds: [opt.id] },
|
|
98
|
-
});
|
|
99
|
-
setIsOpen(false);
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
const isSelected = (optId: string) => selectedIds.includes(optId);
|
|
16
|
+
const data = blockData?.data ?? block.data ?? {};
|
|
104
17
|
|
|
105
18
|
const handleChange = (field: string, value: any) => {
|
|
106
19
|
updateBlock(block.id, { data: { ...data, [field]: value } });
|
|
107
20
|
};
|
|
108
21
|
|
|
109
|
-
const selectedOptions = options.filter((o) => selectedIds.includes(o.id));
|
|
110
|
-
|
|
111
22
|
return (
|
|
112
|
-
<div className="block-relationship border border-[var(--kyro-border)] rounded-
|
|
113
|
-
<div className="flex items-center justify-between mb-
|
|
23
|
+
<div className="block-relationship border border-[var(--kyro-border)] rounded-md p-3 mb-2 relative group">
|
|
24
|
+
<div className="flex items-center justify-between mb-1">
|
|
114
25
|
<div className="flex items-center gap-2">
|
|
115
|
-
<span className="text-
|
|
26
|
+
<span className="text-xs font-semibold text-[var(--kyro-text-muted)] uppercase">
|
|
116
27
|
Relationship
|
|
117
28
|
</span>
|
|
118
|
-
<span className="text-
|
|
119
|
-
→ {relationTo}
|
|
29
|
+
<span className="text-[10px] text-[var(--kyro-text-muted)]">
|
|
30
|
+
→ {data.relationTo || "pages"}
|
|
120
31
|
</span>
|
|
121
32
|
</div>
|
|
122
|
-
<div className="flex gap-
|
|
33
|
+
<div className="flex gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
123
34
|
<button
|
|
124
35
|
type="button"
|
|
125
36
|
onClick={() => moveBlock(block.id, "up")}
|
|
126
|
-
className="p-1
|
|
37
|
+
className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
|
|
127
38
|
title="Move up"
|
|
128
39
|
>
|
|
129
|
-
<ChevronRight className="w-3
|
|
40
|
+
<ChevronRight className="w-3 h-3 rotate-90" />
|
|
130
41
|
</button>
|
|
131
42
|
<button
|
|
132
43
|
type="button"
|
|
133
44
|
onClick={() => moveBlock(block.id, "down")}
|
|
134
|
-
className="p-1
|
|
45
|
+
className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
|
|
135
46
|
title="Move down"
|
|
136
47
|
>
|
|
137
|
-
<ChevronRight className="w-3
|
|
48
|
+
<ChevronRight className="w-3 h-3" />
|
|
138
49
|
</button>
|
|
139
50
|
<button
|
|
140
51
|
type="button"
|
|
141
52
|
onClick={() => removeBlock(block.id)}
|
|
142
|
-
className="p-1
|
|
53
|
+
className="p-1 hover:bg-red-50 rounded text-red-500"
|
|
143
54
|
title="Remove"
|
|
144
55
|
>
|
|
145
|
-
<X className="w-3
|
|
56
|
+
<X className="w-3 h-3" />
|
|
146
57
|
</button>
|
|
147
58
|
</div>
|
|
148
59
|
</div>
|
|
149
60
|
|
|
150
|
-
<
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
</div>
|
|
160
|
-
) : (
|
|
161
|
-
<select
|
|
162
|
-
value={relationTo}
|
|
163
|
-
onChange={(e) => handleChange("relationTo", e.target.value)}
|
|
164
|
-
className="w-full px-3 py-2 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
|
|
165
|
-
>
|
|
166
|
-
<option value="">Select collection...</option>
|
|
167
|
-
{collections.map((col) => (
|
|
168
|
-
<option key={col} value={col}>
|
|
169
|
-
{col}
|
|
170
|
-
</option>
|
|
171
|
-
))}
|
|
172
|
-
</select>
|
|
173
|
-
)}
|
|
174
|
-
</div>
|
|
175
|
-
<div>
|
|
176
|
-
<label className="flex items-center gap-2 cursor-pointer mt-5">
|
|
177
|
-
<input
|
|
178
|
-
type="checkbox"
|
|
179
|
-
checked={isMultiple || false}
|
|
180
|
-
onChange={(e) => handleChange("hasMany", e.target.checked)}
|
|
181
|
-
className="w-4 h-4 rounded border-[var(--kyro-border)] focus:ring-[var(--kyro-sidebar-active)] focus:ring-offset-0"
|
|
182
|
-
/>
|
|
183
|
-
<span className="text-sm text-[var(--kyro-text-primary)]">
|
|
184
|
-
Allow multiple
|
|
185
|
-
</span>
|
|
186
|
-
</label>
|
|
187
|
-
</div>
|
|
188
|
-
</div>
|
|
189
|
-
|
|
190
|
-
<div className="relative">
|
|
191
|
-
<div className="relative">
|
|
192
|
-
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--kyro-text-muted)]" />
|
|
193
|
-
<input
|
|
194
|
-
type="text"
|
|
195
|
-
value={search}
|
|
196
|
-
onChange={(e) => {
|
|
197
|
-
setSearch(e.target.value);
|
|
198
|
-
setIsOpen(true);
|
|
199
|
-
}}
|
|
200
|
-
onFocus={() => setIsOpen(true)}
|
|
201
|
-
placeholder={`Search ${relationTo}...`}
|
|
202
|
-
className="w-full pl-9 pr-10 py-2 border border-[var(--kyro-border)] rounded-lg bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
|
|
203
|
-
/>
|
|
204
|
-
<div className="absolute right-3 top-1/2 -translate-y-1/2">
|
|
205
|
-
{loading ? (
|
|
206
|
-
<Loader2 className="w-4 h-4 text-[var(--kyro-text-muted)] animate-spin" />
|
|
207
|
-
) : null}
|
|
208
|
-
</div>
|
|
209
|
-
</div>
|
|
210
|
-
|
|
211
|
-
{isOpen && (
|
|
212
|
-
<div className="absolute z-20 w-full mt-1 border border-[var(--kyro-border)] rounded-lg shadow-lg bg-[var(--kyro-surface)] max-h-48 overflow-auto">
|
|
213
|
-
{loading ? (
|
|
214
|
-
<div className="p-3 text-center text-sm text-[var(--kyro-text-muted)]">
|
|
215
|
-
Loading...
|
|
216
|
-
</div>
|
|
217
|
-
) : options.length === 0 ? (
|
|
218
|
-
<div className="p-3 text-center text-sm text-[var(--kyro-text-muted)]">
|
|
219
|
-
No results found
|
|
220
|
-
</div>
|
|
221
|
-
) : (
|
|
222
|
-
<div className="py-1">
|
|
223
|
-
{options.map((opt) => (
|
|
224
|
-
<button
|
|
225
|
-
key={opt.id}
|
|
226
|
-
type="button"
|
|
227
|
-
onClick={() => handleSelect(opt)}
|
|
228
|
-
className={`w-full px-3 py-2 text-left text-sm hover:bg-[var(--kyro-surface-accent)] transition-colors flex items-center justify-between ${
|
|
229
|
-
isSelected(opt.id)
|
|
230
|
-
? "bg-[var(--kyro-sidebar-active)]/10 text-[var(--kyro-sidebar-active)]"
|
|
231
|
-
: "text-[var(--kyro-text-primary)]"
|
|
232
|
-
}`}
|
|
233
|
-
>
|
|
234
|
-
<span>{getLabel(opt)}</span>
|
|
235
|
-
{isSelected(opt) && <span>✓</span>}
|
|
236
|
-
</button>
|
|
237
|
-
))}
|
|
238
|
-
</div>
|
|
239
|
-
)}
|
|
240
|
-
</div>
|
|
241
|
-
)}
|
|
242
|
-
</div>
|
|
243
|
-
|
|
244
|
-
{selectedIds.length > 0 && (
|
|
245
|
-
<div className="flex flex-wrap gap-2">
|
|
246
|
-
{selectedIds.map((id: string) => {
|
|
247
|
-
const opt = options.find((o) => o.id === id) || { id, title: id };
|
|
248
|
-
return (
|
|
249
|
-
<span
|
|
250
|
-
key={id}
|
|
251
|
-
className="inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs rounded-md bg-[var(--kyro-sidebar-active)]/10 text-[var(--kyro-sidebar-active)]"
|
|
252
|
-
>
|
|
253
|
-
{getLabel(opt)}
|
|
254
|
-
<button
|
|
255
|
-
type="button"
|
|
256
|
-
onClick={() => {
|
|
257
|
-
if (isMultiple) {
|
|
258
|
-
handleChange(
|
|
259
|
-
"selectedIds",
|
|
260
|
-
selectedIds.filter((sid: string) => sid !== id),
|
|
261
|
-
);
|
|
262
|
-
} else {
|
|
263
|
-
handleChange("selectedId", null);
|
|
264
|
-
handleChange("selectedIds", []);
|
|
265
|
-
}
|
|
266
|
-
}}
|
|
267
|
-
className="hover:opacity-70"
|
|
268
|
-
>
|
|
269
|
-
<X className="w-3 h-3" />
|
|
270
|
-
</button>
|
|
271
|
-
</span>
|
|
272
|
-
);
|
|
273
|
-
})}
|
|
274
|
-
</div>
|
|
275
|
-
)}
|
|
276
|
-
</div>
|
|
61
|
+
<RelationshipBlockField
|
|
62
|
+
relationTo={data.relationTo || "pages"}
|
|
63
|
+
hasMany={data.hasMany || false}
|
|
64
|
+
selectedIds={Array.isArray(data.selectedIds) ? data.selectedIds : []}
|
|
65
|
+
selectedId={data.selectedId}
|
|
66
|
+
labelField={data.labelField || "title"}
|
|
67
|
+
onChange={handleChange}
|
|
68
|
+
compact
|
|
69
|
+
/>
|
|
277
70
|
</div>
|
|
278
71
|
);
|
|
279
72
|
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {
|
|
3
|
+
useBlockById,
|
|
4
|
+
useBlockActions,
|
|
5
|
+
} from "../fields/extensions/blocksStore";
|
|
6
|
+
import { ChevronRight, X, AlignLeft } from "lucide-react";
|
|
7
|
+
import PortableTextField from "../fields/PortableTextField";
|
|
8
|
+
|
|
9
|
+
export const RichTextBlock: React.FC<{ block: any; index: number }> = ({
|
|
10
|
+
block,
|
|
11
|
+
index,
|
|
12
|
+
}) => {
|
|
13
|
+
const blockData = useBlockById(block.id);
|
|
14
|
+
const { updateBlock, removeBlock, moveBlock } = useBlockActions();
|
|
15
|
+
|
|
16
|
+
const data = blockData?.data || block.data || {};
|
|
17
|
+
|
|
18
|
+
const handleChange = (newValue: any) => {
|
|
19
|
+
updateBlock(block.id, { data: { ...data, content: newValue } });
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="block-richtext border border-[var(--kyro-border)] rounded-md p-3 mb-2 relative group">
|
|
24
|
+
<div className="flex items-center justify-between mb-2">
|
|
25
|
+
<div className="flex items-center gap-2">
|
|
26
|
+
<AlignLeft className="w-3.5 h-3.5 text-[var(--kyro-primary)]" />
|
|
27
|
+
<span className="text-xs font-semibold text-[var(--kyro-text-muted)] uppercase">
|
|
28
|
+
Rich Text
|
|
29
|
+
</span>
|
|
30
|
+
</div>
|
|
31
|
+
<div className="flex gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
32
|
+
<button
|
|
33
|
+
type="button"
|
|
34
|
+
onClick={() => moveBlock(block.id, "up")}
|
|
35
|
+
className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
|
|
36
|
+
title="Move up"
|
|
37
|
+
>
|
|
38
|
+
<ChevronRight className="w-3 h-3 rotate-90" />
|
|
39
|
+
</button>
|
|
40
|
+
<button
|
|
41
|
+
type="button"
|
|
42
|
+
onClick={() => moveBlock(block.id, "down")}
|
|
43
|
+
className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
|
|
44
|
+
title="Move down"
|
|
45
|
+
>
|
|
46
|
+
<ChevronRight className="w-3 h-3" />
|
|
47
|
+
</button>
|
|
48
|
+
<button
|
|
49
|
+
type="button"
|
|
50
|
+
onClick={() => removeBlock(block.id)}
|
|
51
|
+
className="p-1 hover:bg-red-50 rounded text-red-500"
|
|
52
|
+
title="Remove"
|
|
53
|
+
>
|
|
54
|
+
<X className="w-3 h-3" />
|
|
55
|
+
</button>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<PortableTextField
|
|
60
|
+
field={{ name: "content", label: "Content" }}
|
|
61
|
+
value={data.content}
|
|
62
|
+
onChange={handleChange}
|
|
63
|
+
/>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
};
|
|
@@ -4,72 +4,58 @@ import {
|
|
|
4
4
|
useBlockActions,
|
|
5
5
|
} from "../fields/extensions/blocksStore";
|
|
6
6
|
import { ChevronRight, X } from "lucide-react";
|
|
7
|
-
import {
|
|
7
|
+
import { ChildrenField } from "../fields/ChildrenField";
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
block
|
|
11
|
-
index
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export const VStackBlock: React.FC<VStackBlockProps> = ({ block, index }) => {
|
|
9
|
+
export const VStackBlock: React.FC<{ block: any; index: number }> = ({
|
|
10
|
+
block,
|
|
11
|
+
index,
|
|
12
|
+
}) => {
|
|
15
13
|
const blockData = useBlockById(block.id);
|
|
16
14
|
const { updateBlock, removeBlock, moveBlock } = useBlockActions();
|
|
15
|
+
|
|
17
16
|
const data = blockData?.data ?? block.data ?? {};
|
|
18
17
|
const children = blockData?.children ?? block.children ?? [];
|
|
19
18
|
|
|
20
|
-
const handleUpdateChildren = (newChildren: any[]) => {
|
|
21
|
-
updateBlock(block.id, { children: newChildren });
|
|
22
|
-
};
|
|
23
|
-
|
|
24
19
|
return (
|
|
25
|
-
<div className="block-vstack border border-[var(--kyro-border)] rounded-
|
|
26
|
-
<div className="flex items-center justify-between mb-
|
|
27
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
</span>
|
|
31
|
-
<span className="text-xs text-[var(--kyro-text-muted)]">
|
|
32
|
-
({children.length} children)
|
|
33
|
-
</span>
|
|
34
|
-
</div>
|
|
20
|
+
<div className="block-vstack border border-[var(--kyro-border)] rounded-md p-3 mb-2 relative group">
|
|
21
|
+
<div className="flex items-center justify-between mb-1">
|
|
22
|
+
<span className="text-xs font-semibold text-[var(--kyro-text-muted)] uppercase">
|
|
23
|
+
Stack
|
|
24
|
+
</span>
|
|
35
25
|
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
36
26
|
<button
|
|
37
27
|
type="button"
|
|
38
28
|
onClick={() => moveBlock(block.id, "up")}
|
|
39
|
-
className="p-1
|
|
29
|
+
className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
|
|
40
30
|
title="Move up"
|
|
41
31
|
>
|
|
42
|
-
<ChevronRight className="w-3
|
|
32
|
+
<ChevronRight className="w-3 h-3 rotate-90" />
|
|
43
33
|
</button>
|
|
44
34
|
<button
|
|
45
35
|
type="button"
|
|
46
36
|
onClick={() => moveBlock(block.id, "down")}
|
|
47
|
-
className="p-1
|
|
37
|
+
className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
|
|
48
38
|
title="Move down"
|
|
49
39
|
>
|
|
50
|
-
<ChevronRight className="w-3
|
|
40
|
+
<ChevronRight className="w-3 h-3" />
|
|
51
41
|
</button>
|
|
52
42
|
<button
|
|
53
43
|
type="button"
|
|
54
44
|
onClick={() => removeBlock(block.id)}
|
|
55
|
-
className="p-1
|
|
45
|
+
className="p-1 hover:bg-red-50 rounded text-red-500"
|
|
56
46
|
title="Remove"
|
|
57
47
|
>
|
|
58
|
-
<X className="w-3
|
|
48
|
+
<X className="w-3 h-3" />
|
|
59
49
|
</button>
|
|
60
50
|
</div>
|
|
61
51
|
</div>
|
|
62
52
|
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
children={children}
|
|
70
|
-
onUpdateChildren={handleUpdateChildren}
|
|
71
|
-
/>
|
|
72
|
-
</div>
|
|
53
|
+
<ChildrenField
|
|
54
|
+
blockId={block.id}
|
|
55
|
+
children={children}
|
|
56
|
+
onUpdateChildren={(c) => updateBlock(block.id, { children: c })}
|
|
57
|
+
compact
|
|
58
|
+
/>
|
|
73
59
|
</div>
|
|
74
60
|
);
|
|
75
61
|
};
|
|
@@ -1,45 +1,65 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
useBlockById,
|
|
4
|
+
useBlockActions,
|
|
5
|
+
} from "../fields/extensions/blocksStore";
|
|
3
6
|
import { ChevronRight, X } from "lucide-react";
|
|
7
|
+
import { VideoField } from "../fields/VideoField";
|
|
8
|
+
|
|
9
|
+
export const VideoBlock: React.FC<{ block: any; index: number }> = ({
|
|
10
|
+
block,
|
|
11
|
+
index,
|
|
12
|
+
}) => {
|
|
13
|
+
const blockData = useBlockById(block.id);
|
|
14
|
+
const { updateBlock, removeBlock, moveBlock } = useBlockActions();
|
|
15
|
+
|
|
16
|
+
const data = blockData?.data ?? block.data ?? {};
|
|
4
17
|
|
|
5
|
-
export const VideoBlock: React.FC<{ block: any; index: number }> = ({ block, index }) => {
|
|
6
|
-
const { updateBlock, removeBlock, moveBlock } = useBlocksStore();
|
|
7
|
-
|
|
8
18
|
const handleChange = (field: string, value: any) => {
|
|
9
|
-
updateBlock(block.id, { data: { ...
|
|
19
|
+
updateBlock(block.id, { data: { ...data, [field]: value } });
|
|
10
20
|
};
|
|
11
21
|
|
|
12
22
|
return (
|
|
13
|
-
<div className="block-video border border-[var(--kyro-border)] rounded-md p-
|
|
14
|
-
<div className="flex items-center justify-between mb-
|
|
15
|
-
<span className="text-xs font-semibold text-[var(--kyro-text-muted)] uppercase">
|
|
23
|
+
<div className="block-video border border-[var(--kyro-border)] rounded-md p-3 mb-2 relative group">
|
|
24
|
+
<div className="flex items-center justify-between mb-1">
|
|
25
|
+
<span className="text-xs font-semibold text-[var(--kyro-text-muted)] uppercase">
|
|
26
|
+
Video
|
|
27
|
+
</span>
|
|
16
28
|
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
17
|
-
<button
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
className="
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
29
|
+
<button
|
|
30
|
+
type="button"
|
|
31
|
+
onClick={() => moveBlock(block.id, "up")}
|
|
32
|
+
className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
|
|
33
|
+
title="Move up"
|
|
34
|
+
>
|
|
35
|
+
<ChevronRight className="w-3 h-3 rotate-90" />
|
|
36
|
+
</button>
|
|
37
|
+
<button
|
|
38
|
+
type="button"
|
|
39
|
+
onClick={() => moveBlock(block.id, "down")}
|
|
40
|
+
className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
|
|
41
|
+
title="Move down"
|
|
42
|
+
>
|
|
43
|
+
<ChevronRight className="w-3 h-3" />
|
|
44
|
+
</button>
|
|
45
|
+
<button
|
|
46
|
+
type="button"
|
|
47
|
+
onClick={() => removeBlock(block.id)}
|
|
48
|
+
className="p-1 hover:bg-red-50 rounded text-red-500"
|
|
49
|
+
title="Remove"
|
|
50
|
+
>
|
|
51
|
+
<X className="w-3 h-3" />
|
|
52
|
+
</button>
|
|
41
53
|
</div>
|
|
42
54
|
</div>
|
|
55
|
+
|
|
56
|
+
<VideoField
|
|
57
|
+
src={data.src || ""}
|
|
58
|
+
title={data.title || ""}
|
|
59
|
+
onChange={handleChange}
|
|
60
|
+
onUploadChange={(v) => handleChange("src", v)}
|
|
61
|
+
compact
|
|
62
|
+
/>
|
|
43
63
|
</div>
|
|
44
64
|
);
|
|
45
65
|
};
|