diff --git a/apps/site/components/withFooter.tsx b/apps/site/components/withFooter.tsx index b6fb622df253f..72a200ccb18df 100644 --- a/apps/site/components/withFooter.tsx +++ b/apps/site/components/withFooter.tsx @@ -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 = () => { @@ -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 = ( @@ -50,12 +54,14 @@ const WithFooter: FC = () => { ); + const legal = ; + return ( ); }; diff --git a/apps/site/components/withLegal.tsx b/apps/site/components/withLegal.tsx new file mode 100644 index 0000000000000..45d76b20e38ee --- /dev/null +++ b/apps/site/components/withLegal.tsx @@ -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 = ({ 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) => ( + {chunks} + ); + return acc; + }, + {} as Record React.ReactNode> + ); + + return ( + <> + {t.rich('components.containers.footer.legal', richComponents)} + + + {footerLinks.map(link => ( + + {link.translation} + + ))} + + > + ); +}; + +export default WithLegal; diff --git a/apps/site/navigation.json b/apps/site/navigation.json index a767fafa79e8c..afac06ea50205 100644 --- a/apps/site/navigation.json +++ b/apps/site/navigation.json @@ -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": [ diff --git a/packages/i18n/src/locales/en.json b/packages/i18n/src/locales/en.json index ecf327b1018ec..940491462ec30 100644 --- a/packages/i18n/src/locales/en.json +++ b/packages/i18n/src/locales/en.json @@ -2,11 +2,16 @@ "components": { "containers": { "footer": { + "legal": "Copyright OpenJS Foundation and Node.js contributors. All rights reserved. The OpenJS Foundation has registered trademarks and uses trademarks. For a list of trademarks of the OpenJS Foundation, please see our Trademark Policy and Trademark List. Trademarks and logos not indicated on the list of OpenJS Foundation trademarks 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": { diff --git a/packages/ui-components/src/Containers/Footer/index.module.css b/packages/ui-components/src/Containers/Footer/index.module.css index d52a1027da39a..5347cdf8da6cb 100644 --- a/packages/ui-components/src/Containers/Footer/index.module.css +++ b/packages/ui-components/src/Containers/Footer/index.module.css @@ -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 @@ -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; + } + } + } } diff --git a/packages/ui-components/src/Containers/Footer/index.tsx b/packages/ui-components/src/Containers/Footer/index.tsx index cb006285d8410..419bf770bbb7c 100644 --- a/packages/ui-components/src/Containers/Footer/index.tsx +++ b/packages/ui-components/src/Containers/Footer/index.tsx @@ -1,3 +1,5 @@ +import classNames from 'classnames'; + import NavItem from '#ui/Containers/NavBar/NavItem'; import { Bluesky, @@ -38,6 +40,7 @@ type Navigation = { type ExtraNavigationSlots = { primary?: ReactNode; secondary?: ReactNode; + legal?: ReactNode; }; type FooterProps = { @@ -53,56 +56,34 @@ const Footer: FC = ({ navigation, slots, }) => { - const openJSlink = navigation.footerLinks.at(-1)!; - return (
{t.rich('components.containers.footer.legal', richComponents)}
+ {footerLinks.map(link => ( + + {link.translation} + + ))} +