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,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-tractstack",
3
- "version": "2.0.0-rc.49",
3
+ "version": "2.0.0-rc.50",
4
4
  "description": "Astro integration for TractStack - redeeming the web from boring experiences",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,49 +1,44 @@
1
1
  interface BunnyVideoProps {
2
- embedUrl: string;
2
+ videoId: string;
3
3
  title: string;
4
4
  className?: string;
5
5
  }
6
6
 
7
- const BunnyVideo = ({ embedUrl, title, className = '' }: BunnyVideoProps) => {
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
- // Render placeholder when URL is invalid
19
- if (!isValidUrl(embedUrl)) {
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 URL not set</div>
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 Stream URL
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 URL with default parameters for preview
35
- let videoUrl;
26
+ // Build the full, final URL from the videoId.
27
+ let finalVideoUrl;
36
28
  try {
37
- videoUrl = new URL(embedUrl);
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 video URL</div>
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={videoUrl.toString()}
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 embedUrl={value1} title={value2 || 'Bunny Video'} />
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 [embedUrl, setEmbedUrl] = useState(
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
- setEmbedUrl(String(node.codeHookParams?.[0] || ''));
25
+ const newVideoId = String(node.codeHookParams?.[0] || '');
26
+ setVideoId(newVideoId);
24
27
  setTitle(String(node.codeHookParams?.[1] || ''));
25
- validateUrl(String(node.codeHookParams?.[0] || ''));
28
+ validateVideoId(newVideoId);
26
29
  }, [node]);
27
30
 
28
- const checkForDuplicates = (url: string): boolean => {
29
- if (!url) return false;
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
- // Check if this video ID already exists in another node
39
- return existingVideos.some(
40
- (video) =>
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 extractVideoId = (url: string): string | null => {
51
- try {
52
- const match = url.match(/embed\/([^/]+\/[^/?]+)/);
53
- return match ? match[1] : null;
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
- // Validate URL format and check for duplicates
61
- const validateUrl = (url: string): void => {
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
- const isValid = isValidUrl(url);
69
- if (!isValid) {
70
- setValidationError('URL should be a valid Bunny embed URL');
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(url);
67
+ const duplicate = checkForDuplicates(id);
76
68
  setIsDuplicate(duplicate);
77
69
  setValidationError(
78
- duplicate ? 'This video is already used elsewhere in this page' : null
70
+ duplicate ? 'This video is already used elsewhere on this page.' : null
79
71
  );
80
72
  };
81
73
 
82
- const handleEmbedUrlChange = (value: string) => {
83
- setEmbedUrl(value);
84
- validateUrl(value);
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([embedUrl, value]);
82
+ onUpdate([videoId, value]);
91
83
  };
92
84
 
93
- // Validate URL format
94
- const isValidUrl = (url: string): boolean => {
95
- try {
96
- if (!url) return false;
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={widgetInfo.parameters[0].label}
111
- value={embedUrl}
112
- onChange={handleEmbedUrlChange}
93
+ label="Video ID"
94
+ value={videoId}
95
+ onChange={handleVideoIdChange}
96
+ placeholder="e.g., 12345/abcde-12345-fghij-67890"
113
97
  />
114
- {validationError && embedUrl && (
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 in this page. Using the
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: 'Embed URL', defaultValue: '*', type: 'string' },
222
+ { label: 'Video ID', defaultValue: '234/uuid', type: 'string' },
223
223
  { label: 'Title', defaultValue: 'Descriptive Title', type: 'string' },
224
224
  ],
225
225
  },