@ndla/ui 36.0.2 → 37.0.1

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 (174) hide show
  1. package/es/Article/Article.js +7 -13
  2. package/es/Article/ArticleByline.js +79 -123
  3. package/es/Article/ArticleFootNotes.js +16 -11
  4. package/es/AudioPlayer/AudioPlayer.js +33 -35
  5. package/es/AudioPlayer/initAudioPlayers.js +6 -1
  6. package/es/ContentTypeBadge/ContentTypeBadge.js +27 -6
  7. package/es/Embed/AudioEmbed.js +44 -188
  8. package/es/Embed/BrightcoveEmbed.js +32 -127
  9. package/es/Embed/ConceptEmbed.js +53 -75
  10. package/es/Embed/EmbedErrorPlaceholder.js +41 -0
  11. package/es/Embed/ExternalEmbed.js +5 -12
  12. package/es/Embed/H5pEmbed.js +5 -15
  13. package/es/Embed/IframeEmbed.js +4 -4
  14. package/es/Embed/ImageEmbed.js +41 -153
  15. package/es/Embed/RelatedContentEmbed.js +3 -3
  16. package/es/Embed/conceptComponents.js +62 -228
  17. package/es/Embed/types.js +1 -0
  18. package/es/KeyFigure/KeyFigure.js +57 -0
  19. package/es/{KeyPerformanceIndicator → KeyFigure}/index.js +1 -1
  20. package/es/LicenseByline/EmbedByline.js +33 -8
  21. package/es/LicenseByline/LicenseDescription.js +16 -14
  22. package/es/List/OrderedList.js +48 -0
  23. package/es/List/UnOrderedList.js +36 -0
  24. package/es/List/index.js +10 -0
  25. package/es/Navigation/NavigationBox.js +41 -48
  26. package/es/Navigation/NavigationHeading.js +18 -29
  27. package/es/Notion/Notion.js +5 -5
  28. package/es/Resource/ListResource.js +9 -9
  29. package/es/Resource/resourceComponents.js +12 -11
  30. package/es/Typography/Heading.js +38 -0
  31. package/es/Typography/index.js +9 -0
  32. package/es/all.css +1 -1
  33. package/es/index.js +4 -2
  34. package/es/locale/messages-en.js +6 -2
  35. package/es/locale/messages-nb.js +6 -2
  36. package/es/locale/messages-nn.js +6 -2
  37. package/es/locale/messages-se.js +6 -2
  38. package/es/locale/messages-sma.js +6 -2
  39. package/es/model/ContentType.js +7 -1
  40. package/lib/Article/Article.d.ts +1 -3
  41. package/lib/Article/Article.js +7 -13
  42. package/lib/Article/ArticleByline.d.ts +3 -5
  43. package/lib/Article/ArticleByline.js +83 -126
  44. package/lib/Article/ArticleFootNotes.js +16 -11
  45. package/lib/AudioPlayer/AudioPlayer.d.ts +1 -2
  46. package/lib/AudioPlayer/AudioPlayer.js +33 -36
  47. package/lib/AudioPlayer/initAudioPlayers.d.ts +1 -0
  48. package/lib/AudioPlayer/initAudioPlayers.js +9 -3
  49. package/lib/ContentTypeBadge/ContentTypeBadge.js +27 -6
  50. package/lib/Embed/AudioEmbed.d.ts +3 -2
  51. package/lib/Embed/AudioEmbed.js +53 -192
  52. package/lib/Embed/BrightcoveEmbed.d.ts +3 -1
  53. package/lib/Embed/BrightcoveEmbed.js +32 -126
  54. package/lib/Embed/ConceptEmbed.d.ts +7 -2
  55. package/lib/Embed/ConceptEmbed.js +51 -73
  56. package/lib/Embed/EmbedErrorPlaceholder.d.ts +17 -0
  57. package/lib/Embed/EmbedErrorPlaceholder.js +48 -0
  58. package/lib/Embed/ExternalEmbed.js +5 -11
  59. package/lib/Embed/H5pEmbed.js +5 -14
  60. package/lib/Embed/IframeEmbed.d.ts +2 -2
  61. package/lib/Embed/IframeEmbed.js +4 -4
  62. package/lib/Embed/ImageEmbed.d.ts +3 -10
  63. package/lib/Embed/ImageEmbed.js +48 -161
  64. package/lib/Embed/RelatedContentEmbed.js +3 -3
  65. package/lib/Embed/conceptComponents.d.ts +4 -2
  66. package/lib/Embed/conceptComponents.js +67 -231
  67. package/lib/Embed/index.d.ts +1 -0
  68. package/lib/Embed/types.d.ts +14 -0
  69. package/lib/Embed/types.js +5 -0
  70. package/lib/KeyFigure/KeyFigure.d.ts +10 -0
  71. package/lib/KeyFigure/KeyFigure.js +62 -0
  72. package/lib/KeyFigure/index.d.ts +1 -0
  73. package/lib/KeyFigure/index.js +13 -0
  74. package/lib/LicenseByline/EmbedByline.d.ts +10 -2
  75. package/lib/LicenseByline/EmbedByline.js +32 -7
  76. package/lib/LicenseByline/LicenseDescription.d.ts +3 -1
  77. package/lib/LicenseByline/LicenseDescription.js +14 -13
  78. package/lib/List/OrderedList.d.ts +15 -0
  79. package/lib/List/OrderedList.js +56 -0
  80. package/lib/List/UnOrderedList.d.ts +10 -0
  81. package/lib/List/UnOrderedList.js +43 -0
  82. package/lib/List/index.d.ts +9 -0
  83. package/lib/List/index.js +20 -0
  84. package/lib/Navigation/NavigationBox.js +40 -47
  85. package/lib/Navigation/NavigationHeading.js +17 -28
  86. package/lib/Notion/Notion.js +5 -5
  87. package/lib/Resource/ListResource.js +8 -8
  88. package/lib/Resource/resourceComponents.js +12 -11
  89. package/lib/Typography/Heading.d.ts +26 -0
  90. package/lib/Typography/Heading.js +45 -0
  91. package/lib/Typography/index.d.ts +8 -0
  92. package/lib/Typography/index.js +13 -0
  93. package/lib/all.css +1 -1
  94. package/lib/index.d.ts +4 -1
  95. package/lib/index.js +23 -3
  96. package/lib/locale/messages-en.d.ts +4 -0
  97. package/lib/locale/messages-en.js +6 -2
  98. package/lib/locale/messages-nb.d.ts +4 -0
  99. package/lib/locale/messages-nb.js +6 -2
  100. package/lib/locale/messages-nn.d.ts +4 -0
  101. package/lib/locale/messages-nn.js +6 -2
  102. package/lib/locale/messages-se.d.ts +4 -0
  103. package/lib/locale/messages-se.js +6 -2
  104. package/lib/locale/messages-sma.d.ts +4 -0
  105. package/lib/locale/messages-sma.js +6 -2
  106. package/lib/model/ContentType.d.ts +1 -0
  107. package/lib/model/ContentType.js +9 -2
  108. package/package.json +15 -15
  109. package/src/Article/Article.tsx +1 -8
  110. package/src/Article/ArticleByline.tsx +78 -127
  111. package/src/Article/ArticleFootNotes.tsx +33 -10
  112. package/src/Article/component.article.scss +1 -52
  113. package/src/Article/component.footnotes.scss +2 -2
  114. package/src/Aside/component.aside.scss +3 -3
  115. package/src/AudioPlayer/AudioPlayer.tsx +11 -24
  116. package/src/AudioPlayer/initAudioPlayers.tsx +7 -2
  117. package/src/ContentTypeBadge/ContentTypeBadge.tsx +29 -6
  118. package/src/ContentTypeBadge/component.content-type-badge.scss +9 -3
  119. package/src/Dialog/component.dialog.scss +4 -5
  120. package/src/Embed/AudioEmbed.stories.tsx +5 -3
  121. package/src/Embed/AudioEmbed.tsx +45 -192
  122. package/src/Embed/BrightcoveEmbed.stories.tsx +5 -1
  123. package/src/Embed/BrightcoveEmbed.tsx +24 -98
  124. package/src/Embed/ConceptEmbed.stories.tsx +5 -0
  125. package/src/Embed/ConceptEmbed.tsx +43 -54
  126. package/src/Embed/EmbedErrorPlaceholder.tsx +59 -0
  127. package/src/Embed/ExternalEmbed.stories.tsx +86 -0
  128. package/src/Embed/ExternalEmbed.tsx +3 -8
  129. package/src/Embed/H5pEmbed.stories.tsx +92 -0
  130. package/src/Embed/H5pEmbed.tsx +3 -11
  131. package/src/Embed/IframeEmbed.stories.tsx +130 -0
  132. package/src/Embed/IframeEmbed.tsx +3 -3
  133. package/src/Embed/ImageEmbed.stories.tsx +3 -1
  134. package/src/Embed/ImageEmbed.tsx +21 -116
  135. package/src/Embed/RelatedContentEmbed.tsx +3 -1
  136. package/src/Embed/conceptComponents.tsx +67 -257
  137. package/src/Embed/index.ts +1 -0
  138. package/src/Embed/types.ts +12 -0
  139. package/src/FactBox/component.factbox.scss +3 -3
  140. package/src/Figure/component.figure-license.scss +4 -4
  141. package/src/Figure/component.figure.scss +1 -1
  142. package/src/KeyFigure/KeyFigure.stories.tsx +36 -0
  143. package/src/{KeyPerformanceIndicator/KeyPerformanceIndicator.tsx → KeyFigure/KeyFigure.tsx} +9 -7
  144. package/src/{KeyPerformanceIndicator → KeyFigure}/index.ts +1 -1
  145. package/src/LicenseByline/EmbedByline.stories.tsx +1 -0
  146. package/src/LicenseByline/EmbedByline.tsx +57 -9
  147. package/src/LicenseByline/LicenseDescription.tsx +9 -3
  148. package/src/List/OrderedList.tsx +115 -0
  149. package/src/List/UnOrderedList.tsx +49 -0
  150. package/src/List/index.ts +10 -0
  151. package/src/MediaList/component.medialist.scss +2 -2
  152. package/src/Navigation/NavigationBox.tsx +10 -14
  153. package/src/Navigation/NavigationHeading.tsx +15 -24
  154. package/src/Notion/Notion.tsx +1 -1
  155. package/src/RelatedArticleList/component.related-articles.scss +3 -13
  156. package/src/Resource/ListResource.tsx +6 -2
  157. package/src/Resource/resourceComponents.tsx +4 -2
  158. package/src/Table/component.tables.scss +0 -46
  159. package/src/Translation/component.translation.scss +3 -5
  160. package/src/Typography/Heading.tsx +96 -0
  161. package/src/Typography/index.ts +9 -0
  162. package/src/index.ts +5 -1
  163. package/src/locale/messages-en.ts +4 -0
  164. package/src/locale/messages-nb.ts +4 -0
  165. package/src/locale/messages-nn.ts +4 -0
  166. package/src/locale/messages-se.ts +4 -0
  167. package/src/locale/messages-sma.ts +4 -0
  168. package/src/model/ContentType.ts +7 -0
  169. package/es/KeyPerformanceIndicator/KeyPerformanceIndicator.js +0 -57
  170. package/lib/KeyPerformanceIndicator/KeyPerformanceIndicator.d.ts +0 -8
  171. package/lib/KeyPerformanceIndicator/KeyPerformanceIndicator.js +0 -62
  172. package/lib/KeyPerformanceIndicator/index.d.ts +0 -1
  173. package/lib/KeyPerformanceIndicator/index.js +0 -13
  174. package/src/KeyPerformanceIndicator/KeyPerformanceIndicator.stories.tsx +0 -79
@@ -11,6 +11,7 @@ import { Meta, StoryObj } from '@storybook/react';
11
11
  import { AudioEmbedData, AudioMeta } from '@ndla/types-embed';
12
12
  import AudioEmbed from './AudioEmbed';
13
13
  import { defaultParameters } from '../../../../stories/defaults';
14
+ import StoryFavoriteButton from '../../../../stories/StoryFavoriteButton';
14
15
 
15
16
  const embedData: AudioEmbedData = {
16
17
  resource: 'audio',
@@ -156,9 +157,6 @@ const meta: Meta<typeof AudioEmbed> = {
156
157
  title: 'Enkle komponenter/Embeds/AudioEmbed',
157
158
  component: AudioEmbed,
158
159
  tags: ['autodocs'],
159
- args: {
160
- articlePath: undefined,
161
- },
162
160
  decorators: [
163
161
  (Story) => (
164
162
  <div className="o-wrapper">
@@ -179,6 +177,7 @@ export default meta;
179
177
 
180
178
  export const AudioEmbedStory: StoryObj<typeof AudioEmbed> = {
181
179
  args: {
180
+ heartButton: StoryFavoriteButton,
182
181
  embed: {
183
182
  resource: 'audio',
184
183
  status: 'success',
@@ -191,6 +190,7 @@ export const AudioEmbedStory: StoryObj<typeof AudioEmbed> = {
191
190
 
192
191
  export const AudioEmbedFailed: StoryObj<typeof AudioEmbed> = {
193
192
  args: {
193
+ heartButton: StoryFavoriteButton,
194
194
  embed: {
195
195
  resource: 'audio',
196
196
  status: 'error',
@@ -202,6 +202,7 @@ export const AudioEmbedFailed: StoryObj<typeof AudioEmbed> = {
202
202
 
203
203
  export const Podcast: StoryObj<typeof AudioEmbed> = {
204
204
  args: {
205
+ heartButton: StoryFavoriteButton,
205
206
  embed: {
206
207
  resource: 'audio',
207
208
  status: 'success',
@@ -214,6 +215,7 @@ export const Podcast: StoryObj<typeof AudioEmbed> = {
214
215
 
215
216
  export const PodcastFailed: StoryObj<typeof AudioEmbed> = {
216
217
  args: {
218
+ heartButton: StoryFavoriteButton,
217
219
  embed: {
218
220
  resource: 'audio',
219
221
  status: 'error',
@@ -6,30 +6,21 @@
6
6
  *
7
7
  */
8
8
 
9
- import { AudioMetaData } from '@ndla/types-embed';
10
- import { ICopyright } from '@ndla/types-backend/image-api';
11
- import {
12
- figureApa7CopyString,
13
- getGroupedContributorDescriptionList,
14
- getLicenseByAbbreviation,
15
- getLicenseCredits,
16
- } from '@ndla/licenses';
17
- import { ModalV2 } from '@ndla/modal';
18
- import { useState } from 'react';
19
- import { useTranslation } from 'react-i18next';
9
+ import { AudioMetaData, ImageMetaData } from '@ndla/types-embed';
20
10
  //@ts-ignore
21
11
  import { Remarkable } from 'remarkable';
22
- import { ButtonV2, CopyButton } from '@ndla/button';
23
- import { SafeLinkButton } from '@ndla/safelink';
24
12
  import AudioPlayer from '../AudioPlayer';
25
- import { Figure, FigureCaption } from '../Figure';
26
- import { FigureLicenseDialogContent } from '../Figure/FigureLicenseDialogContent';
13
+ import { Figure } from '../Figure';
27
14
  import { Author } from './ImageEmbed';
15
+ import { EmbedByline } from '../LicenseByline';
16
+ import EmbedErrorPlaceholder from './EmbedErrorPlaceholder';
17
+ import { HeartButtonType } from './types';
28
18
 
29
19
  interface Props {
30
20
  embed: AudioMetaData;
31
- articlePath?: string;
21
+ heartButton?: HeartButtonType;
32
22
  }
23
+
33
24
  export const getFirstNonEmptyLicenseCredits = (authors: {
34
25
  creators: Author[];
35
26
  rightsholders: Author[];
@@ -42,29 +33,25 @@ const renderMarkdown = (text: string) => {
42
33
  return <span dangerouslySetInnerHTML={{ __html: rendered }} />;
43
34
  };
44
35
 
45
- const AudioEmbed = ({ embed, articlePath }: Props) => {
46
- const { t, i18n } = useTranslation();
47
- const [isOpen, setIsOpen] = useState(false);
36
+ const imageMetaToMockEmbed = (
37
+ imageMeta: Extract<AudioMetaData, { status: 'success' }>,
38
+ ): Extract<ImageMetaData, { status: 'success' }> => ({
39
+ resource: 'image',
40
+ status: 'success',
41
+ // Make sure the seq is unused. It's rarely used, but it's nice to ensure uniqueness.
42
+ seq: imageMeta.seq + 0.1,
43
+ // We check that this exists where the function is used.
44
+ data: imageMeta.data.imageMeta!,
45
+ embedData: {
46
+ resource: 'image',
47
+ resourceId: imageMeta.data.imageMeta?.id?.toString() || '',
48
+ alt: imageMeta.data.imageMeta?.alttext.alttext ?? '',
49
+ },
50
+ });
51
+
52
+ const AudioEmbed = ({ embed, heartButton: HeartButton }: Props) => {
48
53
  if (embed.status === 'error') {
49
- return (
50
- <Figure>
51
- <svg
52
- fill="#8A8888"
53
- height="50"
54
- viewBox="0 0 24 12"
55
- width="100%"
56
- xmlns="http://www.w3.org/2000/svg"
57
- style={{ backgroundColor: '#EFF0F2' }}
58
- >
59
- <path d="M0 0h24v24H0V0z" fill="none" />
60
- <path
61
- transform="scale(0.3) translate(28, 8.5)"
62
- 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"
63
- />
64
- </svg>
65
- <figcaption>{t('audio.error.caption')}</figcaption>
66
- </Figure>
67
- );
54
+ return <EmbedErrorPlaceholder type={embed.embedData.type === 'standard' ? 'audio' : 'podcast'} />;
68
55
  }
69
56
 
70
57
  const { data, embedData, seq } = embed;
@@ -75,181 +62,47 @@ const AudioEmbed = ({ embed, articlePath }: Props) => {
75
62
 
76
63
  const subtitle = data.series ? { title: data.series.title.title, url: `/podkast/${data.series.id}` } : undefined;
77
64
 
78
- const textVersion = data.manuscript && renderMarkdown(data.manuscript.manuscript);
79
- const description = renderMarkdown(data.podcastMeta?.introduction ?? '');
65
+ const textVersion = data.manuscript?.manuscript.length ? renderMarkdown(data.manuscript.manuscript) : undefined;
80
66
 
81
67
  const coverPhoto = data.podcastMeta?.coverPhoto;
82
68
 
83
69
  const img = coverPhoto && { url: coverPhoto.url, alt: coverPhoto.altText };
84
70
 
85
- const authors = getLicenseCredits(data.copyright);
86
-
87
- const license = getLicenseByAbbreviation(data.copyright.license.license, i18n.language);
88
-
89
- const contributors = getGroupedContributorDescriptionList(data.copyright, i18n.language).map((item) => ({
90
- name: item.description,
91
- type: item.label,
92
- }));
93
-
94
71
  const figureId = `figure-${seq}-${data.id}`;
95
72
 
96
- const copyString =
97
- data.audioType === 'podcast'
98
- ? figureApa7CopyString(
99
- data.title.title,
100
- undefined,
101
- data.audioFile.url,
102
- articlePath,
103
- data.copyright,
104
- data.copyright.license.license,
105
- '',
106
- (id: string) => t(id),
107
- i18n.language,
108
- )
109
- : undefined;
110
- const captionAuthors = getFirstNonEmptyLicenseCredits(authors);
111
73
  return (
112
74
  <Figure id={figureId} type="full">
113
75
  <AudioPlayer
114
- description={description}
76
+ description={data.podcastMeta?.introduction ?? ''}
115
77
  img={img}
116
78
  src={data.audioFile.url}
117
79
  textVersion={textVersion}
118
80
  title={data.title.title}
119
81
  subtitle={subtitle}
120
82
  />
121
- <FigureCaption
122
- id=""
123
- figureId=""
124
- modalButton={
125
- <ButtonV2 variant="outline" shape="pill" size="small" onClick={() => setIsOpen(true)}>
126
- {t('audio.reuse')}
127
- </ButtonV2>
128
- }
129
- licenseRights={license.rights}
130
- authors={captionAuthors}
131
- locale={i18n.language}
132
- />
133
- <ModalV2 controlled isOpen={isOpen} onClose={() => setIsOpen(false)} labelledBy="license-dialog-rules-heading">
134
- {(close) => (
135
- <FigureLicenseDialogContent
136
- onClose={close}
137
- title={data.title.title}
138
- license={license}
139
- authors={contributors}
140
- origin={data.copyright.origin}
141
- locale={i18n.language}
142
- type="audio"
143
- >
144
- {data.copyright.license.license !== 'COPYRIGHT' && (
145
- <>
146
- {copyString && (
147
- <CopyButton
148
- variant="outline"
149
- copyNode={t('license.hasCopiedTitle')}
150
- onClick={() => navigator.clipboard.writeText(copyString)}
151
- >
152
- {t('license.copyTitle')}
153
- </CopyButton>
154
- )}
155
- <SafeLinkButton download to={data.audioFile.url} variant="outline">
156
- {t('audio.download')}
157
- </SafeLinkButton>
158
- </>
159
- )}
160
- </FigureLicenseDialogContent>
161
- )}
162
- </ModalV2>
83
+ <EmbedByline
84
+ error={false}
85
+ type={embedData.type === 'standard' ? 'audio' : 'podcast'}
86
+ topRounded={false}
87
+ bottomRounded={!data.imageMeta}
88
+ copyright={embed.data.copyright}
89
+ >
90
+ {HeartButton && <HeartButton embed={embed} />}
91
+ </EmbedByline>
163
92
  {data.imageMeta && (
164
- <ImageLicense
165
- title={data.imageMeta.title.title}
166
- imageUrl={data.imageMeta.imageUrl}
93
+ <EmbedByline
94
+ error={false}
95
+ first={false}
96
+ type="image"
97
+ topRounded={false}
98
+ bottomRounded
167
99
  copyright={data.imageMeta.copyright}
168
- articlePath={articlePath}
169
- />
100
+ >
101
+ {HeartButton && <HeartButton embed={imageMetaToMockEmbed(embed)} />}
102
+ </EmbedByline>
170
103
  )}
171
104
  </Figure>
172
105
  );
173
106
  };
174
107
 
175
- interface ImageLicenseProps {
176
- title: string;
177
- imageUrl: string;
178
- copyright: ICopyright;
179
- articlePath?: string;
180
- }
181
-
182
- const ImageLicense = ({ articlePath, title, imageUrl, copyright }: ImageLicenseProps) => {
183
- const { t, i18n } = useTranslation();
184
- const [isOpen, setIsOpen] = useState(false);
185
- const copyString = figureApa7CopyString(
186
- title,
187
- undefined,
188
- imageUrl,
189
- articlePath,
190
- copyright,
191
- copyright.license.license,
192
- undefined,
193
- (id: string) => t(id),
194
- i18n.language,
195
- );
196
- const license = getLicenseByAbbreviation(copyright.license.license, i18n.language);
197
- const authors = getLicenseCredits(copyright);
198
-
199
- const contributors = getGroupedContributorDescriptionList(copyright, i18n.language).map((item) => ({
200
- name: item.description,
201
- type: item.label,
202
- }));
203
-
204
- const captionAuthors = getFirstNonEmptyLicenseCredits(authors);
205
-
206
- return (
207
- <>
208
- <FigureCaption
209
- figureId=""
210
- id=""
211
- licenseRights={license.rights}
212
- modalButton={
213
- <ButtonV2 variant="outline" shape="pill" size="small" onClick={() => setIsOpen(true)}>
214
- {t('image.reuse')}
215
- </ButtonV2>
216
- }
217
- authors={captionAuthors}
218
- locale={i18n.language}
219
- >
220
- <ModalV2 controlled isOpen={isOpen} onClose={() => setIsOpen(false)}>
221
- {(close) => (
222
- <FigureLicenseDialogContent
223
- onClose={close}
224
- title={title}
225
- license={license}
226
- authors={contributors}
227
- origin={copyright.origin}
228
- locale={i18n.language}
229
- type="image"
230
- >
231
- {copyright.license.license !== 'COPYRIGHTED' && (
232
- <>
233
- {copyString && (
234
- <CopyButton
235
- variant="outline"
236
- copyNode={t('license.hasCopiedTitle')}
237
- onClick={() => navigator.clipboard.writeText(copyString)}
238
- >
239
- {t('license.copyTitle')}
240
- </CopyButton>
241
- )}
242
- <SafeLinkButton download to={imageUrl} variant="outline">
243
- {t('image.download')}
244
- </SafeLinkButton>
245
- </>
246
- )}
247
- </FigureLicenseDialogContent>
248
- )}
249
- </ModalV2>
250
- </FigureCaption>
251
- </>
252
- );
253
- };
254
-
255
108
  export default AudioEmbed;
@@ -8,9 +8,10 @@
8
8
 
9
9
  import React from 'react';
10
10
  import { Meta, StoryObj } from '@storybook/react';
11
- import { BrightcoveData, BrightcoveEmbedData, BrightcoveMetaData } from '@ndla/types-embed';
11
+ import { BrightcoveData, BrightcoveEmbedData, BrightcoveMetaData, EmbedMetaData } from '@ndla/types-embed';
12
12
  import BrightcoveEmbed from './BrightcoveEmbed';
13
13
  import { defaultParameters } from '../../../../stories/defaults';
14
+ import StoryFavoriteButton from '../../../../stories/StoryFavoriteButton';
14
15
 
15
16
  const embedData: BrightcoveEmbedData = {
16
17
  resource: 'brightcove',
@@ -179,6 +180,7 @@ export default meta;
179
180
 
180
181
  export const BrightcoveEmbedStory: StoryObj<typeof BrightcoveEmbed> = {
181
182
  args: {
183
+ heartButton: StoryFavoriteButton,
182
184
  embed: {
183
185
  resource: 'brightcove',
184
186
  status: 'success',
@@ -191,12 +193,14 @@ export const BrightcoveEmbedStory: StoryObj<typeof BrightcoveEmbed> = {
191
193
 
192
194
  export const VisuallyInterpreted: StoryObj<typeof BrightcoveEmbed> = {
193
195
  args: {
196
+ heartButton: StoryFavoriteButton,
194
197
  embed: visuallyInterpretedEmbedMetaData,
195
198
  },
196
199
  };
197
200
 
198
201
  export const BrightcoveFailed: StoryObj<typeof BrightcoveEmbed> = {
199
202
  args: {
203
+ heartButton: StoryFavoriteButton,
200
204
  embed: {
201
205
  resource: 'brightcove',
202
206
  status: 'error',
@@ -10,20 +10,19 @@ import sortBy from 'lodash/sortBy';
10
10
  import isNumber from 'lodash/isNumber';
11
11
  import styled from '@emotion/styled';
12
12
  import { spacing } from '@ndla/core';
13
- import { getGroupedContributorDescriptionList, getLicenseByAbbreviation } from '@ndla/licenses';
14
13
  import { useEffect, useRef, useState } from 'react';
15
- import { ModalV2 } from '@ndla/modal';
16
- import { SafeLinkButton } from '@ndla/safelink';
17
14
  import { BrightcoveEmbedData, BrightcoveMetaData, BrightcoveVideoSource } from '@ndla/types-embed';
18
15
  import { useTranslation } from 'react-i18next';
19
- import { ButtonV2, CopyButton } from '@ndla/button';
20
- import { Figure, FigureCaption } from '../Figure';
21
- import { FigureLicenseDialogContent } from '../Figure/FigureLicenseDialogContent';
22
- import { getFirstNonEmptyLicenseCredits } from './AudioEmbed';
16
+ import { ButtonV2 } from '@ndla/button';
17
+ import { Figure } from '../Figure';
18
+ import { EmbedByline } from '../LicenseByline';
19
+ import EmbedErrorPlaceholder from './EmbedErrorPlaceholder';
20
+ import { HeartButtonType } from './types';
23
21
 
24
22
  interface Props {
25
23
  embed: BrightcoveMetaData;
26
24
  isConcept?: boolean;
25
+ heartButton?: HeartButtonType;
27
26
  }
28
27
 
29
28
  const LinkedVideoButton = styled(ButtonV2)`
@@ -57,12 +56,12 @@ const getIframeProps = (data: BrightcoveEmbedData, sources: BrightcoveVideoSourc
57
56
  width: source?.width ?? '640',
58
57
  };
59
58
  };
60
- const BrightcoveEmbed = ({ embed, isConcept }: Props) => {
61
- const [isOpen, setIsOpen] = useState(false);
59
+ const BrightcoveEmbed = ({ embed, isConcept, heartButton: HeartButton }: Props) => {
62
60
  const [showOriginalVideo, setShowOriginalVideo] = useState(true);
63
- const { t, i18n } = useTranslation();
61
+ const { t } = useTranslation();
64
62
  const iframeRef = useRef<HTMLIFrameElement>(null);
65
63
  const { embedData } = embed;
64
+ const fallbackTitle = `${t('embed.type.video')}: ${embedData.videoid}`;
66
65
 
67
66
  useEffect(() => {
68
67
  const iframe = iframeRef.current;
@@ -75,44 +74,27 @@ const BrightcoveEmbed = ({ embed, isConcept }: Props) => {
75
74
  }, []);
76
75
  if (embed.status === 'error') {
77
76
  return (
78
- <Figure type={isConcept ? 'full-column' : 'full'} resizeIframe>
77
+ <EmbedErrorPlaceholder type="video">
79
78
  <BrightcoveIframe
80
79
  ref={iframeRef}
81
- title={`Video: ${embedData.videoid ?? ''}`}
82
- aria-label={`Video: ${embedData.videoid ?? ''}`}
80
+ title={embedData.alt ?? fallbackTitle}
81
+ aria-label={embedData.alt ?? fallbackTitle}
83
82
  frameBorder="0"
84
83
  {...getIframeProps(embedData, [])}
85
84
  allowFullScreen
86
85
  />
87
- <figcaption>{t('video.error')}</figcaption>
88
- </Figure>
86
+ </EmbedErrorPlaceholder>
89
87
  );
90
88
  }
91
89
  const { data, seq } = embed;
92
90
 
93
91
  const linkedVideoId = isNumeric(data.link?.text) ? data.link?.text : undefined;
94
92
 
95
- const license = getLicenseByAbbreviation(data.copyright?.license.license ?? '', i18n.language);
96
- const contributors = data.copyright
97
- ? getGroupedContributorDescriptionList(data.copyright, i18n.language).map((item) => ({
98
- name: item.description,
99
- type: item.label,
100
- }))
101
- : [];
102
-
103
- const { rightsholders = [], creators = [], processors = [] } = data.copyright ?? {};
104
-
105
- const download = sortBy(
106
- data.sources.filter((src) => src.container === 'MP4' && src.src),
107
- (src) => src.size,
108
- )?.[0]?.src;
109
-
110
93
  const figureId = `figure-${seq}-${data.id}`;
111
94
  const originalVideoProps = getIframeProps(embedData, data.sources);
112
95
  const alternativeVideoProps = linkedVideoId
113
96
  ? getIframeProps({ ...embedData, videoid: linkedVideoId }, data.sources)
114
97
  : undefined;
115
- const captionAuthors = getFirstNonEmptyLicenseCredits({ rightsholders, creators, processors });
116
98
 
117
99
  return (
118
100
  <Figure id={figureId} type={isConcept ? 'full-column' : 'full'} resizeIframe>
@@ -120,24 +102,20 @@ const BrightcoveEmbed = ({ embed, isConcept }: Props) => {
120
102
  <BrightcoveIframe
121
103
  ref={iframeRef}
122
104
  className="original"
123
- title={`Video: ${data.name}`}
124
- aria-label={`Video: ${data.name}`}
105
+ title={embedData.alt ?? data.name ?? fallbackTitle}
106
+ aria-label={embedData.alt ?? data.name ?? fallbackTitle}
125
107
  frameBorder="0"
126
108
  {...(alternativeVideoProps && !showOriginalVideo ? alternativeVideoProps : originalVideoProps)}
127
109
  allowFullScreen
128
110
  />
129
111
  </div>
130
- <FigureCaption
131
- figureId={figureId}
132
- id={data.id}
133
- locale={i18n.language}
134
- caption={embedData.caption ?? ''}
135
- modalButton={
136
- <ButtonV2 variant="outline" shape="pill" size="small" onClick={() => setIsOpen(true)}>
137
- {t('video.reuse')}
138
- </ButtonV2>
139
- }
140
- linkedVideoButton={
112
+ <EmbedByline
113
+ type="video"
114
+ copyright={data.copyright!}
115
+ description={embedData.caption ?? data.description ?? ''}
116
+ bottomRounded
117
+ >
118
+ {!!linkedVideoId && (
141
119
  <LinkedVideoButton
142
120
  variant="outline"
143
121
  shape="pill"
@@ -146,63 +124,11 @@ const BrightcoveEmbed = ({ embed, isConcept }: Props) => {
146
124
  >
147
125
  {t(`figure.button.${!showOriginalVideo ? 'original' : 'alternative'}`)}
148
126
  </LinkedVideoButton>
149
- }
150
- licenseRights={license.rights}
151
- authors={captionAuthors}
152
- hasLinkedVideo={!!linkedVideoId}
153
- />
154
- <ModalV2 controlled isOpen={isOpen} onClose={() => setIsOpen(false)} labelledBy="license-dialog-rules-heading">
155
- {(close) => (
156
- <FigureLicenseDialogContent
157
- onClose={close}
158
- title={data.name}
159
- locale={i18n.language}
160
- license={license}
161
- authors={contributors}
162
- type="video"
163
- >
164
- <VideoLicenseButtons
165
- download={download}
166
- licenseCode={data.copyright?.license.license}
167
- src={originalVideoProps.src}
168
- width={originalVideoProps.width}
169
- height={originalVideoProps.height}
170
- name={data.name}
171
- />
172
- </FigureLicenseDialogContent>
173
127
  )}
174
- </ModalV2>
128
+ {HeartButton && <HeartButton embed={embed} />}
129
+ </EmbedByline>
175
130
  </Figure>
176
131
  );
177
132
  };
178
133
 
179
- interface VideoLicenseButtonsProps {
180
- download: string;
181
- licenseCode?: string;
182
- src: string;
183
- width: string | number;
184
- height: string | number;
185
- name?: string;
186
- }
187
-
188
- const VideoLicenseButtons = ({ download, src, width, height, name, licenseCode }: VideoLicenseButtonsProps) => {
189
- const { t } = useTranslation();
190
- return (
191
- <>
192
- {licenseCode !== 'COPYRIGHTED' && (
193
- <SafeLinkButton key="download" to={download} variant="outline" download>
194
- {t('video.download')}
195
- </SafeLinkButton>
196
- )}
197
- <CopyButton
198
- variant="outline"
199
- copyNode={t('license.hasCopiedTitle')}
200
- onClick={() => navigator.clipboard.writeText(makeIframeString(src, width, height, name))}
201
- >
202
- {t('license.embed')}
203
- </CopyButton>
204
- </>
205
- );
206
- };
207
-
208
134
  export default BrightcoveEmbed;
@@ -11,6 +11,7 @@ import { Meta, StoryObj } from '@storybook/react';
11
11
  import { ConceptData, ConceptEmbedData } from '@ndla/types-embed';
12
12
  import ConceptEmbed from './ConceptEmbed';
13
13
  import { defaultParameters } from '../../../../stories/defaults';
14
+ import StoryFavoriteButton from '../../../../stories/StoryFavoriteButton';
14
15
 
15
16
  const blockEmbedData: ConceptEmbedData = {
16
17
  contentId: '35',
@@ -145,6 +146,7 @@ export default meta;
145
146
 
146
147
  export const Block: StoryObj<typeof ConceptEmbed> = {
147
148
  args: {
149
+ heartButton: StoryFavoriteButton,
148
150
  embed: {
149
151
  resource: 'concept',
150
152
  status: 'success',
@@ -157,6 +159,7 @@ export const Block: StoryObj<typeof ConceptEmbed> = {
157
159
 
158
160
  export const BlockFailed: StoryObj<typeof ConceptEmbed> = {
159
161
  args: {
162
+ heartButton: StoryFavoriteButton,
160
163
  embed: {
161
164
  resource: 'concept',
162
165
  status: 'error',
@@ -168,6 +171,7 @@ export const BlockFailed: StoryObj<typeof ConceptEmbed> = {
168
171
 
169
172
  export const Inline: StoryObj<typeof ConceptEmbed> = {
170
173
  args: {
174
+ heartButton: StoryFavoriteButton,
171
175
  embed: {
172
176
  resource: 'concept',
173
177
  status: 'success',
@@ -180,6 +184,7 @@ export const Inline: StoryObj<typeof ConceptEmbed> = {
180
184
 
181
185
  export const InlineFailed: StoryObj<typeof ConceptEmbed> = {
182
186
  args: {
187
+ heartButton: StoryFavoriteButton,
183
188
  embed: {
184
189
  resource: 'concept',
185
190
  status: 'error',