diff --git a/components/sections/Footer/index.module.css b/components/sections/Footer/index.module.css new file mode 100644 index 0000000000000..b0cd6da94e0de --- /dev/null +++ b/components/sections/Footer/index.module.css @@ -0,0 +1,49 @@ +.footer { + @apply flex + flex-col + items-center + gap-6 + px-8 + py-4 + md:flex-row + md:justify-between + md:py-5; + + .sectionPrimary { + @apply flex + flex-wrap + content-start + items-center + justify-center + gap-1 + self-stretch; + + a { + @apply whitespace-nowrap; + } + } + + .sectionSecondary { + @apply flex + flex-col + items-center + gap-1 + md:flex-row; + + .social { + @apply flex + items-center + gap-1; + } + } + + .darkImage { + @apply hidden + dark:block; + } + + .lightImage { + @apply block + dark:hidden; + } +} diff --git a/components/sections/Footer/index.stories.tsx b/components/sections/Footer/index.stories.tsx new file mode 100644 index 0000000000000..44dccdfd67a46 --- /dev/null +++ b/components/sections/Footer/index.stories.tsx @@ -0,0 +1,10 @@ +import type { Meta as MetaObj, StoryObj } from '@storybook/react'; + +import Footer from './index'; + +type Story = StoryObj; +type Meta = MetaObj; + +export const Default: Story = {}; + +export default { component: Footer } as Meta; diff --git a/components/sections/Footer/index.tsx b/components/sections/Footer/index.tsx new file mode 100644 index 0000000000000..d11310bab6d45 --- /dev/null +++ b/components/sections/Footer/index.tsx @@ -0,0 +1,58 @@ +import classNames from 'classnames'; +import Image from 'next/image'; +import type { FC } from 'react'; +import { FormattedMessage } from 'react-intl'; + +import NavItem from '@/components/sections/NavItem'; +import { useSiteConfig } from '@/hooks/useSiteConfig'; + +import styles from './index.module.css'; + +const Footer: FC = () => { + const { footerLinks, socialLinks } = useSiteConfig(); + + const openJSlink = footerLinks.at(-1)!; + + return ( +
+
+ {footerLinks.slice(0, -1).map(item => ( + + + + ))} +
+
+ + © + +
+ {socialLinks.map(link => { + const navClass = classNames({ + [styles.darkImage]: link.kind === 'dark', + [styles.lightImage]: link.kind === 'light', + }); + + return ( + + {link.alt + + ); + })} +
+
+
+ ); +}; + +export default Footer; diff --git a/components/sections/NavItem/index.stories.tsx b/components/sections/NavItem/index.stories.tsx index 358b37feb8998..3d1a94ff05b01 100644 --- a/components/sections/NavItem/index.stories.tsx +++ b/components/sections/NavItem/index.stories.tsx @@ -8,21 +8,28 @@ type Meta = MetaObj; export const Default: Story = { args: { href: '/learn', - label: 'Learn', + children: 'Learn', }, }; export const WithExternalLink: Story = { args: { href: 'https://nodejs.org/en', - label: 'Learn', + children: 'Learn', + }, +}; + +export const WithChildren: Story = { + args: { + href: 'https://nodejs.org/en', + children: Learn, }, }; export const FooterItem: Story = { args: { href: '/about', - label: 'Trademark Policy', + children: 'Trademark Policy', type: 'footer', }, }; diff --git a/components/sections/NavItem/index.tsx b/components/sections/NavItem/index.tsx index 7ce1d73235206..68d12c22e0034 100644 --- a/components/sections/NavItem/index.tsx +++ b/components/sections/NavItem/index.tsx @@ -1,6 +1,6 @@ import { ArrowUpRightIcon } from '@heroicons/react/24/solid'; import classNames from 'classnames'; -import type { FC } from 'react'; +import type { FC, PropsWithChildren } from 'react'; import { useMemo } from 'react'; import LocalizedLink from '@/components/LocalizedLink'; @@ -11,11 +11,16 @@ type NavItemType = 'nav' | 'footer'; type NavItemProps = { href: string; - label?: string; type?: NavItemType; + className?: string; }; -const NavItem: FC = ({ href, label, type = 'nav' }) => { +const NavItem: FC> = ({ + href, + type = 'nav', + children, + className, +}) => { const showIcon = useMemo( () => type === 'nav' && /^https?:\/\//.test(href), [href, type] @@ -24,9 +29,9 @@ const NavItem: FC = ({ href, label, type = 'nav' }) => { return ( - {label} + {children} {showIcon && } ); diff --git a/i18n/locales/en.json b/i18n/locales/en.json index 4c1f2e4be56c7..b6136e52ddbbc 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -8,6 +8,11 @@ "components.header.links.security": "Security", "components.header.links.certification": "Certification", "components.header.links.blog": "News", + "components.footer.links.trademarkPolicy": "Trademark Policy", + "components.footer.links.privacyPolicy": "Privacy Policy", + "components.footer.links.codeOfConduct": "Code of Conduct", + "components.footer.links.security": "Security", + "components.footer.links.openJS": "OpenJS Foundation", "components.navigation.about.links.governance": "Governance", "components.navigation.docs.links.es6": "ES6 and beyond", "components.navigation.docs.links.apiLts": "{fullLtsNodeVersion} API {spanLts}", diff --git a/providers/siteProvider.tsx b/providers/siteProvider.tsx index ad535fbffe7d6..198d1dc3953a1 100644 --- a/providers/siteProvider.tsx +++ b/providers/siteProvider.tsx @@ -4,8 +4,10 @@ import type { FC, PropsWithChildren } from 'react'; import { siteConfig } from '@/next.json.mjs'; import type { SiteConfig } from '@/types'; -export const SiteContext = createContext(siteConfig); +const config = siteConfig as SiteConfig; + +export const SiteContext = createContext(config); export const SiteProvider: FC = ({ children }) => ( - {children} + {children} ); diff --git a/site.json b/site.json index a663036cd836d..be2a200a6cb54 100644 --- a/site.json +++ b/site.json @@ -38,5 +38,65 @@ "text": "Security releases now available", "link": "https://nodejs.org/en/blog/vulnerability/october-2023-security-releases/" } - } + }, + "footerLinks": [ + { + "link": "https://openjsf.org/wp-content/uploads/sites/84/2021/01/OpenJS-Foundation-Trademark-Policy-2021-01-12.docx.pdf", + "text": "components.footer.links.trademarkPolicy" + }, + { + "link": "https://openjsf.org/wp-content/uploads/sites/84/2021/04/OpenJS-Foundation-Privacy-Policy-2019-11-15.pdf", + "text": "components.footer.links.privacyPolicy" + }, + { + "link": "https://github.com/openjs-foundation/cross-project-council/blob/main/CODE_OF_CONDUCT.md", + "text": "components.footer.links.codeOfConduct" + }, + { + "link": "https://github.com/nodejs/node/blob/HEAD/SECURITY.md#security", + "text": "components.footer.links.security" + }, + { + "link": "https://openjsf.org/", + "text": "components.footer.links.openJS" + } + ], + "socialLinks": [ + { + "icon": "/static/images/logos/social-github.svg", + "link": "https://github.com/nodejs/node", + "kind": "dark", + "alt": "GitHub" + }, + { + "icon": "/static/images/logos/social-github-dark.svg", + "link": "https://github.com/nodejs/node", + "kind": "light", + "alt": "GitHub" + }, + { + "icon": "/static/images/logos/social-mastodon.svg", + "link": "https://social.lfx.dev/@nodejs", + "kind": "neutral", + "alt": "Mastodon" + }, + { + "icon": "/static/images/logos/social-twitter.svg", + "link": "https://twitter.com/nodejs", + "kind": "neutral", + "alt": "Twitter" + }, + { + "icon": "/static/images/logos/social-slack.svg", + "link": "https://openjs-foundation.slack.com/join/shared_invite/zt-238w9sb83-Qk9NcsrEMomq94Y~3gW8EQ#/shared-invite/email", + "kind": "neutral", + "alt": "Slack" + }, + { + "icon": "/static/images/logos/social-linkedin.svg", + "link": "https://www.linkedin.com/company/node-js", + "kind": "neutral", + "alt": "LinkedIn" + } + ] } diff --git a/types/config.ts b/types/config.ts index ac03a12b0f48d..0c9f7aae76d27 100644 --- a/types/config.ts +++ b/types/config.ts @@ -13,6 +13,18 @@ export interface OGConfig { imgHeight: string; } +export interface FooterConfig { + text: string; + link: string; +} + +export interface SocialConfig { + icon: string; + link: string; + kind: 'dark' | 'light' | 'neutral'; + alt?: string; +} + export interface SiteConfig { title: string; description: string; @@ -23,4 +35,6 @@ export interface SiteConfig { twitter: TwitterConfig; rssFeeds: Array; websiteBanners: Record; + footerLinks: Array; + socialLinks: Array; }