Skip to content

Commit 7228f53

Browse files
Adds OpenJS Footer (#8577)
* introduce third slot to footer * add and order new keys and links for footer * add withLegal with full translation support * add withLegal to the new legal slot * you'd think the formatter and precommit would catch this * improve types * rename key * fix html entities * match style of markdown.css anchors * fix rendering of legal paragraph * change to NavLink
1 parent c475ee6 commit 7228f53

File tree

6 files changed

+186
-54
lines changed

6 files changed

+186
-54
lines changed

apps/site/components/withFooter.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { siteNavigation } from '#site/next.json.mjs';
88

99
import type { FC } from 'react';
1010

11+
import WithLegal from './withLegal';
1112
import WithNodeRelease from './withNodeRelease';
1213

1314
const WithFooter: FC = () => {
@@ -18,7 +19,10 @@ const WithFooter: FC = () => {
1819

1920
const navigation = {
2021
socialLinks,
21-
footerLinks: footerLinks.map(link => ({ ...link, text: t(link.text) })),
22+
footerLinks: footerLinks.map(link => ({
23+
...link,
24+
translation: t(link.text),
25+
})),
2226
};
2327

2428
const primary = (
@@ -50,12 +54,14 @@ const WithFooter: FC = () => {
5054
</div>
5155
);
5256

57+
const legal = <WithLegal footerLinks={navigation.footerLinks} />;
58+
5359
return (
5460
<Footer
5561
navigation={navigation}
5662
as={Link}
5763
pathname={pathname}
58-
slots={{ primary }}
64+
slots={{ primary, legal }}
5965
/>
6066
);
6167
};

apps/site/components/withLegal.tsx

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import NavItem from '@node-core/ui-components/Containers/NavBar/NavItem';
2+
import { useTranslations } from 'next-intl';
3+
4+
import Link from '#site/components/Link';
5+
6+
import type { FC } from 'react';
7+
8+
type LegalProps = {
9+
footerLinks: Array<{
10+
text: string;
11+
link: string;
12+
translation: string;
13+
}>;
14+
};
15+
16+
/**
17+
* These keys match the following locations, and are kept in sync to lessen duplication:
18+
* - translation keys within [locale].json components.containers.footer.links
19+
* - keys within the large [locale].json components.containers.footer.legal paragraph
20+
* - used directly to find the passed links from navigation.footerLinks
21+
*/
22+
const RICH_TRANSLATION_KEYS = [
23+
'foundationName',
24+
'trademarkPolicy',
25+
'trademarkList',
26+
'termsOfUse',
27+
'privacyPolicy',
28+
'bylaws',
29+
'codeOfConduct',
30+
'cookiePolicy',
31+
];
32+
33+
const WithLegal: FC<LegalProps> = ({ footerLinks }) => {
34+
const t = useTranslations();
35+
36+
/**
37+
* Takes the footerLinks from navigation constants and returns the link based on the final part of the translation key.
38+
*
39+
* Example: {
40+
"link": "https://openjsf.org/",
41+
"text": "components.containers.footer.links.foundationName"
42+
},
43+
*
44+
*
45+
* @param key the final part of a translation string
46+
* @returns the link URL matching the translation key
47+
*/
48+
const getLinkFromTranslationKey = (key: string) => {
49+
return footerLinks.find(link => link.text.split('.').pop() === key)?.link;
50+
};
51+
52+
const richComponents = RICH_TRANSLATION_KEYS.reduce(
53+
(acc, key) => {
54+
acc[key] = (chunks: React.ReactNode) => (
55+
<Link href={getLinkFromTranslationKey(key)}>{chunks}</Link>
56+
);
57+
return acc;
58+
},
59+
{} as Record<string, (text: React.ReactNode) => React.ReactNode>
60+
);
61+
62+
return (
63+
<>
64+
<p>{t.rich('components.containers.footer.legal', richComponents)}</p>
65+
66+
<p>
67+
{footerLinks.map(link => (
68+
<NavItem
69+
key={link.link}
70+
type="footer"
71+
href={link.link}
72+
as={Link}
73+
pathname={'/'}
74+
>
75+
{link.translation}
76+
</NavItem>
77+
))}
78+
</p>
79+
</>
80+
);
81+
};
82+
83+
export default WithLegal;

apps/site/navigation.json

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,24 +33,40 @@
3333
},
3434
"footerLinks": [
3535
{
36-
"link": "https://trademark-policy.openjsf.org/",
37-
"text": "components.containers.footer.links.trademarkPolicy"
36+
"link": "https://openjsf.org/",
37+
"text": "components.containers.footer.links.foundationName"
38+
},
39+
{
40+
"link": "https://terms-of-use.openjsf.org/",
41+
"text": "components.containers.footer.links.termsOfUse"
3842
},
3943
{
4044
"link": "https://privacy-policy.openjsf.org/",
4145
"text": "components.containers.footer.links.privacyPolicy"
4246
},
47+
{
48+
"link": "https://bylaws.openjsf.org/",
49+
"text": "components.containers.footer.links.bylaws"
50+
},
4351
{
4452
"link": "https://github.com/openjs-foundation/cross-project-council/blob/main/CODE_OF_CONDUCT.md",
4553
"text": "components.containers.footer.links.codeOfConduct"
4654
},
4755
{
48-
"link": "https://github.com/nodejs/node/security/policy",
49-
"text": "components.containers.footer.links.security"
56+
"link": "https://trademark-policy.openjsf.org/",
57+
"text": "components.containers.footer.links.trademarkPolicy"
5058
},
5159
{
52-
"link": "https://openjsf.org/",
53-
"text": "components.containers.footer.links.openJSFoundation"
60+
"link": "https://trademark-list.openjsf.org/",
61+
"text": "components.containers.footer.links.trademarkList"
62+
},
63+
{
64+
"link": "https://www.linuxfoundation.org/cookies/",
65+
"text": "components.containers.footer.links.cookiePolicy"
66+
},
67+
{
68+
"link": "https://github.com/nodejs/node/security/policy",
69+
"text": "components.containers.footer.links.security"
5470
}
5571
],
5672
"socialLinks": [

packages/i18n/src/locales/en.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,16 @@
22
"components": {
33
"containers": {
44
"footer": {
5+
"legal": "Copyright <foundationName>OpenJS Foundation</foundationName> and Node.js contributors. All rights reserved. The <foundationName>OpenJS Foundation</foundationName> has registered trademarks and uses trademarks. For a list of trademarks of the <foundationName>OpenJS Foundation</foundationName>, please see our <trademarkPolicy>Trademark Policy</trademarkPolicy> and <trademarkList>Trademark List</trademarkList>. Trademarks and logos not indicated on the <trademarkList>list of OpenJS Foundation trademarks</trademarkList> are trademarks™ or registered® trademarks of their respective holders. Use of them does not imply any affiliation with or endorsement by them.",
56
"links": {
6-
"openJSFoundation": "OpenJS Foundation",
7-
"trademarkPolicy": "Trademark Policy",
7+
"foundationName": "OpenJS Foundation",
8+
"termsOfUse": "Terms of Use",
89
"privacyPolicy": "Privacy Policy",
10+
"bylaws": "Bylaws",
911
"codeOfConduct": "Code of Conduct",
12+
"trademarkPolicy": "Trademark Policy",
13+
"trademarkList": "Trademark List",
14+
"cookiePolicy": "Cookie Policy",
1015
"security": "Security Policy"
1116
},
1217
"releasePills": {

packages/ui-components/src/Containers/Footer/index.module.css

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,24 @@
99
border-neutral-200
1010
bg-white
1111
py-4
12+
text-neutral-500
1213
sm:px-8
13-
md:flex-row
1414
md:justify-between
1515
md:py-5
1616
dark:border-neutral-900
1717
dark:bg-neutral-950;
1818

19+
.row {
20+
@apply flex
21+
flex-col
22+
items-center
23+
gap-6
24+
md:flex-row
25+
md:justify-between
26+
md:gap-0
27+
md:self-stretch;
28+
}
29+
1930
.sectionPrimary {
2031
@apply flex
2132
flex-wrap
@@ -43,4 +54,34 @@
4354
gap-1;
4455
}
4556
}
57+
58+
.legal {
59+
@apply flex
60+
flex-col
61+
gap-2
62+
px-4
63+
text-center
64+
text-xs
65+
text-balance
66+
md:px-14;
67+
68+
p {
69+
@apply text-center
70+
text-sm
71+
text-neutral-800
72+
dark:text-neutral-500;
73+
}
74+
75+
a {
76+
@apply max-xs:font-semibold
77+
text-green-600
78+
dark:text-green-400;
79+
80+
&:hover {
81+
@apply cursor-pointer
82+
text-green-900
83+
dark:text-green-200;
84+
}
85+
}
86+
}
4687
}

packages/ui-components/src/Containers/Footer/index.tsx

Lines changed: 24 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import classNames from 'classnames';
2+
13
import NavItem from '#ui/Containers/NavBar/NavItem';
24
import {
35
Bluesky,
@@ -38,6 +40,7 @@ type Navigation = {
3840
type ExtraNavigationSlots = {
3941
primary?: ReactNode;
4042
secondary?: ReactNode;
43+
legal?: ReactNode;
4144
};
4245

4346
type FooterProps = {
@@ -53,56 +56,34 @@ const Footer: FC<FooterProps> = ({
5356
navigation,
5457
slots,
5558
}) => {
56-
const openJSlink = navigation.footerLinks.at(-1)!;
57-
5859
return (
5960
<footer className={styles.footer}>
60-
<div className={styles.sectionPrimary}>
61-
{slots?.primary}
62-
63-
{navigation.footerLinks.slice(0, -1).map(item => (
64-
<NavItem
65-
key={item.link}
66-
type="footer"
67-
href={item.link}
68-
as={as}
69-
pathname={pathname}
70-
>
71-
{item.text}
72-
</NavItem>
73-
))}
74-
</div>
75-
76-
<div className={styles.sectionSecondary}>
77-
{slots?.secondary}
61+
<div className={styles.row}>
62+
<div className={styles.sectionPrimary}>{slots?.primary}</div>
7863

79-
<NavItem
80-
type="footer"
81-
href={openJSlink.link}
82-
as={as}
83-
pathname={pathname}
84-
>
85-
&copy; {openJSlink.text}
86-
</NavItem>
64+
<div className={styles.sectionSecondary}>
65+
{slots?.secondary}
8766

88-
<div className={styles.social}>
89-
{navigation.socialLinks.map(link => {
90-
const SocialIcon = footerSocialIcons[link.icon];
67+
<div className={styles.social}>
68+
{navigation.socialLinks.map(link => {
69+
const SocialIcon = footerSocialIcons[link.icon];
9170

92-
return (
93-
<NavItem
94-
key={link.icon}
95-
href={link.link}
96-
type="footer"
97-
as={as}
98-
pathname={pathname}
99-
>
100-
<SocialIcon width={20} height={20} aria-label={link.link} />
101-
</NavItem>
102-
);
103-
})}
71+
return (
72+
<NavItem
73+
key={link.icon}
74+
href={link.link}
75+
type="footer"
76+
as={as}
77+
pathname={pathname}
78+
>
79+
<SocialIcon width={20} height={20} aria-label={link.link} />
80+
</NavItem>
81+
);
82+
})}
83+
</div>
10484
</div>
10585
</div>
86+
<div className={classNames(styles.row, styles.legal)}>{slots?.legal}</div>
10687
</footer>
10788
);
10889
};

0 commit comments

Comments
 (0)