@ndla/ui 47.3.0 → 47.4.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 (80) hide show
  1. package/es/CampaignBlock/CampaignBlock.js +8 -6
  2. package/es/ContactBlock/ContactBlock.js +17 -14
  3. package/es/CopyParagraphButton/CopyParagraphButtonV2.js +5 -3
  4. package/es/Embed/ConceptEmbed.js +22 -13
  5. package/es/Embed/ConceptListEmbed.js +6 -3
  6. package/es/Embed/ImageEmbed.js +5 -3
  7. package/es/Embed/conceptComponents.js +19 -13
  8. package/es/Footer/Footer.js +11 -9
  9. package/es/Footer/FooterLinks.js +19 -34
  10. package/es/FrontpageArticle/FrontpageArticle.js +5 -2
  11. package/es/Gloss/Gloss.js +8 -8
  12. package/es/Image/Image.js +8 -5
  13. package/es/KeyFigure/KeyFigure.js +8 -5
  14. package/es/Notion/Notion.js +7 -5
  15. package/es/Table/Table.js +6 -6
  16. package/es/locale/messages-en.js +1 -0
  17. package/es/locale/messages-nb.js +1 -0
  18. package/es/locale/messages-nn.js +2 -1
  19. package/es/locale/messages-se.js +1 -0
  20. package/es/locale/messages-sma.js +1 -0
  21. package/lib/CampaignBlock/CampaignBlock.js +8 -6
  22. package/lib/ContactBlock/ContactBlock.d.ts +2 -1
  23. package/lib/ContactBlock/ContactBlock.js +17 -14
  24. package/lib/CopyParagraphButton/CopyParagraphButtonV2.d.ts +2 -1
  25. package/lib/CopyParagraphButton/CopyParagraphButtonV2.js +5 -3
  26. package/lib/Embed/ConceptEmbed.d.ts +4 -3
  27. package/lib/Embed/ConceptEmbed.js +22 -13
  28. package/lib/Embed/ConceptListEmbed.d.ts +2 -1
  29. package/lib/Embed/ConceptListEmbed.js +6 -3
  30. package/lib/Embed/ImageEmbed.d.ts +2 -1
  31. package/lib/Embed/ImageEmbed.js +5 -3
  32. package/lib/Embed/conceptComponents.d.ts +1 -0
  33. package/lib/Embed/conceptComponents.js +19 -13
  34. package/lib/Footer/Footer.d.ts +6 -1
  35. package/lib/Footer/Footer.js +11 -9
  36. package/lib/Footer/FooterLinks.d.ts +7 -2
  37. package/lib/Footer/FooterLinks.js +19 -34
  38. package/lib/FrontpageArticle/FrontpageArticle.d.ts +2 -1
  39. package/lib/FrontpageArticle/FrontpageArticle.js +5 -2
  40. package/lib/Gloss/Gloss.js +8 -8
  41. package/lib/Image/Image.d.ts +2 -1
  42. package/lib/Image/Image.js +8 -5
  43. package/lib/KeyFigure/KeyFigure.d.ts +2 -1
  44. package/lib/KeyFigure/KeyFigure.js +8 -5
  45. package/lib/Notion/Notion.d.ts +2 -1
  46. package/lib/Notion/Notion.js +7 -5
  47. package/lib/Table/Table.d.ts +1 -0
  48. package/lib/Table/Table.js +6 -6
  49. package/lib/locale/messages-en.d.ts +1 -0
  50. package/lib/locale/messages-en.js +1 -0
  51. package/lib/locale/messages-nb.d.ts +1 -0
  52. package/lib/locale/messages-nb.js +1 -0
  53. package/lib/locale/messages-nn.d.ts +1 -0
  54. package/lib/locale/messages-nn.js +2 -1
  55. package/lib/locale/messages-se.d.ts +1 -0
  56. package/lib/locale/messages-se.js +1 -0
  57. package/lib/locale/messages-sma.d.ts +1 -0
  58. package/lib/locale/messages-sma.js +1 -0
  59. package/package.json +17 -17
  60. package/src/CampaignBlock/CampaignBlock.tsx +4 -2
  61. package/src/ContactBlock/ContactBlock.tsx +13 -3
  62. package/src/CopyParagraphButton/CopyParagraphButtonV2.tsx +3 -2
  63. package/src/Embed/ConceptEmbed.tsx +10 -1
  64. package/src/Embed/ConceptListEmbed.tsx +4 -3
  65. package/src/Embed/ImageEmbed.tsx +3 -1
  66. package/src/Embed/conceptComponents.tsx +14 -9
  67. package/src/Footer/Footer.stories.tsx +22 -0
  68. package/src/Footer/Footer.tsx +24 -18
  69. package/src/Footer/FooterLinks.tsx +17 -24
  70. package/src/FrontpageArticle/FrontpageArticle.tsx +4 -3
  71. package/src/Gloss/Gloss.tsx +1 -0
  72. package/src/Image/Image.tsx +11 -2
  73. package/src/KeyFigure/KeyFigure.tsx +4 -3
  74. package/src/Notion/Notion.tsx +3 -2
  75. package/src/Table/Table.tsx +1 -0
  76. package/src/locale/messages-en.ts +1 -0
  77. package/src/locale/messages-nb.ts +1 -0
  78. package/src/locale/messages-nn.ts +2 -1
  79. package/src/locale/messages-se.ts +1 -0
  80. package/src/locale/messages-sma.ts +1 -0
@@ -155,6 +155,7 @@ declare const messages: {
155
155
  listView: string;
156
156
  detailView: string;
157
157
  shortView: string;
158
+ userPictureAltText: string;
158
159
  sharedFolder: {
159
160
  folderCopied: string;
160
161
  info: string;
@@ -1164,6 +1164,7 @@ var messages = _objectSpread(_objectSpread({
1164
1164
  listView: 'Listevisning',
1165
1165
  detailView: 'Detaljert listevisning',
1166
1166
  shortView: 'Kort visning',
1167
+ userPictureAltText: 'Profijleguvviem',
1167
1168
  sharedFolder: {
1168
1169
  folderCopied: 'Mappen har blitt kopiert.',
1169
1170
  info: 'Denne mappa inneheld fagstoff og oppgåver frå NDLA, samla av ein lærar.',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ndla/ui",
3
- "version": "47.3.0",
3
+ "version": "47.4.0",
4
4
  "description": "UI component library for NDLA.",
5
5
  "license": "GPL-3.0",
6
6
  "main": "lib/index.js",
@@ -31,23 +31,23 @@
31
31
  "types"
32
32
  ],
33
33
  "dependencies": {
34
- "@ndla/accordion": "^2.2.31",
35
- "@ndla/button": "^12.0.5",
36
- "@ndla/carousel": "^4.0.8",
37
- "@ndla/core": "^4.2.2",
38
- "@ndla/dropdown-menu": "^1.0.11",
39
- "@ndla/forms": "^5.0.7",
34
+ "@ndla/accordion": "^2.2.32",
35
+ "@ndla/button": "^12.0.6",
36
+ "@ndla/carousel": "^4.0.9",
37
+ "@ndla/core": "^4.2.3",
38
+ "@ndla/dropdown-menu": "^1.0.12",
39
+ "@ndla/forms": "^5.0.8",
40
40
  "@ndla/hooks": "^2.1.1",
41
- "@ndla/icons": "^4.1.3",
41
+ "@ndla/icons": "^4.1.4",
42
42
  "@ndla/licenses": "^7.2.2",
43
- "@ndla/modal": "^5.0.5",
44
- "@ndla/notion": "^6.0.7",
45
- "@ndla/safelink": "^4.1.30",
46
- "@ndla/select": "^3.1.3",
47
- "@ndla/switch": "^1.1.17",
48
- "@ndla/tabs": "^3.1.2",
49
- "@ndla/tooltip": "^5.0.5",
50
- "@ndla/typography": "^0.2.4",
43
+ "@ndla/modal": "^5.0.6",
44
+ "@ndla/notion": "^6.0.8",
45
+ "@ndla/safelink": "^4.1.31",
46
+ "@ndla/select": "^3.1.4",
47
+ "@ndla/switch": "^1.1.18",
48
+ "@ndla/tabs": "^3.1.3",
49
+ "@ndla/tooltip": "^5.0.6",
50
+ "@ndla/typography": "^0.2.5",
51
51
  "@ndla/util": "^4.0.0",
52
52
  "@radix-ui/react-popover": "^1.0.7",
53
53
  "@radix-ui/react-radio-group": "^1.1.3",
@@ -80,5 +80,5 @@
80
80
  "publishConfig": {
81
81
  "access": "public"
82
82
  },
83
- "gitHead": "6efaf50569b477800eb4a8093eb01c6ea2205479"
83
+ "gitHead": "726e4d5d54cad9fdd7090a2a1f92a36bc5893f65"
84
84
  }
@@ -99,8 +99,10 @@ const CampaignBlock = ({
99
99
  <Container className={className} data-type="campaign-block" data-image-side={imageSide}>
100
100
  {image && <StyledImg src={image.src} height={200} width={240} alt={image.alt} />}
101
101
  <TextWrapper>
102
- <Heading css={headingStyle}>{title.title}</Heading>
103
- <StyledDescription>{description.text}</StyledDescription>
102
+ <Heading css={headingStyle} lang={title.language}>
103
+ {title.title}
104
+ </Heading>
105
+ <StyledDescription lang={description.language}>{description.text}</StyledDescription>
104
106
  <StyledLink to={href}>
105
107
  {url.text}
106
108
  <Forward />
@@ -27,6 +27,7 @@ interface Props {
27
27
  imageWidth?: number;
28
28
  name: string;
29
29
  email: string;
30
+ lang?: string;
30
31
  }
31
32
  const BlockWrapper = styled.div`
32
33
  display: flex;
@@ -126,7 +127,16 @@ const StyledImage = styled(Image)`
126
127
  object-fit: cover;
127
128
  `;
128
129
 
129
- const ContactBlock = ({ image, jobTitle, description, name, email, blobColor = 'green', blob = 'pointy' }: Props) => {
130
+ const ContactBlock = ({
131
+ image,
132
+ jobTitle,
133
+ description,
134
+ name,
135
+ email,
136
+ blobColor = 'green',
137
+ blob = 'pointy',
138
+ lang,
139
+ }: Props) => {
130
140
  const { t } = useTranslation();
131
141
  const isGreenBlob = blobColor === 'green';
132
142
  const Blob = blob === 'pointy' ? BlobPointy : BlobRound;
@@ -153,8 +163,8 @@ const ContactBlock = ({ image, jobTitle, description, name, email, blobColor = '
153
163
  <ContentWrapper>
154
164
  <TextWrapper>
155
165
  <InfoWrapper>
156
- <StyledHeader>{name}</StyledHeader>
157
- <StyledText>{jobTitle}</StyledText>
166
+ <StyledHeader lang={lang}>{name}</StyledHeader>
167
+ <StyledText lang={lang}>{jobTitle}</StyledText>
158
168
  <StyledText>
159
169
  <Email>{`${t('email')}:`}</Email>
160
170
  <EmailLink href={`mailto:${email}`}>{email}</EmailLink>
@@ -44,8 +44,9 @@ interface Props {
44
44
  // What to render within the h2
45
45
  children: ReactNode;
46
46
  copyText: string;
47
+ lang?: string;
47
48
  }
48
- const CopyParagraphButtonV2 = ({ children, copyText }: Props) => {
49
+ const CopyParagraphButtonV2 = ({ children, copyText, lang }: Props) => {
49
50
  const [hasCopied, setHasCopied] = useState(false);
50
51
  const { t } = useTranslation();
51
52
  const sanitizedTitle = useMemo(() => encodeURIComponent(copyText.replace(/ /g, '-')), [copyText]);
@@ -74,7 +75,7 @@ const CopyParagraphButtonV2 = ({ children, copyText }: Props) => {
74
75
  <Link />
75
76
  </IconButton>
76
77
  </Tooltip>
77
- <h2 id={sanitizedTitle} tabIndex={-1}>
78
+ <h2 id={sanitizedTitle} tabIndex={-1} lang={lang}>
78
79
  {children}
79
80
  </h2>
80
81
  </ContainerDiv>
@@ -71,6 +71,7 @@ interface Props {
71
71
  embed: ConceptMetaData;
72
72
  fullWidth?: boolean;
73
73
  heartButton?: HeartButtonType;
74
+ lang?: string;
74
75
  }
75
76
 
76
77
  const StyledButton = styled.button`
@@ -92,7 +93,7 @@ const StyledButton = styled.button`
92
93
  }
93
94
  `;
94
95
 
95
- export const ConceptEmbed = ({ embed, fullWidth, heartButton: HeartButton }: Props) => {
96
+ export const ConceptEmbed = ({ embed, fullWidth, heartButton: HeartButton, lang }: Props) => {
96
97
  const parsedContent = useMemo(() => {
97
98
  if (embed.status === 'error' || !embed.data.concept.content) return undefined;
98
99
  return parse(embed.data.concept.content.content);
@@ -121,6 +122,7 @@ export const ConceptEmbed = ({ embed, fullWidth, heartButton: HeartButton }: Pro
121
122
  conceptHeartButton={HeartButton && <HeartButton embed={embed} />}
122
123
  conceptType={concept.conceptType}
123
124
  glossData={concept.glossData}
125
+ lang={lang}
124
126
  />
125
127
  );
126
128
  } else if (embed.embedData.type === 'inline') {
@@ -137,6 +139,7 @@ export const ConceptEmbed = ({ embed, fullWidth, heartButton: HeartButton }: Pro
137
139
  conceptHeartButton={HeartButton && <HeartButton embed={embed} />}
138
140
  conceptType={concept.conceptType}
139
141
  glossData={concept.glossData}
142
+ lang={lang}
140
143
  />
141
144
  );
142
145
  } else {
@@ -152,6 +155,7 @@ export const ConceptEmbed = ({ embed, fullWidth, heartButton: HeartButton }: Pro
152
155
  conceptHeartButton={HeartButton && <HeartButton embed={embed} />}
153
156
  conceptType={concept.conceptType}
154
157
  glossData={concept.glossData}
158
+ lang={lang}
155
159
  />
156
160
  );
157
161
  }
@@ -234,6 +238,7 @@ export const InlineConcept = ({
234
238
  glossData,
235
239
  conceptType,
236
240
  headerButtons,
241
+ lang,
237
242
  }: InlineConceptProps) => {
238
243
  const { t } = useTranslation();
239
244
  const anchorRef = useRef<HTMLDivElement>(null);
@@ -275,6 +280,7 @@ export const InlineConcept = ({
275
280
  heartButton={heartButton}
276
281
  headerButtons={headerButtons}
277
282
  conceptHeartButton={conceptHeartButton}
283
+ lang={lang}
278
284
  closeButton={
279
285
  <Close asChild>
280
286
  <IconButtonV2 aria-label={t('close')} variant="ghost">
@@ -310,6 +316,7 @@ export const BlockConcept = ({
310
316
  conceptHeartButton,
311
317
  glossData,
312
318
  conceptType,
319
+ lang,
313
320
  }: ConceptProps) => {
314
321
  const { t } = useTranslation();
315
322
  const anchorRef = useRef<HTMLDivElement>(null);
@@ -339,6 +346,7 @@ export const BlockConcept = ({
339
346
  id=""
340
347
  title={title.title}
341
348
  text={content}
349
+ lang={lang}
342
350
  visualElement={
343
351
  visualElement?.status === 'success' && (
344
352
  <>
@@ -383,6 +391,7 @@ export const BlockConcept = ({
383
391
  heartButton={heartButton}
384
392
  conceptHeartButton={conceptHeartButton}
385
393
  inPopover
394
+ lang={lang}
386
395
  closeButton={
387
396
  <Close asChild>
388
397
  <IconButtonV2 aria-label={t('close')} variant="ghost">
@@ -15,6 +15,7 @@ import { BlockConcept } from './ConceptEmbed';
15
15
 
16
16
  interface Props {
17
17
  embed: ConceptListMetaData;
18
+ lang?: string;
18
19
  }
19
20
 
20
21
  const ConceptList = styled.div`
@@ -30,7 +31,7 @@ const StyledSpan = styled.span`
30
31
  color: ${colors.support.red};
31
32
  `;
32
33
 
33
- const ConceptListEmbed = ({ embed }: Props) => {
34
+ const ConceptListEmbed = ({ embed, lang }: Props) => {
34
35
  const { t } = useTranslation();
35
36
  if (embed.status === 'error') {
36
37
  return <StyledSpan>{t('embed.conceptListError')}</StyledSpan>;
@@ -39,9 +40,9 @@ const ConceptListEmbed = ({ embed }: Props) => {
39
40
  return (
40
41
  <div>
41
42
  <Figure type="full" resizeIframe>
42
- {embedData.title && <h2>{embedData.title}</h2>}
43
+ {embedData.title && <h2 lang={lang}>{embedData.title}</h2>}
43
44
  <ConceptList>
44
- <ul>
45
+ <ul lang={lang}>
45
46
  {data.concepts.map(({ concept, visualElement }) => (
46
47
  <li key={concept.id}>
47
48
  <BlockConcept
@@ -27,6 +27,7 @@ interface Props {
27
27
  path?: string;
28
28
  heartButton?: HeartButtonType;
29
29
  inGrid?: boolean;
30
+ lang?: string;
30
31
  }
31
32
 
32
33
  export interface Author {
@@ -104,7 +105,7 @@ export const getCrop = (data: ImageEmbedData) => {
104
105
 
105
106
  const expandedSizes = '(min-width: 1024px) 1024px, 100vw';
106
107
 
107
- const ImageEmbed = ({ embed, previewAlt, heartButton: HeartButton, inGrid, path }: Props) => {
108
+ const ImageEmbed = ({ embed, previewAlt, heartButton: HeartButton, inGrid, path, lang }: Props) => {
108
109
  const [isBylineHidden, setIsBylineHidden] = useState(hideByline(embed.embedData.size));
109
110
  const [imageSizes, setImageSizes] = useState<string | undefined>(undefined);
110
111
 
@@ -162,6 +163,7 @@ const ImageEmbed = ({ embed, previewAlt, heartButton: HeartButton, inGrid, path
162
163
  onHideByline={() => setIsBylineHidden((p) => !p)}
163
164
  />
164
165
  }
166
+ lang={lang}
165
167
  />
166
168
  </ImageWrapper>
167
169
  {isBylineHidden || (isSmall(embedData.size) && !imageSizes) ? null : (
@@ -35,6 +35,7 @@ export interface ConceptNotionData {
35
35
  visualElement?: ConceptVisualElementMeta;
36
36
  conceptType: ConceptData['concept']['conceptType'];
37
37
  glossData?: ConceptData['concept']['glossData'];
38
+ lang?: string;
38
39
  }
39
40
 
40
41
  interface ConceptNotionProps extends RefAttributes<HTMLDivElement>, ConceptNotionData {
@@ -112,9 +113,11 @@ const NotionHeader = styled.div`
112
113
  ${fonts.sizes('22px', 1.2)};
113
114
  }
114
115
  small {
115
- padding-left: ${spacing.small};
116
- margin-left: ${spacing.xsmall};
117
- border-left: 1px solid ${colors.brand.greyLight};
116
+ &[data-show-separator='true'] {
117
+ border-left: 1px solid ${colors.brand.greyLight};
118
+ padding-left: ${spacing.small};
119
+ margin-left: ${spacing.xsmall};
120
+ }
118
121
  ${fonts.sizes('20px', 1.2)};
119
122
  font-weight: ${fonts.weight.normal};
120
123
  }
@@ -177,29 +180,31 @@ export const ConceptNotionV2 = forwardRef<HTMLDivElement, ConceptNotionProps>(
177
180
  conceptType,
178
181
  glossData,
179
182
  headerButtons,
183
+ lang,
180
184
  ...rest
181
185
  },
182
186
  ref,
183
187
  ) => {
184
188
  const { t } = useTranslation();
185
-
189
+ const isConcept = conceptType === 'concept';
186
190
  return (
187
191
  <div css={inPopover ? notionContentCss : undefined} {...rest} ref={ref}>
188
192
  <ContentPadding>
189
- <NotionHeader>
193
+ <NotionHeader data-show-separator={isConcept}>
190
194
  <h1>
191
- {title.title} {<small>{t(`searchPage.resultType.${conceptType}`)}</small>}
195
+ {isConcept && title.title}
196
+ {<small data-show-separator={isConcept}>{t(`searchPage.resultType.${conceptType}`)}</small>}
192
197
  </h1>
193
198
  <ButtonWrapper>
194
199
  {headerButtons}
195
200
  {closeButton}
196
201
  </ButtonWrapper>
197
202
  </NotionHeader>
198
- {conceptType !== 'gloss' ? (
203
+ {isConcept ? (
199
204
  <>
200
205
  <StyledNotionDialogContent>
201
206
  {visualElement?.resource === 'image' ? (
202
- <ImageEmbed embed={visualElement} heartButton={heartButton} />
207
+ <ImageEmbed embed={visualElement} heartButton={heartButton} lang={lang} />
203
208
  ) : visualElement?.resource === 'brightcove' ? (
204
209
  <BrightcoveEmbed embed={visualElement} heartButton={heartButton} />
205
210
  ) : visualElement?.resource === 'h5p' ? (
@@ -209,7 +214,7 @@ export const ConceptNotionV2 = forwardRef<HTMLDivElement, ConceptNotionProps>(
209
214
  ) : visualElement?.resource === 'external' ? (
210
215
  <ExternalEmbed embed={visualElement} />
211
216
  ) : null}
212
- {content && <NotionDialogText>{content}</NotionDialogText>}
217
+ {content && <NotionDialogText lang={lang}>{content}</NotionDialogText>}
213
218
  </StyledNotionDialogContent>
214
219
  {tags && (
215
220
  <ListWrapper>
@@ -14,6 +14,24 @@ import { FooterText } from './FooterText';
14
14
  import { EditorName } from './EditorName';
15
15
  import { LanguageSelector } from '../LanguageSelector';
16
16
 
17
+ const mockCommonLinks = [
18
+ {
19
+ to: 'https://ndla.no/about/om-ndla',
20
+ text: 'Om NDLA',
21
+ external: false,
22
+ },
23
+ {
24
+ to: 'https://ndla.no/about/about-ndla',
25
+ text: 'About NDLA',
26
+ external: false,
27
+ },
28
+ {
29
+ to: 'https://blogg.ndla.no/',
30
+ text: 'NDLA fagblogg',
31
+ external: true,
32
+ },
33
+ ];
34
+
17
35
  const mockFooterLinks = [
18
36
  {
19
37
  to: 'https://www.facebook.com/ndla.no',
@@ -84,6 +102,7 @@ export const Default: StoryObj<typeof Footer> = {};
84
102
  export const WithContentAndLinks: StoryObj<typeof Footer> = {
85
103
  args: {
86
104
  privacyLinks: privacyLinks,
105
+ commonLinks: mockCommonLinks,
87
106
  links: mockFooterLinks,
88
107
  },
89
108
  };
@@ -91,6 +110,7 @@ export const WithContentAndLinks: StoryObj<typeof Footer> = {
91
110
  export const WithoutContent: StoryObj<typeof Footer> = {
92
111
  args: {
93
112
  children: undefined,
113
+ commonLinks: mockCommonLinks,
94
114
  links: mockFooterLinks,
95
115
  privacyLinks: privacyLinks,
96
116
  },
@@ -99,6 +119,7 @@ export const WithoutContent: StoryObj<typeof Footer> = {
99
119
  export const WithLanguageSelector: StoryObj<typeof Footer> = {
100
120
  args: {
101
121
  privacyLinks: privacyLinks,
122
+ commonLinks: mockCommonLinks,
102
123
  links: mockFooterLinks,
103
124
  // eslint-disable-next-line no-console
104
125
  languageSelector: <LanguageSelector inverted locales={['nn', 'nb']} onSelect={console.log} />,
@@ -108,6 +129,7 @@ export const WithLanguageSelector: StoryObj<typeof Footer> = {
108
129
  export const WithAuthBlock: StoryObj<typeof Footer> = {
109
130
  args: {
110
131
  privacyLinks: privacyLinks,
132
+ commonLinks: mockCommonLinks,
111
133
  links: mockFooterLinks,
112
134
  // eslint-disable-next-line no-console
113
135
  languageSelector: <LanguageSelector inverted locales={['nn', 'nb']} onSelect={console.log} />,
@@ -113,6 +113,11 @@ const StyledLanguageWrapper = styled.div`
113
113
  type Props = {
114
114
  children: ReactNode;
115
115
  lang: Locale;
116
+ commonLinks?: {
117
+ to: string;
118
+ text: string;
119
+ external: boolean;
120
+ }[];
116
121
  links?: {
117
122
  to: string;
118
123
  text: string;
@@ -126,7 +131,7 @@ type Props = {
126
131
  auth?: ReactNode;
127
132
  };
128
133
 
129
- const Footer = ({ children, links, languageSelector, auth, privacyLinks }: Props) => {
134
+ const Footer = ({ children, commonLinks, links, languageSelector, auth, privacyLinks }: Props) => {
130
135
  const { t } = useTranslation();
131
136
 
132
137
  const mainContent = (
@@ -136,23 +141,24 @@ const Footer = ({ children, links, languageSelector, auth, privacyLinks }: Props
136
141
  </>
137
142
  );
138
143
 
139
- const footerContent = links ? (
140
- <>
141
- <StyledColumns>
142
- <div>
143
- <StyledFooterHeaderIcon />
144
- </div>
145
- <div>
146
- <StyledHeader>{t('footer.vision')}</StyledHeader>
147
- <FooterLinks links={links} />
148
- </div>
149
- </StyledColumns>
150
- <StyledHr />
151
- {mainContent}
152
- </>
153
- ) : (
154
- mainContent
155
- );
144
+ const footerContent =
145
+ links || commonLinks ? (
146
+ <>
147
+ <StyledColumns>
148
+ <div>
149
+ <StyledFooterHeaderIcon />
150
+ </div>
151
+ <div>
152
+ <StyledHeader>{t('footer.vision')}</StyledHeader>
153
+ <FooterLinks commonLinks={commonLinks} links={links} />
154
+ </div>
155
+ </StyledColumns>
156
+ <StyledHr />
157
+ {mainContent}
158
+ </>
159
+ ) : (
160
+ mainContent
161
+ );
156
162
 
157
163
  return (
158
164
  <>
@@ -26,24 +26,18 @@ const StyledLinksWrapper = styled.div`
26
26
  `;
27
27
 
28
28
  type FooterLinksProps = {
29
- links: {
29
+ commonLinks?: {
30
+ to: string;
31
+ text: string;
32
+ external: boolean;
33
+ }[];
34
+ links?: {
30
35
  to: string;
31
36
  text: string;
32
37
  icon: ReactNode;
33
38
  }[];
34
39
  };
35
40
 
36
- const commonLinks = [
37
- { key: 'omNdla', url: 'https://om.ndla.no' },
38
- {
39
- key: 'aboutNdla',
40
- url: 'https://om.ndla.no/about-ndla',
41
- },
42
- { key: 'blog', url: 'https://blogg.ndla.no' },
43
- { key: 'tips', url: 'https://blogg.ndla.no/elever' },
44
- { key: 'vacancies', url: 'https://om.ndla.no/jobb-for-ndla/' },
45
- ];
46
-
47
41
  const StyledNav = styled.nav`
48
42
  display: flex;
49
43
  flex-direction: column;
@@ -89,33 +83,32 @@ const StyledHeaderLinks = styled.h3`
89
83
  margin: ${spacing.xsmall} 0;
90
84
  `;
91
85
 
92
- const FooterLinks = ({ links }: FooterLinksProps) => {
86
+ const FooterLinks = ({ links, commonLinks }: FooterLinksProps) => {
93
87
  const { t } = useTranslation();
94
88
  return (
95
89
  <>
96
90
  <StyledLinksWrapper>
97
91
  <div>
98
- <StyledHeaderLinks id="otherLinks">
99
- {t('footer.linksHeader')} <Launch />
100
- </StyledHeaderLinks>
92
+ <StyledHeaderLinks id="otherLinks">{t('footer.linksHeader')}</StyledHeaderLinks>
101
93
  <StyledNav aria-labelledby="otherLinks">
102
- {commonLinks.map((link) => (
103
- <div key={link.url}>
94
+ {commonLinks?.map((link) => (
95
+ <div key={link.to}>
104
96
  <StyledSafeLink
105
- key={t(`footer.ndlaLinks.${link.key}`)}
106
- aria-label={t(`footer.ndlaLinks.${link.key}`)}
107
- to={link.url}
108
- target="_blank"
97
+ key={link.text}
98
+ aria-label={link.text}
99
+ to={link.to}
100
+ target={link.external ? '_blank' : ''}
109
101
  rel="noopener noreferrer"
110
102
  >
111
- {t(`footer.ndlaLinks.${link.key}`)}
103
+ {link.text}
104
+ {link.external && <Launch />}
112
105
  </StyledSafeLink>
113
106
  </div>
114
107
  ))}
115
108
  </StyledNav>
116
109
  </div>
117
110
  <StyledNav aria-label={t('footer.socialMedia')}>
118
- {links.map((link) => (
111
+ {links?.map((link) => (
119
112
  <StyledSocialMediaLinkWrapper key={link.to}>
120
113
  <StyledSocialMediaIcon>{link.icon}</StyledSocialMediaIcon>
121
114
  <StyledSafeLink to={link.to}>
@@ -19,6 +19,7 @@ interface Props {
19
19
  id: string;
20
20
  isWide?: boolean;
21
21
  licenseBox?: ReactNode;
22
+ lang?: string;
22
23
  }
23
24
 
24
25
  export const FRONTPAGE_ARTICLE_MAX_WIDTH = '773px';
@@ -56,7 +57,7 @@ const StyledArticle = styled.article`
56
57
  }
57
58
  `;
58
59
 
59
- export const FrontpageArticle = ({ article, id, isWide, licenseBox }: Props) => {
60
+ export const FrontpageArticle = ({ article, id, isWide, licenseBox, lang }: Props) => {
60
61
  const { height = 0 } = useMastheadHeight();
61
62
  const cssVars = useMemo(() => ({ '--masthead-height': `${height}px` } as unknown as CSSProperties), [height]);
62
63
  const { title, introduction, content } = article;
@@ -71,10 +72,10 @@ export const FrontpageArticle = ({ article, id, isWide, licenseBox }: Props) =>
71
72
 
72
73
  return (
73
74
  <StyledArticle style={cssVars}>
74
- <Heading id={id} headingStyle="h1-resource" element="h1" margin="normal" tabIndex={-1}>
75
+ <Heading id={id} headingStyle="h1-resource" element="h1" margin="normal" tabIndex={-1} lang={lang}>
75
76
  {title}
76
77
  </Heading>
77
- <Text textStyle="ingress" element="div">
78
+ <Text textStyle="ingress" element="div" lang={lang}>
78
79
  {introduction}
79
80
  </Text>
80
81
  {content}
@@ -38,6 +38,7 @@ export interface Props {
38
38
  }
39
39
 
40
40
  const Container = styled.div`
41
+ font-family: ${fonts.sans};
41
42
  display: flex;
42
43
  flex-direction: column;
43
44
  background-color: ${colors.background.lightBlue};
@@ -78,6 +78,7 @@ interface Props {
78
78
  crop?: ImageCrop;
79
79
  focalPoint?: ImageFocalPoint;
80
80
  border?: string;
81
+ lang?: string;
81
82
  }
82
83
 
83
84
  const Image = ({
@@ -92,6 +93,7 @@ const Image = ({
92
93
  expandButton,
93
94
  fallbackWidth = 1024,
94
95
  border,
96
+ lang,
95
97
  ...rest
96
98
  }: Props) => {
97
99
  const srcSet = rest.srcSet ?? getSrcSet(src, crop, focalPoint);
@@ -101,7 +103,7 @@ const Image = ({
101
103
  if (contentType && contentType === 'image/gif') {
102
104
  return (
103
105
  <StyledImageWrapper data-border={border}>
104
- <StyledImage alt={alt} loading={loading} src={`${src}`} {...rest} data-border={border} />
106
+ <StyledImage alt={alt} loading={loading} src={`${src}`} {...rest} data-border={border} lang={lang} />
105
107
  </StyledImageWrapper>
106
108
  );
107
109
  }
@@ -110,7 +112,14 @@ const Image = ({
110
112
  <StyledImageWrapper data-svg={contentType === 'image/svg+xml'} data-border={border}>
111
113
  <picture>
112
114
  <source type={contentType} srcSet={srcSet} sizes={sizes} />
113
- <StyledImage alt={alt} loading={loading} src={`${src}?${queryString}`} {...rest} data-border={border} />
115
+ <StyledImage
116
+ alt={alt}
117
+ loading={loading}
118
+ src={`${src}?${queryString}`}
119
+ {...rest}
120
+ data-border={border}
121
+ lang={lang}
122
+ />
114
123
  </picture>
115
124
  {expandButton}
116
125
  </StyledImageWrapper>
@@ -59,14 +59,15 @@ interface Props {
59
59
  };
60
60
  title: string;
61
61
  subtitle: string;
62
+ lang?: string;
62
63
  }
63
64
 
64
- const KeyFigure = ({ image, title, subtitle }: Props) => {
65
+ const KeyFigure = ({ image, title, subtitle, lang }: Props) => {
65
66
  return (
66
67
  <ContentWrapper>
67
68
  <StyledImage src={image?.src} width={150} height={150} alt={image?.alt} />
68
- <TitleWrapper>{title}</TitleWrapper>
69
- <SubTitleWrapper>{subtitle}</SubTitleWrapper>
69
+ <TitleWrapper lang={lang}>{title}</TitleWrapper>
70
+ <SubTitleWrapper lang={lang}>{subtitle}</SubTitleWrapper>
70
71
  </ContentWrapper>
71
72
  );
72
73
  };
@@ -71,9 +71,10 @@ export type NotionProps = {
71
71
  visualElement: ReactNode;
72
72
  imageElement?: ReactNode;
73
73
  children?: ReactNode;
74
+ lang?: string;
74
75
  };
75
76
 
76
- const Notion = ({ id, labels = [], text, title, visualElement, imageElement, children }: NotionProps) => {
77
+ const Notion = ({ id, labels = [], text, title, visualElement, imageElement, children, lang }: NotionProps) => {
77
78
  const { t } = useTranslation();
78
79
 
79
80
  return (
@@ -81,7 +82,7 @@ const Notion = ({ id, labels = [], text, title, visualElement, imageElement, chi
81
82
  <ContentWrapper>
82
83
  {imageElement}
83
84
  {visualElement}
84
- <TextWrapper hasVisualElement={!!(imageElement || visualElement)}>
85
+ <TextWrapper hasVisualElement={!!(imageElement || visualElement)} lang={lang}>
85
86
  <b>{title.trim()}</b>
86
87
  {text}
87
88
  {!!labels.length && (
@@ -22,6 +22,7 @@ interface Props {
22
22
  dangerouslySetInnerHTML?: {
23
23
  __html: string;
24
24
  };
25
+ lang?: string;
25
26
  }
26
27
 
27
28
  const ScrollBorder = styled.div`