@learncard/react 2.11.0 → 2.12.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.
Files changed (127) hide show
  1. package/dist/base.css +1 -1
  2. package/dist/base.js +1 -1
  3. package/dist/cjs/{BoostGenericCard-feaf99bd.js → BoostGenericCard-71899305.js} +22 -3
  4. package/dist/cjs/BoostGenericCard-71899305.js.map +1 -0
  5. package/dist/cjs/{CertificateDisplayCard-de7d7318.js → CertificateDisplayCard-f10b46b1.js} +5 -5
  6. package/dist/cjs/{CertificateDisplayCard-de7d7318.js.map → CertificateDisplayCard-f10b46b1.js.map} +1 -1
  7. package/dist/cjs/{Lightbox-d72e19ba.js → Lightbox-e2996736.js} +100 -2
  8. package/dist/cjs/Lightbox-e2996736.js.map +1 -0
  9. package/dist/cjs/{MeritBadgeDisplayCard-6edd0a69.js → MeritBadgeDisplayCard-d50cfb5e.js} +5 -5
  10. package/dist/cjs/{MeritBadgeDisplayCard-6edd0a69.js.map → MeritBadgeDisplayCard-d50cfb5e.js.map} +1 -1
  11. package/dist/cjs/{MeritBadgeSkillsCount-7ac48ce3.js → MeritBadgeSkillsCount-e46503f5.js} +2 -2
  12. package/dist/cjs/{MeritBadgeSkillsCount-7ac48ce3.js.map → MeritBadgeSkillsCount-e46503f5.js.map} +1 -1
  13. package/dist/cjs/{SchoolIDCard-10796199.js → SchoolIDCard-2d3cbb2c.js} +3 -1184
  14. package/dist/cjs/SchoolIDCard-2d3cbb2c.js.map +1 -0
  15. package/dist/cjs/{VCCard-0b85bc7d.js → VCCard-42479c97.js} +3 -3
  16. package/dist/cjs/{VCCard-0b85bc7d.js.map → VCCard-42479c97.js.map} +1 -1
  17. package/dist/cjs/{VCDisplayBackFace-45a8d94f.js → VCDisplayBackFace-d1cf9f73.js} +3 -3
  18. package/dist/cjs/{VCDisplayBackFace-45a8d94f.js.map → VCDisplayBackFace-d1cf9f73.js.map} +1 -1
  19. package/dist/cjs/{VCDisplayCard-9e43c7db.js → VCDisplayCard-cf46c224.js} +3 -3
  20. package/dist/cjs/{VCDisplayCard-9e43c7db.js.map → VCDisplayCard-cf46c224.js.map} +1 -1
  21. package/dist/cjs/{VCDisplayCard2-81d04ca4.js → VCDisplayCard2-4a201a8a.js} +93 -68
  22. package/dist/cjs/VCDisplayCard2-4a201a8a.js.map +1 -0
  23. package/dist/cjs/{VCVerificationPill-9416f420.js → VCVerificationPill-6c9dcf80.js} +2 -2
  24. package/dist/cjs/{VCVerificationPill-9416f420.js.map → VCVerificationPill-6c9dcf80.js.map} +1 -1
  25. package/dist/cjs/{VerifierStateBadgeAndText-54065ad8.js → VerifierStateBadgeAndText-934239ab.js} +13 -4
  26. package/dist/{esm/VerifierStateBadgeAndText-2859597e.js.map → cjs/VerifierStateBadgeAndText-934239ab.js.map} +1 -1
  27. package/dist/cjs/{credential.helpers-cf5b7e3c.js → credential.helpers-17a10601.js} +2 -2
  28. package/dist/cjs/{credential.helpers-cf5b7e3c.js.map → credential.helpers-17a10601.js.map} +1 -1
  29. package/dist/cjs/index-47efbf5b.js +1186 -0
  30. package/dist/cjs/index-47efbf5b.js.map +1 -0
  31. package/dist/cjs/index.js +24 -16
  32. package/dist/cjs/index.js.map +1 -1
  33. package/dist/cjs/index14.js +17 -14
  34. package/dist/cjs/index14.js.map +1 -1
  35. package/dist/cjs/index20.js +2 -1
  36. package/dist/cjs/index20.js.map +1 -1
  37. package/dist/cjs/index22.js +6 -6
  38. package/dist/cjs/index31.js +2 -1
  39. package/dist/cjs/index31.js.map +1 -1
  40. package/dist/cjs/index39.js +12 -11
  41. package/dist/cjs/index39.js.map +1 -1
  42. package/dist/cjs/index4.js +3 -3
  43. package/dist/cjs/index40.js +4 -4
  44. package/dist/cjs/index41.js +5 -5
  45. package/dist/cjs/index42.js +9 -7
  46. package/dist/cjs/index42.js.map +1 -1
  47. package/dist/cjs/index43.js +1 -1
  48. package/dist/cjs/index46.js +2 -2
  49. package/dist/cjs/index7.js +5 -5
  50. package/dist/cjs/{types.esm-e775534b.js → types.esm-73624b33.js} +12 -3
  51. package/dist/cjs/types.esm-73624b33.js.map +1 -0
  52. package/dist/esm/{BoostGenericCard-f0c9952c.js → BoostGenericCard-3b3ff784.js} +22 -3
  53. package/dist/esm/BoostGenericCard-3b3ff784.js.map +1 -0
  54. package/dist/esm/{CertificateDisplayCard-34e0b6a8.js → CertificateDisplayCard-f26ef278.js} +5 -5
  55. package/dist/esm/{CertificateDisplayCard-34e0b6a8.js.map → CertificateDisplayCard-f26ef278.js.map} +1 -1
  56. package/dist/esm/{Lightbox-f0338a0c.js → Lightbox-11f4888d.js} +95 -3
  57. package/dist/esm/Lightbox-11f4888d.js.map +1 -0
  58. package/dist/esm/{MeritBadgeDisplayCard-c6f65ac6.js → MeritBadgeDisplayCard-0d0b28c7.js} +5 -5
  59. package/dist/esm/{MeritBadgeDisplayCard-c6f65ac6.js.map → MeritBadgeDisplayCard-0d0b28c7.js.map} +1 -1
  60. package/dist/esm/{MeritBadgeSkillsCount-c51c5111.js → MeritBadgeSkillsCount-09e211fc.js} +2 -2
  61. package/dist/esm/{MeritBadgeSkillsCount-c51c5111.js.map → MeritBadgeSkillsCount-09e211fc.js.map} +1 -1
  62. package/dist/esm/{SchoolIDCard-f82e40bc.js → SchoolIDCard-fbd1cf6c.js} +2 -1183
  63. package/dist/esm/SchoolIDCard-fbd1cf6c.js.map +1 -0
  64. package/dist/esm/{VCCard-6c85ef2e.js → VCCard-7e5e00d7.js} +3 -3
  65. package/dist/esm/{VCCard-6c85ef2e.js.map → VCCard-7e5e00d7.js.map} +1 -1
  66. package/dist/esm/{VCDisplayBackFace-211f4bfb.js → VCDisplayBackFace-ae74476d.js} +3 -3
  67. package/dist/esm/{VCDisplayBackFace-211f4bfb.js.map → VCDisplayBackFace-ae74476d.js.map} +1 -1
  68. package/dist/esm/{VCDisplayCard-04660178.js → VCDisplayCard-ac1f367a.js} +3 -3
  69. package/dist/esm/{VCDisplayCard-04660178.js.map → VCDisplayCard-ac1f367a.js.map} +1 -1
  70. package/dist/esm/{VCDisplayCard2-d4594742.js → VCDisplayCard2-7011596e.js} +94 -70
  71. package/dist/esm/VCDisplayCard2-7011596e.js.map +1 -0
  72. package/dist/esm/{VCVerificationPill-eafbb82e.js → VCVerificationPill-04ffdf56.js} +2 -2
  73. package/dist/esm/{VCVerificationPill-eafbb82e.js.map → VCVerificationPill-04ffdf56.js.map} +1 -1
  74. package/dist/esm/{VerifierStateBadgeAndText-2859597e.js → VerifierStateBadgeAndText-750a6c24.js} +13 -4
  75. package/dist/{cjs/VerifierStateBadgeAndText-54065ad8.js.map → esm/VerifierStateBadgeAndText-750a6c24.js.map} +1 -1
  76. package/dist/esm/{credential.helpers-845e8c09.js → credential.helpers-4fbdd8e8.js} +2 -2
  77. package/dist/esm/{credential.helpers-845e8c09.js.map → credential.helpers-4fbdd8e8.js.map} +1 -1
  78. package/dist/esm/index-cbd2e9db.js +1184 -0
  79. package/dist/esm/index-cbd2e9db.js.map +1 -0
  80. package/dist/esm/index.js +16 -15
  81. package/dist/esm/index.js.map +1 -1
  82. package/dist/esm/index14.js +15 -14
  83. package/dist/esm/index14.js.map +1 -1
  84. package/dist/esm/index20.js +1 -1
  85. package/dist/esm/index22.js +6 -6
  86. package/dist/esm/index31.js +2 -1
  87. package/dist/esm/index31.js.map +1 -1
  88. package/dist/esm/index39.js +12 -11
  89. package/dist/esm/index39.js.map +1 -1
  90. package/dist/esm/index4.js +3 -3
  91. package/dist/esm/index40.js +4 -4
  92. package/dist/esm/index41.js +5 -5
  93. package/dist/esm/index42.js +8 -7
  94. package/dist/esm/index42.js.map +1 -1
  95. package/dist/esm/index43.js +1 -1
  96. package/dist/esm/index46.js +2 -2
  97. package/dist/esm/index7.js +5 -5
  98. package/dist/esm/{types.esm-5b5092be.js → types.esm-5f75bd4a.js} +12 -3
  99. package/dist/esm/types.esm-5f75bd4a.js.map +1 -0
  100. package/dist/index.d.ts +81 -9
  101. package/dist/main.css +1 -1
  102. package/dist/main.js +1 -1
  103. package/package.json +3 -3
  104. package/src/assets/styles/main.css +10 -0
  105. package/src/components/BoostGenericCard/BoostGenericCard.tsx +9 -2
  106. package/src/components/BoostGenericCard/__tests__/BoostGenericCard.thumb.test.tsx +59 -0
  107. package/src/components/Lightbox/ExternalVideoFallback.tsx +53 -0
  108. package/src/components/Lightbox/Lightbox.tsx +23 -9
  109. package/src/components/Lightbox/__tests__/Lightbox.video.test.tsx +139 -0
  110. package/src/components/Lightbox/index.ts +1 -0
  111. package/src/components/VCDisplayCard2/FitText.tsx +75 -46
  112. package/src/components/VCDisplayCard2/VCDisplayCard2.tsx +59 -19
  113. package/src/components/VCDisplayCard2/__tests__/FitText.test.tsx +140 -0
  114. package/src/components/VCDisplayCard2/__tests__/VCDisplayCard2Variant.test.ts +31 -0
  115. package/src/helpers/externalUrl.helpers.ts +51 -0
  116. package/src/helpers/video.helpers.ts +41 -0
  117. package/src/index.ts +5 -0
  118. package/dist/cjs/BoostGenericCard-feaf99bd.js.map +0 -1
  119. package/dist/cjs/Lightbox-d72e19ba.js.map +0 -1
  120. package/dist/cjs/SchoolIDCard-10796199.js.map +0 -1
  121. package/dist/cjs/VCDisplayCard2-81d04ca4.js.map +0 -1
  122. package/dist/cjs/types.esm-e775534b.js.map +0 -1
  123. package/dist/esm/BoostGenericCard-f0c9952c.js.map +0 -1
  124. package/dist/esm/Lightbox-f0338a0c.js.map +0 -1
  125. package/dist/esm/SchoolIDCard-f82e40bc.js.map +0 -1
  126. package/dist/esm/VCDisplayCard2-d4594742.js.map +0 -1
  127. package/dist/esm/types.esm-5b5092be.js.map +0 -1
@@ -43,6 +43,32 @@ export type CredentialIconType = {
43
43
  color?: string;
44
44
  };
45
45
 
46
+ export type VCDisplayCardVariant = 'ribbon' | 'award' | 'certificate' | 'id';
47
+
48
+ /**
49
+ * Returns the visual branch VCDisplayCard2 will render. Preview hosts use this
50
+ * to preserve their specialized-card spacing while the ribbon card owns its
51
+ * wider horizontal safe area.
52
+ */
53
+ export const getVCDisplayCardVariant = (
54
+ credential?: VC | BoostAchievementCredential,
55
+ categoryType?: LCCategoryEnum | string,
56
+ formattedDisplayType?: string
57
+ ): VCDisplayCardVariant => {
58
+ const resolvedDisplayType = (
59
+ credential?.display?.displayType ?? formattedDisplayType
60
+ )?.toLocaleLowerCase();
61
+
62
+ if (categoryType === LCCategoryEnum.meritBadge || resolvedDisplayType === 'award') {
63
+ return 'award';
64
+ }
65
+
66
+ if (resolvedDisplayType === 'certificate') return 'certificate';
67
+ if (resolvedDisplayType === 'id' || categoryType === LCCategoryEnum.id) return 'id';
68
+
69
+ return 'ribbon';
70
+ };
71
+
46
72
  export type VCDisplayCard2Props = {
47
73
  categoryType?: LCCategoryEnum;
48
74
  credential: VC | BoostAchievementCredential;
@@ -157,19 +183,30 @@ export const VCDisplayCard2: React.FC<VCDisplayCard2Props> = ({
157
183
  const isFront = isFrontOverride ?? _isFront;
158
184
  const setIsFront = setIsFrontOverride ?? _setIsFront;
159
185
 
160
- const [headerHeight, setHeaderHeight] = useState(100); // 79 is the height if the header is one line
161
- const [headerWidth, setHeaderWidth] = useState(0);
186
+ const [headerHeight, setHeaderHeight] = useState(79);
162
187
 
163
188
  const headerRef = useRef<HTMLHeadingElement>(null);
164
189
 
165
190
  useLayoutEffect(() => {
166
- // Needs a small setTimeout otherwise it'll be wrong sometimes with multiline header.
167
- // Probably because of the interaction with FitText
168
- setTimeout(() => {
169
- setHeaderHeight(headerRef.current?.clientHeight || 100);
170
- setHeaderWidth(headerRef.current?.clientWidth ?? 0);
171
- }, 10);
172
- });
191
+ const header = headerRef.current;
192
+ if (!header) return;
193
+
194
+ // Keep the ribbon tails aligned when a long title makes the center
195
+ // section taller than its normal single-line height.
196
+ const updateHeaderHeight = () => setHeaderHeight(header.clientHeight || 79);
197
+
198
+ updateHeaderHeight();
199
+ if (typeof ResizeObserver === 'undefined') return;
200
+
201
+ const resizeObserver = new ResizeObserver(updateHeaderHeight);
202
+ resizeObserver.observe(header);
203
+
204
+ return () => resizeObserver.disconnect();
205
+ }, []);
206
+
207
+ // The tails start 10px below the center ribbon and visually overlap its
208
+ // lower border, so they are slightly shorter than the measured header.
209
+ const ribbonEndHeight = Math.max(headerHeight - 4, 75).toString();
173
210
 
174
211
  let worstVerificationStatus = verificationItems.reduce(
175
212
  (
@@ -204,10 +241,13 @@ export const VCDisplayCard2: React.FC<VCDisplayCard2Props> = ({
204
241
 
205
242
  const _title = titleOverride || title;
206
243
 
207
- const resolvedDisplayType =
208
- credential?.display?.displayType ?? formattedDisplayType?.toLocaleLowerCase();
244
+ const displayCardVariant = getVCDisplayCardVariant(
245
+ credential,
246
+ categoryType,
247
+ formattedDisplayType
248
+ );
209
249
 
210
- if (categoryType === LCCategoryEnum.meritBadge || resolvedDisplayType === 'award') {
250
+ if (displayCardVariant === 'award') {
211
251
  return (
212
252
  <MeritBadgeDisplayCard
213
253
  credential={credential}
@@ -241,7 +281,7 @@ export const VCDisplayCard2: React.FC<VCDisplayCard2Props> = ({
241
281
  );
242
282
  }
243
283
 
244
- if (resolvedDisplayType === 'certificate') {
284
+ if (displayCardVariant === 'certificate') {
245
285
  return (
246
286
  <CertificateDisplayCard
247
287
  credential={credential}
@@ -273,7 +313,7 @@ export const VCDisplayCard2: React.FC<VCDisplayCard2Props> = ({
273
313
  onVerifierClick={onVerifierClick}
274
314
  />
275
315
  );
276
- } else if (resolvedDisplayType === 'id' || categoryType === 'ID') {
316
+ } else if (displayCardVariant === 'id') {
277
317
  return (
278
318
  <div>
279
319
  <VCIDDisplayCard
@@ -314,7 +354,7 @@ export const VCDisplayCard2: React.FC<VCDisplayCard2Props> = ({
314
354
  'vc-card-header-main-title text-[#18224E] pt-[3px] leading-[80%] text-[32px]';
315
355
 
316
356
  return (
317
- <Flipper className="w-full" flipKey={isFront}>
357
+ <Flipper className="vc-display-card-ribbon-safe-area w-full" flipKey={isFront}>
318
358
  <Flipped flipId="card">
319
359
  <section
320
360
  className="vc-display-card font-poppins flex flex-col items-center border-solid border-[5px] border-white rounded-[30px] z-10 min-h-[800px] max-w-[400px] relative bg-white shadow-3xl"
@@ -325,14 +365,14 @@ export const VCDisplayCard2: React.FC<VCDisplayCard2Props> = ({
325
365
  <RibbonEnd
326
366
  side="left"
327
367
  className="absolute left-[-30px] top-[50px] z-0"
328
- height={'75'}
368
+ height={ribbonEndHeight}
329
369
  />
330
370
  </Flipped>
331
371
  <Flipped inverseFlipId="card">
332
372
  <RibbonEnd
333
373
  side="right"
334
374
  className="absolute right-[-30px] top-[50px] z-0"
335
- height={'75'}
375
+ height={ribbonEndHeight}
336
376
  />
337
377
  </Flipped>
338
378
 
@@ -351,7 +391,7 @@ export const VCDisplayCard2: React.FC<VCDisplayCard2Props> = ({
351
391
  text={_title ?? ''}
352
392
  maxFontSize={32}
353
393
  minFontSize={20}
354
- width={((headerWidth ?? 290) - 40).toString()}
394
+ width="100%"
355
395
  className={headerFitTextClassName}
356
396
  />
357
397
  </h1>
@@ -390,7 +430,7 @@ export const VCDisplayCard2: React.FC<VCDisplayCard2Props> = ({
390
430
  text={_title ?? ''}
391
431
  maxFontSize={32}
392
432
  minFontSize={20}
393
- width={((headerWidth ?? 290) - 40).toString()}
433
+ width="100%"
394
434
  className={headerFitTextClassName}
395
435
  />
396
436
  </h1>
@@ -0,0 +1,140 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import React from 'react';
4
+ import { render } from '@testing-library/react';
5
+ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
6
+ import type { MockInstance } from 'vitest';
7
+
8
+ import FitText from '../FitText';
9
+
10
+ let resizeObserverCallback: ResizeObserverCallback;
11
+ let clientWidth = 212;
12
+ let scrollWidth = 400;
13
+
14
+ const observe = vi.fn();
15
+ const disconnect = vi.fn();
16
+
17
+ class ResizeObserverMock {
18
+ constructor(callback: ResizeObserverCallback) {
19
+ resizeObserverCallback = callback;
20
+ }
21
+
22
+ observe = observe;
23
+ disconnect = disconnect;
24
+ unobserve = vi.fn();
25
+ }
26
+
27
+ describe('FitText', () => {
28
+ let clientWidthSpy: MockInstance<[], number>;
29
+ let scrollWidthSpy: MockInstance<[], number>;
30
+
31
+ beforeEach(() => {
32
+ clientWidth = 212;
33
+ scrollWidth = 400;
34
+ observe.mockClear();
35
+ disconnect.mockClear();
36
+
37
+ vi.stubGlobal('ResizeObserver', ResizeObserverMock);
38
+ clientWidthSpy = vi
39
+ .spyOn(HTMLElement.prototype, 'clientWidth', 'get')
40
+ .mockImplementation(() => clientWidth);
41
+ scrollWidthSpy = vi
42
+ .spyOn(HTMLElement.prototype, 'scrollWidth', 'get')
43
+ .mockImplementation(function () {
44
+ const fontSize = Number.parseFloat(this.style.fontSize) || 100;
45
+ return (scrollWidth * fontSize) / 100;
46
+ });
47
+ });
48
+
49
+ afterEach(() => {
50
+ clientWidthSpy.mockRestore();
51
+ scrollWidthSpy.mockRestore();
52
+ vi.unstubAllGlobals();
53
+ });
54
+
55
+ test('refits only when the observed width changes', () => {
56
+ const { container, getByText } = render(
57
+ <FitText text="A responsive ribbon title" width="100%" maxFontSize={100} />
58
+ );
59
+
60
+ const observedElement = container.firstElementChild as Element;
61
+
62
+ resizeObserverCallback(
63
+ [
64
+ {
65
+ contentRect: { width: 200, height: 40 },
66
+ target: observedElement,
67
+ } as ResizeObserverEntry,
68
+ ],
69
+ {} as ResizeObserver
70
+ );
71
+ const readsAfterFirstResize = scrollWidthSpy.mock.calls.length;
72
+
73
+ resizeObserverCallback(
74
+ [
75
+ {
76
+ contentRect: { width: 200, height: 80 },
77
+ target: observedElement,
78
+ } as ResizeObserverEntry,
79
+ ],
80
+ {} as ResizeObserver
81
+ );
82
+
83
+ expect(scrollWidthSpy).toHaveBeenCalledTimes(readsAfterFirstResize);
84
+
85
+ clientWidth = 172;
86
+ resizeObserverCallback(
87
+ [
88
+ {
89
+ contentRect: { width: 160, height: 80 },
90
+ target: observedElement,
91
+ } as ResizeObserverEntry,
92
+ ],
93
+ {} as ResizeObserver
94
+ );
95
+
96
+ expect(scrollWidthSpy.mock.calls.length).toBeGreaterThan(readsAfterFirstResize);
97
+ expect(getByText('A responsive ribbon title').style.fontSize).toBe('40px');
98
+ });
99
+
100
+ test('wraps only after reaching the minimum font size', () => {
101
+ clientWidth = 112;
102
+ scrollWidth = 1_200;
103
+
104
+ const { getByText } = render(
105
+ <FitText
106
+ text="An exceptionally long ribbon title"
107
+ width="100%"
108
+ minFontSize={10}
109
+ maxFontSize={100}
110
+ />
111
+ );
112
+ const textElement = getByText('An exceptionally long ribbon title');
113
+
114
+ expect(textElement.style.fontSize).toBe('10px');
115
+ expect(textElement.style.whiteSpace).toBe('normal');
116
+ });
117
+
118
+ test('keeps the maximum font size when the text fits', () => {
119
+ clientWidth = 350;
120
+ scrollWidth = 200;
121
+
122
+ const { getByText } = render(
123
+ <FitText text="Short title" width="100%" minFontSize={10} maxFontSize={32} />
124
+ );
125
+
126
+ expect(getByText('Short title').style.fontSize).toBe('32px');
127
+ });
128
+
129
+ test('still fits when ResizeObserver is unavailable', () => {
130
+ vi.stubGlobal('ResizeObserver', undefined);
131
+ clientWidth = 350;
132
+ scrollWidth = 200;
133
+
134
+ const { getByText } = render(
135
+ <FitText text="Short title" width="100%" minFontSize={10} maxFontSize={32} />
136
+ );
137
+
138
+ expect(getByText('Short title').style.fontSize).toBe('32px');
139
+ });
140
+ });
@@ -0,0 +1,31 @@
1
+ import type { VC } from '@learncard/types';
2
+
3
+ import { getVCDisplayCardVariant } from '../VCDisplayCard2';
4
+ import { LCCategoryEnum } from '../../../types';
5
+
6
+ const credentialWithDisplayType = (displayType: string): VC =>
7
+ ({ display: { displayType } } as unknown as VC);
8
+
9
+ describe('getVCDisplayCardVariant', () => {
10
+ it('uses the ribbon layout for a generic credential', () => {
11
+ expect(getVCDisplayCardVariant({} as VC)).toBe('ribbon');
12
+ });
13
+
14
+ it('detects each specialized display type', () => {
15
+ expect(getVCDisplayCardVariant(credentialWithDisplayType('award'))).toBe('award');
16
+ expect(getVCDisplayCardVariant(credentialWithDisplayType('certificate'))).toBe(
17
+ 'certificate'
18
+ );
19
+ expect(getVCDisplayCardVariant(credentialWithDisplayType('id'))).toBe('id');
20
+ expect(getVCDisplayCardVariant(credentialWithDisplayType('ID'))).toBe('id');
21
+ });
22
+
23
+ it('uses category metadata when display metadata is absent', () => {
24
+ expect(getVCDisplayCardVariant({} as VC, LCCategoryEnum.meritBadge)).toBe('award');
25
+ expect(getVCDisplayCardVariant({} as VC, LCCategoryEnum.id)).toBe('id');
26
+ });
27
+
28
+ it('uses the formatted display type as a final fallback', () => {
29
+ expect(getVCDisplayCardVariant({} as VC, undefined, 'Certificate')).toBe('certificate');
30
+ });
31
+ });
@@ -0,0 +1,51 @@
1
+ export type ExternalUrlOpener = (url: string) => void | Promise<void>;
2
+
3
+ /**
4
+ * Attachment URLs come from credentials issued by arbitrary third parties, so
5
+ * they are untrusted input. Handing a `javascript:` (or `data:`) URL to
6
+ * `window.open` can execute script in the app's own origin — `noopener` does
7
+ * not prevent that. Only ever open real web URLs.
8
+ */
9
+ const isOpenableUrl = (url: string): boolean => {
10
+ try {
11
+ const base = typeof window === 'undefined' ? undefined : window.location?.href;
12
+ const { protocol } = new URL(url, base);
13
+
14
+ return protocol === 'https:' || protocol === 'http:';
15
+ } catch {
16
+ return false;
17
+ }
18
+ };
19
+
20
+ const defaultOpener: ExternalUrlOpener = url => {
21
+ if (typeof window === 'undefined') return;
22
+
23
+ window.open(url, '_blank', 'noopener,noreferrer');
24
+ };
25
+
26
+ let opener: ExternalUrlOpener = defaultOpener;
27
+
28
+ /**
29
+ * Register a platform-specific way to open external URLs.
30
+ *
31
+ * This package has no native dependencies, so `window.open` is the default. Hosts
32
+ * running inside a native shell should register their own opener at startup —
33
+ * `learn-card-base` registers Capacitor's `Browser.open`, which presents the URL
34
+ * in an in-app browser instead of kicking the user out to Safari.
35
+ *
36
+ * Pass `undefined` to restore the default.
37
+ */
38
+ export const setExternalUrlOpener = (fn: ExternalUrlOpener | undefined): void => {
39
+ opener = fn ?? defaultOpener;
40
+ };
41
+
42
+ /**
43
+ * Open a URL outside of the current document, using the registered opener.
44
+ *
45
+ * Non-web schemes are dropped rather than forwarded — see `isOpenableUrl`.
46
+ */
47
+ export const openExternalUrl = async (url: string): Promise<void> => {
48
+ if (!url || !isOpenableUrl(url)) return;
49
+
50
+ await opener(url);
51
+ };
@@ -7,6 +7,47 @@ export type VideoMetadata = {
7
7
  thumbnailUrl: string | null;
8
8
  };
9
9
 
10
+ /**
11
+ * Whether a third-party video `<iframe>` embed can be expected to work in the
12
+ * current document.
13
+ *
14
+ * YouTube's embed endpoint validates the `Referer` header and returns an
15
+ * unplayable player ("Error 153: Video player configuration error") when it is
16
+ * missing or not `http(s)`. Capacitor's iOS WebView serves the app from
17
+ * `capacitor://localhost`, and WKWebView will not send a custom-scheme URL as a
18
+ * referrer, so every YouTube embed fails inside the native iOS app.
19
+ *
20
+ * Android is unaffected (Capacitor serves it from `https://localhost`), as is
21
+ * the web app. Rather than sniffing for Capacitor — which would drag a native
22
+ * dependency into this package — we test the actual condition YouTube cares
23
+ * about: is this document served over `http(s)`?
24
+ *
25
+ * Pass `platform` where it is known. Only YouTube rejects embeds this way —
26
+ * Vimeo, Google Drive and Loom serve their players regardless of referrer — so
27
+ * without the hint we stay conservative and treat any video as at risk.
28
+ */
29
+ export const canEmbedVideoIframe = (platform?: VideoPlatform): boolean => {
30
+ if (platform && platform !== 'youtube') return true;
31
+
32
+ if (typeof window === 'undefined') return true;
33
+
34
+ const protocol = window.location?.protocol;
35
+
36
+ return protocol === 'https:' || protocol === 'http:';
37
+ };
38
+
39
+ /** Public `watch` URL for a video, used when we have to hand playback off to an external browser. */
40
+ export const getExternalVideoUrl = (
41
+ metadata: VideoMetadata | null | undefined,
42
+ fallbackUrl: string
43
+ ): string => {
44
+ if (metadata?.type === 'youtube' && metadata.videoId) {
45
+ return `https://www.youtube.com/watch?v=${metadata.videoId}`;
46
+ }
47
+
48
+ return fallbackUrl;
49
+ };
50
+
10
51
  export const getVideoMetadata = async (url: string): Promise<VideoMetadata> => {
11
52
  try {
12
53
  const parsed = new URL(url);
package/src/index.ts CHANGED
@@ -10,6 +10,11 @@ export * from './helpers/useScrollBorders';
10
10
  // Export DID display helpers
11
11
  export * from './helpers/did-display.helpers';
12
12
 
13
+ // Export video embed capability helpers + the external-URL opener seam that
14
+ // native hosts (Capacitor) register into
15
+ export * from './helpers/video.helpers';
16
+ export * from './helpers/externalUrl.helpers';
17
+
13
18
  // Export credential lifecycle (revoked/suspended) treatment helper
14
19
  export * from './helpers/lifecycle.helpers';
15
20
 
@@ -1 +0,0 @@
1
- {"version":3,"file":"BoostGenericCard-feaf99bd.js","sources":["../../src/components/BoostGenericCard/AlignmentSkillsCount.tsx","../../src/helpers/display.helpers.ts","../../src/helpers/lifecycle.helpers.ts","../../src/components/BoostGenericCard/BoostGenericCard.tsx"],"sourcesContent":["import React from 'react';\nimport PuzzlePiece from '../svgs/PuzzlePiece';\nimport { VC } from '@learncard/types';\nimport { categorizeSkills } from '../../helpers/credential.helpers';\n\ntype AlignmentSkillsCountProps = {\n credential?: VC;\n onClick?: () => void;\n className?: string;\n};\n\nconst AlignmentSkillsCount: React.FC<AlignmentSkillsCountProps> = ({\n credential,\n onClick,\n className = '',\n}) => {\n if (!credential) return <></>;\n\n const achievement = (credential?.credentialSubject as any)?.achievement;\n let alignmentCount = achievement?.alignment?.length || 0;\n\n if (alignmentCount === 0 && credential.skills?.length > 0) {\n // fallback to legacy skills field if present and no alignments\n const skillsMap = categorizeSkills(credential.skills ?? []);\n\n // Calculate total count of skills and subskills\n const totalSkills: any = Object.values(skillsMap).reduce(\n (total: any, category: any) => total + category?.length,\n 0\n );\n const totalSubskills: any = Object.values(skillsMap).reduce(\n (total: any, category: any) => total + (category?.totalSubskillsCount || 0),\n 0\n );\n\n const total: any = totalSkills + totalSubskills;\n alignmentCount = total;\n }\n\n return (\n <div\n role={onClick ? ('button' as const) : undefined}\n onClick={onClick}\n className={`px-[10px] py-[2px] flex gap-[3px] items-center rounded-[5px] overflow-hidden bg-violet-100 ${className}`}\n >\n <PuzzlePiece className=\"w-[20px] h-[20px] text-grayscale-800\" version=\"filled\" />\n <p className=\"text-[12px] text-grayscale-800 font-poppins font-[600]\">\n {`${alignmentCount} Skill${alignmentCount !== 1 ? 's' : ''}`}\n </p>\n </div>\n );\n};\n\nexport default AlignmentSkillsCount;\n","import { LCCategoryEnum } from '../types';\n\nimport { BadgeDisplayIcon } from '../components/svgs/BadgeDisplayIcon';\nimport { CertificateDisplayIcon } from '../components/svgs/CertificateDisplayIcon';\nimport { IDDisplayIcon } from '../components/svgs/IDDisplayIcon';\nimport { CourseDisplayIcon } from '../components/svgs/CourseDisplayIcon';\nimport { AwardDisplayIcon } from '../components/svgs/AwardDisplayIcon';\n\nexport enum DisplayTypeEnum {\n Badge = 'badge',\n Certificate = 'certificate',\n ID = 'id',\n Course = 'course',\n Award = 'award',\n}\n\nexport const getDefaultDisplayType = (category: string): DisplayTypeEnum => {\n if (category === LCCategoryEnum.socialBadge || category === LCCategoryEnum.workHistory) {\n return DisplayTypeEnum.Badge;\n }\n\n if (category === LCCategoryEnum.id || category === LCCategoryEnum.membership) {\n return DisplayTypeEnum.ID;\n }\n\n if (\n category === LCCategoryEnum.achievement ||\n category === LCCategoryEnum.accommodations ||\n category === LCCategoryEnum.accomplishments ||\n category === LCCategoryEnum.learningHistory\n ) {\n return DisplayTypeEnum.Certificate;\n }\n\n return DisplayTypeEnum.Badge;\n};\n\nexport const getDisplayIcon = (displayType: DisplayTypeEnum): React.FC<{ className?: string }> => {\n switch (displayType) {\n case DisplayTypeEnum.Badge:\n return BadgeDisplayIcon;\n case DisplayTypeEnum.Certificate:\n return CertificateDisplayIcon;\n case DisplayTypeEnum.ID:\n return IDDisplayIcon;\n case DisplayTypeEnum.Course:\n return CourseDisplayIcon;\n case DisplayTypeEnum.Award:\n return AwardDisplayIcon;\n default:\n return BadgeDisplayIcon;\n }\n};\n","import type { CSSProperties } from 'react';\n\nexport type CredentialLifecycleStatus = 'active' | 'revoked' | 'suspended';\n\nexport type LifecycleTreatment = {\n /** True when the credential is revoked or suspended. */\n isInactive: boolean;\n /** Inline filter for media (images / thumbnails); `undefined` when active.\n * Inline style (not a Tailwind class) so it applies regardless of whether the\n * consumer app's Tailwind scans this package or generates the utility. */\n mediaStyle: CSSProperties | undefined;\n /** Inline style for de-emphasizing text; `undefined` when active. */\n textStyle: CSSProperties | undefined;\n /** Pill background color for the status chip (only meaningful when `isInactive`). */\n pillBg: string;\n};\n\nconst REVOKED_COLOR = '#DC2626'; // red-600\nconst SUSPENDED_COLOR = '#D97706'; // amber-600\n\n/**\n * Shared visual treatment for a revoked/suspended credential, used by both the\n * grid card (`BoostGenericCard`) and the list row (`BoostListItem`) so the two\n * views stay in sync. Values are inert (`isInactive: false`, `undefined` styles)\n * for active credentials.\n */\nexport const getLifecycleTreatment = (\n status: CredentialLifecycleStatus = 'active'\n): LifecycleTreatment => {\n const isInactive = status === 'revoked' || status === 'suspended';\n\n return {\n isInactive,\n mediaStyle: isInactive ? { filter: 'grayscale(1) brightness(0.9)' } : undefined,\n textStyle: isInactive ? { filter: 'grayscale(1)', opacity: 0.6 } : undefined,\n pillBg: status === 'suspended' ? SUSPENDED_COLOR : REVOKED_COLOR,\n };\n};\n","import React from 'react';\nimport { BoostGenericCardProps, WalletCategoryTypes } from '../../types';\nimport { TYPE_TO_IMG_SRC, TYPE_TO_WALLET_DARK_COLOR } from '../../constants';\nimport { DisplayTypeEnum, getDisplayIcon } from '../../helpers/display.helpers';\nimport AlignmentSkillsCount from './AlignmentSkillsCount';\nimport ThreeDotVertical from '../svgs/ThreeDotVertical';\nimport { CircleCheckButton } from '../CircleCheckButton';\nimport BadgeThumbnailImg from '../BadgeThumbnailImg/BadgeThumbnailImg';\nimport { getLifecycleTreatment } from '../../helpers/lifecycle.helpers';\nimport { useT } from '../../i18n';\n\nexport const BoostGenericCard: React.FC<BoostGenericCardProps> = ({\n title,\n customTitle,\n thumbImgSrc,\n customThumbClass = '',\n customHeaderClass = '',\n type = WalletCategoryTypes.achievements,\n className,\n onCheckClick,\n showChecked,\n checkStatus,\n customThumbComponent,\n innerOnClick,\n bgImgSrc,\n issuerName,\n customIssuerName,\n dateDisplay,\n customDateDisplay,\n optionsTriggerOnClick,\n verifierBadge,\n credential,\n isInSkillsModal,\n displayType,\n linkedCredentialsCount = 0,\n linkedCredentialsClassName = '',\n checkBtnClass = '',\n lifecycleStatus = 'active',\n}) => {\n const t = useT();\n const thumbClass = TYPE_TO_WALLET_DARK_COLOR[type]\n ? `bg-${TYPE_TO_WALLET_DARK_COLOR[type]}`\n : 'bg-grayscale-50';\n const defaultThumbClass = `small-boost-card-thumb flex h-[110px] w-[110px] my-[10px] mx-auto ${thumbClass} overflow-hidden flex-col justify-center items-center rounded-full ${customThumbClass}`;\n const imgSrc = thumbImgSrc?.trim() !== '' ? thumbImgSrc : TYPE_TO_IMG_SRC[type];\n\n const handleInnerClick = () => innerOnClick?.();\n const handleOptionsClick = () => optionsTriggerOnClick?.();\n\n const DisplayIcon = getDisplayIcon(displayType as DisplayTypeEnum);\n\n // Shared revoked/suspended treatment (see getLifecycleTreatment). Inline styles are\n // used (not Tailwind classes) because the consumer app's Tailwind doesn't scan this\n // package and it ships no utility CSS, so utility classes here have no backing rule.\n const {\n isInactive,\n mediaStyle: inactiveMediaStyle,\n textStyle: inactiveTextStyle,\n pillBg,\n } = getLifecycleTreatment(lifecycleStatus);\n const pillLabel = t(`credential.lifecycle.${lifecycleStatus}`);\n const pillLeft = showChecked ? 'left-[44px]' : 'left-[8px]';\n\n return (\n <div\n className={`flex bg-white flex-col shadow-bottom relative p-0 w-[160px] h-[285px] rounded-[20px] overflow-hidden ${className}`}\n >\n {isInactive && (\n <span\n className={`absolute top-[8px] ${pillLeft} z-20 rounded-full px-[9px] py-[3px] text-[10px] font-extrabold uppercase tracking-wide text-white`}\n style={{ backgroundColor: pillBg }}\n >\n {pillLabel}\n </span>\n )}\n\n {optionsTriggerOnClick && (\n <section\n className=\"absolute cursor-pointer h-[30px] w-[30px] top-[5px] right-[5px] rounded-full bg-white/70 flex items-center justify-center z-20\"\n onClick={handleOptionsClick}\n >\n <ThreeDotVertical className=\"h-[20px] w-[20px] text-grayscale-900\" />\n </section>\n )}\n\n {bgImgSrc && (\n <section\n className=\"absolute top-[-50px] left-0 rounded-b-full overflow-hidden z-0\"\n style={inactiveMediaStyle}\n >\n <img className=\"h-full w-full object-cover\" src={bgImgSrc} />\n </section>\n )}\n\n <button\n type=\"button\"\n className=\"z-10 flex flex-col flex-grow\"\n onClick={handleInnerClick}\n >\n {/* Thumbnail — filter on the wrapper so it desaturates a\n customThumbComponent too, not just the default thumb section. */}\n <div style={inactiveMediaStyle}>\n {customThumbComponent || (\n <section className={defaultThumbClass}>\n {thumbImgSrc?.trim() ? (\n <BadgeThumbnailImg\n className=\"w-full h-full rounded-full object-cover\"\n src={thumbImgSrc}\n alt=\"Credential Achievement\"\n />\n ) : (\n <img\n className=\"max-w-full p-0 object-cover rounded-full\"\n src={imgSrc}\n />\n )}\n </section>\n )}\n </div>\n\n {/* Details Section: grows to fill available space */}\n <section\n className={`flex flex-col flex-grow items-center justify-end pt-1 w-full ${\n linkedCredentialsCount === 0 ? 'pb-[20px]' : ''\n }`}\n >\n <div className=\"px-1 flex flex-col items-center justify-center w-full\">\n {/* Title - dynamically size based on length */}\n {!customTitle ? (\n <p\n className={`\n ${customHeaderClass}\n font-medium text-center text-grayscale-900 line-clamp-2\n h-[50px] flex items-center justify-center\n ${\n (title?.length ?? 0) > 35\n ? 'text-[13px] leading-tight'\n : 'text-[16px]'\n }\n `}\n style={inactiveTextStyle}\n title={title}\n >\n {title}\n </p>\n ) : (\n customTitle\n )}\n\n {/* Verifier & Issuer */}\n <div className=\"mt-1 w-full px-4 text-center\">\n <span className=\"line-clamp-2 break-words text-[12px] leading-snug text-grayscale-700\">\n {verifierBadge && (\n <span className=\"mr-1 inline-block align-middle\">\n {verifierBadge}\n </span>\n )}\n {customIssuerName || (\n <span className=\"font-bold\" style={inactiveTextStyle}>\n {issuerName}\n </span>\n )}\n </span>\n </div>\n\n {/* Date */}\n {customDateDisplay || (\n <span className=\"text-[11px] text-grayscale-700 mt-1\">\n {dateDisplay}\n </span>\n )}\n\n {/* Skills count if in modal */}\n {isInSkillsModal && credential && (\n <AlignmentSkillsCount\n credential={credential}\n onClick={handleInnerClick}\n className=\"mt-[16px]\"\n />\n )}\n </div>\n\n {/* Linked Credentials moved inside details */}\n {linkedCredentialsCount > 0 && (\n <div\n className={`mt-auto flex items-center justify-center py-1 w-full ${linkedCredentialsClassName}`}\n >\n <DisplayIcon className=\"h-[20px] w-[20px] text-grayscale-900\" />\n <span className=\"ml-1 text-sm font-semibold text-grayscale-900\">\n +{linkedCredentialsCount} Linked\n </span>\n </div>\n )}\n </section>\n </button>\n\n {/* Optional Check Button overlay */}\n {showChecked && (\n <div className=\"absolute top-[5px] left-[5px] z-20\">\n <CircleCheckButton\n checked={checkStatus}\n onClick={onCheckClick}\n className={checkBtnClass}\n />\n </div>\n )}\n </div>\n );\n};\n\nexport default BoostGenericCard;\n"],"names":["React","categorizeSkills","total","PuzzlePiece","BadgeDisplayIcon","CertificateDisplayIcon","IDDisplayIcon","CourseDisplayIcon","AwardDisplayIcon","WalletCategoryTypes","useT","TYPE_TO_WALLET_DARK_COLOR","TYPE_TO_IMG_SRC","ThreeDotVertical","BadgeThumbnailImg","CircleCheckButton"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"Lightbox-d72e19ba.js","sources":["../../src/components/svgs/X.tsx","../../src/helpers/video.helpers.ts","../../src/components/Lightbox/Lightbox.tsx"],"sourcesContent":["import React from 'react';\n\ntype XProps = {\n className?: string;\n strokeWidth?: string;\n};\n\nconst X: React.FC<XProps> = ({ className = '', strokeWidth = '2' }) => {\n return (\n <svg\n viewBox=\"0 0 32 32\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n className={className}\n aria-label=\"X\"\n >\n <path\n d=\"M25 7L7 25\"\n stroke=\"currentColor\"\n strokeWidth={strokeWidth}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n <path\n d=\"M25 25L7 7\"\n stroke=\"currentColor\"\n strokeWidth={strokeWidth}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n );\n};\n\nexport default X;\n","export type VideoPlatform = 'youtube' | 'vimeo' | 'drive' | 'loom' | 'unknown';\n\nexport type VideoMetadata = {\n type: VideoPlatform;\n videoId: string | null;\n embedUrl: string | null;\n thumbnailUrl: string | null;\n};\n\nexport const getVideoMetadata = async (url: string): Promise<VideoMetadata> => {\n try {\n const parsed = new URL(url);\n const host = parsed.hostname.replace(/^www\\./, '');\n let id: string | null = null;\n\n // YouTube\n if (host === 'youtu.be' || host.includes('youtube.com')) {\n if (host === 'youtu.be') {\n id = parsed.pathname.slice(1);\n } else {\n id =\n parsed.searchParams.get('v') ||\n parsed.pathname.split('/').filter(Boolean).pop() ||\n null;\n }\n if (id) {\n let thumb = `https://img.youtube.com/vi/${id}/maxresdefault.jpg`;\n try {\n const res = await fetch(thumb, { method: 'HEAD' });\n if (!res.ok) thumb = `https://img.youtube.com/vi/${id}/hqdefault.jpg`;\n } catch {\n thumb = `https://img.youtube.com/vi/${id}/hqdefault.jpg`;\n }\n return {\n type: 'youtube',\n videoId: id,\n embedUrl: `https://www.youtube.com/embed/${id}`,\n thumbnailUrl: thumb,\n };\n }\n }\n\n // Vimeo\n if (host.includes('vimeo.com')) {\n const match = url.match(/vimeo\\.com\\/(\\d+)/);\n id = match?.[1] ?? null;\n if (id) {\n try {\n const oembed = `https://vimeo.com/api/oembed.json?url=${encodeURIComponent(\n url\n )}`;\n const data = await fetch(oembed).then(r => r.json());\n return {\n type: 'vimeo',\n videoId: id,\n embedUrl: `https://player.vimeo.com/video/${id}`,\n thumbnailUrl: data.thumbnail_url,\n };\n } catch (e) {\n // Fallback if oEmbed fails\n return {\n type: 'vimeo',\n videoId: id,\n embedUrl: `https://player.vimeo.com/video/${id}`,\n thumbnailUrl: null,\n };\n }\n }\n }\n\n // Google Drive\n if (host === 'drive.google.com' || host === 'docs.google.com') {\n const match = parsed.pathname.match(/\\/d\\/([^/]+)/);\n id = match?.[1] ?? parsed.searchParams.get('id');\n if (id) {\n return {\n type: 'drive',\n videoId: id,\n embedUrl: `https://drive.google.com/file/d/${id}/preview`,\n thumbnailUrl: `https://drive.google.com/thumbnail?sz=w320-h320&id=${id}`,\n };\n }\n }\n\n // Loom\n if (host === 'loom.com') {\n const match = parsed.pathname.match(/\\/(?:share|embed|watch)\\/([A-Za-z0-9]+)/);\n id = match?.[1] ?? null;\n if (id) {\n const embedUrl = `https://www.loom.com/embed/${id}`;\n try {\n const oembedUrl = `https://www.loom.com/api/oembed?url=${encodeURIComponent(\n url\n )}`;\n const response = await fetch(oembedUrl);\n if (!response.ok) {\n throw new Error(`oEmbed API returned ${response.status}`);\n }\n const data = await response.json();\n return {\n type: 'loom',\n videoId: id,\n embedUrl,\n thumbnailUrl: data.thumbnail_url,\n };\n } catch (e) {\n // Fallback if oEmbed fails\n console.warn('Loom oEmbed fetch failed:', e);\n return {\n type: 'loom',\n videoId: id,\n embedUrl,\n thumbnailUrl: null,\n };\n }\n }\n }\n\n // Fallback\n return { type: 'unknown', videoId: null, embedUrl: null, thumbnailUrl: null };\n } catch (e) {\n console.error('getVideoMetadata error:', e);\n return { type: 'unknown', videoId: null, embedUrl: null, thumbnailUrl: null };\n }\n};\n","import React, { useCallback, useEffect, useRef, useState } from 'react';\nimport { createPortal } from 'react-dom';\n\nimport X from '../svgs/X';\n\nimport { getVideoMetadata, VideoMetadata } from '../../helpers/video.helpers';\n\nexport type LightboxItemType = 'photo' | 'video';\n\nexport type LightboxItem = {\n url: string;\n type: LightboxItemType;\n};\n\nexport type LightboxImage = LightboxItem & {\n type: 'photo';\n alt?: string;\n};\n\nexport type LightboxVideo = LightboxItem & {\n type: 'video';\n};\n\nexport type LightboxProps = {\n items: LightboxItem[];\n currentUrl: string | undefined;\n setCurrentUrl: (url: string | undefined) => void;\n showCloseButton?: boolean;\n enableImageClick?: boolean;\n};\n\nexport const Lightbox: React.FC<LightboxProps> = ({\n items,\n currentUrl,\n setCurrentUrl,\n showCloseButton = true,\n enableImageClick = false,\n}) => {\n const currentItem = items.find(item => item.url === currentUrl);\n const innerRef = useRef<HTMLImageElement>(null);\n const [videoMetadata, setVideoMetadata] = useState<VideoMetadata | null>(null);\n\n const goToNextItem = useCallback(() => {\n const currentIndex = items.findIndex(item => item.url === currentUrl);\n const nextItem = items.at((currentIndex + 1) % items.length);\n if (nextItem) setCurrentUrl(nextItem.url);\n }, [currentUrl, items, setCurrentUrl]);\n\n const goToPreviousItem = useCallback(() => {\n const currentIndex = items.findIndex(item => item.url === currentUrl);\n const previousItem = items.at((currentIndex - 1 + items.length) % items.length);\n if (previousItem) setCurrentUrl(previousItem.url);\n }, [currentUrl, items, setCurrentUrl]);\n\n useEffect(() => {\n const keydownListener = (e: KeyboardEvent) => {\n if (!currentUrl || !currentItem) return;\n\n if (e.key === 'Escape') {\n e.preventDefault();\n setCurrentUrl(undefined);\n }\n\n if (e.key === 'ArrowRight') {\n e.preventDefault();\n goToNextItem();\n }\n\n if (e.key === 'ArrowLeft') {\n e.preventDefault();\n goToPreviousItem();\n }\n };\n\n window.addEventListener('keydown', keydownListener);\n return () => window.removeEventListener('keydown', keydownListener);\n }, [currentUrl, currentItem, goToNextItem, goToPreviousItem, setCurrentUrl]);\n\n useEffect(() => {\n const fetchMetadata = async () => {\n if (currentItem?.type === 'video') {\n const metadata = await getVideoMetadata(currentItem.url);\n setVideoMetadata(metadata);\n } else {\n setVideoMetadata(null);\n }\n };\n\n fetchMetadata();\n }, [currentItem]);\n\n if (!currentUrl || !currentItem) return null;\n\n return createPortal(\n <div\n className=\"absolute top-0 left-0 w-screen h-screen bg-[rgba(0,0,0,0.9)] text-white flex justify-center items-center z-[999999]\"\n onClick={e => {\n if (e.target !== innerRef.current) {\n setCurrentUrl(undefined);\n }\n }}\n >\n {showCloseButton && (\n <button onClick={() => setCurrentUrl(undefined)}>\n <X className=\"absolute w-[30px] h-[30px] top-[20px] right-[20px] cursor-pointer text-white z-[999999]\" />\n </button>\n )}\n {currentItem.type === 'photo' && (\n <img\n className=\"cursor-pointer max-w-[90vw] max-h-[90vh]\"\n src={currentUrl}\n onClick={() => enableImageClick && window.open(currentUrl, '_blank')}\n ref={innerRef}\n alt=\"\"\n />\n )}\n\n {currentItem.type === 'video' && videoMetadata?.embedUrl && (\n <div className=\"relative w-[90vw] max-w-[800px] aspect-video\">\n <iframe\n src={videoMetadata.embedUrl}\n className=\"absolute top-0 left-0 w-full h-full rounded-md\"\n title=\"Video player\"\n frameBorder=\"0\"\n allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\"\n allowFullScreen\n />\n </div>\n )}\n </div>,\n document.body\n );\n};\n"],"names":["React","__async","useRef","useState","useCallback","useEffect","createPortal"],"mappings":"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"}