astro-tractstack 2.0.0-rc.50 → 2.0.0-rc.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/templates/src/components/Footer.astro +1 -6
- package/templates/src/components/compositor/Node.tsx +1 -1
- package/templates/src/components/compositor/nodes/Pane.tsx +5 -1
- package/templates/src/components/compositor/nodes/RenderChildren.tsx +6 -1
- package/templates/src/components/edit/panels/StyleWidgetPanel_config.tsx +3 -1
- package/templates/src/components/edit/widgets/BunnyWidget.tsx +520 -39
- package/templates/src/components/fields/ColorPickerCombo.tsx +63 -49
- package/templates/src/pages/[...slug].astro +3 -1
- package/templates/src/stores/nodes.ts +15 -36
package/package.json
CHANGED
|
@@ -106,12 +106,7 @@ const createdDate = created ? new Date(created) : new Date();
|
|
|
106
106
|
{allLinks.map((item: any) => (
|
|
107
107
|
<a
|
|
108
108
|
href={item.to}
|
|
109
|
-
class=
|
|
110
|
-
'z-10 whitespace-nowrap rounded px-3.5 py-1.5 text-lg shadow-sm transition-colors ' +
|
|
111
|
-
(item.featured
|
|
112
|
-
? 'bg-brand-7 hover:bg-myblack focus:bg-brand-7 text-white hover:text-white focus:text-white'
|
|
113
|
-
: 'text-brand-7 hover:bg-myblack focus:bg-brand-7 bg-white hover:text-white focus:text-white')
|
|
114
|
-
}
|
|
109
|
+
class="bg-brand-7 hover:bg-myblack focus:bg-brand-7 z-10 whitespace-nowrap rounded px-3.5 py-1.5 text-lg text-white shadow-sm transition-colors hover:text-white focus:text-white"
|
|
115
110
|
title={item.description}
|
|
116
111
|
>
|
|
117
112
|
<span class="font-bold">{item.name}</span>
|
|
@@ -87,7 +87,7 @@ const getElement = (
|
|
|
87
87
|
const isPreview = getCtx(props).rootNodeId.get() === `tmp`;
|
|
88
88
|
const hasPanes = useStore(getCtx(props).hasPanes);
|
|
89
89
|
const isTemplate = useStore(getCtx(props).isTemplate);
|
|
90
|
-
const sharedProps = { nodeId: node.id, ctx: props.ctx };
|
|
90
|
+
const sharedProps = { nodeId: node.id, ctx: props.ctx, config: props.config };
|
|
91
91
|
const type = getType(node);
|
|
92
92
|
|
|
93
93
|
switch (type) {
|
|
@@ -163,7 +163,11 @@ const Pane = memo(
|
|
|
163
163
|
) : codeHookPayload && codeHookTarget === 'list-content' ? (
|
|
164
164
|
<ListContentSetup nodeId={props.nodeId} params={codeHookParams} />
|
|
165
165
|
) : codeHookPayload && codeHookTarget === 'bunny-video' ? (
|
|
166
|
-
<BunnyVideoSetup
|
|
166
|
+
<BunnyVideoSetup
|
|
167
|
+
nodeId={props.nodeId}
|
|
168
|
+
params={codeHookParams}
|
|
169
|
+
config={props.config}
|
|
170
|
+
/>
|
|
167
171
|
) : codeHookPayload && codeHookTarget ? (
|
|
168
172
|
<CodeHookContainer
|
|
169
173
|
payload={{ target: codeHookTarget, params: codeHookParams }}
|
|
@@ -12,7 +12,12 @@ export const RenderChildren = (props: RenderChildrenProps) => {
|
|
|
12
12
|
return (
|
|
13
13
|
<>
|
|
14
14
|
{children.map((id: string) => (
|
|
15
|
-
<Node
|
|
15
|
+
<Node
|
|
16
|
+
nodeId={id}
|
|
17
|
+
key={id}
|
|
18
|
+
ctx={nodeProps.ctx}
|
|
19
|
+
config={nodeProps.config}
|
|
20
|
+
/>
|
|
16
21
|
))}
|
|
17
22
|
</>
|
|
18
23
|
);
|
|
@@ -75,7 +75,9 @@ function StyleWidgetConfigPanel({ node }: StyleWidgetConfigPanelProps) {
|
|
|
75
75
|
newNode.codeHookParams = stringParams;
|
|
76
76
|
|
|
77
77
|
// Update the copy field to match the new params
|
|
78
|
-
|
|
78
|
+
const paramsForCopy = stringParams.slice(0, widgetInfo.parameters.length);
|
|
79
|
+
newNode.copy = `${widgetType}(${paramsForCopy.join('|')})`;
|
|
80
|
+
console.log(newNode.copy);
|
|
79
81
|
|
|
80
82
|
// Mark the node as changed
|
|
81
83
|
newNode.isChanged = true;
|
|
@@ -2,51 +2,168 @@ import { useState, useEffect } from 'react';
|
|
|
2
2
|
import { getCtx } from '@/stores/nodes';
|
|
3
3
|
import SingleParam from '@/components/fields/SingleParam';
|
|
4
4
|
import { widgetMeta } from '@/constants';
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
FlatNode,
|
|
7
|
+
VideoMoment,
|
|
8
|
+
PaneNode,
|
|
9
|
+
StoryFragmentNode,
|
|
10
|
+
} from '@/types/compositorTypes';
|
|
11
|
+
import PlusIcon from '@heroicons/react/24/outline/PlusIcon';
|
|
12
|
+
import TrashIcon from '@heroicons/react/24/outline/TrashIcon';
|
|
13
|
+
import ClipboardDocumentIcon from '@heroicons/react/24/outline/ClipboardDocumentIcon';
|
|
14
|
+
import CheckIcon from '@heroicons/react/24/outline/CheckIcon';
|
|
15
|
+
import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon';
|
|
16
|
+
import ActionBuilderSlugSelector from '@/components/form/ActionBuilderSlugSelector';
|
|
17
|
+
import ChevronDownIcon from '@heroicons/react/24/outline/ChevronDownIcon';
|
|
18
|
+
import { Dialog } from '@ark-ui/react/dialog';
|
|
19
|
+
import { Portal } from '@ark-ui/react/portal';
|
|
20
|
+
import { canonicalURLStore } from '@/stores/storykeep';
|
|
6
21
|
|
|
7
22
|
interface BunnyWidgetProps {
|
|
8
23
|
node: FlatNode;
|
|
9
24
|
onUpdate: (params: string[]) => void;
|
|
10
25
|
}
|
|
11
26
|
|
|
12
|
-
|
|
27
|
+
interface Chapter extends VideoMoment {
|
|
28
|
+
id: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface PaneListItem {
|
|
32
|
+
id: string;
|
|
33
|
+
title: string;
|
|
34
|
+
slug: string;
|
|
35
|
+
type: 'Pane';
|
|
36
|
+
isContext: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface SelectorItem {
|
|
40
|
+
id: string;
|
|
41
|
+
title: string;
|
|
42
|
+
slug: string;
|
|
43
|
+
type: 'Pane' | 'StoryFragment';
|
|
44
|
+
panes?: string[];
|
|
45
|
+
isContext?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const generateId = (): string => {
|
|
49
|
+
return Math.random().toString(36).substring(2, 9);
|
|
50
|
+
};
|
|
13
51
|
|
|
14
52
|
function BunnyWidget({ node, onUpdate }: BunnyWidgetProps) {
|
|
15
53
|
const [videoId, setVideoId] = useState(
|
|
16
54
|
String(node.codeHookParams?.[0] || '')
|
|
17
55
|
);
|
|
18
56
|
const [title, setTitle] = useState(String(node.codeHookParams?.[1] || ''));
|
|
57
|
+
const [chapters, setChapters] = useState<Chapter[]>([]);
|
|
58
|
+
const [showChapterModal, setShowChapterModal] = useState(false);
|
|
59
|
+
const [formErrors, setFormErrors] = useState<Record<string, string>>({});
|
|
19
60
|
const [isDuplicate, setIsDuplicate] = useState(false);
|
|
20
61
|
const [validationError, setValidationError] = useState<string | null>(null);
|
|
62
|
+
const [isCopied, setIsCopied] = useState(false);
|
|
21
63
|
|
|
22
64
|
const widgetInfo = widgetMeta.bunny;
|
|
65
|
+
const ctx = getCtx();
|
|
66
|
+
const allNodes = ctx.allNodes.get();
|
|
67
|
+
|
|
68
|
+
const storyFragmentId = ctx.getClosestNodeTypeFromId(
|
|
69
|
+
node.id,
|
|
70
|
+
'StoryFragment'
|
|
71
|
+
);
|
|
72
|
+
const storyFragmentNode = allNodes.get(storyFragmentId) as
|
|
73
|
+
| StoryFragmentNode
|
|
74
|
+
| undefined;
|
|
75
|
+
const paneIds = storyFragmentNode?.paneIds || [];
|
|
76
|
+
|
|
77
|
+
const paneList: PaneListItem[] = paneIds
|
|
78
|
+
.map((paneId): PaneListItem | null => {
|
|
79
|
+
const paneNode = allNodes.get(paneId) as PaneNode | undefined;
|
|
80
|
+
if (paneNode && paneNode.nodeType === 'Pane') {
|
|
81
|
+
return {
|
|
82
|
+
id: paneNode.id,
|
|
83
|
+
title: paneNode.title,
|
|
84
|
+
slug: paneNode.slug,
|
|
85
|
+
type: 'Pane',
|
|
86
|
+
isContext: paneNode.isContextPane || false,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
})
|
|
91
|
+
.filter((item): item is PaneListItem => item !== null);
|
|
92
|
+
|
|
93
|
+
const storyFragmentEntry: SelectorItem | null = storyFragmentNode
|
|
94
|
+
? {
|
|
95
|
+
id: storyFragmentNode.id,
|
|
96
|
+
title: storyFragmentNode.title,
|
|
97
|
+
slug: storyFragmentNode.slug,
|
|
98
|
+
type: 'StoryFragment',
|
|
99
|
+
panes: storyFragmentNode.paneIds,
|
|
100
|
+
}
|
|
101
|
+
: null;
|
|
102
|
+
|
|
103
|
+
const contentMapForSelector: SelectorItem[] = [
|
|
104
|
+
...(storyFragmentEntry ? [storyFragmentEntry] : []),
|
|
105
|
+
...paneList,
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
const sortChapters = (chapArr: Chapter[]) =>
|
|
109
|
+
[...chapArr].sort((a, b) => a.startTime - b.startTime);
|
|
23
110
|
|
|
24
111
|
useEffect(() => {
|
|
25
112
|
const newVideoId = String(node.codeHookParams?.[0] || '');
|
|
113
|
+
const newTitle = String(node.codeHookParams?.[1] || '');
|
|
114
|
+
const chaptersJson = String(node.codeHookParams?.[2] || '');
|
|
115
|
+
|
|
26
116
|
setVideoId(newVideoId);
|
|
27
|
-
setTitle(
|
|
117
|
+
setTitle(newTitle);
|
|
28
118
|
validateVideoId(newVideoId);
|
|
119
|
+
|
|
120
|
+
if (chaptersJson) {
|
|
121
|
+
try {
|
|
122
|
+
const parsedChapters = JSON.parse(chaptersJson);
|
|
123
|
+
if (Array.isArray(parsedChapters)) {
|
|
124
|
+
const chaptersWithIds = parsedChapters.map(
|
|
125
|
+
(chapter: VideoMoment) => ({ ...chapter, id: generateId() })
|
|
126
|
+
);
|
|
127
|
+
setChapters(sortChapters(chaptersWithIds));
|
|
128
|
+
}
|
|
129
|
+
} catch (e) {
|
|
130
|
+
setChapters([]);
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
setChapters([]);
|
|
134
|
+
}
|
|
29
135
|
}, [node]);
|
|
30
136
|
|
|
137
|
+
const handleUpdate = (
|
|
138
|
+
newVideoId: string,
|
|
139
|
+
newTitle: string,
|
|
140
|
+
newChapters: Chapter[]
|
|
141
|
+
) => {
|
|
142
|
+
const chaptersToStore = sortChapters(newChapters).map(
|
|
143
|
+
({ id, ...rest }) => rest
|
|
144
|
+
);
|
|
145
|
+
if (chaptersToStore.length > 0) {
|
|
146
|
+
onUpdate([newVideoId, newTitle, JSON.stringify(chaptersToStore)]);
|
|
147
|
+
} else {
|
|
148
|
+
onUpdate([newVideoId, newTitle]);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
31
152
|
const checkForDuplicates = (id: string): boolean => {
|
|
32
153
|
if (!id) return false;
|
|
33
154
|
try {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
).length;
|
|
39
|
-
return count > 1;
|
|
155
|
+
return (
|
|
156
|
+
ctx.getAllBunnyVideoInfo().filter((video) => video.videoId === id)
|
|
157
|
+
.length > 1
|
|
158
|
+
);
|
|
40
159
|
} catch (e) {
|
|
41
|
-
console.error('Error checking for duplicates:', e);
|
|
42
160
|
return false;
|
|
43
161
|
}
|
|
44
162
|
};
|
|
45
163
|
|
|
46
164
|
const isValidVideoIdFormat = (id: string): boolean => {
|
|
47
|
-
if (!id) return true;
|
|
48
|
-
|
|
49
|
-
return videoIdRegex.test(id);
|
|
165
|
+
if (!id) return true;
|
|
166
|
+
return /^\d+\/[a-f0-9\-]{36}$/.test(id);
|
|
50
167
|
};
|
|
51
168
|
|
|
52
169
|
const validateVideoId = (id: string) => {
|
|
@@ -55,7 +172,6 @@ function BunnyWidget({ node, onUpdate }: BunnyWidgetProps) {
|
|
|
55
172
|
setIsDuplicate(false);
|
|
56
173
|
return;
|
|
57
174
|
}
|
|
58
|
-
|
|
59
175
|
if (!isValidVideoIdFormat(id)) {
|
|
60
176
|
setValidationError(
|
|
61
177
|
"Invalid format. Use 'LibraryID/VideoGUID' from Bunny."
|
|
@@ -63,7 +179,6 @@ function BunnyWidget({ node, onUpdate }: BunnyWidgetProps) {
|
|
|
63
179
|
setIsDuplicate(false);
|
|
64
180
|
return;
|
|
65
181
|
}
|
|
66
|
-
|
|
67
182
|
const duplicate = checkForDuplicates(id);
|
|
68
183
|
setIsDuplicate(duplicate);
|
|
69
184
|
setValidationError(
|
|
@@ -74,18 +189,139 @@ function BunnyWidget({ node, onUpdate }: BunnyWidgetProps) {
|
|
|
74
189
|
const handleVideoIdChange = (value: string) => {
|
|
75
190
|
setVideoId(value);
|
|
76
191
|
validateVideoId(value);
|
|
77
|
-
|
|
192
|
+
handleUpdate(value, title, chapters);
|
|
78
193
|
};
|
|
79
194
|
|
|
80
195
|
const handleTitleChange = (value: string) => {
|
|
81
196
|
setTitle(value);
|
|
82
|
-
|
|
197
|
+
handleUpdate(videoId, value, chapters);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const validateAllChapters = (allChaps: Chapter[]) =>
|
|
201
|
+
allChaps.reduce(
|
|
202
|
+
(acc, chap, idx) => ({ ...acc, ...validateChapter(chap, idx, allChaps) }),
|
|
203
|
+
{}
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const validateChapter = (
|
|
207
|
+
chap: Chapter,
|
|
208
|
+
index: number,
|
|
209
|
+
allChaps: Chapter[]
|
|
210
|
+
): Record<string, string> => {
|
|
211
|
+
const errors: Record<string, string> = {};
|
|
212
|
+
if (!chap.title?.trim()) {
|
|
213
|
+
errors[`title-${index}`] = 'Title is required';
|
|
214
|
+
}
|
|
215
|
+
if (
|
|
216
|
+
typeof chap.startTime !== 'number' ||
|
|
217
|
+
isNaN(chap.startTime) ||
|
|
218
|
+
chap.startTime < 0
|
|
219
|
+
) {
|
|
220
|
+
errors[`startTime-${index}`] = 'Start time is required';
|
|
221
|
+
}
|
|
222
|
+
if (typeof chap.endTime !== 'number' || isNaN(chap.endTime)) {
|
|
223
|
+
errors[`endTime-${index}`] = 'End time is required';
|
|
224
|
+
} else if (chap.startTime !== undefined && chap.endTime <= chap.startTime) {
|
|
225
|
+
errors[`endTime-${index}`] = 'End time must be > start time';
|
|
226
|
+
}
|
|
227
|
+
const otherChapters = allChaps.filter((_, i) => i !== index);
|
|
228
|
+
for (const other of otherChapters) {
|
|
229
|
+
if (
|
|
230
|
+
Math.max(chap.startTime, other.startTime) <
|
|
231
|
+
Math.min(chap.endTime, other.endTime)
|
|
232
|
+
) {
|
|
233
|
+
errors[`overlap-${index}`] = 'Chapter times overlap';
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return errors;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const addChapter = () => {
|
|
241
|
+
const newChapter: Chapter = {
|
|
242
|
+
id: generateId(),
|
|
243
|
+
title: 'New Chapter',
|
|
244
|
+
startTime:
|
|
245
|
+
chapters.length > 0 ? chapters[chapters.length - 1].endTime : 0,
|
|
246
|
+
endTime:
|
|
247
|
+
chapters.length > 0 ? chapters[chapters.length - 1].endTime + 60 : 60,
|
|
248
|
+
};
|
|
249
|
+
const updatedChapters = sortChapters([...chapters, newChapter]);
|
|
250
|
+
setFormErrors(validateAllChapters(updatedChapters));
|
|
251
|
+
setChapters(updatedChapters);
|
|
252
|
+
handleUpdate(videoId, title, updatedChapters);
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const updateChapter = (chapterId: string, updates: Partial<Chapter>) => {
|
|
256
|
+
const chapterIndex = chapters.findIndex((c) => c.id === chapterId);
|
|
257
|
+
if (chapterIndex === -1) return;
|
|
258
|
+
const updatedChapters = [...chapters];
|
|
259
|
+
updatedChapters[chapterIndex] = {
|
|
260
|
+
...updatedChapters[chapterIndex],
|
|
261
|
+
...updates,
|
|
262
|
+
};
|
|
263
|
+
const sortedChapters = sortChapters(updatedChapters);
|
|
264
|
+
setFormErrors(validateAllChapters(sortedChapters));
|
|
265
|
+
setChapters(sortedChapters);
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const removeChapter = (idToRemove: string) => {
|
|
269
|
+
const updatedChapters = chapters.filter((c) => c.id !== idToRemove);
|
|
270
|
+
setFormErrors(validateAllChapters(updatedChapters));
|
|
271
|
+
setChapters(updatedChapters);
|
|
272
|
+
handleUpdate(videoId, title, updatedChapters);
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const handlePaneSelect = (chapterId: string, slug: string) => {
|
|
276
|
+
const chapterIndex = chapters.findIndex((c) => c.id === chapterId);
|
|
277
|
+
if (chapterIndex === -1) return;
|
|
278
|
+
const updatedChapters = [...chapters];
|
|
279
|
+
const selectedPane = paneList.find((p) => p.slug === slug);
|
|
280
|
+
updatedChapters[chapterIndex].linkedPaneId = selectedPane
|
|
281
|
+
? selectedPane.id
|
|
282
|
+
: undefined;
|
|
283
|
+
setChapters(updatedChapters);
|
|
284
|
+
handleUpdate(videoId, title, updatedChapters);
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const handleUnlinkPane = (chapterId: string) => {
|
|
288
|
+
const chapterIndex = chapters.findIndex((c) => c.id === chapterId);
|
|
289
|
+
if (chapterIndex === -1) return;
|
|
290
|
+
const updatedChapters = [...chapters];
|
|
291
|
+
delete updatedChapters[chapterIndex].linkedPaneId;
|
|
292
|
+
setChapters(updatedChapters);
|
|
293
|
+
handleUpdate(videoId, title, updatedChapters);
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const getLinkedPaneSlug = (linkedPaneId?: string): string => {
|
|
297
|
+
if (!linkedPaneId) return '';
|
|
298
|
+
const paneNode = allNodes.get(linkedPaneId) as PaneNode | undefined;
|
|
299
|
+
return paneNode?.slug || '';
|
|
83
300
|
};
|
|
84
301
|
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
302
|
+
const handleCopyAll = () => {
|
|
303
|
+
const canonicalURL = getCanonicalURL();
|
|
304
|
+
if (!canonicalURL) return;
|
|
305
|
+
const linksText = chapters
|
|
306
|
+
.filter((c) => c.linkedPaneId && getLinkedPaneSlug(c.linkedPaneId))
|
|
307
|
+
.map((chapter) => {
|
|
308
|
+
const paneSlug = getLinkedPaneSlug(chapter.linkedPaneId);
|
|
309
|
+
return `${chapter.title}\n${canonicalURL}#${paneSlug}\n${canonicalURL}?t=${chapter.startTime}s`;
|
|
310
|
+
})
|
|
311
|
+
.join('\n\n');
|
|
312
|
+
const fullBlock = `${canonicalURL}\n${canonicalURL}?t=0s\n\n${linksText}`;
|
|
313
|
+
navigator.clipboard.writeText(fullBlock);
|
|
314
|
+
setIsCopied(true);
|
|
315
|
+
setTimeout(() => setIsCopied(false), 2000);
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const getCanonicalURL = () => {
|
|
319
|
+
try {
|
|
320
|
+
return canonicalURLStore.get();
|
|
321
|
+
} catch (e) {
|
|
322
|
+
return '';
|
|
323
|
+
}
|
|
324
|
+
};
|
|
89
325
|
|
|
90
326
|
return (
|
|
91
327
|
<div className="space-y-4">
|
|
@@ -100,31 +336,276 @@ function BunnyWidget({ node, onUpdate }: BunnyWidgetProps) {
|
|
|
100
336
|
)}
|
|
101
337
|
{isDuplicate && (
|
|
102
338
|
<div className="rounded border border-yellow-200 bg-yellow-50 p-2 text-xs text-yellow-800">
|
|
103
|
-
Warning: This video is already used elsewhere
|
|
339
|
+
Warning: This video is already used elsewhere.
|
|
104
340
|
</div>
|
|
105
341
|
)}
|
|
106
|
-
|
|
107
342
|
<SingleParam
|
|
108
343
|
label={widgetInfo.parameters[1].label}
|
|
109
344
|
value={title}
|
|
110
345
|
onChange={handleTitleChange}
|
|
111
346
|
/>
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
<
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
347
|
+
<div className="mt-4 border-t border-gray-200 pt-4">
|
|
348
|
+
<button
|
|
349
|
+
type="button"
|
|
350
|
+
onClick={() => setShowChapterModal(true)}
|
|
351
|
+
className="flex w-full items-center justify-center rounded-md bg-gray-100 px-3 py-2 text-sm font-bold text-gray-700 hover:bg-gray-200"
|
|
352
|
+
>
|
|
353
|
+
<ChevronDownIcon className="mr-2 h-5 w-5" />
|
|
354
|
+
{chapters.length > 0
|
|
355
|
+
? `Configure ${chapters.length} Chapter(s)`
|
|
356
|
+
: 'Configure Chapters'}
|
|
357
|
+
</button>
|
|
358
|
+
</div>
|
|
359
|
+
<Dialog.Root
|
|
360
|
+
open={showChapterModal}
|
|
361
|
+
onOpenChange={(details) => setShowChapterModal(details.open)}
|
|
362
|
+
modal={true}
|
|
363
|
+
preventScroll={true}
|
|
364
|
+
>
|
|
365
|
+
<Portal>
|
|
366
|
+
<Dialog.Backdrop
|
|
367
|
+
className="fixed inset-0 bg-black bg-opacity-75 backdrop-blur-sm"
|
|
368
|
+
style={{ zIndex: 9005 }}
|
|
369
|
+
/>
|
|
370
|
+
<Dialog.Positioner
|
|
371
|
+
className="fixed inset-0 flex items-center justify-center p-4"
|
|
372
|
+
style={{ zIndex: 9005 }}
|
|
373
|
+
>
|
|
374
|
+
<Dialog.Content
|
|
375
|
+
className="w-full max-w-4xl overflow-hidden rounded-lg bg-slate-50 shadow-xl"
|
|
376
|
+
style={{ height: '80vh' }}
|
|
377
|
+
>
|
|
378
|
+
<div className="flex h-full flex-col">
|
|
379
|
+
<div className="flex-shrink-0 border-b border-gray-200 bg-white px-6 py-3">
|
|
380
|
+
<Dialog.Title className="text-lg font-bold text-gray-900">
|
|
381
|
+
Chapter Configuration
|
|
382
|
+
</Dialog.Title>
|
|
383
|
+
</div>
|
|
384
|
+
<div className="flex-1 overflow-y-auto p-4">
|
|
385
|
+
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
|
386
|
+
<div className="rounded-lg border border-gray-200 bg-white shadow-sm">
|
|
387
|
+
<div className="flex items-center justify-between border-b border-gray-200 p-3">
|
|
388
|
+
<h3 className="text-base font-bold text-gray-900">
|
|
389
|
+
Video Chapters
|
|
390
|
+
</h3>
|
|
391
|
+
<button
|
|
392
|
+
type="button"
|
|
393
|
+
onClick={addChapter}
|
|
394
|
+
className="flex items-center rounded bg-cyan-600 px-3 py-1 text-sm font-bold text-white hover:bg-cyan-700"
|
|
395
|
+
>
|
|
396
|
+
<PlusIcon className="mr-1 h-4 w-4" /> Add
|
|
397
|
+
</button>
|
|
398
|
+
</div>
|
|
399
|
+
<div className="divide-y divide-gray-200">
|
|
400
|
+
{chapters.length === 0 && (
|
|
401
|
+
<div className="p-6 text-center text-sm text-gray-500">
|
|
402
|
+
No chapters added yet.
|
|
403
|
+
</div>
|
|
404
|
+
)}
|
|
405
|
+
{chapters.map((chapter, index) => (
|
|
406
|
+
<div key={chapter.id} className="p-3">
|
|
407
|
+
<div className="mb-2 flex items-center justify-between">
|
|
408
|
+
<h4 className="text-sm font-bold text-gray-900">
|
|
409
|
+
Chapter {index + 1}: {chapter.title}
|
|
410
|
+
</h4>
|
|
411
|
+
<button
|
|
412
|
+
type="button"
|
|
413
|
+
onClick={() => removeChapter(chapter.id)}
|
|
414
|
+
className="rounded p-1 text-red-600 hover:bg-gray-100 hover:text-red-700"
|
|
415
|
+
title="Remove chapter"
|
|
416
|
+
>
|
|
417
|
+
<TrashIcon className="h-4 w-4" />
|
|
418
|
+
</button>
|
|
419
|
+
</div>
|
|
420
|
+
<div className="grid grid-cols-1 gap-2">
|
|
421
|
+
<div>
|
|
422
|
+
<label className="block text-xs font-bold text-gray-700">
|
|
423
|
+
Title
|
|
424
|
+
</label>
|
|
425
|
+
<input
|
|
426
|
+
type="text"
|
|
427
|
+
value={chapter.title}
|
|
428
|
+
onChange={(e) =>
|
|
429
|
+
updateChapter(chapter.id, {
|
|
430
|
+
title: e.target.value,
|
|
431
|
+
})
|
|
432
|
+
}
|
|
433
|
+
onBlur={() =>
|
|
434
|
+
handleUpdate(videoId, title, chapters)
|
|
435
|
+
}
|
|
436
|
+
className={`mt-1 block w-full rounded-md border-gray-300 px-2 py-1 shadow-sm sm:text-sm ${formErrors[`title-${index}`] ? 'border-red-300' : 'focus:border-cyan-500 focus:ring-cyan-500'}`}
|
|
437
|
+
/>
|
|
438
|
+
</div>
|
|
439
|
+
<div>
|
|
440
|
+
<label className="block text-xs font-bold text-gray-700">
|
|
441
|
+
Description
|
|
442
|
+
</label>
|
|
443
|
+
<input
|
|
444
|
+
type="text"
|
|
445
|
+
value={chapter.description || ''}
|
|
446
|
+
onChange={(e) =>
|
|
447
|
+
updateChapter(chapter.id, {
|
|
448
|
+
description: e.target.value,
|
|
449
|
+
})
|
|
450
|
+
}
|
|
451
|
+
onBlur={() =>
|
|
452
|
+
handleUpdate(videoId, title, chapters)
|
|
453
|
+
}
|
|
454
|
+
className="mt-1 block w-full rounded-md border-gray-300 px-2 py-1 shadow-sm focus:border-cyan-500 focus:ring-cyan-500 sm:text-sm"
|
|
455
|
+
/>
|
|
456
|
+
</div>
|
|
457
|
+
<div className="grid grid-cols-2 gap-2">
|
|
458
|
+
<div>
|
|
459
|
+
<label className="block text-xs font-bold text-gray-700">
|
|
460
|
+
Start (s)
|
|
461
|
+
</label>
|
|
462
|
+
<input
|
|
463
|
+
type="number"
|
|
464
|
+
min="0"
|
|
465
|
+
value={chapter.startTime}
|
|
466
|
+
onChange={(e) =>
|
|
467
|
+
updateChapter(chapter.id, {
|
|
468
|
+
startTime:
|
|
469
|
+
parseInt(e.target.value) || 0,
|
|
470
|
+
})
|
|
471
|
+
}
|
|
472
|
+
onBlur={() =>
|
|
473
|
+
handleUpdate(videoId, title, chapters)
|
|
474
|
+
}
|
|
475
|
+
className={`mt-1 block w-full rounded-md border-gray-300 px-2 py-1 shadow-sm sm:text-sm ${formErrors[`startTime-${index}`] || formErrors[`overlap-${index}`] ? 'border-red-300' : 'focus:border-cyan-500 focus:ring-cyan-500'}`}
|
|
476
|
+
/>
|
|
477
|
+
</div>
|
|
478
|
+
<div>
|
|
479
|
+
<label className="block text-xs font-bold text-gray-700">
|
|
480
|
+
End (s)
|
|
481
|
+
</label>
|
|
482
|
+
<input
|
|
483
|
+
type="number"
|
|
484
|
+
min="0"
|
|
485
|
+
value={chapter.endTime}
|
|
486
|
+
onChange={(e) =>
|
|
487
|
+
updateChapter(chapter.id, {
|
|
488
|
+
endTime: parseInt(e.target.value) || 0,
|
|
489
|
+
})
|
|
490
|
+
}
|
|
491
|
+
onBlur={() =>
|
|
492
|
+
handleUpdate(videoId, title, chapters)
|
|
493
|
+
}
|
|
494
|
+
className={`mt-1 block w-full rounded-md border-gray-300 px-2 py-1 shadow-sm sm:text-sm ${formErrors[`endTime-${index}`] || formErrors[`overlap-${index}`] ? 'border-red-300' : 'focus:border-cyan-500 focus:ring-cyan-500'}`}
|
|
495
|
+
/>
|
|
496
|
+
</div>
|
|
497
|
+
</div>
|
|
498
|
+
{(formErrors[`overlap-${index}`] ||
|
|
499
|
+
formErrors[`endTime-${index}`]) && (
|
|
500
|
+
<p className="mt-1 text-xs text-red-600">
|
|
501
|
+
{formErrors[`overlap-${index}`] ||
|
|
502
|
+
formErrors[`endTime-${index}`]}
|
|
503
|
+
</p>
|
|
504
|
+
)}
|
|
505
|
+
<div className="relative">
|
|
506
|
+
<div className="flex items-center justify-between">
|
|
507
|
+
<label className="block text-xs font-bold text-gray-700">
|
|
508
|
+
Linked Pane
|
|
509
|
+
</label>
|
|
510
|
+
{chapter.linkedPaneId && (
|
|
511
|
+
<button
|
|
512
|
+
type="button"
|
|
513
|
+
onClick={() =>
|
|
514
|
+
handleUnlinkPane(chapter.id)
|
|
515
|
+
}
|
|
516
|
+
className="flex items-center text-xs text-red-600 hover:underline"
|
|
517
|
+
>
|
|
518
|
+
<XMarkIcon className="mr-1 h-3 w-3" />{' '}
|
|
519
|
+
Unlink
|
|
520
|
+
</button>
|
|
521
|
+
)}
|
|
522
|
+
</div>
|
|
523
|
+
<ActionBuilderSlugSelector
|
|
524
|
+
type="pane"
|
|
525
|
+
value={getLinkedPaneSlug(
|
|
526
|
+
chapter.linkedPaneId
|
|
527
|
+
)}
|
|
528
|
+
onSelect={(slug: string) =>
|
|
529
|
+
handlePaneSelect(chapter.id, slug)
|
|
530
|
+
}
|
|
531
|
+
label="Linked Pane"
|
|
532
|
+
placeholder="Select a pane"
|
|
533
|
+
contentMap={contentMapForSelector}
|
|
534
|
+
parentSlug={storyFragmentNode?.slug}
|
|
535
|
+
/>
|
|
536
|
+
</div>
|
|
537
|
+
</div>
|
|
538
|
+
</div>
|
|
539
|
+
))}
|
|
540
|
+
</div>
|
|
541
|
+
</div>
|
|
542
|
+
<div className="rounded-lg border border-gray-200 bg-white shadow-sm">
|
|
543
|
+
<div className="flex items-center justify-between border-b border-gray-200 p-3">
|
|
544
|
+
<h3 className="text-base font-bold text-gray-900">
|
|
545
|
+
Chapter Links
|
|
546
|
+
</h3>
|
|
547
|
+
<button
|
|
548
|
+
onClick={handleCopyAll}
|
|
549
|
+
className="flex items-center rounded bg-gray-200 px-3 py-1 text-sm font-bold text-gray-700 hover:bg-gray-300"
|
|
550
|
+
>
|
|
551
|
+
{isCopied ? (
|
|
552
|
+
<>
|
|
553
|
+
<CheckIcon className="mr-1 h-4 w-4 text-green-500" />{' '}
|
|
554
|
+
Copied
|
|
555
|
+
</>
|
|
556
|
+
) : (
|
|
557
|
+
<>
|
|
558
|
+
<ClipboardDocumentIcon className="mr-1 h-4 w-4" />{' '}
|
|
559
|
+
Copy All
|
|
560
|
+
</>
|
|
561
|
+
)}
|
|
562
|
+
</button>
|
|
563
|
+
</div>
|
|
564
|
+
<div className="overflow-y-auto bg-gray-50 p-4 font-mono text-xs">
|
|
565
|
+
{getCanonicalURL() ? (
|
|
566
|
+
<>
|
|
567
|
+
<p className="mb-2 font-bold">
|
|
568
|
+
{getCanonicalURL()}
|
|
569
|
+
</p>
|
|
570
|
+
<p className="mb-3">{getCanonicalURL()}?t=0s</p>
|
|
571
|
+
{chapters
|
|
572
|
+
.filter(
|
|
573
|
+
(c) =>
|
|
574
|
+
c.linkedPaneId &&
|
|
575
|
+
getLinkedPaneSlug(c.linkedPaneId)
|
|
576
|
+
)
|
|
577
|
+
.map((chapter) => (
|
|
578
|
+
<div
|
|
579
|
+
key={chapter.id}
|
|
580
|
+
className="mb-3 border-t pt-3"
|
|
581
|
+
>
|
|
582
|
+
<p className="mb-1 italic">{chapter.title}</p>
|
|
583
|
+
<p>{`${getCanonicalURL()}#${getLinkedPaneSlug(chapter.linkedPaneId)}`}</p>
|
|
584
|
+
<p>{`${getCanonicalURL()}?t=${chapter.startTime}s`}</p>
|
|
585
|
+
</div>
|
|
586
|
+
))}
|
|
587
|
+
</>
|
|
588
|
+
) : (
|
|
589
|
+
<p className="text-gray-500">
|
|
590
|
+
Canonical URL not available.
|
|
591
|
+
</p>
|
|
592
|
+
)}
|
|
593
|
+
</div>
|
|
594
|
+
</div>
|
|
595
|
+
</div>
|
|
596
|
+
</div>
|
|
597
|
+
<div className="flex-shrink-0 justify-end border-t border-gray-200 bg-white px-6 py-3">
|
|
598
|
+
<Dialog.CloseTrigger asChild>
|
|
599
|
+
<button className="rounded bg-gray-600 px-4 py-2 text-sm font-bold text-white hover:bg-gray-700">
|
|
600
|
+
Close
|
|
601
|
+
</button>
|
|
602
|
+
</Dialog.CloseTrigger>
|
|
603
|
+
</div>
|
|
604
|
+
</div>
|
|
605
|
+
</Dialog.Content>
|
|
606
|
+
</Dialog.Positioner>
|
|
607
|
+
</Portal>
|
|
608
|
+
</Dialog.Root>
|
|
128
609
|
</div>
|
|
129
610
|
);
|
|
130
611
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useState, useCallback, useMemo, useEffect, useRef } from 'react';
|
|
2
2
|
import { Combobox } from '@ark-ui/react';
|
|
3
|
+
import { Portal } from '@ark-ui/react/portal';
|
|
3
4
|
import { createListCollection } from '@ark-ui/react/collection';
|
|
4
5
|
import ChevronUpDownIcon from '@heroicons/react/24/outline/ChevronUpDownIcon';
|
|
5
6
|
import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon';
|
|
@@ -42,7 +43,7 @@ const ColorPickerCombo = ({
|
|
|
42
43
|
|
|
43
44
|
// Add ref and useDropdownDirection hook
|
|
44
45
|
const comboboxRef = useRef<HTMLDivElement>(null);
|
|
45
|
-
const { openAbove
|
|
46
|
+
const { openAbove } = useDropdownDirection(comboboxRef);
|
|
46
47
|
|
|
47
48
|
// Get all available Tailwind color options
|
|
48
49
|
const allTailwindColorOptions = useMemo(() => {
|
|
@@ -223,56 +224,69 @@ const ColorPickerCombo = ({
|
|
|
223
224
|
onValueChange={handleTailwindColorChange}
|
|
224
225
|
onInputValueChange={handleInputChange}
|
|
225
226
|
selectionBehavior="replace"
|
|
227
|
+
loopFocus={true}
|
|
228
|
+
openOnKeyPress={true}
|
|
229
|
+
composite={true}
|
|
230
|
+
positioning={{
|
|
231
|
+
placement: openAbove ? 'top' : 'bottom',
|
|
232
|
+
gutter: 4,
|
|
233
|
+
sameWidth: true,
|
|
234
|
+
}}
|
|
226
235
|
>
|
|
227
|
-
<
|
|
228
|
-
<
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
<Combobox.Trigger className="absolute inset-y-0 right-0 flex items-center pr-2">
|
|
234
|
-
<ChevronUpDownIcon
|
|
235
|
-
className="text-mydarkgrey h-5 w-5"
|
|
236
|
-
aria-hidden="true"
|
|
236
|
+
<Combobox.Control ref={comboboxRef}>
|
|
237
|
+
<div className="relative">
|
|
238
|
+
<Combobox.Input
|
|
239
|
+
className="border-mydarkgrey focus:border-myblue focus:ring-myblue xs:text-sm w-full max-w-48 rounded-md py-2 pl-3 pr-10 shadow-sm"
|
|
240
|
+
placeholder="Search Tailwind colors..."
|
|
241
|
+
autoComplete="off"
|
|
237
242
|
/>
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
>
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
filteredColors.
|
|
251
|
-
<
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
<
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
243
|
+
<Combobox.Trigger className="absolute inset-y-0 right-0 flex items-center pr-2">
|
|
244
|
+
<ChevronUpDownIcon
|
|
245
|
+
className="text-mydarkgrey h-5 w-5"
|
|
246
|
+
aria-hidden="true"
|
|
247
|
+
/>
|
|
248
|
+
</Combobox.Trigger>
|
|
249
|
+
</div>
|
|
250
|
+
</Combobox.Control>
|
|
251
|
+
|
|
252
|
+
<Portal>
|
|
253
|
+
<Combobox.Positioner style={{ zIndex: 1002 }}>
|
|
254
|
+
<Combobox.Content className="xs:text-sm max-h-64 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
|
255
|
+
{filteredColors.length === 0 ? (
|
|
256
|
+
<div className="relative cursor-default select-none py-2 pl-3 pr-4 text-black">
|
|
257
|
+
Nothing found.
|
|
258
|
+
</div>
|
|
259
|
+
) : (
|
|
260
|
+
filteredColors.map((color) => (
|
|
261
|
+
<Combobox.Item
|
|
262
|
+
key={color}
|
|
263
|
+
item={color}
|
|
264
|
+
className="color-item relative cursor-default select-none py-2 pl-3 pr-4"
|
|
265
|
+
>
|
|
266
|
+
<div className="flex items-center">
|
|
267
|
+
<div
|
|
268
|
+
className="mr-3 h-6 w-6 flex-shrink-0 rounded"
|
|
269
|
+
style={{
|
|
270
|
+
backgroundColor: tailwindToHex(
|
|
271
|
+
`bg-${color}`,
|
|
272
|
+
config.BRAND_COLOURS || null
|
|
273
|
+
),
|
|
274
|
+
}}
|
|
275
|
+
/>
|
|
276
|
+
<span className="block truncate">{color}</span>
|
|
277
|
+
<span className="color-indicator absolute inset-y-0 right-0 flex items-center pr-3 text-cyan-600">
|
|
278
|
+
<CheckIcon
|
|
279
|
+
className="h-5 w-5"
|
|
280
|
+
aria-hidden="true"
|
|
281
|
+
/>
|
|
282
|
+
</span>
|
|
283
|
+
</div>
|
|
284
|
+
</Combobox.Item>
|
|
285
|
+
))
|
|
286
|
+
)}
|
|
287
|
+
</Combobox.Content>
|
|
288
|
+
</Combobox.Positioner>
|
|
289
|
+
</Portal>
|
|
276
290
|
</Combobox.Root>
|
|
277
291
|
</div>
|
|
278
292
|
)}
|
|
@@ -30,7 +30,9 @@ let storyData;
|
|
|
30
30
|
try {
|
|
31
31
|
storyData = await getStoryData(Astro, lookup, sessionId, tenantId);
|
|
32
32
|
} catch (error) {
|
|
33
|
-
if (error instanceof Response) {
|
|
33
|
+
if (error instanceof Response && error.status === 404) {
|
|
34
|
+
return Astro.redirect('/404');
|
|
35
|
+
} else if (error instanceof Response) {
|
|
34
36
|
return error;
|
|
35
37
|
}
|
|
36
38
|
console.error('Error fetching storyfragment:', error);
|
|
@@ -2190,9 +2190,10 @@ export class NodesContext {
|
|
|
2190
2190
|
getAllBunnyVideoInfo(): { url: string; title: string; videoId: string }[] {
|
|
2191
2191
|
const results: { url: string; title: string; videoId: string }[] = [];
|
|
2192
2192
|
const processedVideoIds = new Set<string>();
|
|
2193
|
-
|
|
2194
|
-
// Find panes with bunny-video code hook
|
|
2195
2193
|
const allNodes = Array.from(this.allNodes.get().values());
|
|
2194
|
+
const BUNNY_EMBED_BASE_URL = 'https://iframe.mediadelivery.net/embed/';
|
|
2195
|
+
|
|
2196
|
+
// Process pane-level bunny videos (which use full URLs)
|
|
2196
2197
|
const paneNodes = allNodes.filter(
|
|
2197
2198
|
(node) =>
|
|
2198
2199
|
node.nodeType === 'Pane' &&
|
|
@@ -2200,7 +2201,6 @@ export class NodesContext {
|
|
|
2200
2201
|
node.codeHookTarget === 'bunny-video'
|
|
2201
2202
|
) as PaneNode[];
|
|
2202
2203
|
|
|
2203
|
-
// Process pane-level bunny videos
|
|
2204
2204
|
for (const paneNode of paneNodes) {
|
|
2205
2205
|
try {
|
|
2206
2206
|
if (
|
|
@@ -2243,7 +2243,7 @@ export class NodesContext {
|
|
|
2243
2243
|
}
|
|
2244
2244
|
}
|
|
2245
2245
|
|
|
2246
|
-
//
|
|
2246
|
+
// Process inline bunny widgets (which use ID/GUID fragments)
|
|
2247
2247
|
const codeNodes = allNodes.filter(
|
|
2248
2248
|
(node) =>
|
|
2249
2249
|
node.nodeType === 'TagElement' &&
|
|
@@ -2255,47 +2255,26 @@ export class NodesContext {
|
|
|
2255
2255
|
node.copy.includes('bunny(')
|
|
2256
2256
|
) as FlatNode[];
|
|
2257
2257
|
|
|
2258
|
-
// Process inline widgets
|
|
2259
2258
|
for (const codeNode of codeNodes) {
|
|
2260
2259
|
if (
|
|
2261
2260
|
Array.isArray(codeNode.codeHookParams) &&
|
|
2262
|
-
codeNode.codeHookParams.length
|
|
2261
|
+
codeNode.codeHookParams.length > 0
|
|
2263
2262
|
) {
|
|
2264
|
-
const
|
|
2265
|
-
const
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
if (url) {
|
|
2275
|
-
let videoId = '';
|
|
2276
|
-
try {
|
|
2277
|
-
const urlObj = new URL(url);
|
|
2278
|
-
if (
|
|
2279
|
-
urlObj.hostname === 'iframe.mediadelivery.net' &&
|
|
2280
|
-
urlObj.pathname.startsWith('/embed/')
|
|
2281
|
-
) {
|
|
2282
|
-
const pathParts = urlObj.pathname.split('/');
|
|
2283
|
-
if (pathParts.length >= 4) {
|
|
2284
|
-
videoId = `${pathParts[2]}/${pathParts[3]}`;
|
|
2285
|
-
}
|
|
2286
|
-
}
|
|
2287
|
-
} catch (error) {
|
|
2288
|
-
console.error('Error extracting video ID from URL:', error);
|
|
2289
|
-
}
|
|
2290
|
-
|
|
2291
|
-
if (videoId && !processedVideoIds.has(videoId)) {
|
|
2292
|
-
results.push({ url, title, videoId });
|
|
2263
|
+
const videoId = String(codeNode.codeHookParams[0] || '');
|
|
2264
|
+
const title = String(codeNode.codeHookParams[1] || 'Untitled Video');
|
|
2265
|
+
|
|
2266
|
+
if (videoId && /^\d+\/[a-f0-9\-]{36}$/.test(videoId)) {
|
|
2267
|
+
if (!processedVideoIds.has(videoId)) {
|
|
2268
|
+
results.push({
|
|
2269
|
+
url: `${BUNNY_EMBED_BASE_URL}${videoId}`,
|
|
2270
|
+
title,
|
|
2271
|
+
videoId,
|
|
2272
|
+
});
|
|
2293
2273
|
processedVideoIds.add(videoId);
|
|
2294
2274
|
}
|
|
2295
2275
|
}
|
|
2296
2276
|
}
|
|
2297
2277
|
}
|
|
2298
|
-
|
|
2299
2278
|
return results;
|
|
2300
2279
|
}
|
|
2301
2280
|
|