@ndla/ui 36.0.1 → 37.0.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 (188) 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/BlogPost/BlogPost.js +4 -4
  7. package/es/CampaignBlock/CampaignBlock.js +77 -0
  8. package/es/CampaignBlock/index.js +9 -0
  9. package/es/ContactBlock/ContactBlock.js +63 -39
  10. package/es/Embed/AudioEmbed.js +44 -188
  11. package/es/Embed/BrightcoveEmbed.js +27 -123
  12. package/es/Embed/ConceptEmbed.js +53 -75
  13. package/es/Embed/EmbedErrorPlaceholder.js +41 -0
  14. package/es/Embed/ExternalEmbed.js +5 -12
  15. package/es/Embed/H5pEmbed.js +4 -14
  16. package/es/Embed/IframeEmbed.js +4 -4
  17. package/es/Embed/ImageEmbed.js +41 -153
  18. package/es/Embed/conceptComponents.js +62 -228
  19. package/es/Embed/types.js +1 -0
  20. package/es/KeyFigure/KeyFigure.js +57 -0
  21. package/es/{KeyPerformanceIndicator → KeyFigure}/index.js +1 -1
  22. package/es/LicenseByline/EmbedByline.js +115 -0
  23. package/es/LicenseByline/LicenseDescription.js +39 -0
  24. package/es/LicenseByline/LicenseLink.js +36 -0
  25. package/es/LicenseByline/index.js +1 -0
  26. package/es/List/OrderedList.js +48 -0
  27. package/es/List/UnOrderedList.js +36 -0
  28. package/es/List/index.js +10 -0
  29. package/es/Navigation/NavigationBox.js +41 -48
  30. package/es/Navigation/NavigationHeading.js +18 -29
  31. package/es/Notion/Notion.js +5 -5
  32. package/es/Resource/resourceComponents.js +12 -11
  33. package/es/Typography/Heading.js +38 -0
  34. package/es/Typography/index.js +9 -0
  35. package/es/all.css +1 -1
  36. package/es/index.js +4 -2
  37. package/es/locale/messages-en.js +13 -2
  38. package/es/locale/messages-nb.js +13 -2
  39. package/es/locale/messages-nn.js +13 -2
  40. package/es/locale/messages-se.js +13 -2
  41. package/es/locale/messages-sma.js +13 -2
  42. package/es/model/ContentType.js +7 -1
  43. package/lib/Article/Article.d.ts +1 -3
  44. package/lib/Article/Article.js +7 -13
  45. package/lib/Article/ArticleByline.d.ts +3 -5
  46. package/lib/Article/ArticleByline.js +83 -126
  47. package/lib/Article/ArticleFootNotes.js +16 -11
  48. package/lib/AudioPlayer/AudioPlayer.d.ts +1 -2
  49. package/lib/AudioPlayer/AudioPlayer.js +33 -36
  50. package/lib/AudioPlayer/initAudioPlayers.d.ts +1 -0
  51. package/lib/AudioPlayer/initAudioPlayers.js +9 -3
  52. package/lib/BlogPost/BlogPost.js +4 -4
  53. package/lib/CampaignBlock/CampaignBlock.d.ts +31 -0
  54. package/lib/CampaignBlock/CampaignBlock.js +82 -0
  55. package/lib/CampaignBlock/index.d.ts +8 -0
  56. package/lib/CampaignBlock/index.js +13 -0
  57. package/lib/ContactBlock/ContactBlock.js +63 -39
  58. package/lib/Embed/AudioEmbed.d.ts +3 -2
  59. package/lib/Embed/AudioEmbed.js +53 -192
  60. package/lib/Embed/BrightcoveEmbed.d.ts +3 -1
  61. package/lib/Embed/BrightcoveEmbed.js +27 -122
  62. package/lib/Embed/ConceptEmbed.d.ts +7 -2
  63. package/lib/Embed/ConceptEmbed.js +51 -73
  64. package/lib/Embed/EmbedErrorPlaceholder.d.ts +17 -0
  65. package/lib/Embed/EmbedErrorPlaceholder.js +48 -0
  66. package/lib/Embed/ExternalEmbed.js +5 -11
  67. package/lib/Embed/H5pEmbed.js +4 -13
  68. package/lib/Embed/IframeEmbed.d.ts +2 -2
  69. package/lib/Embed/IframeEmbed.js +4 -4
  70. package/lib/Embed/ImageEmbed.d.ts +3 -10
  71. package/lib/Embed/ImageEmbed.js +48 -161
  72. package/lib/Embed/conceptComponents.d.ts +4 -2
  73. package/lib/Embed/conceptComponents.js +67 -231
  74. package/lib/Embed/index.d.ts +1 -0
  75. package/lib/Embed/types.d.ts +14 -0
  76. package/lib/Embed/types.js +5 -0
  77. package/lib/KeyFigure/KeyFigure.d.ts +10 -0
  78. package/lib/KeyFigure/KeyFigure.js +62 -0
  79. package/lib/KeyFigure/index.d.ts +1 -0
  80. package/lib/KeyFigure/index.js +13 -0
  81. package/lib/LicenseByline/EmbedByline.d.ts +51 -0
  82. package/lib/LicenseByline/EmbedByline.js +120 -0
  83. package/lib/LicenseByline/LicenseDescription.d.ts +14 -0
  84. package/lib/LicenseByline/LicenseDescription.js +44 -0
  85. package/lib/LicenseByline/LicenseLink.d.ts +14 -0
  86. package/lib/LicenseByline/LicenseLink.js +44 -0
  87. package/lib/LicenseByline/index.d.ts +1 -0
  88. package/lib/LicenseByline/index.js +13 -0
  89. package/lib/List/OrderedList.d.ts +15 -0
  90. package/lib/List/OrderedList.js +56 -0
  91. package/lib/List/UnOrderedList.d.ts +10 -0
  92. package/lib/List/UnOrderedList.js +43 -0
  93. package/lib/List/index.d.ts +9 -0
  94. package/lib/List/index.js +20 -0
  95. package/lib/Navigation/NavigationBox.js +40 -47
  96. package/lib/Navigation/NavigationHeading.js +17 -28
  97. package/lib/Notion/Notion.js +5 -5
  98. package/lib/Resource/resourceComponents.js +12 -11
  99. package/lib/Typography/Heading.d.ts +26 -0
  100. package/lib/Typography/Heading.js +45 -0
  101. package/lib/Typography/index.d.ts +8 -0
  102. package/lib/Typography/index.js +13 -0
  103. package/lib/all.css +1 -1
  104. package/lib/index.d.ts +4 -1
  105. package/lib/index.js +23 -3
  106. package/lib/locale/messages-en.d.ts +11 -0
  107. package/lib/locale/messages-en.js +13 -2
  108. package/lib/locale/messages-nb.d.ts +11 -0
  109. package/lib/locale/messages-nb.js +13 -2
  110. package/lib/locale/messages-nn.d.ts +11 -0
  111. package/lib/locale/messages-nn.js +13 -2
  112. package/lib/locale/messages-se.d.ts +11 -0
  113. package/lib/locale/messages-se.js +13 -2
  114. package/lib/locale/messages-sma.d.ts +11 -0
  115. package/lib/locale/messages-sma.js +13 -2
  116. package/lib/model/ContentType.d.ts +1 -0
  117. package/lib/model/ContentType.js +9 -2
  118. package/package.json +15 -15
  119. package/src/Article/Article.tsx +1 -8
  120. package/src/Article/ArticleByline.tsx +78 -127
  121. package/src/Article/ArticleFootNotes.tsx +33 -10
  122. package/src/Article/component.article.scss +1 -52
  123. package/src/Article/component.footnotes.scss +2 -2
  124. package/src/Aside/component.aside.scss +3 -3
  125. package/src/AudioPlayer/AudioPlayer.tsx +11 -24
  126. package/src/AudioPlayer/initAudioPlayers.tsx +7 -2
  127. package/src/BlogPost/BlogPost.tsx +0 -4
  128. package/src/CampaignBlock/CampaignBlock.stories.tsx +63 -0
  129. package/src/CampaignBlock/CampaignBlock.tsx +99 -0
  130. package/src/CampaignBlock/index.ts +9 -0
  131. package/src/ContactBlock/ContactBlock.tsx +27 -19
  132. package/src/ContactBlock/Contactblock.stories.tsx +0 -1
  133. package/src/Dialog/component.dialog.scss +4 -5
  134. package/src/Embed/AudioEmbed.stories.tsx +5 -3
  135. package/src/Embed/AudioEmbed.tsx +45 -192
  136. package/src/Embed/BrightcoveEmbed.stories.tsx +5 -1
  137. package/src/Embed/BrightcoveEmbed.tsx +20 -95
  138. package/src/Embed/ConceptEmbed.stories.tsx +5 -0
  139. package/src/Embed/ConceptEmbed.tsx +43 -54
  140. package/src/Embed/EmbedErrorPlaceholder.tsx +59 -0
  141. package/src/Embed/ExternalEmbed.stories.tsx +86 -0
  142. package/src/Embed/ExternalEmbed.tsx +3 -8
  143. package/src/Embed/H5pEmbed.stories.tsx +92 -0
  144. package/src/Embed/H5pEmbed.tsx +2 -10
  145. package/src/Embed/IframeEmbed.stories.tsx +130 -0
  146. package/src/Embed/IframeEmbed.tsx +3 -3
  147. package/src/Embed/ImageEmbed.stories.tsx +3 -1
  148. package/src/Embed/ImageEmbed.tsx +21 -116
  149. package/src/Embed/conceptComponents.tsx +67 -257
  150. package/src/Embed/index.ts +1 -0
  151. package/src/Embed/types.ts +12 -0
  152. package/src/FactBox/component.factbox.scss +3 -3
  153. package/src/Figure/component.figure-license.scss +4 -4
  154. package/src/Figure/component.figure.scss +1 -1
  155. package/src/KeyFigure/KeyFigure.stories.tsx +36 -0
  156. package/src/{KeyPerformanceIndicator/KeyPerformanceIndicator.tsx → KeyFigure/KeyFigure.tsx} +9 -7
  157. package/src/{KeyPerformanceIndicator → KeyFigure}/index.ts +1 -1
  158. package/src/LicenseByline/EmbedByline.stories.tsx +83 -0
  159. package/src/LicenseByline/EmbedByline.tsx +165 -0
  160. package/src/LicenseByline/LicenseDescription.tsx +43 -0
  161. package/src/LicenseByline/LicenseLink.tsx +42 -0
  162. package/src/LicenseByline/index.tsx +1 -0
  163. package/src/List/OrderedList.tsx +115 -0
  164. package/src/List/UnOrderedList.tsx +49 -0
  165. package/src/List/index.ts +10 -0
  166. package/src/MediaList/component.medialist.scss +2 -2
  167. package/src/Navigation/NavigationBox.tsx +10 -14
  168. package/src/Navigation/NavigationHeading.tsx +15 -24
  169. package/src/Notion/Notion.tsx +1 -1
  170. package/src/RelatedArticleList/component.related-articles.scss +3 -13
  171. package/src/Resource/resourceComponents.tsx +4 -2
  172. package/src/Table/component.tables.scss +0 -46
  173. package/src/Translation/component.translation.scss +3 -5
  174. package/src/Typography/Heading.tsx +96 -0
  175. package/src/Typography/index.ts +9 -0
  176. package/src/index.ts +5 -1
  177. package/src/locale/messages-en.ts +11 -0
  178. package/src/locale/messages-nb.ts +11 -0
  179. package/src/locale/messages-nn.ts +11 -0
  180. package/src/locale/messages-se.ts +11 -0
  181. package/src/locale/messages-sma.ts +11 -0
  182. package/src/model/ContentType.ts +7 -0
  183. package/es/KeyPerformanceIndicator/KeyPerformanceIndicator.js +0 -57
  184. package/lib/KeyPerformanceIndicator/KeyPerformanceIndicator.d.ts +0 -8
  185. package/lib/KeyPerformanceIndicator/KeyPerformanceIndicator.js +0 -62
  186. package/lib/KeyPerformanceIndicator/index.d.ts +0 -1
  187. package/lib/KeyPerformanceIndicator/index.js +0 -13
  188. package/src/KeyPerformanceIndicator/KeyPerformanceIndicator.stories.tsx +0 -79
@@ -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,10 +56,9 @@ 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;
66
64
 
@@ -75,7 +73,7 @@ const BrightcoveEmbed = ({ embed, isConcept }: Props) => {
75
73
  }, []);
76
74
  if (embed.status === 'error') {
77
75
  return (
78
- <Figure type={isConcept ? 'full-column' : 'full'} resizeIframe>
76
+ <EmbedErrorPlaceholder type="video">
79
77
  <BrightcoveIframe
80
78
  ref={iframeRef}
81
79
  title={`Video: ${embedData.videoid ?? ''}`}
@@ -84,35 +82,18 @@ const BrightcoveEmbed = ({ embed, isConcept }: Props) => {
84
82
  {...getIframeProps(embedData, [])}
85
83
  allowFullScreen
86
84
  />
87
- <figcaption>{t('video.error')}</figcaption>
88
- </Figure>
85
+ </EmbedErrorPlaceholder>
89
86
  );
90
87
  }
91
88
  const { data, seq } = embed;
92
89
 
93
90
  const linkedVideoId = isNumeric(data.link?.text) ? data.link?.text : undefined;
94
91
 
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
92
  const figureId = `figure-${seq}-${data.id}`;
111
93
  const originalVideoProps = getIframeProps(embedData, data.sources);
112
94
  const alternativeVideoProps = linkedVideoId
113
95
  ? getIframeProps({ ...embedData, videoid: linkedVideoId }, data.sources)
114
96
  : undefined;
115
- const captionAuthors = getFirstNonEmptyLicenseCredits({ rightsholders, creators, processors });
116
97
 
117
98
  return (
118
99
  <Figure id={figureId} type={isConcept ? 'full-column' : 'full'} resizeIframe>
@@ -127,82 +108,26 @@ const BrightcoveEmbed = ({ embed, isConcept }: Props) => {
127
108
  allowFullScreen
128
109
  />
129
110
  </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={
111
+ <EmbedByline
112
+ type="video"
113
+ copyright={data.copyright!}
114
+ description={embedData.caption ?? data.description ?? ''}
115
+ bottomRounded
116
+ >
117
+ {!!linkedVideoId && (
141
118
  <LinkedVideoButton
142
119
  variant="outline"
143
120
  shape="pill"
144
121
  size="small"
145
122
  onClick={() => setShowOriginalVideo((p) => !p)}
146
123
  >
147
- {t(`figure.button.${showOriginalVideo ? 'original' : 'alternative'}`)}
124
+ {t(`figure.button.${!showOriginalVideo ? 'original' : 'alternative'}`)}
148
125
  </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
126
  )}
174
- </ModalV2>
127
+ {HeartButton && <HeartButton embed={embed} />}
128
+ </EmbedByline>
175
129
  </Figure>
176
130
  );
177
131
  };
178
132
 
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
133
  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',
@@ -6,23 +6,23 @@
6
6
  *
7
7
  */
8
8
 
9
- import { useCallback, useRef, useState } from 'react';
9
+ import { ReactElement, ReactNode, useCallback, useRef, useState } from 'react';
10
10
  import { useTranslation } from 'react-i18next';
11
11
  import styled from '@emotion/styled';
12
12
  import { isMobile } from 'react-device-detect';
13
13
  import { Root, Trigger, Content, Anchor, Close, Portal } from '@radix-ui/react-popover';
14
- import { ButtonV2, IconButtonV2 } from '@ndla/button';
14
+ import { IconButtonV2 } from '@ndla/button';
15
15
  import { Cross } from '@ndla/icons/action';
16
16
  import { breakpoints, colors, mq, spacing } from '@ndla/core';
17
- import { getGroupedContributorDescriptionList, getLicenseByAbbreviation, getLicenseCredits } from '@ndla/licenses';
18
- import { ModalV2 } from '@ndla/modal';
19
17
  import { ConceptMetaData } from '@ndla/types-embed';
20
18
  import Tooltip from '@ndla/tooltip';
21
19
  import { Notion as UINotion } from '../Notion';
22
- import { Figure, FigureCaption } from '../Figure';
23
- import { FigureLicenseDialogContent } from '../Figure/FigureLicenseDialogContent';
20
+ import { Figure } from '../Figure';
24
21
  import { NotionImage } from '../Notion/NotionImage';
25
22
  import { ConceptNotionV2, ConceptNotionData } from './conceptComponents';
23
+ import { EmbedByline } from '../LicenseByline';
24
+ import EmbedErrorPlaceholder from './EmbedErrorPlaceholder';
25
+ import { HeartButtonType } from './types';
26
26
 
27
27
  const BottomBorder = styled.div`
28
28
  margin-top: ${spacing.normal};
@@ -72,6 +72,7 @@ const ImageWrapper = styled.div`
72
72
  interface Props {
73
73
  embed: ConceptMetaData;
74
74
  fullWidth?: boolean;
75
+ heartButton?: HeartButtonType;
75
76
  }
76
77
 
77
78
  const StyledButton = styled.button`
@@ -93,9 +94,11 @@ const StyledButton = styled.button`
93
94
  }
94
95
  `;
95
96
 
96
- export const ConceptEmbed = ({ embed, fullWidth }: Props) => {
97
- if (embed.status === 'error') {
97
+ export const ConceptEmbed = ({ embed, fullWidth, heartButton: HeartButton }: Props) => {
98
+ if (embed.status === 'error' && embed.embedData.type === 'inline') {
98
99
  return <span>{embed.embedData.linkText}</span>;
100
+ } else if (embed.status === 'error') {
101
+ return <EmbedErrorPlaceholder type="concept" />;
99
102
  }
100
103
 
101
104
  const {
@@ -112,6 +115,8 @@ export const ConceptEmbed = ({ embed, fullWidth }: Props) => {
112
115
  copyright={concept.copyright}
113
116
  source={concept.source}
114
117
  visualElement={visualElement}
118
+ heartButton={HeartButton}
119
+ conceptHeartButton={HeartButton && <HeartButton embed={embed} />}
115
120
  />
116
121
  );
117
122
  } else if (embed.embedData.type === 'inline') {
@@ -124,6 +129,8 @@ export const ConceptEmbed = ({ embed, fullWidth }: Props) => {
124
129
  source={concept.source}
125
130
  visualElement={visualElement}
126
131
  linkText={embed.embedData.linkText}
132
+ heartButton={HeartButton}
133
+ conceptHeartButton={HeartButton && <HeartButton embed={embed} />}
127
134
  />
128
135
  );
129
136
  } else {
@@ -135,6 +142,8 @@ export const ConceptEmbed = ({ embed, fullWidth }: Props) => {
135
142
  copyright={concept.copyright}
136
143
  source={concept.source}
137
144
  visualElement={visualElement}
145
+ heartButton={HeartButton}
146
+ conceptHeartButton={HeartButton && <HeartButton embed={embed} />}
138
147
  />
139
148
  );
140
149
  }
@@ -142,6 +151,8 @@ export const ConceptEmbed = ({ embed, fullWidth }: Props) => {
142
151
 
143
152
  interface InlineConceptProps extends ConceptNotionData {
144
153
  linkText: string;
154
+ heartButton?: HeartButtonType;
155
+ conceptHeartButton?: ReactNode;
145
156
  }
146
157
 
147
158
  const BaselineIcon = styled.span`
@@ -202,7 +213,16 @@ const getModalPosition = (anchor: HTMLElement) => {
202
213
  return anchorPos.top - (articlePos?.top || -window.scrollY);
203
214
  };
204
215
 
205
- const InlineConcept = ({ title, content, copyright, source, visualElement, linkText }: InlineConceptProps) => {
216
+ const InlineConcept = ({
217
+ title,
218
+ content,
219
+ copyright,
220
+ source,
221
+ visualElement,
222
+ linkText,
223
+ heartButton,
224
+ conceptHeartButton,
225
+ }: InlineConceptProps) => {
206
226
  const { t } = useTranslation();
207
227
  const anchorRef = useRef<HTMLDivElement>(null);
208
228
  const [modalPos, setModalPos] = useState(-9999);
@@ -240,6 +260,8 @@ const InlineConcept = ({ title, content, copyright, source, visualElement, linkT
240
260
  source={source}
241
261
  visualElement={visualElement}
242
262
  inPopover
263
+ heartButton={heartButton}
264
+ conceptHeartButton={conceptHeartButton}
243
265
  closeButton={
244
266
  <Close asChild>
245
267
  <IconButtonV2 aria-label={t('close')} variant="ghost">
@@ -257,6 +279,8 @@ const InlineConcept = ({ title, content, copyright, source, visualElement, linkT
257
279
 
258
280
  interface ConceptProps extends ConceptNotionData {
259
281
  fullWidth?: boolean;
282
+ heartButton?: HeartButtonType;
283
+ conceptHeartButton?: ReactElement;
260
284
  }
261
285
 
262
286
  export const BlockConcept = ({
@@ -267,24 +291,16 @@ export const BlockConcept = ({
267
291
  source,
268
292
  visualElement,
269
293
  fullWidth,
294
+ heartButton,
295
+ conceptHeartButton,
270
296
  }: ConceptProps) => {
271
- const { t, i18n } = useTranslation();
297
+ const { t } = useTranslation();
272
298
  const anchorRef = useRef<HTMLDivElement>(null);
273
299
  const [modalPos, setModalPos] = useState(-9999);
274
300
 
275
- const [isOpen, setIsOpen] = useState(false);
276
- const licenseCredits = getLicenseCredits(copyright);
277
- const { creators, rightsholders, processors } = licenseCredits;
278
- const authors = creators.length || rightsholders.length ? creators.concat(rightsholders) : processors;
279
301
  const visualElementType =
280
302
  visualElement?.embedData.resource === 'brightcove' ? 'video' : visualElement?.embedData.resource;
281
303
 
282
- const groupedAuthors = getGroupedContributorDescriptionList(licenseCredits, i18n.language).map((item) => ({
283
- name: item.description,
284
- type: item.label,
285
- }));
286
- const license = copyright?.license && getLicenseByAbbreviation(copyright?.license?.license, i18n.language);
287
-
288
304
  const onOpenChange = useCallback((open: boolean) => {
289
305
  if (open) {
290
306
  const anchor = anchorRef.current;
@@ -316,7 +332,7 @@ export const BlockConcept = ({
316
332
  <NotionImage
317
333
  type={visualElementType}
318
334
  id={''}
319
- src={visualElement.data.imageUrl}
335
+ src={visualElement.data.image.imageUrl}
320
336
  alt={visualElement.data.alttext.alttext}
321
337
  />
322
338
  ) : metaImage ? (
@@ -346,6 +362,8 @@ export const BlockConcept = ({
346
362
  copyright={copyright}
347
363
  source={source}
348
364
  visualElement={visualElement}
365
+ heartButton={heartButton}
366
+ conceptHeartButton={conceptHeartButton}
349
367
  inPopover
350
368
  closeButton={
351
369
  <Close asChild>
@@ -362,39 +380,10 @@ export const BlockConcept = ({
362
380
  )
363
381
  }
364
382
  />
365
- {copyright?.license && license ? (
366
- <FigureCaption
367
- figureId=""
368
- id=""
369
- authors={authors}
370
- licenseRights={license.rights}
371
- locale={i18n.language}
372
- hideIconsAndAuthors
373
- modalButton={
374
- <ButtonV2 variant="outline" size="small" shape="pill" onClick={() => setIsOpen(true)}>
375
- {t('concept.reuse')}
376
- </ButtonV2>
377
- }
378
- >
379
- <ModalV2
380
- controlled
381
- isOpen={isOpen}
382
- onClose={() => setIsOpen(false)}
383
- labelledBy="license-dialog-rules-heading"
384
- >
385
- {(close) => (
386
- <FigureLicenseDialogContent
387
- authors={groupedAuthors}
388
- locale={i18n.language}
389
- title={title}
390
- origin={copyright.origin}
391
- license={license}
392
- onClose={close}
393
- type="concept"
394
- />
395
- )}
396
- </ModalV2>
397
- </FigureCaption>
383
+ {copyright ? (
384
+ <EmbedByline copyright={copyright} bottomRounded topRounded type="concept">
385
+ {conceptHeartButton}
386
+ </EmbedByline>
398
387
  ) : (
399
388
  <BottomBorder />
400
389
  )}
@@ -0,0 +1,59 @@
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 { ReactNode } from 'react';
10
+ import styled from '@emotion/styled';
11
+ import { WarningOutline } from '@ndla/icons/common';
12
+ import { colors, spacing } from '@ndla/core';
13
+ import { Figure, FigureType } from '../Figure';
14
+ import { EmbedByline } from '../LicenseByline';
15
+ import { EmbedBylineErrorProps } from '../LicenseByline/EmbedByline';
16
+
17
+ interface Props {
18
+ type: EmbedBylineErrorProps['type'];
19
+ figureType?: FigureType;
20
+ children?: ReactNode;
21
+ }
22
+
23
+ const ErrorPlaceholder = styled.div`
24
+ display: flex;
25
+ align-items: center;
26
+ justify-content: center;
27
+ background-color: ${colors.brand.greyLighter};
28
+ height: 330px;
29
+
30
+ svg {
31
+ fill: ${colors.text.light};
32
+ height: 90%;
33
+ width: 90%;
34
+ }
35
+ &[data-embed-type='audio'] {
36
+ height: 150px;
37
+ }
38
+ `;
39
+
40
+ const StyledFigure = styled(Figure)`
41
+ display: flex;
42
+ flex-direction: column;
43
+ gap: ${spacing.xsmall};
44
+ `;
45
+
46
+ const EmbedErrorPlaceholder = ({ type, children, figureType }: Props) => {
47
+ return (
48
+ <StyledFigure type={figureType}>
49
+ {children ?? (
50
+ <ErrorPlaceholder data-embed-type={type}>
51
+ <WarningOutline />
52
+ </ErrorPlaceholder>
53
+ )}
54
+ <EmbedByline error type={type} topRounded bottomRounded />
55
+ </StyledFigure>
56
+ );
57
+ };
58
+
59
+ export default EmbedErrorPlaceholder;
@@ -0,0 +1,86 @@
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 React from 'react';
10
+ import { Meta, StoryObj } from '@storybook/react';
11
+ import { H5pEmbedData, H5pData, OembedEmbedData, OembedData } from '@ndla/types-embed';
12
+ import ExternalEmbed from './ExternalEmbed';
13
+ import { defaultParameters } from '../../../../stories/defaults';
14
+
15
+ const embedData: OembedEmbedData = {
16
+ resource: 'external',
17
+ url: 'https://embed.ted.com/talks/zahra_biabani_the_eco_creators_helping_the_climate_through_social_media',
18
+ type: 'iframe',
19
+ };
20
+
21
+ const metaData: OembedData = {
22
+ oembed: {
23
+ type: 'video',
24
+ version: '1.0',
25
+ title: 'Zahra Biabani: The eco-creators helping the climate through social media',
26
+ description:
27
+ '"Climate doom-ism," or a pessimistic outlook on the future of the planet, rivals climate denialism in holding up the fight against climate change, says activist Zahra Biabani. Illuminating how hope combats inaction, she takes us inside the world of eco-friendly content on TikTok -- and shows that we all have what it takes to make real change.',
28
+ authorName: 'Zahra Biabani',
29
+ authorUrl: 'https://www.ted.com/speakers/zahra_biabani',
30
+ providerName: 'TED',
31
+ providerUrl: 'https://www.ted.com',
32
+ cacheAge: 300,
33
+ thumbnailUrl:
34
+ 'https://pi.tedcdn.com/r/talkstar-photos.s3.amazonaws.com/uploads/803ab5d5-2cff-4764-b5b6-545217159538/ZahraBiabani_2022T-embed.jpg?h=315&w=560',
35
+ thumbnailWidth: 560,
36
+ thumbnailHeight: 315,
37
+ width: 560,
38
+ height: 315,
39
+ html: '<iframe src="https://embed.ted.com/talks/zahra_biabani_the_eco_creators_helping_the_climate_through_social_media" width="560" height="315" frameborder="0" scrolling="no" webkitAllowFullScreen mozallowfullscreen allowFullScreen></iframe>',
40
+ },
41
+ };
42
+
43
+ const meta: Meta<typeof ExternalEmbed> = {
44
+ title: 'Enkle komponenter/Embeds/ExternalEmbed',
45
+ component: ExternalEmbed,
46
+ tags: ['autodocs'],
47
+ decorators: [
48
+ (Story) => (
49
+ <div className="o-wrapper">
50
+ <article className="c-article c-article--clean">
51
+ <section className="u-4/6@desktop u-push-1/6@desktop u-10/12@tablet u-push-1/12@tablet">
52
+ <section>
53
+ <Story />
54
+ </section>
55
+ </section>
56
+ </article>
57
+ </div>
58
+ ),
59
+ ],
60
+ parameters: defaultParameters,
61
+ };
62
+
63
+ export default meta;
64
+
65
+ export const Regular: StoryObj<typeof ExternalEmbed> = {
66
+ args: {
67
+ embed: {
68
+ resource: 'external',
69
+ status: 'success',
70
+ seq: 8,
71
+ embedData: embedData,
72
+ data: metaData,
73
+ },
74
+ },
75
+ };
76
+
77
+ export const Failed: StoryObj<typeof ExternalEmbed> = {
78
+ args: {
79
+ embed: {
80
+ resource: 'external',
81
+ status: 'error',
82
+ seq: 3,
83
+ embedData: embedData,
84
+ },
85
+ },
86
+ };
@@ -12,7 +12,7 @@ import { useEffect, useRef } from 'react';
12
12
  import { useTranslation } from 'react-i18next';
13
13
  import { Figure } from '../Figure';
14
14
  import { ResourceBox } from '../ResourceBox';
15
- import { errorSvgSrc } from './ImageEmbed';
15
+ import EmbedErrorPlaceholder from './EmbedErrorPlaceholder';
16
16
 
17
17
  interface Props {
18
18
  embed: OembedMetaData;
@@ -39,18 +39,13 @@ const ExternalEmbed = ({ embed, isConcept }: Props) => {
39
39
  }
40
40
  }, []);
41
41
  if (embed.status === 'error') {
42
- return (
43
- <figure className={isConcept ? '' : 'c-figure'}>
44
- <img alt={t('external.error')} src={errorSvgSrc} />
45
- <figcaption>{t('external.error')}</figcaption>
46
- </figure>
47
- );
42
+ return <EmbedErrorPlaceholder type="external" />;
48
43
  }
49
44
 
50
45
  const { embedData, data } = embed;
51
46
 
52
47
  if (embedData.type === 'fullscreen') {
53
- const image = { src: data.iframeImage?.imageUrl ?? '', alt: data.iframeImage?.alttext?.alttext ?? '' };
48
+ const image = { src: data.iframeImage?.image.imageUrl ?? '', alt: data.iframeImage?.alttext?.alttext ?? '' };
54
49
  return (
55
50
  <Figure type="full">
56
51
  <ResourceBox