astro-tractstack 2.0.0-rc.49 → 2.0.0-rc.50
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
CHANGED
|
@@ -1,49 +1,44 @@
|
|
|
1
1
|
interface BunnyVideoProps {
|
|
2
|
-
|
|
2
|
+
videoId: string;
|
|
3
3
|
title: string;
|
|
4
4
|
className?: string;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
const
|
|
8
|
-
// Check if a string is a valid URL
|
|
9
|
-
const isValidUrl = (url: string): boolean => {
|
|
10
|
-
try {
|
|
11
|
-
new URL(url);
|
|
12
|
-
return true;
|
|
13
|
-
} catch (e) {
|
|
14
|
-
return false;
|
|
15
|
-
}
|
|
16
|
-
};
|
|
7
|
+
const BUNNY_EMBED_BASE_URL = 'https://iframe.mediadelivery.net/embed/';
|
|
17
8
|
|
|
18
|
-
|
|
19
|
-
|
|
9
|
+
const BunnyVideo = ({ videoId, title, className = '' }: BunnyVideoProps) => {
|
|
10
|
+
// If no videoId is provided, render the placeholder.
|
|
11
|
+
if (!videoId) {
|
|
20
12
|
return (
|
|
21
13
|
<div
|
|
22
14
|
className={`flex aspect-video w-full items-center justify-center bg-gray-100 ${className}`}
|
|
23
15
|
>
|
|
24
16
|
<div className="p-4 text-center">
|
|
25
|
-
<div className="text-mydarkgrey mb-2">Video
|
|
17
|
+
<div className="text-mydarkgrey mb-2">Video ID not set</div>
|
|
26
18
|
<div className="text-mygrey text-sm">
|
|
27
|
-
Configure this widget with a valid Bunny
|
|
19
|
+
Configure this widget with a valid Bunny Video ID
|
|
28
20
|
</div>
|
|
29
21
|
</div>
|
|
30
22
|
</div>
|
|
31
23
|
);
|
|
32
24
|
}
|
|
33
25
|
|
|
34
|
-
// Build
|
|
35
|
-
let
|
|
26
|
+
// Build the full, final URL from the videoId.
|
|
27
|
+
let finalVideoUrl;
|
|
36
28
|
try {
|
|
37
|
-
|
|
29
|
+
const baseURL = `${BUNNY_EMBED_BASE_URL}${videoId}`;
|
|
30
|
+
const videoUrl = new URL(baseURL);
|
|
38
31
|
videoUrl.searchParams.set('autoplay', '0');
|
|
39
32
|
videoUrl.searchParams.set('preload', 'false');
|
|
40
33
|
videoUrl.searchParams.set('responsive', 'true');
|
|
34
|
+
finalVideoUrl = videoUrl.toString();
|
|
41
35
|
} catch (e) {
|
|
36
|
+
// This handles cases where a malformed videoId might be passed.
|
|
42
37
|
return (
|
|
43
38
|
<div
|
|
44
39
|
className={`flex aspect-video w-full items-center justify-center bg-gray-100 ${className}`}
|
|
45
40
|
>
|
|
46
|
-
<div className="text-mydarkgrey text-center">Invalid
|
|
41
|
+
<div className="text-mydarkgrey text-center">Invalid Video ID</div>
|
|
47
42
|
</div>
|
|
48
43
|
);
|
|
49
44
|
}
|
|
@@ -51,7 +46,7 @@ const BunnyVideo = ({ embedUrl, title, className = '' }: BunnyVideoProps) => {
|
|
|
51
46
|
return (
|
|
52
47
|
<div className={`relative w-full ${className}`}>
|
|
53
48
|
<iframe
|
|
54
|
-
src={
|
|
49
|
+
src={finalVideoUrl}
|
|
55
50
|
className="aspect-video w-full"
|
|
56
51
|
title={title}
|
|
57
52
|
allow="autoplay; fullscreen"
|
|
@@ -79,7 +79,7 @@ const getWidgetElement = (
|
|
|
79
79
|
case 'bunny':
|
|
80
80
|
return value1 ? (
|
|
81
81
|
<div className={`${classNames} pointer-events-none`}>
|
|
82
|
-
<BunnyVideo
|
|
82
|
+
<BunnyVideo videoId={value1} title={value2 || 'Bunny Video'} />
|
|
83
83
|
</div>
|
|
84
84
|
) : null;
|
|
85
85
|
|
|
@@ -9,8 +9,10 @@ interface BunnyWidgetProps {
|
|
|
9
9
|
onUpdate: (params: string[]) => void;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
const BUNNY_EMBED_BASE_URL = 'https://iframe.mediadelivery.net/embed/';
|
|
13
|
+
|
|
12
14
|
function BunnyWidget({ node, onUpdate }: BunnyWidgetProps) {
|
|
13
|
-
const [
|
|
15
|
+
const [videoId, setVideoId] = useState(
|
|
14
16
|
String(node.codeHookParams?.[0] || '')
|
|
15
17
|
);
|
|
16
18
|
const [title, setTitle] = useState(String(node.codeHookParams?.[1] || ''));
|
|
@@ -20,105 +22,85 @@ function BunnyWidget({ node, onUpdate }: BunnyWidgetProps) {
|
|
|
20
22
|
const widgetInfo = widgetMeta.bunny;
|
|
21
23
|
|
|
22
24
|
useEffect(() => {
|
|
23
|
-
|
|
25
|
+
const newVideoId = String(node.codeHookParams?.[0] || '');
|
|
26
|
+
setVideoId(newVideoId);
|
|
24
27
|
setTitle(String(node.codeHookParams?.[1] || ''));
|
|
25
|
-
|
|
28
|
+
validateVideoId(newVideoId);
|
|
26
29
|
}, [node]);
|
|
27
30
|
|
|
28
|
-
const checkForDuplicates = (
|
|
29
|
-
if (!
|
|
30
|
-
|
|
31
|
+
const checkForDuplicates = (id: string): boolean => {
|
|
32
|
+
if (!id) return false;
|
|
31
33
|
try {
|
|
32
|
-
const videoId = extractVideoId(url);
|
|
33
|
-
if (!videoId) return false;
|
|
34
|
-
|
|
35
34
|
const ctx = getCtx();
|
|
36
35
|
const existingVideos = ctx.getAllBunnyVideoInfo();
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
video.videoId === videoId &&
|
|
42
|
-
!(node.codeHookParams?.[0] || '').includes(videoId)
|
|
43
|
-
);
|
|
36
|
+
const count = existingVideos.filter(
|
|
37
|
+
(video) => video.videoId === id
|
|
38
|
+
).length;
|
|
39
|
+
return count > 1;
|
|
44
40
|
} catch (e) {
|
|
45
41
|
console.error('Error checking for duplicates:', e);
|
|
46
42
|
return false;
|
|
47
43
|
}
|
|
48
44
|
};
|
|
49
45
|
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
} catch (e) {
|
|
55
|
-
console.error('Error extracting video ID:', e);
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
46
|
+
const isValidVideoIdFormat = (id: string): boolean => {
|
|
47
|
+
if (!id) return true; // An empty string is not an error itself.
|
|
48
|
+
const videoIdRegex = /^\d+\/[a-f0-9\-]{36}$/;
|
|
49
|
+
return videoIdRegex.test(id);
|
|
58
50
|
};
|
|
59
51
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (!url) {
|
|
52
|
+
const validateVideoId = (id: string) => {
|
|
53
|
+
if (!id) {
|
|
63
54
|
setValidationError(null);
|
|
64
55
|
setIsDuplicate(false);
|
|
65
56
|
return;
|
|
66
57
|
}
|
|
67
58
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
59
|
+
if (!isValidVideoIdFormat(id)) {
|
|
60
|
+
setValidationError(
|
|
61
|
+
"Invalid format. Use 'LibraryID/VideoGUID' from Bunny."
|
|
62
|
+
);
|
|
71
63
|
setIsDuplicate(false);
|
|
72
64
|
return;
|
|
73
65
|
}
|
|
74
66
|
|
|
75
|
-
const duplicate = checkForDuplicates(
|
|
67
|
+
const duplicate = checkForDuplicates(id);
|
|
76
68
|
setIsDuplicate(duplicate);
|
|
77
69
|
setValidationError(
|
|
78
|
-
duplicate ? 'This video is already used elsewhere
|
|
70
|
+
duplicate ? 'This video is already used elsewhere on this page.' : null
|
|
79
71
|
);
|
|
80
72
|
};
|
|
81
73
|
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
74
|
+
const handleVideoIdChange = (value: string) => {
|
|
75
|
+
setVideoId(value);
|
|
76
|
+
validateVideoId(value);
|
|
85
77
|
onUpdate([value, title]);
|
|
86
78
|
};
|
|
87
79
|
|
|
88
80
|
const handleTitleChange = (value: string) => {
|
|
89
81
|
setTitle(value);
|
|
90
|
-
onUpdate([
|
|
82
|
+
onUpdate([videoId, value]);
|
|
91
83
|
};
|
|
92
84
|
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
new URL(url);
|
|
98
|
-
return (
|
|
99
|
-
url.includes('//iframe.mediadelivery.net/embed/') ||
|
|
100
|
-
url.includes('//video.bunnycdn.com/')
|
|
101
|
-
);
|
|
102
|
-
} catch (e) {
|
|
103
|
-
return false;
|
|
104
|
-
}
|
|
105
|
-
};
|
|
85
|
+
const showPreview = videoId && !validationError && !isDuplicate;
|
|
86
|
+
const embedUrlForPreview = showPreview
|
|
87
|
+
? `${BUNNY_EMBED_BASE_URL}${videoId}`
|
|
88
|
+
: '';
|
|
106
89
|
|
|
107
90
|
return (
|
|
108
91
|
<div className="space-y-4">
|
|
109
92
|
<SingleParam
|
|
110
|
-
label=
|
|
111
|
-
value={
|
|
112
|
-
onChange={
|
|
93
|
+
label="Video ID"
|
|
94
|
+
value={videoId}
|
|
95
|
+
onChange={handleVideoIdChange}
|
|
96
|
+
placeholder="e.g., 12345/abcde-12345-fghij-67890"
|
|
113
97
|
/>
|
|
114
|
-
{validationError &&
|
|
98
|
+
{validationError && videoId && (
|
|
115
99
|
<div className="mt-1 text-xs text-red-500">{validationError}</div>
|
|
116
100
|
)}
|
|
117
101
|
{isDuplicate && (
|
|
118
102
|
<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.
|
|
103
|
+
Warning: This video is already used elsewhere on this page.
|
|
122
104
|
</div>
|
|
123
105
|
)}
|
|
124
106
|
|
|
@@ -127,6 +109,22 @@ function BunnyWidget({ node, onUpdate }: BunnyWidgetProps) {
|
|
|
127
109
|
value={title}
|
|
128
110
|
onChange={handleTitleChange}
|
|
129
111
|
/>
|
|
112
|
+
|
|
113
|
+
{showPreview && (
|
|
114
|
+
<div className="mt-4 space-y-2">
|
|
115
|
+
<label className="block text-sm font-medium text-gray-500">
|
|
116
|
+
Preview
|
|
117
|
+
</label>
|
|
118
|
+
<div className="aspect-video w-full">
|
|
119
|
+
<iframe
|
|
120
|
+
src={embedUrlForPreview}
|
|
121
|
+
className="h-full w-full rounded border"
|
|
122
|
+
title={`Preview: ${title}`}
|
|
123
|
+
allow="autoplay; fullscreen"
|
|
124
|
+
/>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
)}
|
|
130
128
|
</div>
|
|
131
129
|
);
|
|
132
130
|
}
|
|
@@ -219,7 +219,7 @@ export const widgetMeta: WidgetMeta = {
|
|
|
219
219
|
bunny: {
|
|
220
220
|
title: `BunnyCDN Video Embed`,
|
|
221
221
|
parameters: [
|
|
222
|
-
{ label: '
|
|
222
|
+
{ label: 'Video ID', defaultValue: '234/uuid', type: 'string' },
|
|
223
223
|
{ label: 'Title', defaultValue: 'Descriptive Title', type: 'string' },
|
|
224
224
|
],
|
|
225
225
|
},
|