Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions apps/site/components/withFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { siteNavigation } from '#site/next.json.mjs';

import type { FC } from 'react';

import WithLegal from './withLegal';
import WithNodeRelease from './withNodeRelease';

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

const navigation = {
socialLinks,
footerLinks: footerLinks.map(link => ({ ...link, text: t(link.text) })),
footerLinks: footerLinks.map(link => ({
...link,
translation: t(link.text),
})),
};

const primary = (
Expand Down Expand Up @@ -50,12 +54,14 @@ const WithFooter: FC = () => {
</div>
);

const legal = <WithLegal footerLinks={navigation.footerLinks} />;

return (
<Footer
navigation={navigation}
as={Link}
pathname={pathname}
slots={{ primary }}
slots={{ primary, legal }}
/>
);
};
Expand Down
83 changes: 83 additions & 0 deletions apps/site/components/withLegal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import NavItem from '@node-core/ui-components/Containers/NavBar/NavItem';
import { useTranslations } from 'next-intl';

import Link from '#site/components/Link';

import type { FC } from 'react';

type LegalProps = {
footerLinks: Array<{
text: string;
link: string;
translation: string;
}>;
};

/**
* These keys match the following locations, and are kept in sync to lessen duplication:
* - translation keys within [locale].json components.containers.footer.links
* - keys within the large [locale].json components.containers.footer.legal paragraph
* - used directly to find the passed links from navigation.footerLinks
*/
const RICH_TRANSLATION_KEYS = [
'foundationName',
'trademarkPolicy',
'trademarkList',
'termsOfUse',
'privacyPolicy',
'bylaws',
'codeOfConduct',
'cookiePolicy',
];

const WithLegal: FC<LegalProps> = ({ footerLinks }) => {
const t = useTranslations();

/**
* Takes the footerLinks from navigation constants and returns the link based on the final part of the translation key.
*
* Example: {
"link": "https://openjsf.org/",
"text": "components.containers.footer.links.foundationName"
},
*
*
* @param key the final part of a translation string
* @returns the link URL matching the translation key
*/
const getLinkFromTranslationKey = (key: string) => {
return footerLinks.find(link => link.text.split('.').pop() === key)?.link;
};

const richComponents = RICH_TRANSLATION_KEYS.reduce(
(acc, key) => {
acc[key] = (chunks: React.ReactNode) => (
<Link href={getLinkFromTranslationKey(key)}>{chunks}</Link>
);
return acc;
},
{} as Record<string, (text: React.ReactNode) => React.ReactNode>
);

return (
<>
<p>{t.rich('components.containers.footer.legal', richComponents)}</p>

<p>
{footerLinks.map(link => (
<NavItem
key={link.link}
type="footer"
href={link.link}
as={Link}
pathname={'/'}
>
{link.translation}
</NavItem>
))}
</p>
</>
);
};

export default WithLegal;
28 changes: 22 additions & 6 deletions apps/site/navigation.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,24 +33,40 @@
},
"footerLinks": [
{
"link": "https://trademark-policy.openjsf.org/",
"text": "components.containers.footer.links.trademarkPolicy"
"link": "https://openjsf.org/",
"text": "components.containers.footer.links.foundationName"
},
{
"link": "https://terms-of-use.openjsf.org/",
"text": "components.containers.footer.links.termsOfUse"
},
{
"link": "https://privacy-policy.openjsf.org/",
"text": "components.containers.footer.links.privacyPolicy"
},
{
"link": "https://bylaws.openjsf.org/",
"text": "components.containers.footer.links.bylaws"
},
{
"link": "https://github.com/openjs-foundation/cross-project-council/blob/main/CODE_OF_CONDUCT.md",
"text": "components.containers.footer.links.codeOfConduct"
},
{
"link": "https://github.com/nodejs/node/security/policy",
"text": "components.containers.footer.links.security"
"link": "https://trademark-policy.openjsf.org/",
"text": "components.containers.footer.links.trademarkPolicy"
},
{
"link": "https://openjsf.org/",
"text": "components.containers.footer.links.openJSFoundation"
"link": "https://trademark-list.openjsf.org/",
"text": "components.containers.footer.links.trademarkList"
},
{
"link": "https://www.linuxfoundation.org/cookies/",
"text": "components.containers.footer.links.cookiePolicy"
},
{
"link": "https://github.com/nodejs/node/security/policy",
"text": "components.containers.footer.links.security"
}
],
"socialLinks": [
Expand Down
9 changes: 7 additions & 2 deletions packages/i18n/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@
"components": {
"containers": {
"footer": {
"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.",
"links": {
"openJSFoundation": "OpenJS Foundation",
"trademarkPolicy": "Trademark Policy",
"foundationName": "OpenJS Foundation",
"termsOfUse": "Terms of Use",
"privacyPolicy": "Privacy Policy",
"bylaws": "Bylaws",
"codeOfConduct": "Code of Conduct",
"trademarkPolicy": "Trademark Policy",
"trademarkList": "Trademark List",
"cookiePolicy": "Cookie Policy",
"security": "Security Policy"
},
"releasePills": {
Expand Down
43 changes: 42 additions & 1 deletion packages/ui-components/src/Containers/Footer/index.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,24 @@
border-neutral-200
bg-white
py-4
text-neutral-500
sm:px-8
md:flex-row
md:justify-between
md:py-5
dark:border-neutral-900
dark:bg-neutral-950;

.row {
@apply flex
flex-col
items-center
gap-6
md:flex-row
md:justify-between
md:gap-0
md:self-stretch;
}

.sectionPrimary {
@apply flex
flex-wrap
Expand Down Expand Up @@ -43,4 +54,34 @@
gap-1;
}
}

.legal {
@apply flex
flex-col
gap-2
px-4
text-center
text-xs
text-balance
md:px-14;

p {
@apply text-center
text-sm
text-neutral-800
dark:text-neutral-500;
}

a {
@apply max-xs:font-semibold
text-green-600
dark:text-green-400;

&:hover {
@apply cursor-pointer
text-green-900
dark:text-green-200;
}
}
}
}
67 changes: 24 additions & 43 deletions packages/ui-components/src/Containers/Footer/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import classNames from 'classnames';

import NavItem from '#ui/Containers/NavBar/NavItem';
import {
Bluesky,
Expand Down Expand Up @@ -38,6 +40,7 @@ type Navigation = {
type ExtraNavigationSlots = {
primary?: ReactNode;
secondary?: ReactNode;
legal?: ReactNode;
};

type FooterProps = {
Expand All @@ -53,56 +56,34 @@ const Footer: FC<FooterProps> = ({
navigation,
slots,
}) => {
const openJSlink = navigation.footerLinks.at(-1)!;

return (
<footer className={styles.footer}>
<div className={styles.sectionPrimary}>
{slots?.primary}

{navigation.footerLinks.slice(0, -1).map(item => (
<NavItem
key={item.link}
type="footer"
href={item.link}
as={as}
pathname={pathname}
>
{item.text}
</NavItem>
))}
</div>

<div className={styles.sectionSecondary}>
{slots?.secondary}
<div className={styles.row}>
<div className={styles.sectionPrimary}>{slots?.primary}</div>

<NavItem
type="footer"
href={openJSlink.link}
as={as}
pathname={pathname}
>
&copy; {openJSlink.text}
</NavItem>
<div className={styles.sectionSecondary}>
{slots?.secondary}

<div className={styles.social}>
{navigation.socialLinks.map(link => {
const SocialIcon = footerSocialIcons[link.icon];
<div className={styles.social}>
{navigation.socialLinks.map(link => {
const SocialIcon = footerSocialIcons[link.icon];

return (
<NavItem
key={link.icon}
href={link.link}
type="footer"
as={as}
pathname={pathname}
>
<SocialIcon width={20} height={20} aria-label={link.link} />
</NavItem>
);
})}
return (
<NavItem
key={link.icon}
href={link.link}
type="footer"
as={as}
pathname={pathname}
>
<SocialIcon width={20} height={20} aria-label={link.link} />
</NavItem>
);
})}
</div>
</div>
</div>
<div className={classNames(styles.row, styles.legal)}>{slots?.legal}</div>
</footer>
);
};
Expand Down
Loading