@ndla/ui 34.6.2 → 34.6.3

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 (153) hide show
  1. package/es/Article/Article.js +11 -6
  2. package/es/Aside/Aside.js +5 -2
  3. package/es/CopyParagraphButton/CopyParagraphButtonV2.js +85 -0
  4. package/es/CopyParagraphButton/index.js +2 -1
  5. package/es/Embed/AudioEmbed.js +254 -0
  6. package/es/Embed/BrightcoveEmbed.js +250 -0
  7. package/es/Embed/ConceptEmbed.js +358 -0
  8. package/es/Embed/ConceptListEmbed.js +71 -0
  9. package/es/Embed/ContentLinkEmbed.js +42 -0
  10. package/es/Embed/ExternalEmbed.js +91 -0
  11. package/es/Embed/FootnoteEmbed.js +32 -0
  12. package/es/Embed/H5pEmbed.js +87 -0
  13. package/es/Embed/IframeEmbed.js +83 -0
  14. package/es/Embed/ImageEmbed.js +322 -0
  15. package/es/Embed/RelatedContentEmbed.js +58 -0
  16. package/es/Embed/UnknownEmbed.js +27 -0
  17. package/es/Embed/conceptComponents.js +282 -0
  18. package/es/Embed/index.js +21 -0
  19. package/es/FactBox/FactBoxV2.js +90 -0
  20. package/es/FactBox/index.js +1 -0
  21. package/es/Figure/Figure.js +8 -5
  22. package/es/Figure/FigureLicenseDialogContent.js +72 -0
  23. package/es/FileList/FileListV2.js +47 -0
  24. package/es/FileList/FileV2.js +34 -0
  25. package/es/FileList/PdfFile.js +25 -0
  26. package/es/FileList/index.js +3 -0
  27. package/es/Notion/Notion.js +5 -5
  28. package/es/Notion/NotionVisualElement.js +2 -2
  29. package/es/RelatedArticleList/RelatedArticleV2.js +101 -0
  30. package/es/RelatedArticleList/index.js +2 -1
  31. package/es/Table/Table.js +95 -8
  32. package/es/all.css +1 -1
  33. package/es/index.js +5 -4
  34. package/es/locale/messages-en.js +8 -1
  35. package/es/locale/messages-nb.js +8 -1
  36. package/es/locale/messages-nn.js +8 -1
  37. package/es/locale/messages-se.js +8 -1
  38. package/es/locale/messages-sma.js +8 -1
  39. package/lib/Article/Article.d.ts +2 -1
  40. package/lib/Article/Article.js +11 -6
  41. package/lib/Aside/Aside.d.ts +2 -1
  42. package/lib/Aside/Aside.js +5 -2
  43. package/lib/CopyParagraphButton/CopyParagraphButtonV2.d.ts +14 -0
  44. package/lib/CopyParagraphButton/CopyParagraphButtonV2.js +84 -0
  45. package/lib/CopyParagraphButton/index.d.ts +2 -1
  46. package/lib/CopyParagraphButton/index.js +7 -0
  47. package/lib/Embed/AudioEmbed.d.ts +20 -0
  48. package/lib/Embed/AudioEmbed.js +252 -0
  49. package/lib/Embed/BrightcoveEmbed.d.ts +16 -0
  50. package/lib/Embed/BrightcoveEmbed.js +250 -0
  51. package/lib/Embed/ConceptEmbed.d.ts +19 -0
  52. package/lib/Embed/ConceptEmbed.js +358 -0
  53. package/lib/Embed/ConceptListEmbed.d.ts +13 -0
  54. package/lib/Embed/ConceptListEmbed.js +70 -0
  55. package/lib/Embed/ContentLinkEmbed.d.ts +14 -0
  56. package/lib/Embed/ContentLinkEmbed.js +50 -0
  57. package/lib/Embed/ExternalEmbed.d.ts +14 -0
  58. package/lib/Embed/ExternalEmbed.js +90 -0
  59. package/lib/Embed/FootnoteEmbed.d.ts +13 -0
  60. package/lib/Embed/FootnoteEmbed.js +39 -0
  61. package/lib/Embed/H5pEmbed.d.ts +14 -0
  62. package/lib/Embed/H5pEmbed.js +86 -0
  63. package/lib/Embed/IframeEmbed.d.ts +14 -0
  64. package/lib/Embed/IframeEmbed.js +91 -0
  65. package/lib/Embed/ImageEmbed.d.ts +37 -0
  66. package/lib/Embed/ImageEmbed.js +326 -0
  67. package/lib/Embed/RelatedContentEmbed.d.ts +16 -0
  68. package/lib/Embed/RelatedContentEmbed.js +64 -0
  69. package/lib/Embed/UnknownEmbed.d.ts +13 -0
  70. package/lib/Embed/UnknownEmbed.js +35 -0
  71. package/lib/Embed/conceptComponents.d.ts +32 -0
  72. package/lib/Embed/conceptComponents.js +280 -0
  73. package/lib/Embed/index.d.ts +20 -0
  74. package/lib/Embed/index.js +97 -0
  75. package/lib/FactBox/FactBoxV2.d.ts +13 -0
  76. package/lib/FactBox/FactBoxV2.js +92 -0
  77. package/lib/FactBox/index.d.ts +1 -0
  78. package/lib/FactBox/index.js +7 -0
  79. package/lib/Figure/Figure.d.ts +5 -2
  80. package/lib/Figure/Figure.js +8 -5
  81. package/lib/Figure/FigureLicenseDialogContent.d.ts +22 -0
  82. package/lib/Figure/FigureLicenseDialogContent.js +71 -0
  83. package/lib/FileList/FileListV2.d.ts +13 -0
  84. package/lib/FileList/FileListV2.js +46 -0
  85. package/lib/FileList/FileV2.d.ts +16 -0
  86. package/lib/FileList/FileV2.js +42 -0
  87. package/lib/FileList/PdfFile.d.ts +13 -0
  88. package/lib/FileList/PdfFile.js +31 -0
  89. package/lib/FileList/index.d.ts +3 -0
  90. package/lib/FileList/index.js +21 -0
  91. package/lib/Notion/Notion.js +5 -5
  92. package/lib/Notion/NotionVisualElement.d.ts +1 -1
  93. package/lib/Notion/NotionVisualElement.js +2 -2
  94. package/lib/RelatedArticleList/RelatedArticleV2.d.ts +25 -0
  95. package/lib/RelatedArticleList/RelatedArticleV2.js +101 -0
  96. package/lib/RelatedArticleList/index.d.ts +2 -1
  97. package/lib/RelatedArticleList/index.js +7 -0
  98. package/lib/Table/Table.js +98 -8
  99. package/lib/all.css +1 -1
  100. package/lib/index.d.ts +5 -4
  101. package/lib/index.js +117 -2
  102. package/lib/locale/messages-en.d.ts +7 -0
  103. package/lib/locale/messages-en.js +8 -1
  104. package/lib/locale/messages-nb.d.ts +7 -0
  105. package/lib/locale/messages-nb.js +8 -1
  106. package/lib/locale/messages-nn.d.ts +7 -0
  107. package/lib/locale/messages-nn.js +8 -1
  108. package/lib/locale/messages-se.d.ts +7 -0
  109. package/lib/locale/messages-se.js +8 -1
  110. package/lib/locale/messages-sma.d.ts +7 -0
  111. package/lib/locale/messages-sma.js +8 -1
  112. package/lib/types.d.ts +1 -1
  113. package/package.json +16 -12
  114. package/src/Article/Article.tsx +8 -3
  115. package/src/Aside/Aside.tsx +9 -1
  116. package/src/Aside/component.aside.scss +3 -0
  117. package/src/CopyParagraphButton/CopyParagraphButtonV2.tsx +84 -0
  118. package/src/CopyParagraphButton/index.tsx +2 -1
  119. package/src/Embed/AudioEmbed.tsx +249 -0
  120. package/src/Embed/BrightcoveEmbed.tsx +203 -0
  121. package/src/Embed/ConceptEmbed.tsx +408 -0
  122. package/src/Embed/ConceptListEmbed.tsx +64 -0
  123. package/src/Embed/ContentLinkEmbed.tsx +41 -0
  124. package/src/Embed/ExternalEmbed.tsx +80 -0
  125. package/src/Embed/FootnoteEmbed.tsx +30 -0
  126. package/src/Embed/H5pEmbed.tsx +74 -0
  127. package/src/Embed/IframeEmbed.tsx +84 -0
  128. package/src/Embed/ImageEmbed.tsx +314 -0
  129. package/src/Embed/RelatedContentEmbed.tsx +62 -0
  130. package/src/Embed/UnknownEmbed.tsx +27 -0
  131. package/src/Embed/conceptComponents.tsx +393 -0
  132. package/src/Embed/index.ts +21 -0
  133. package/src/FactBox/FactBoxV2.tsx +56 -0
  134. package/src/FactBox/index.ts +2 -0
  135. package/src/Figure/Figure.tsx +28 -15
  136. package/src/Figure/FigureLicenseDialogContent.tsx +80 -0
  137. package/src/Figure/component.figure.scss +0 -1
  138. package/src/FileList/FileListV2.tsx +58 -0
  139. package/src/FileList/FileV2.tsx +35 -0
  140. package/src/FileList/PdfFile.tsx +25 -0
  141. package/src/FileList/index.ts +3 -0
  142. package/src/Notion/Notion.tsx +0 -1
  143. package/src/Notion/NotionVisualElement.tsx +1 -1
  144. package/src/RelatedArticleList/RelatedArticleV2.tsx +84 -0
  145. package/src/RelatedArticleList/index.ts +2 -1
  146. package/src/Table/Table.tsx +77 -4
  147. package/src/index.ts +19 -4
  148. package/src/locale/messages-en.ts +7 -0
  149. package/src/locale/messages-nb.ts +7 -0
  150. package/src/locale/messages-nn.ts +7 -0
  151. package/src/locale/messages-se.ts +7 -0
  152. package/src/locale/messages-sma.ts +7 -0
  153. package/src/types.ts +1 -1
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Copyright (c) 2023-present, NDLA.
3
+ *
4
+ * This source code is licensed under the GPLv3 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+
9
+ import isNumber from 'lodash/isNumber';
10
+ import { useEffect, useRef } from 'react';
11
+ import { IframeMetaData } from '@ndla/types-embed';
12
+ import { useTranslation } from 'react-i18next';
13
+ import { Figure } from '../Figure';
14
+ import { ResourceBox } from '../ResourceBox';
15
+
16
+ interface Props {
17
+ embed: IframeMetaData;
18
+ isConcept?: boolean;
19
+ }
20
+
21
+ const ExternalEmbed = ({ embed, isConcept }: Props) => {
22
+ const { t } = useTranslation();
23
+ const iframeRef = useRef<HTMLIFrameElement>(null);
24
+
25
+ const { embedData } = embed;
26
+
27
+ useEffect(() => {
28
+ const iframe = iframeRef.current;
29
+ if (iframe) {
30
+ const [width, height] = [parseInt(iframe.width), parseInt(iframe.height)];
31
+ iframe.style.aspectRatio = `${width ? width : 16}/${height ? height : 9}`;
32
+ }
33
+ }, []);
34
+
35
+ if (embedData.type === 'fullscreen') {
36
+ const iframeImage = embed.status === 'success' ? embed.data.iframeImage : undefined;
37
+ const image = { src: iframeImage?.imageUrl ?? '', alt: iframeImage?.alttext?.alttext ?? '' };
38
+ return (
39
+ <Figure type="full">
40
+ <ResourceBox
41
+ image={image}
42
+ title={embedData.title ?? ''}
43
+ url={embedData.url}
44
+ caption={embedData.caption ?? ''}
45
+ buttonText={t('license.other.itemImage.ariaLabel')}
46
+ />
47
+ </Figure>
48
+ );
49
+ }
50
+
51
+ const resize = !embedData.url.includes('trinket.io');
52
+
53
+ const fullColumnClass = isConcept ? 'c-figure--full-column' : '';
54
+ const resizeClass = resize ? 'c-figure--resize' : '';
55
+ const classes = `c-figure ${fullColumnClass} ${resizeClass}`;
56
+
57
+ const { width, height, title, url } = embedData;
58
+
59
+ const strippedWidth = isNumber(width) ? width : width?.replace(/\s*px/, '');
60
+ const strippedHeight = isNumber(height) ? height : height?.replace(/\s*px/, '');
61
+ const urlOrTitle = title || url;
62
+
63
+ return (
64
+ //@ts-ignore
65
+ // eslint-disable-next-line react/no-unknown-property
66
+ <figure className={classes} resizeiframe={`${resize}`}>
67
+ <iframe
68
+ ref={iframeRef}
69
+ title={urlOrTitle}
70
+ aria-label={urlOrTitle}
71
+ src={url}
72
+ width={strippedWidth}
73
+ height={strippedHeight}
74
+ // eslint-disable-next-line react/no-unknown-property
75
+ allowFullScreen
76
+ scrolling="no"
77
+ frameBorder="0"
78
+ loading="lazy"
79
+ />
80
+ </figure>
81
+ );
82
+ };
83
+
84
+ export default ExternalEmbed;
@@ -0,0 +1,314 @@
1
+ /**
2
+ * Copyright (c) 2023-present, NDLA.
3
+ *
4
+ * This source code is licensed under the GPLv3 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+
9
+ import isNumber from 'lodash/isNumber';
10
+ import styled from '@emotion/styled';
11
+ import { figureApa7CopyString, getGroupedContributorDescriptionList, getLicenseByAbbreviation } from '@ndla/licenses';
12
+ import { ImageEmbedData, ImageMetaData } from '@ndla/types-embed';
13
+ import { useTranslation } from 'react-i18next';
14
+ import { ModalV2 } from '@ndla/modal';
15
+ import { SafeLinkButton } from '@ndla/safelink';
16
+ import { MouseEventHandler, useState } from 'react';
17
+ import { ButtonV2, CopyButton } from '@ndla/button';
18
+ import { ExpandTwoArrows } from '@ndla/icons/action';
19
+ import { ArrowCollapse, ChevronDown, ChevronUp } from '@ndla/icons/common';
20
+ import { Figure, FigureCaption, FigureType } from '../Figure';
21
+ import Image, { ImageLink } from '../Image';
22
+ import { FigureLicenseDialogContent } from '../Figure/FigureLicenseDialogContent';
23
+ import { Copyright } from '../types';
24
+
25
+ interface Props {
26
+ embed: ImageMetaData;
27
+ articlePath?: string;
28
+ previewAlt?: boolean;
29
+ }
30
+
31
+ export interface Author {
32
+ name: string;
33
+ type: string;
34
+ }
35
+
36
+ export const getLicenseCredits = (copyright?: {
37
+ creators?: Author[];
38
+ rightsholders?: Author[];
39
+ processors?: Author[];
40
+ }) => {
41
+ return {
42
+ creators: copyright?.creators ?? [],
43
+ rightsholders: copyright?.rightsholders ?? [],
44
+ processors: copyright?.processors ?? [],
45
+ };
46
+ };
47
+
48
+ export const errorSvgSrc = `data:image/svg+xml;charset=UTF-8,%3Csvg fill='%238A8888' height='400' viewBox='0 0 24 12' width='100%25' xmlns='http://www.w3.org/2000/svg' style='background-color: %23EFF0F2'%3E%3Cpath d='M0 0h24v24H0V0z' fill='none'/%3E%3Cpath transform='scale(0.3) translate(28, 8.5)' d='M11 15h2v2h-2zm0-8h2v6h-2zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z'/%3E%3C/svg%3E`;
49
+ const isSmall = (size?: string): size is 'xsmall' | 'small' => size === 'xsmall' || size === 'small';
50
+
51
+ const isAlign = (align?: string): align is 'left' | 'right' => align === 'left' || align === 'right';
52
+
53
+ const getFigureType = (size?: string, align?: string): FigureType => {
54
+ if (size && isSmall(size) && align && isAlign(align)) {
55
+ return `${size}-${align}`;
56
+ }
57
+ if (size && isSmall(size) && !align) {
58
+ return size as FigureType;
59
+ }
60
+ if (align && isAlign(align)) {
61
+ return align;
62
+ }
63
+ return 'full';
64
+ };
65
+
66
+ const getSizes = (size?: string, align?: string) => {
67
+ if (align && size === 'full') {
68
+ return '(min-width: 1024px) 512px, (min-width: 768px) 350px, 100vw';
69
+ }
70
+ if (align && size === 'small') {
71
+ return '(min-width: 1024px) 350px, (min-width: 768px) 180px, 100vw';
72
+ }
73
+ if (align && size === 'xsmall') {
74
+ return '(min-width: 1024px) 180px, (min-width: 768px) 180px, 100vw';
75
+ }
76
+ return '(min-width: 1024px) 1024px, 100vw';
77
+ };
78
+
79
+ const getFocalPoint = (data: ImageEmbedData) => {
80
+ if (isNumber(data.focalX) && isNumber(data.focalY)) {
81
+ return { x: data.focalX, y: data.focalY };
82
+ }
83
+ return undefined;
84
+ };
85
+
86
+ const getCrop = (data: ImageEmbedData) => {
87
+ if (
88
+ isNumber(data.lowerRightX) &&
89
+ isNumber(data.lowerRightY) &&
90
+ isNumber(data.upperLeftX) &&
91
+ isNumber(data.upperLeftY)
92
+ ) {
93
+ return {
94
+ startX: data.lowerRightX,
95
+ startY: data.lowerRightY,
96
+ endX: data.upperLeftX,
97
+ endY: data.upperLeftY,
98
+ };
99
+ }
100
+ return undefined;
101
+ };
102
+
103
+ const StyledSpan = styled.span`
104
+ font-style: italic;
105
+ color: grey;
106
+ `;
107
+
108
+ const expandedSizes = '(min-width: 1024px) 1024px, 100vw';
109
+
110
+ const ImageEmbed = ({ embed, articlePath, previewAlt }: Props) => {
111
+ const [isOpen, setIsOpen] = useState(false);
112
+ const [isBylineHidden, setIsBylineHidden] = useState(hideByline(embed.embedData.size));
113
+ const [imageSizes, setImageSizes] = useState<string | undefined>(undefined);
114
+ const { t, i18n } = useTranslation();
115
+ if (embed.status === 'error') {
116
+ const { align, size } = embed.embedData;
117
+ const figureType = getFigureType(size, align);
118
+ return (
119
+ <Figure type={figureType}>
120
+ <div className="c-figure__img">
121
+ <img alt={t('image.error.url')} src={errorSvgSrc} />
122
+ </div>
123
+ <figcaption>{t('image.error.caption')}</figcaption>
124
+ </Figure>
125
+ );
126
+ }
127
+
128
+ const { data, embedData, seq } = embed;
129
+
130
+ const authors = getLicenseCredits(data.copyright);
131
+
132
+ const altText = embedData.alt || '';
133
+ const caption = embedData.caption || '';
134
+ const license = getLicenseByAbbreviation(data.copyright.license.license, i18n.language);
135
+
136
+ const figureType = getFigureType(embedData.size, embedData.align);
137
+ const sizes = getSizes(embedData.size, embedData.align);
138
+
139
+ const focalPoint = getFocalPoint(embedData);
140
+ const crop = getCrop(embedData);
141
+
142
+ const contributors = getGroupedContributorDescriptionList(data.copyright, i18n.language).map((item) => ({
143
+ name: item.description,
144
+ type: item.label,
145
+ }));
146
+
147
+ const figureId = `figure-${seq}-${data.id}`;
148
+
149
+ const { creators, rightsholders, processors } = authors;
150
+ const captionAuthors = creators.length || rightsholders.length ? [...creators, ...rightsholders] : processors;
151
+ return (
152
+ <Figure
153
+ id={figureId}
154
+ type={imageSizes ? undefined : figureType}
155
+ className={imageSizes ? 'c-figure--right expanded' : ''}>
156
+ <ImageWrapper src={data.imageUrl} crop={crop} size={embedData.size}>
157
+ <Image
158
+ focalPoint={focalPoint}
159
+ contentType={data.contentType}
160
+ crop={crop}
161
+ sizes={imageSizes ?? sizes}
162
+ alt={altText}
163
+ src={data.imageUrl}
164
+ expandButton={
165
+ <ExpandButton
166
+ size={embedData.size}
167
+ expanded={!!imageSizes}
168
+ bylineHidden={isBylineHidden}
169
+ onExpand={() => setImageSizes((p) => (p ? undefined : expandedSizes))}
170
+ onHideByline={() => setIsBylineHidden((p) => !p)}
171
+ />
172
+ }
173
+ />
174
+ </ImageWrapper>
175
+ {previewAlt ? <StyledSpan>{`Alt: ${embedData.alt}`}</StyledSpan> : null}
176
+ <FigureCaption
177
+ hideFigcaption={isSmall(embedData.size) || isBylineHidden}
178
+ figureId={figureId}
179
+ id={figureId}
180
+ caption={caption}
181
+ reuseLabel={t('image.reuse')}
182
+ modalButton={
183
+ <ButtonV2 shape="pill" variant="outline" size="small" onClick={() => setIsOpen(true)}>
184
+ {t('image.reuse')}
185
+ </ButtonV2>
186
+ }
187
+ licenseRights={license.rights}
188
+ authors={captionAuthors}
189
+ locale={i18n.language}>
190
+ <ModalV2 controlled isOpen={isOpen} onClose={() => setIsOpen(false)} labelledBy="license-dialog-rules-heading">
191
+ {(close) => (
192
+ <FigureLicenseDialogContent
193
+ title={data.title.title}
194
+ license={license}
195
+ onClose={close}
196
+ authors={contributors}
197
+ origin={data.copyright.origin}
198
+ locale={i18n.language}
199
+ type="image">
200
+ <ImageLicenseButtons
201
+ articlePath={articlePath}
202
+ title={data.title.title}
203
+ imageUrl={data.imageUrl}
204
+ copyright={data.copyright}
205
+ />
206
+ </FigureLicenseDialogContent>
207
+ )}
208
+ </ModalV2>
209
+ </FigureCaption>
210
+ </Figure>
211
+ );
212
+ };
213
+
214
+ interface ImageWrapperProps {
215
+ src: string;
216
+ children: React.ReactNode;
217
+ crop?: {
218
+ startX: number;
219
+ startY: number;
220
+ endX: number;
221
+ endY: number;
222
+ };
223
+ size?: string;
224
+ }
225
+ const hideByline = (size?: string): boolean => {
226
+ return !!size && size.endsWith('-hide-byline');
227
+ };
228
+
229
+ interface ImageLicenseButtonsProps {
230
+ imageUrl: string;
231
+ title?: string;
232
+ articlePath?: string;
233
+ copyright?: Partial<Copyright>;
234
+ }
235
+
236
+ export const ImageLicenseButtons = ({ imageUrl, title, articlePath, copyright }: ImageLicenseButtonsProps) => {
237
+ const { t, i18n } = useTranslation();
238
+ if (!copyright?.license?.license || copyright?.license?.license === 'COPYRIGHTED') return null;
239
+
240
+ const copyString = figureApa7CopyString(
241
+ title,
242
+ undefined,
243
+ imageUrl,
244
+ articlePath,
245
+ copyright,
246
+ copyright?.license?.license,
247
+ '',
248
+ t,
249
+ i18n.language,
250
+ );
251
+
252
+ return (
253
+ <>
254
+ <CopyButton
255
+ variant="outline"
256
+ onClick={() => navigator.clipboard.writeText(copyString)}
257
+ copyNode={t('license.hasCopiedTitle')}
258
+ aria-live="assertive">
259
+ {t('license.copyTitle')}
260
+ </CopyButton>
261
+ <SafeLinkButton to={`${imageUrl}?download=true`} download variant="outline">
262
+ {t('image.download')}
263
+ </SafeLinkButton>
264
+ </>
265
+ );
266
+ };
267
+
268
+ const ImageWrapper = ({ src, crop, size, children }: ImageWrapperProps) => {
269
+ const { t } = useTranslation();
270
+ if (isSmall(size) || hideByline(size)) {
271
+ return <>{children}</>;
272
+ }
273
+
274
+ return (
275
+ <ImageLink src={src} crop={crop} aria-label={t('license.images.itemImage.ariaLabel')}>
276
+ {children}
277
+ </ImageLink>
278
+ );
279
+ };
280
+
281
+ interface ExpandButtonProps {
282
+ size?: string;
283
+ expanded: boolean;
284
+ bylineHidden: boolean;
285
+ onExpand: MouseEventHandler<HTMLButtonElement>;
286
+ onHideByline: MouseEventHandler<HTMLButtonElement>;
287
+ }
288
+
289
+ const ExpandButton = ({ size, expanded, bylineHidden, onExpand, onHideByline }: ExpandButtonProps) => {
290
+ const { t } = useTranslation();
291
+ if (isSmall(size)) {
292
+ return (
293
+ <button
294
+ type="button"
295
+ className="c-figure__fullscreen-btn"
296
+ aria-label={t(`license.images.itemImage.zoom${expanded ? 'Out' : ''}ImageButtonLabel`)}
297
+ onClick={onExpand}>
298
+ {expanded ? <ArrowCollapse /> : <ExpandTwoArrows />}
299
+ </button>
300
+ );
301
+ } else if (hideByline(size)) {
302
+ return (
303
+ <button
304
+ type="button"
305
+ className="c-figure__show-byline-btn"
306
+ aria-label={t(`license.images.itemImage.${bylineHidden ? 'expandByline' : 'minimizeByline'}`)}
307
+ onClick={onHideByline}>
308
+ {bylineHidden ? <ChevronDown /> : <ChevronUp />}
309
+ </button>
310
+ );
311
+ } else return null;
312
+ };
313
+
314
+ export default ImageEmbed;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Copyright (c) 2023-present, NDLA.
3
+ *
4
+ * This source code is licensed under the GPLv3 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+
9
+ import { RelatedContentMetaData } from '@ndla/types-embed';
10
+ import { useTranslation } from 'react-i18next';
11
+ import { contentTypeMapping } from '../model/ContentType';
12
+ import { RelatedArticleV2 } from '../RelatedArticleList/RelatedArticleV2';
13
+
14
+ interface Props {
15
+ embed: RelatedContentMetaData;
16
+ isOembed?: boolean;
17
+ subject?: string;
18
+ ndlaFrontendDomain?: string;
19
+ }
20
+
21
+ const RelatedContentEmbed = ({ embed, isOembed, subject, ndlaFrontendDomain }: Props) => {
22
+ const { t } = useTranslation();
23
+ if (embed.status === 'error') {
24
+ return <></>;
25
+ }
26
+
27
+ const { data, embedData } = embed;
28
+
29
+ if (embedData.articleId && data) {
30
+ const typeId = data.resource?.resourceTypes.find((rt) => contentTypeMapping[rt.id])?.id;
31
+ const type = typeId ? contentTypeMapping[typeId] : undefined;
32
+ const path =
33
+ data.resource?.paths.find((p) => p.split('/')[1] === subject?.replace('urn:', '')) ?? data.resource?.path;
34
+ return (
35
+ <RelatedArticleV2
36
+ title={data.article.title.title ?? ''}
37
+ introduction={data.article.metaDescription?.metaDescription ?? ''}
38
+ target={isOembed ? '_blank' : undefined}
39
+ to={`${ndlaFrontendDomain ?? ''}${path ?? ''}`}
40
+ type={type}
41
+ />
42
+ );
43
+ } else if (typeof embedData.url === 'string') {
44
+ return (
45
+ <RelatedArticleV2
46
+ title={embedData.title ?? ''}
47
+ introduction=""
48
+ to={embedData.url}
49
+ target="_blank"
50
+ type={'external-learning-resources'}
51
+ linkInfo={`${t('related.linkInfo')} ${
52
+ // Get domain name only from url
53
+ embedData.url.match(/^(?:https?:\/\/)?(?:[^@\n]+@)?(?:www\.)?([^:/\n]+)/im)?.[1] || embedData.url
54
+ }`}
55
+ />
56
+ );
57
+ } else {
58
+ return <></>;
59
+ }
60
+ };
61
+
62
+ export default RelatedContentEmbed;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Copyright (c) 2023-present, NDLA.
3
+ *
4
+ * This source code is licensed under the GPLv3 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+
9
+ import { useTranslation } from 'react-i18next';
10
+ import styled from '@emotion/styled';
11
+ import { colors } from '@ndla/core';
12
+ import { MetaData } from '@ndla/types-embed';
13
+
14
+ interface Props {
15
+ embed: MetaData<any, any>;
16
+ }
17
+
18
+ const StyledSpan = styled.span`
19
+ color: ${colors.support.red};
20
+ `;
21
+
22
+ const UnknownEmbed = ({ embed }: Props) => {
23
+ const { t } = useTranslation();
24
+ return <StyledSpan>{t('embed.unsupported', { type: embed.resource })}</StyledSpan>;
25
+ };
26
+
27
+ export default UnknownEmbed;