@hubspot/cms-component-library 0.3.11 → 0.3.13

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.
Files changed (46) hide show
  1. package/components/componentLibrary/Accordion/AccordionContent/index.module.scss +3 -5
  2. package/components/componentLibrary/Accordion/AccordionItem/StyleFields.tsx +6 -14
  3. package/components/componentLibrary/Accordion/AccordionItem/index.module.scss +7 -5
  4. package/components/componentLibrary/Accordion/AccordionItem/index.tsx +6 -15
  5. package/components/componentLibrary/Accordion/AccordionItem/types.ts +5 -5
  6. package/components/componentLibrary/Accordion/AccordionTitle/StyleFields.tsx +1 -1
  7. package/components/componentLibrary/Accordion/AccordionTitle/index.module.scss +6 -6
  8. package/components/componentLibrary/Accordion/llm.txt +15 -15
  9. package/components/componentLibrary/Accordion/stories/Accordion.stories.tsx +3 -3
  10. package/components/componentLibrary/Accordion/stories/AccordionDecorator.module.scss +38 -0
  11. package/components/componentLibrary/Accordion/stories/AccordionDecorator.tsx +7 -35
  12. package/components/componentLibrary/Button/index.module.scss +12 -4
  13. package/components/componentLibrary/Button/llm.txt +12 -4
  14. package/components/componentLibrary/Button/stories/ButtonDecorator.module.scss +12 -4
  15. package/components/componentLibrary/Card/index.module.scss +5 -5
  16. package/components/componentLibrary/Card/llm.txt +3 -1
  17. package/components/componentLibrary/Card/stories/CardDecorator.module.scss +12 -4
  18. package/components/componentLibrary/Form/PlaceholderForm.module.css +74 -0
  19. package/components/componentLibrary/Form/PlaceholderForm.tsx +31 -0
  20. package/components/componentLibrary/Form/StyleFields.tsx +2 -1
  21. package/components/componentLibrary/Form/index.tsx +24 -2
  22. package/components/componentLibrary/Form/islands/FormIsland.tsx +3 -1
  23. package/components/componentLibrary/Form/islands/LegacyFormIsland.tsx +10 -3
  24. package/components/componentLibrary/Form/islands/legacyForm.module.scss +270 -0
  25. package/components/componentLibrary/Form/islands/v4Form.module.css +59 -57
  26. package/components/componentLibrary/Form/types.ts +7 -2
  27. package/components/componentLibrary/Image/index.module.scss +3 -1
  28. package/components/componentLibrary/Image/llm.txt +3 -1
  29. package/components/componentLibrary/Image/stories/ImageDecorator.module.scss +3 -1
  30. package/components/componentLibrary/Text/ContentFields.tsx +1 -0
  31. package/components/componentLibrary/Text/index.module.scss +40 -1
  32. package/components/componentLibrary/Text/llm.txt +4 -2
  33. package/components/componentLibrary/Video/ContentFields.tsx +1 -0
  34. package/components/componentLibrary/Video/StyleFields.tsx +42 -0
  35. package/components/componentLibrary/Video/TrackHSVideoAnalytics.tsx +445 -0
  36. package/components/componentLibrary/Video/hooks/usePageMeta.tsx +25 -0
  37. package/components/componentLibrary/Video/hooks/useQueryParam.tsx +12 -0
  38. package/components/componentLibrary/Video/hooks/useUserToken.tsx +56 -0
  39. package/components/componentLibrary/Video/index.tsx +11 -2
  40. package/components/componentLibrary/Video/islands/EmbedVideoIsland.tsx +239 -0
  41. package/components/componentLibrary/Video/islands/HSVideoIsland.tsx +4 -0
  42. package/components/componentLibrary/Video/islands/index.module.scss +94 -0
  43. package/components/componentLibrary/Video/types.ts +22 -0
  44. package/components/componentLibrary/Video/utils/videoAnalytics.ts +146 -0
  45. package/package.json +1 -1
  46. package/components/componentLibrary/Form/islands/legacyForm.module.css +0 -251
@@ -12,6 +12,48 @@ const StyleFields = ({
12
12
  name={playButtonColorName}
13
13
  default={playButtonColorDefault}
14
14
  showOpacity={false}
15
+ visibilityRules="ADVANCED"
16
+ advancedVisibility={{
17
+ boolean_operator: 'OR',
18
+ criteria: [],
19
+ children: [
20
+ {
21
+ boolean_operator: 'AND',
22
+ criteria: [
23
+ {
24
+ controlling_field: 'videoType',
25
+ operator: 'EQUAL',
26
+ controlling_value_regex: 'hubspot_video',
27
+ },
28
+ ],
29
+ },
30
+ {
31
+ boolean_operator: 'AND',
32
+ criteria: [
33
+ {
34
+ controlling_field: 'videoType',
35
+ operator: 'EQUAL',
36
+ controlling_value_regex: 'embed',
37
+ },
38
+ {
39
+ controlling_field: 'embedVideo',
40
+ operator: 'MATCHES_REGEX',
41
+ controlling_value_regex: '(?=.*"source_type":"oembed")',
42
+ },
43
+ {
44
+ controlling_field: 'embedVideo',
45
+ operator: 'MATCHES_REGEX',
46
+ controlling_value_regex: '(?=.*"oembed_url":"(?!")+)',
47
+ },
48
+ {
49
+ controlling_field: 'oembedThumbnail',
50
+ operator: 'MATCHES_REGEX',
51
+ controlling_value_regex: '(?=.*"src":"(?!")+)',
52
+ },
53
+ ],
54
+ },
55
+ ],
56
+ }}
15
57
  />
16
58
  );
17
59
  };
@@ -0,0 +1,445 @@
1
+ import { getHSEnv, HSEnvironment } from '@hubspot/cms-components';
2
+ import {
3
+ // @ts-expect-error -- types not flowing through properly
4
+ useVideoPlayer,
5
+ // @ts-expect-error -- types not flowing through properly
6
+ PlayerState,
7
+ // @ts-expect-error -- types not flowing through properly
8
+ VideoCrmObject,
9
+ } from '@hubspot/video-player-core';
10
+ import { useCallback, useEffect, useMemo, useRef } from 'react';
11
+
12
+ import { useUserToken } from './hooks/useUserToken.js';
13
+ import { usePageMeta } from './hooks/usePageMeta.js';
14
+ import {
15
+ trackCompletedPlay,
16
+ trackPlayEvent,
17
+ trackSecondsViewed,
18
+ } from './utils/videoAnalytics.js';
19
+ import { PageMeta } from './types.js';
20
+
21
+ type TrackingProps = {
22
+ env: HSEnvironment;
23
+ portalId: number;
24
+ eventProps: {
25
+ crmObjectId: number;
26
+ sessionId: string;
27
+ contactUtk: string | null;
28
+ pageMeta: PageMeta | null;
29
+ };
30
+ };
31
+
32
+ const CHUNK_DURATION_SECONDS = 1;
33
+ const RETENTION_TRACKING_INTERVAL_SECONDS = 10;
34
+ const RETENTION_MAX_SESSION_DURATION_SECONDS = 60 * 60; // 1 hour
35
+
36
+ const getTimeRangesDuration = (timeRanges?: TimeRanges) => {
37
+ if (!timeRanges) {
38
+ return 0;
39
+ }
40
+
41
+ const durations = [];
42
+ let i = 0;
43
+ while (i < timeRanges.length) {
44
+ durations.push(timeRanges.end(i) - timeRanges.start(i));
45
+ i++;
46
+ }
47
+
48
+ return durations.reduce((sum, d) => sum + d, 0);
49
+ };
50
+
51
+ const useTrackVisibilityChange = ({
52
+ isTrackingEnabled,
53
+ trackingStartedAt,
54
+ onVisibilityHidden,
55
+ }: {
56
+ isTrackingEnabled: boolean;
57
+ trackingStartedAt?: number;
58
+ onVisibilityHidden: () => void;
59
+ }) => {
60
+ useEffect(() => {
61
+ const handleVisibilityChange = () => {
62
+ if (
63
+ !isTrackingEnabled ||
64
+ !trackingStartedAt ||
65
+ document.visibilityState !== 'hidden'
66
+ ) {
67
+ return;
68
+ }
69
+
70
+ const sessionDuration = (Date.now() - trackingStartedAt) / 1000;
71
+ if (sessionDuration >= RETENTION_MAX_SESSION_DURATION_SECONDS) {
72
+ // Detach visibilitychange listener after 1 hour max session duration
73
+ document.removeEventListener(
74
+ 'visibilitychange',
75
+ handleVisibilityChange
76
+ );
77
+ }
78
+
79
+ onVisibilityHidden();
80
+ };
81
+
82
+ document.addEventListener('visibilitychange', handleVisibilityChange);
83
+ return () => {
84
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
85
+ };
86
+ }, [isTrackingEnabled, trackingStartedAt, onVisibilityHidden]);
87
+ };
88
+
89
+ const useInitialPlayTracking = ({
90
+ hasStarted,
91
+ env,
92
+ portalId,
93
+ eventProps,
94
+ }: TrackingProps & {
95
+ hasStarted: boolean;
96
+ }) => {
97
+ const { contactUtk, pageMeta } = eventProps;
98
+ const hasTrackedInitialPlay = useRef(false);
99
+
100
+ const trackInitialPlay = async () => {
101
+ hasTrackedInitialPlay.current = true;
102
+ await trackPlayEvent(env, portalId, {
103
+ ...eventProps,
104
+ contactUtk: contactUtk!,
105
+ pageMeta: pageMeta!,
106
+ });
107
+ };
108
+
109
+ useEffect(() => {
110
+ if (!contactUtk || !pageMeta) {
111
+ return;
112
+ }
113
+
114
+ if (!hasTrackedInitialPlay.current && hasStarted) {
115
+ trackInitialPlay();
116
+ }
117
+ }, [hasStarted, contactUtk, pageMeta]);
118
+ };
119
+
120
+ const useRetentionTracking = ({
121
+ chunksViewed,
122
+ currentTime,
123
+ hasEnded,
124
+ element,
125
+ trackingStartedAt,
126
+ videoDurationSeconds,
127
+ env,
128
+ portalId,
129
+ eventProps,
130
+ }: TrackingProps &
131
+ Pick<PlayerState, 'currentTime' | 'hasEnded' | 'element'> & {
132
+ chunksViewed: Map<number, number>;
133
+ trackingStartedAt?: number;
134
+ videoDurationSeconds: number;
135
+ }) => {
136
+ const { contactUtk, pageMeta } = eventProps;
137
+ const { loop, played } = element || {};
138
+ const lastRetentionReportedAt = useRef(0);
139
+ const chunksViewedSinceReportRef = useRef(new Map<number, number>());
140
+ const hasStoppedTracking = useRef(false);
141
+ const currentChunk = Math.floor(currentTime / CHUNK_DURATION_SECONDS);
142
+
143
+ const trackNewChunksViewed = useCallback(
144
+ async (endState = false) => {
145
+ if (endState === true) {
146
+ hasStoppedTracking.current = true;
147
+ }
148
+
149
+ if (!chunksViewedSinceReportRef.current.size) {
150
+ return;
151
+ }
152
+
153
+ const chunksToReport = Object.fromEntries(
154
+ chunksViewedSinceReportRef.current
155
+ );
156
+ chunksViewedSinceReportRef.current.clear();
157
+ lastRetentionReportedAt.current = Date.now();
158
+
159
+ const retentionParams = {
160
+ secondsToViews: chunksToReport,
161
+ endState,
162
+ };
163
+
164
+ await trackSecondsViewed(
165
+ env,
166
+ portalId,
167
+ { ...eventProps, contactUtk: contactUtk!, pageMeta: pageMeta! },
168
+ retentionParams
169
+ );
170
+ },
171
+ [env, portalId, eventProps]
172
+ );
173
+
174
+ const fillRemainingSecondsViewed = () => {
175
+ const totalChunks = Math.ceil(
176
+ videoDurationSeconds / CHUNK_DURATION_SECONDS
177
+ );
178
+ if (chunksViewed.size < totalChunks) {
179
+ // Fill remaining chunks viewed to track for looped video
180
+ for (let i = 0; i < totalChunks; i++) {
181
+ if (
182
+ !chunksViewed.has(i) &&
183
+ !chunksViewedSinceReportRef.current.has(i)
184
+ ) {
185
+ chunksViewedSinceReportRef.current.set(i, 1);
186
+ }
187
+ }
188
+ }
189
+ };
190
+
191
+ // Since looped videos don't fire an 'ended' event, determine completion based
192
+ // on played duration or total viewed chunks and track remaining chunks viewed
193
+ const handleLoopedVideo = () => {
194
+ const playedDuration = getTimeRangesDuration(played);
195
+ if (playedDuration >= videoDurationSeconds) {
196
+ // End retention tracking for looped video based on played duration
197
+ fillRemainingSecondsViewed();
198
+ trackNewChunksViewed(true);
199
+ return;
200
+ }
201
+
202
+ const totalChunks = Math.floor(
203
+ videoDurationSeconds / CHUNK_DURATION_SECONDS
204
+ );
205
+ if (chunksViewed.size >= totalChunks) {
206
+ // End retention tracking for looped video watched completely
207
+ trackNewChunksViewed(true);
208
+ }
209
+ };
210
+
211
+ useEffect(() => {
212
+ if (
213
+ !contactUtk ||
214
+ !pageMeta ||
215
+ !trackingStartedAt ||
216
+ hasStoppedTracking.current
217
+ ) {
218
+ return;
219
+ }
220
+
221
+ // Check if max session duration exceeded
222
+ const sessionDuration = (Date.now() - trackingStartedAt) / 1000;
223
+ if (sessionDuration >= RETENTION_MAX_SESSION_DURATION_SECONDS) {
224
+ // End retention tracking due to session duration reaching max
225
+ trackNewChunksViewed(false);
226
+ hasStoppedTracking.current = true;
227
+ if (element?.loop) {
228
+ element.loop = false;
229
+ }
230
+ return;
231
+ }
232
+
233
+ if (hasEnded && chunksViewedSinceReportRef.current.size > 0) {
234
+ trackNewChunksViewed(true);
235
+ return;
236
+ }
237
+
238
+ if (loop) {
239
+ handleLoopedVideo();
240
+ }
241
+
242
+ if (chunksViewed.has(currentChunk)) {
243
+ return;
244
+ }
245
+
246
+ chunksViewed.set(currentChunk, (chunksViewed.get(currentChunk) || 0) + 1);
247
+ chunksViewedSinceReportRef.current.set(
248
+ currentChunk,
249
+ (chunksViewedSinceReportRef.current.get(currentChunk) || 0) + 1
250
+ );
251
+
252
+ if (!lastRetentionReportedAt.current) {
253
+ lastRetentionReportedAt.current = Date.now();
254
+ }
255
+
256
+ const secondsSinceLastReport =
257
+ (Date.now() - lastRetentionReportedAt.current) / 1000;
258
+
259
+ if (secondsSinceLastReport >= RETENTION_TRACKING_INTERVAL_SECONDS) {
260
+ trackNewChunksViewed(false);
261
+ }
262
+ }, [
263
+ trackNewChunksViewed,
264
+ trackingStartedAt,
265
+ currentChunk,
266
+ hasEnded,
267
+ contactUtk,
268
+ pageMeta,
269
+ loop,
270
+ played,
271
+ videoDurationSeconds,
272
+ ]);
273
+
274
+ const onVisibilityHidden = useCallback(() => {
275
+ if (!hasStoppedTracking.current) {
276
+ // End retention tracking on visibility change
277
+ trackNewChunksViewed(true);
278
+ }
279
+ }, [trackNewChunksViewed]);
280
+
281
+ useTrackVisibilityChange({
282
+ isTrackingEnabled: Boolean(contactUtk && pageMeta),
283
+ trackingStartedAt,
284
+ onVisibilityHidden,
285
+ });
286
+ };
287
+
288
+ const useCompletedPlayTracking = ({
289
+ chunksViewed,
290
+ hasEnded,
291
+ element,
292
+ trackingStartedAt,
293
+ videoDurationSeconds,
294
+ env,
295
+ portalId,
296
+ eventProps,
297
+ }: TrackingProps &
298
+ Pick<PlayerState, 'hasEnded' | 'element'> & {
299
+ chunksViewed: Map<number, number>;
300
+ trackingStartedAt?: number;
301
+ videoDurationSeconds: number;
302
+ }) => {
303
+ const { contactUtk, pageMeta } = eventProps;
304
+ const { loop, played } = element || {};
305
+ const hasTrackedCompletion = useRef(false);
306
+
307
+ const trackCompletion = useCallback(async () => {
308
+ const secondsViewed = chunksViewed.size * CHUNK_DURATION_SECONDS;
309
+ if (
310
+ !videoDurationSeconds ||
311
+ !secondsViewed ||
312
+ hasTrackedCompletion.current
313
+ ) {
314
+ return;
315
+ }
316
+
317
+ hasTrackedCompletion.current = true;
318
+ const completedPlayParams = {
319
+ secondsViewed,
320
+ percentageViewed: Math.round(
321
+ (secondsViewed / videoDurationSeconds) * 100
322
+ ),
323
+ };
324
+
325
+ await trackCompletedPlay(
326
+ env,
327
+ portalId,
328
+ { ...eventProps, contactUtk: contactUtk!, pageMeta: pageMeta! },
329
+ completedPlayParams
330
+ );
331
+ }, [env, portalId, eventProps, videoDurationSeconds]);
332
+
333
+ // Since looped videos don't fire an 'ended' event, determine completion based
334
+ // on played duration or total viewed chunks and track completed-play
335
+ const handleLoopedVideo = () => {
336
+ const playedDuration = getTimeRangesDuration(played);
337
+ if (playedDuration >= videoDurationSeconds) {
338
+ // Track completed play for looped video based on played duration
339
+ trackCompletion();
340
+ return;
341
+ }
342
+
343
+ const totalChunks = Math.floor(
344
+ videoDurationSeconds / CHUNK_DURATION_SECONDS
345
+ );
346
+ if (chunksViewed.size >= totalChunks) {
347
+ // Track completed play for looped video watched completely
348
+ trackCompletion();
349
+ }
350
+ };
351
+
352
+ useEffect(() => {
353
+ if (!contactUtk || !pageMeta || hasTrackedCompletion.current) {
354
+ return;
355
+ }
356
+
357
+ if (hasEnded) {
358
+ trackCompletion();
359
+ return;
360
+ }
361
+
362
+ if (loop) {
363
+ handleLoopedVideo();
364
+ }
365
+ }, [
366
+ trackCompletion,
367
+ hasEnded,
368
+ contactUtk,
369
+ pageMeta,
370
+ loop,
371
+ played,
372
+ videoDurationSeconds,
373
+ ]);
374
+
375
+ const onVisibilityHidden = useCallback(() => {
376
+ if (!hasTrackedCompletion.current) {
377
+ // End retention tracking on visibility change
378
+ trackCompletion();
379
+ }
380
+ }, [trackCompletion]);
381
+
382
+ useTrackVisibilityChange({
383
+ isTrackingEnabled: Boolean(contactUtk && pageMeta),
384
+ trackingStartedAt,
385
+ onVisibilityHidden,
386
+ });
387
+ };
388
+
389
+ const TrackHSVideoAnalytics = ({
390
+ video,
391
+ portalId,
392
+ }: {
393
+ video: VideoCrmObject;
394
+ portalId: number;
395
+ }) => {
396
+ const hsEnv = getHSEnv();
397
+ const { utk: contactUtk } = useUserToken();
398
+ const { state } = useVideoPlayer();
399
+ const { currentTime, hasStarted, hasEnded, element, playbackStartedAt } =
400
+ state;
401
+ const chunksViewedRef = useRef(new Map<number, number>());
402
+ const pageMeta = usePageMeta();
403
+ // session ID used to match events across initial/secondsViewed/completed events
404
+ const sessionId = useRef(String(Date.now()));
405
+
406
+ const eventProps = useMemo(
407
+ () => ({
408
+ pageMeta,
409
+ crmObjectId: video.crmObjectId,
410
+ sessionId: sessionId.current,
411
+ contactUtk,
412
+ }),
413
+ [pageMeta, video, contactUtk]
414
+ );
415
+ const videoDurationSeconds = video.duration / 1000;
416
+ const sharedProps = { env: hsEnv, portalId, eventProps };
417
+
418
+ useInitialPlayTracking({
419
+ ...sharedProps,
420
+ hasStarted,
421
+ });
422
+
423
+ useRetentionTracking({
424
+ ...sharedProps,
425
+ chunksViewed: chunksViewedRef.current,
426
+ currentTime,
427
+ hasEnded,
428
+ element,
429
+ trackingStartedAt: playbackStartedAt,
430
+ videoDurationSeconds,
431
+ });
432
+
433
+ useCompletedPlayTracking({
434
+ ...sharedProps,
435
+ chunksViewed: chunksViewedRef.current,
436
+ hasEnded,
437
+ element,
438
+ trackingStartedAt: playbackStartedAt,
439
+ videoDurationSeconds,
440
+ });
441
+
442
+ return null;
443
+ };
444
+
445
+ export default TrackHSVideoAnalytics;
@@ -0,0 +1,25 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { PageMeta } from '../types.js';
3
+ import { useQueryParam } from './useQueryParam.js';
4
+
5
+ export const usePageMeta = () => {
6
+ const [pageMeta, setPageMeta] = useState<PageMeta | null>(null);
7
+ const hsencParam = useQueryParam('_hsenc');
8
+
9
+ useEffect(() => {
10
+ const pageId = window.hsVars && window.hsVars.page_id;
11
+ const data: PageMeta = {
12
+ pageId: pageId ?? 0,
13
+ pageUrl: window.location.href,
14
+ pageTitle: document.title,
15
+ };
16
+
17
+ if (hsencParam) {
18
+ data._hsenc = hsencParam;
19
+ }
20
+
21
+ setPageMeta(data);
22
+ }, [hsencParam]);
23
+
24
+ return pageMeta;
25
+ };
@@ -0,0 +1,12 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ export const useQueryParam = (param: string) => {
4
+ const [value, setValue] = useState<string | null>(null);
5
+ useEffect(() => {
6
+ // Setting value in useEffect since `window` won't be available during SSR
7
+ const urlParams = new URLSearchParams(window.location.search);
8
+ setValue(urlParams.get(param));
9
+ }, [param]);
10
+
11
+ return value;
12
+ };
@@ -0,0 +1,56 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { PrivacyConsent } from '../types.js';
3
+
4
+ export const useUserToken = () => {
5
+ const [utk, setUtk] = useState<string | null>(null);
6
+
7
+ useEffect(() => {
8
+ let privacyConsent: PrivacyConsent;
9
+ let isMounted = true;
10
+
11
+ const areAnalyticsAllowed = () => {
12
+ if (!privacyConsent) {
13
+ return false;
14
+ }
15
+ return privacyConsent.categories
16
+ ? privacyConsent.categories.analytics
17
+ : privacyConsent.allowed;
18
+ };
19
+
20
+ const onTrackingCodeLoaded = (_utk: string | null) => {
21
+ if (!privacyConsent || !_utk || !isMounted) {
22
+ // Exit if utk and privacy consent cannot be obtained, or component unmounted
23
+ return;
24
+ }
25
+
26
+ if (areAnalyticsAllowed()) {
27
+ setUtk(_utk);
28
+ } else {
29
+ setUtk(null);
30
+ }
31
+ };
32
+
33
+ if (window._hsq?.push) {
34
+ window._hsq.push([
35
+ 'addPrivacyConsentListener',
36
+ (_consent: PrivacyConsent) => {
37
+ privacyConsent = _consent;
38
+
39
+ if (window.__hsUserToken && areAnalyticsAllowed()) {
40
+ if (isMounted) {
41
+ setUtk(window.__hsUserToken);
42
+ }
43
+ } else {
44
+ window._hsq?.push(['addUserTokenListener', onTrackingCodeLoaded]);
45
+ }
46
+ },
47
+ ]);
48
+ }
49
+
50
+ return () => {
51
+ isMounted = false;
52
+ };
53
+ }, []);
54
+
55
+ return { utk };
56
+ };
@@ -2,6 +2,8 @@ import { Island } from '@hubspot/cms-components';
2
2
  import ContentFields from './ContentFields.js';
3
3
  import StyleFields from './StyleFields.js';
4
4
  // @ts-expect-error -- ?island not typed
5
+ import EmbedVideoIsland from './islands/EmbedVideoIsland.js?island';
6
+ // @ts-expect-error -- ?island not typed
5
7
  import HSVideoIsland from './islands/HSVideoIsland.js?island';
6
8
  import { VideoProps } from './types.js';
7
9
 
@@ -9,6 +11,7 @@ const VideoComponent = ({
9
11
  videoType,
10
12
  hubspotVideoParams,
11
13
  embedVideoParams,
14
+ oembedThumbnail,
12
15
  playButtonColor,
13
16
  video,
14
17
  }: VideoProps) => {
@@ -28,8 +31,14 @@ const VideoComponent = ({
28
31
  videoType === 'embed' &&
29
32
  Boolean(embedVideoParams?.oembed_url || embedVideoParams?.embed_html)
30
33
  ) {
31
- // TODO: implement embed video
32
- return <div />;
34
+ return (
35
+ <Island
36
+ module={EmbedVideoIsland}
37
+ embedVideoParams={embedVideoParams}
38
+ oembedThumbnail={oembedThumbnail}
39
+ playButtonColor={playButtonColor}
40
+ />
41
+ );
33
42
  }
34
43
 
35
44
  return null;