@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.
- package/es/Article/Article.js +7 -13
- package/es/Article/ArticleByline.js +79 -123
- package/es/Article/ArticleFootNotes.js +16 -11
- package/es/AudioPlayer/AudioPlayer.js +33 -35
- package/es/AudioPlayer/initAudioPlayers.js +6 -1
- package/es/BlogPost/BlogPost.js +4 -4
- package/es/CampaignBlock/CampaignBlock.js +77 -0
- package/es/CampaignBlock/index.js +9 -0
- package/es/ContactBlock/ContactBlock.js +63 -39
- package/es/Embed/AudioEmbed.js +44 -188
- package/es/Embed/BrightcoveEmbed.js +27 -123
- package/es/Embed/ConceptEmbed.js +53 -75
- package/es/Embed/EmbedErrorPlaceholder.js +41 -0
- package/es/Embed/ExternalEmbed.js +5 -12
- package/es/Embed/H5pEmbed.js +4 -14
- package/es/Embed/IframeEmbed.js +4 -4
- package/es/Embed/ImageEmbed.js +41 -153
- package/es/Embed/conceptComponents.js +62 -228
- package/es/Embed/types.js +1 -0
- package/es/KeyFigure/KeyFigure.js +57 -0
- package/es/{KeyPerformanceIndicator → KeyFigure}/index.js +1 -1
- package/es/LicenseByline/EmbedByline.js +115 -0
- package/es/LicenseByline/LicenseDescription.js +39 -0
- package/es/LicenseByline/LicenseLink.js +36 -0
- package/es/LicenseByline/index.js +1 -0
- package/es/List/OrderedList.js +48 -0
- package/es/List/UnOrderedList.js +36 -0
- package/es/List/index.js +10 -0
- package/es/Navigation/NavigationBox.js +41 -48
- package/es/Navigation/NavigationHeading.js +18 -29
- package/es/Notion/Notion.js +5 -5
- package/es/Resource/resourceComponents.js +12 -11
- package/es/Typography/Heading.js +38 -0
- package/es/Typography/index.js +9 -0
- package/es/all.css +1 -1
- package/es/index.js +4 -2
- package/es/locale/messages-en.js +13 -2
- package/es/locale/messages-nb.js +13 -2
- package/es/locale/messages-nn.js +13 -2
- package/es/locale/messages-se.js +13 -2
- package/es/locale/messages-sma.js +13 -2
- package/es/model/ContentType.js +7 -1
- package/lib/Article/Article.d.ts +1 -3
- package/lib/Article/Article.js +7 -13
- package/lib/Article/ArticleByline.d.ts +3 -5
- package/lib/Article/ArticleByline.js +83 -126
- package/lib/Article/ArticleFootNotes.js +16 -11
- package/lib/AudioPlayer/AudioPlayer.d.ts +1 -2
- package/lib/AudioPlayer/AudioPlayer.js +33 -36
- package/lib/AudioPlayer/initAudioPlayers.d.ts +1 -0
- package/lib/AudioPlayer/initAudioPlayers.js +9 -3
- package/lib/BlogPost/BlogPost.js +4 -4
- package/lib/CampaignBlock/CampaignBlock.d.ts +31 -0
- package/lib/CampaignBlock/CampaignBlock.js +82 -0
- package/lib/CampaignBlock/index.d.ts +8 -0
- package/lib/CampaignBlock/index.js +13 -0
- package/lib/ContactBlock/ContactBlock.js +63 -39
- package/lib/Embed/AudioEmbed.d.ts +3 -2
- package/lib/Embed/AudioEmbed.js +53 -192
- package/lib/Embed/BrightcoveEmbed.d.ts +3 -1
- package/lib/Embed/BrightcoveEmbed.js +27 -122
- package/lib/Embed/ConceptEmbed.d.ts +7 -2
- package/lib/Embed/ConceptEmbed.js +51 -73
- package/lib/Embed/EmbedErrorPlaceholder.d.ts +17 -0
- package/lib/Embed/EmbedErrorPlaceholder.js +48 -0
- package/lib/Embed/ExternalEmbed.js +5 -11
- package/lib/Embed/H5pEmbed.js +4 -13
- package/lib/Embed/IframeEmbed.d.ts +2 -2
- package/lib/Embed/IframeEmbed.js +4 -4
- package/lib/Embed/ImageEmbed.d.ts +3 -10
- package/lib/Embed/ImageEmbed.js +48 -161
- package/lib/Embed/conceptComponents.d.ts +4 -2
- package/lib/Embed/conceptComponents.js +67 -231
- package/lib/Embed/index.d.ts +1 -0
- package/lib/Embed/types.d.ts +14 -0
- package/lib/Embed/types.js +5 -0
- package/lib/KeyFigure/KeyFigure.d.ts +10 -0
- package/lib/KeyFigure/KeyFigure.js +62 -0
- package/lib/KeyFigure/index.d.ts +1 -0
- package/lib/KeyFigure/index.js +13 -0
- package/lib/LicenseByline/EmbedByline.d.ts +51 -0
- package/lib/LicenseByline/EmbedByline.js +120 -0
- package/lib/LicenseByline/LicenseDescription.d.ts +14 -0
- package/lib/LicenseByline/LicenseDescription.js +44 -0
- package/lib/LicenseByline/LicenseLink.d.ts +14 -0
- package/lib/LicenseByline/LicenseLink.js +44 -0
- package/lib/LicenseByline/index.d.ts +1 -0
- package/lib/LicenseByline/index.js +13 -0
- package/lib/List/OrderedList.d.ts +15 -0
- package/lib/List/OrderedList.js +56 -0
- package/lib/List/UnOrderedList.d.ts +10 -0
- package/lib/List/UnOrderedList.js +43 -0
- package/lib/List/index.d.ts +9 -0
- package/lib/List/index.js +20 -0
- package/lib/Navigation/NavigationBox.js +40 -47
- package/lib/Navigation/NavigationHeading.js +17 -28
- package/lib/Notion/Notion.js +5 -5
- package/lib/Resource/resourceComponents.js +12 -11
- package/lib/Typography/Heading.d.ts +26 -0
- package/lib/Typography/Heading.js +45 -0
- package/lib/Typography/index.d.ts +8 -0
- package/lib/Typography/index.js +13 -0
- package/lib/all.css +1 -1
- package/lib/index.d.ts +4 -1
- package/lib/index.js +23 -3
- package/lib/locale/messages-en.d.ts +11 -0
- package/lib/locale/messages-en.js +13 -2
- package/lib/locale/messages-nb.d.ts +11 -0
- package/lib/locale/messages-nb.js +13 -2
- package/lib/locale/messages-nn.d.ts +11 -0
- package/lib/locale/messages-nn.js +13 -2
- package/lib/locale/messages-se.d.ts +11 -0
- package/lib/locale/messages-se.js +13 -2
- package/lib/locale/messages-sma.d.ts +11 -0
- package/lib/locale/messages-sma.js +13 -2
- package/lib/model/ContentType.d.ts +1 -0
- package/lib/model/ContentType.js +9 -2
- package/package.json +15 -15
- package/src/Article/Article.tsx +1 -8
- package/src/Article/ArticleByline.tsx +78 -127
- package/src/Article/ArticleFootNotes.tsx +33 -10
- package/src/Article/component.article.scss +1 -52
- package/src/Article/component.footnotes.scss +2 -2
- package/src/Aside/component.aside.scss +3 -3
- package/src/AudioPlayer/AudioPlayer.tsx +11 -24
- package/src/AudioPlayer/initAudioPlayers.tsx +7 -2
- package/src/BlogPost/BlogPost.tsx +0 -4
- package/src/CampaignBlock/CampaignBlock.stories.tsx +63 -0
- package/src/CampaignBlock/CampaignBlock.tsx +99 -0
- package/src/CampaignBlock/index.ts +9 -0
- package/src/ContactBlock/ContactBlock.tsx +27 -19
- package/src/ContactBlock/Contactblock.stories.tsx +0 -1
- package/src/Dialog/component.dialog.scss +4 -5
- package/src/Embed/AudioEmbed.stories.tsx +5 -3
- package/src/Embed/AudioEmbed.tsx +45 -192
- package/src/Embed/BrightcoveEmbed.stories.tsx +5 -1
- package/src/Embed/BrightcoveEmbed.tsx +20 -95
- package/src/Embed/ConceptEmbed.stories.tsx +5 -0
- package/src/Embed/ConceptEmbed.tsx +43 -54
- package/src/Embed/EmbedErrorPlaceholder.tsx +59 -0
- package/src/Embed/ExternalEmbed.stories.tsx +86 -0
- package/src/Embed/ExternalEmbed.tsx +3 -8
- package/src/Embed/H5pEmbed.stories.tsx +92 -0
- package/src/Embed/H5pEmbed.tsx +2 -10
- package/src/Embed/IframeEmbed.stories.tsx +130 -0
- package/src/Embed/IframeEmbed.tsx +3 -3
- package/src/Embed/ImageEmbed.stories.tsx +3 -1
- package/src/Embed/ImageEmbed.tsx +21 -116
- package/src/Embed/conceptComponents.tsx +67 -257
- package/src/Embed/index.ts +1 -0
- package/src/Embed/types.ts +12 -0
- package/src/FactBox/component.factbox.scss +3 -3
- package/src/Figure/component.figure-license.scss +4 -4
- package/src/Figure/component.figure.scss +1 -1
- package/src/KeyFigure/KeyFigure.stories.tsx +36 -0
- package/src/{KeyPerformanceIndicator/KeyPerformanceIndicator.tsx → KeyFigure/KeyFigure.tsx} +9 -7
- package/src/{KeyPerformanceIndicator → KeyFigure}/index.ts +1 -1
- package/src/LicenseByline/EmbedByline.stories.tsx +83 -0
- package/src/LicenseByline/EmbedByline.tsx +165 -0
- package/src/LicenseByline/LicenseDescription.tsx +43 -0
- package/src/LicenseByline/LicenseLink.tsx +42 -0
- package/src/LicenseByline/index.tsx +1 -0
- package/src/List/OrderedList.tsx +115 -0
- package/src/List/UnOrderedList.tsx +49 -0
- package/src/List/index.ts +10 -0
- package/src/MediaList/component.medialist.scss +2 -2
- package/src/Navigation/NavigationBox.tsx +10 -14
- package/src/Navigation/NavigationHeading.tsx +15 -24
- package/src/Notion/Notion.tsx +1 -1
- package/src/RelatedArticleList/component.related-articles.scss +3 -13
- package/src/Resource/resourceComponents.tsx +4 -2
- package/src/Table/component.tables.scss +0 -46
- package/src/Translation/component.translation.scss +3 -5
- package/src/Typography/Heading.tsx +96 -0
- package/src/Typography/index.ts +9 -0
- package/src/index.ts +5 -1
- package/src/locale/messages-en.ts +11 -0
- package/src/locale/messages-nb.ts +11 -0
- package/src/locale/messages-nn.ts +11 -0
- package/src/locale/messages-se.ts +11 -0
- package/src/locale/messages-sma.ts +11 -0
- package/src/model/ContentType.ts +7 -0
- package/es/KeyPerformanceIndicator/KeyPerformanceIndicator.js +0 -57
- package/lib/KeyPerformanceIndicator/KeyPerformanceIndicator.d.ts +0 -8
- package/lib/KeyPerformanceIndicator/KeyPerformanceIndicator.js +0 -62
- package/lib/KeyPerformanceIndicator/index.d.ts +0 -1
- package/lib/KeyPerformanceIndicator/index.js +0 -13
- 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
|
|
20
|
-
import { Figure
|
|
21
|
-
import {
|
|
22
|
-
import
|
|
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
|
|
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
|
-
<
|
|
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
|
-
|
|
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
|
-
<
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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 = ({
|
|
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
|
|
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
|
|
366
|
-
<
|
|
367
|
-
|
|
368
|
-
|
|
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
|
|
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
|