astro-tractstack 2.0.0-rc.8 → 2.0.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/LICENSE +8 -97
- package/README.md +7 -5
- package/bin/create-tractstack.js +35 -11
- package/dist/index.js +106 -29
- package/package.json +10 -5
- package/templates/css/frontend.css +1 -1
- package/templates/custom/minimal/CodeHook.astro +13 -12
- package/templates/custom/minimal/CustomRoutes.astro +25 -31
- package/templates/custom/with-examples/CodeHook.astro +22 -11
- package/templates/custom/with-examples/CustomRoutes.astro +4 -8
- package/templates/custom/with-examples/ProductCard.astro +29 -0
- package/templates/custom/with-examples/ProductCardWrapper.astro +43 -0
- package/templates/custom/with-examples/ProductGrid.astro +64 -0
- package/templates/custom/with-examples/pages/Collections.astro +58 -98
- package/templates/gitignore +42 -0
- package/templates/prettierignore +5 -0
- package/templates/prettierrc +19 -0
- package/templates/src/client/app.js +127 -0
- package/templates/src/client/htmx.min.js +3519 -0
- package/templates/src/client/view.js +429 -0
- package/templates/src/components/Footer.astro +4 -9
- package/templates/src/components/Header.astro +67 -60
- package/templates/src/components/Menu.tsx +188 -52
- package/templates/src/components/codehooks/BunnyVideoSetup.tsx +2 -2
- package/templates/src/components/codehooks/EpinetDurationSelector.tsx +9 -13
- package/templates/src/components/codehooks/EpinetTableView.tsx +11 -7
- package/templates/src/components/codehooks/EpinetWrapper.tsx +1 -0
- package/templates/src/components/codehooks/FeaturedArticle.astro +105 -0
- package/templates/src/components/codehooks/FeaturedArticleSetup.tsx +318 -0
- package/templates/src/components/codehooks/ListContent.astro +32 -162
- package/templates/src/components/codehooks/ListContentSetup.tsx +43 -138
- package/templates/src/components/codehooks/ProductCardSetup.tsx +152 -0
- package/templates/src/components/codehooks/ProductGridSetup.tsx +274 -0
- package/templates/src/components/codehooks/SearchWidget.tsx +453 -0
- package/templates/src/components/compositor/Node.tsx +3 -6
- package/templates/src/components/compositor/PanelVisibilityWrapper.tsx +21 -11
- package/templates/src/components/compositor/elements/BunnyVideo.tsx +21 -20
- package/templates/src/components/compositor/nodes/Pane.tsx +51 -21
- package/templates/src/components/compositor/nodes/RenderChildren.tsx +6 -1
- package/templates/src/components/compositor/nodes/Widget.tsx +16 -2
- package/templates/src/components/compositor/preview/FeaturedArticlePreview.tsx +155 -0
- package/templates/src/components/compositor/preview/PaneSnapshotGenerator.tsx +20 -1
- package/templates/src/components/edit/Header.tsx +10 -4
- package/templates/src/components/edit/PanelSwitch.tsx +11 -7
- package/templates/src/components/edit/SettingsPanel.tsx +29 -18
- package/templates/src/components/edit/ToolBar.tsx +1 -28
- package/templates/src/components/edit/ToolMode.tsx +45 -32
- package/templates/src/components/edit/pane/AddPanePanel_break.tsx +12 -2
- package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +8 -2
- package/templates/src/components/edit/pane/AddPanePanel_newAICopy_modal.tsx +1 -1
- package/templates/src/components/edit/pane/ConfigPanePanel.tsx +17 -27
- package/templates/src/components/edit/pane/PageGenSelector.tsx +16 -16
- package/templates/src/components/edit/pane/PageGenSpecial.tsx +26 -49
- package/templates/src/components/edit/pane/PageGen_preview.tsx +17 -2
- package/templates/src/components/edit/pane/PanePanel_path.tsx +2 -4
- package/templates/src/components/edit/pane/PanePanel_title.tsx +243 -76
- package/templates/src/components/edit/panels/StyleBreakPanel.tsx +17 -19
- package/templates/src/components/edit/panels/StyleCodeHookPanel.tsx +48 -37
- package/templates/src/components/edit/panels/StyleElementPanel_add.tsx +60 -55
- package/templates/src/components/edit/panels/StyleImagePanel_add.tsx +56 -50
- package/templates/src/components/edit/panels/StyleLiElementPanel_add.tsx +54 -47
- package/templates/src/components/edit/panels/StyleLinkPanel_add.tsx +54 -44
- package/templates/src/components/edit/panels/StyleLinkPanel_config.tsx +113 -138
- package/templates/src/components/edit/panels/StyleParentPanel_add.tsx +54 -40
- package/templates/src/components/edit/panels/StyleWidgetPanel.tsx +3 -3
- package/templates/src/components/edit/panels/StyleWidgetPanel_add.tsx +56 -49
- package/templates/src/components/edit/panels/StyleWidgetPanel_config.tsx +14 -5
- package/templates/src/components/edit/state/SaveModal.tsx +316 -169
- package/templates/src/components/edit/storyfragment/StoryFragmentPanel_og.tsx +1 -1
- package/templates/src/components/edit/storyfragment/StoryFragmentPanel_slug.tsx +56 -55
- package/templates/src/components/edit/widgets/BunnyWidget.tsx +538 -59
- package/templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx +656 -0
- package/templates/src/components/edit/widgets/ToggleWidget.tsx +9 -16
- package/templates/src/components/fields/ArtpackImage.tsx +4 -1
- package/templates/src/components/fields/BackgroundImage.tsx +1 -1
- package/templates/src/components/fields/BackgroundImageWrapper.tsx +127 -35
- package/templates/src/components/fields/ColorPickerCombo.tsx +66 -62
- package/templates/src/components/fields/ImageUpload.tsx +1 -1
- package/templates/src/components/fields/ViewportComboBox.tsx +59 -42
- package/templates/src/components/form/ActionBuilderBeliefSelector.tsx +117 -0
- package/templates/src/components/form/ActionBuilderField.tsx +306 -87
- package/templates/src/components/search/SearchModal.tsx +420 -0
- package/templates/src/components/search/SearchResults.tsx +367 -0
- package/templates/src/components/search/SearchWrapper.tsx +46 -0
- package/templates/src/components/storykeep/Dashboard_Advanced.tsx +1 -1
- package/templates/src/components/storykeep/Dashboard_Analytics.tsx +34 -8
- package/templates/src/components/storykeep/Dashboard_Content.tsx +6 -0
- package/templates/src/components/storykeep/StoryKeepBackdrop.astro +87 -0
- package/templates/src/components/storykeep/controls/content/BeliefForm.tsx +37 -33
- package/templates/src/components/storykeep/controls/content/MenuForm.tsx +55 -7
- package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +17 -2
- package/templates/src/components/storykeep/controls/content/StoryFragmentTable.tsx +5 -8
- package/templates/src/components/storykeep/state/FetchAnalytics.tsx +274 -228
- package/templates/src/components/storykeep/widgets/Wizard.tsx +14 -7
- package/templates/src/components/tenant/RegistrationForm.tsx +1 -1
- package/templates/src/components/widgets/ImpressionWrapper.tsx +0 -1
- package/templates/src/constants/shapes.ts +9 -0
- package/templates/src/constants.ts +2121 -16
- package/templates/src/hooks/useSearch.ts +228 -0
- package/templates/src/layouts/Layout.astro +213 -104
- package/templates/src/lib/storyData.ts +4 -1
- package/templates/src/pages/[...slug]/edit.astro +14 -14
- package/templates/src/pages/[...slug].astro +82 -21
- package/templates/src/pages/api/orphan-analysis.ts +0 -1
- package/templates/src/pages/api/tailwind.ts +23 -21
- package/templates/src/pages/context/[...contextSlug]/edit.astro +14 -14
- package/templates/src/pages/context/[...contextSlug].astro +7 -2
- package/templates/src/pages/storykeep/advanced.astro +5 -4
- package/templates/src/pages/storykeep/branding.astro +5 -4
- package/templates/src/pages/storykeep/content.astro +5 -4
- package/templates/src/pages/storykeep/init.astro +40 -1
- package/templates/src/pages/storykeep/login.astro +1 -1
- package/templates/src/pages/storykeep.astro +5 -4
- package/templates/src/stores/nodes.ts +59 -88
- package/templates/src/stores/orphanAnalysis.ts +19 -21
- package/templates/src/stores/storykeep.ts +7 -0
- package/templates/src/types/compositorTypes.ts +6 -0
- package/templates/src/types/tractstack.ts +17 -0
- package/templates/src/utils/actions/lispLexer.ts +2 -2
- package/templates/src/utils/actions/preParse_Action.ts +3 -0
- package/templates/src/utils/api/beliefHelpers.ts +12 -36
- package/templates/src/utils/api/menuHelpers.ts +2 -2
- package/templates/src/utils/api.ts +26 -0
- package/templates/src/utils/compositor/TemplateNodes.ts +7 -0
- package/templates/src/utils/compositor/allowInsert.ts +5 -3
- package/templates/src/utils/compositor/nodesHelper.ts +4 -0
- package/templates/src/utils/compositor/processMarkdown.ts +16 -2
- package/templates/src/utils/compositor/reduceNodesClassNames.ts +4 -0
- package/templates/src/utils/compositor/templateMarkdownStyles.ts +13 -13
- package/templates/src/utils/compositor/typeGuards.ts +1 -0
- package/templates/src/utils/customHelpers.ts +38 -0
- package/templates/src/utils/helpers.ts +2 -2
- package/templates/src/utils/layout.ts +65 -144
- package/utils/inject-files.ts +95 -18
- package/templates/src/client/analytics-events.js +0 -207
- package/templates/src/client/belief-events.js +0 -191
- package/templates/src/client/sse.js +0 -613
- package/templates/src/components/codehooks/FeaturedContent.astro +0 -273
- package/templates/src/components/codehooks/FeaturedContentSetup.tsx +0 -738
- package/templates/src/components/compositor/preview/FeaturedContentPreview.tsx +0 -128
- package/templates/src/components/edit/pane/PanePanel_slug.tsx +0 -219
|
@@ -2,131 +2,610 @@ 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
|
|
|
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
|
+
};
|
|
51
|
+
|
|
12
52
|
function BunnyWidget({ node, onUpdate }: BunnyWidgetProps) {
|
|
13
|
-
const [
|
|
53
|
+
const [videoId, setVideoId] = useState(
|
|
14
54
|
String(node.codeHookParams?.[0] || '')
|
|
15
55
|
);
|
|
16
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>>({});
|
|
17
60
|
const [isDuplicate, setIsDuplicate] = useState(false);
|
|
18
61
|
const [validationError, setValidationError] = useState<string | null>(null);
|
|
62
|
+
const [isCopied, setIsCopied] = useState(false);
|
|
19
63
|
|
|
20
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);
|
|
21
110
|
|
|
22
111
|
useEffect(() => {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}, [node]);
|
|
112
|
+
const newVideoId = String(node.codeHookParams?.[0] || '');
|
|
113
|
+
const newTitle = String(node.codeHookParams?.[1] || '');
|
|
114
|
+
const chaptersJson = String(node.codeHookParams?.[2] || '');
|
|
27
115
|
|
|
28
|
-
|
|
29
|
-
|
|
116
|
+
setVideoId(newVideoId);
|
|
117
|
+
setTitle(newTitle);
|
|
118
|
+
validateVideoId(newVideoId);
|
|
30
119
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
+
}
|
|
135
|
+
}, [node]);
|
|
34
136
|
|
|
35
|
-
|
|
36
|
-
|
|
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
|
+
};
|
|
37
151
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
152
|
+
const checkForDuplicates = (id: string): boolean => {
|
|
153
|
+
if (!id) return false;
|
|
154
|
+
try {
|
|
155
|
+
return (
|
|
156
|
+
ctx.getAllBunnyVideoInfo().filter((video) => video.videoId === id)
|
|
157
|
+
.length > 1
|
|
43
158
|
);
|
|
44
159
|
} catch (e) {
|
|
45
|
-
console.error('Error checking for duplicates:', e);
|
|
46
160
|
return false;
|
|
47
161
|
}
|
|
48
162
|
};
|
|
49
163
|
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return match ? match[1] : null;
|
|
54
|
-
} catch (e) {
|
|
55
|
-
console.error('Error extracting video ID:', e);
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
164
|
+
const isValidVideoIdFormat = (id: string): boolean => {
|
|
165
|
+
if (!id) return true;
|
|
166
|
+
return /^\d+\/[a-f0-9\-]{36}$/.test(id);
|
|
58
167
|
};
|
|
59
168
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (!url) {
|
|
169
|
+
const validateVideoId = (id: string) => {
|
|
170
|
+
if (!id) {
|
|
63
171
|
setValidationError(null);
|
|
64
172
|
setIsDuplicate(false);
|
|
65
173
|
return;
|
|
66
174
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
175
|
+
if (!isValidVideoIdFormat(id)) {
|
|
176
|
+
setValidationError(
|
|
177
|
+
"Invalid format. Use 'LibraryID/VideoGUID' from Bunny."
|
|
178
|
+
);
|
|
71
179
|
setIsDuplicate(false);
|
|
72
180
|
return;
|
|
73
181
|
}
|
|
74
|
-
|
|
75
|
-
const duplicate = checkForDuplicates(url);
|
|
182
|
+
const duplicate = checkForDuplicates(id);
|
|
76
183
|
setIsDuplicate(duplicate);
|
|
77
184
|
setValidationError(
|
|
78
|
-
duplicate ? 'This video is already used elsewhere
|
|
185
|
+
duplicate ? 'This video is already used elsewhere on this page.' : null
|
|
79
186
|
);
|
|
80
187
|
};
|
|
81
188
|
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
189
|
+
const handleVideoIdChange = (value: string) => {
|
|
190
|
+
setVideoId(value);
|
|
191
|
+
validateVideoId(value);
|
|
192
|
+
handleUpdate(value, title, chapters);
|
|
86
193
|
};
|
|
87
194
|
|
|
88
195
|
const handleTitleChange = (value: string) => {
|
|
89
196
|
setTitle(value);
|
|
90
|
-
|
|
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);
|
|
91
266
|
};
|
|
92
267
|
|
|
93
|
-
|
|
94
|
-
|
|
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 || '';
|
|
300
|
+
};
|
|
301
|
+
|
|
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 = () => {
|
|
95
319
|
try {
|
|
96
|
-
|
|
97
|
-
new URL(url);
|
|
98
|
-
return (
|
|
99
|
-
url.includes('//iframe.mediadelivery.net/embed/') ||
|
|
100
|
-
url.includes('//video.bunnycdn.com/')
|
|
101
|
-
);
|
|
320
|
+
return canonicalURLStore.get();
|
|
102
321
|
} catch (e) {
|
|
103
|
-
return
|
|
322
|
+
return '';
|
|
104
323
|
}
|
|
105
324
|
};
|
|
106
325
|
|
|
107
326
|
return (
|
|
108
327
|
<div className="space-y-4">
|
|
109
328
|
<SingleParam
|
|
110
|
-
label=
|
|
111
|
-
value={
|
|
112
|
-
onChange={
|
|
329
|
+
label="Video ID"
|
|
330
|
+
value={videoId}
|
|
331
|
+
onChange={handleVideoIdChange}
|
|
332
|
+
placeholder="e.g., 12345/abcde-12345-fghij-67890"
|
|
113
333
|
/>
|
|
114
|
-
{validationError &&
|
|
334
|
+
{validationError && videoId && (
|
|
115
335
|
<div className="mt-1 text-xs text-red-500">{validationError}</div>
|
|
116
336
|
)}
|
|
117
337
|
{isDuplicate && (
|
|
118
338
|
<div className="rounded border border-yellow-200 bg-yellow-50 p-2 text-xs text-yellow-800">
|
|
119
|
-
Warning: This video is already used elsewhere
|
|
120
|
-
same video multiple times may cause playback conflicts. Consider using
|
|
121
|
-
a single video with chapter navigation instead.
|
|
339
|
+
Warning: This video is already used elsewhere.
|
|
122
340
|
</div>
|
|
123
341
|
)}
|
|
124
|
-
|
|
125
342
|
<SingleParam
|
|
126
343
|
label={widgetInfo.parameters[1].label}
|
|
127
344
|
value={title}
|
|
128
345
|
onChange={handleTitleChange}
|
|
129
346
|
/>
|
|
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>
|
|
130
609
|
</div>
|
|
131
610
|
);
|
|
132
611
|
}
|