Files
community-rule/app/components/navigation/Footer.tsx
T
adilalloandCursor c180e79c38 Give every public route its own description, and keep canonicals and structured data on the host that serves the page.
Decorative images on the marketing pages stay quiet, and every dialog close button is named Close.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-04 11:04:52 -07:00

233 lines
9.7 KiB
TypeScript

"use client";
import { memo } from "react";
import { useTranslation } from "../../contexts/MessagesContext";
import Link from "next/link";
import Logo from "../asset/Logo";
import Divider from "../utility/Divider";
import { getAssetPath, ASSETS } from "../../../lib/assetUtils";
import { PRODUCTION_SITE_ORIGIN } from "../../../lib/siteMetadata";
/**
* Figma: "Navigation / Footer" (18411-62917).
* Tiers: smallest viewports (below `md`), `md` through `lg`, `lg` and up.
* Matches `--breakpoint-md: 640px`, `--breakpoint-lg: 1024px` in `app/tailwind.css`.
*/
const Footer = memo(({ siteOrigin = PRODUCTION_SITE_ORIGIN }: { siteOrigin?: string }) => {
const t = useTranslation("footer");
const tChrome = useTranslation("controlsChrome");
const linkFocusClass =
"touch-manipulation [-webkit-tap-highlight-color:transparent] hover:opacity-80 active:opacity-60 focus:opacity-80 focus:outline-none focus:ring-2 focus:ring-[var(--color-content-default-primary)] focus:ring-offset-2 focus:ring-offset-[var(--color-surface-default-primary)] transition-opacity duration-150 ease-out";
const bodyTextClass =
"text-[var(--color-content-default-primary)] text-medium-label lg:text-x-large-label";
/** Figma 18411:62925 (1024+): org name is one line, `w-full whitespace-nowrap`. */
const orgNameClass = `${bodyTextClass} lg:whitespace-nowrap`;
const primaryLinkClass = `inline-flex w-fit max-w-full shrink-0 whitespace-nowrap self-start md:ml-auto md:self-end text-left md:text-right md:justify-end text-[var(--color-content-default-primary)] text-medium-label ${linkFocusClass} p-2 -m-2 cursor-pointer lg:text-x-large-label`;
/** Figma 18411:62944: 40px gaps, w-[396px] link block; `p-2` on links overruns 396px—tighten x at `md+` row. */
const legalLinkClass = `inline-flex w-fit max-w-full self-start text-[var(--color-content-default-secondary)] text-small-paragraph ${linkFocusClass} p-2 -m-2 cursor-pointer underline decoration-solid [text-decoration-skip-ink:none] md:self-auto md:px-0 md:py-1 md:mx-0 md:text-x-small-paragraph md:whitespace-nowrap md:no-underline md:text-[var(--color-content-default-primary)] lg:text-small-paragraph lg:text-[var(--color-content-default-primary)]`;
// Schema markup for organization information
const schemaData = {
"@context": "https://schema.org",
"@type": "Organization",
name: t("organization.name"),
email: t("organization.email"),
url: siteOrigin,
sameAs: [
t("social.bluesky.url"),
t("social.gitea.url"),
t("social.mastodon.url"),
],
};
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaData) }}
/>
<footer className="bg-[var(--color-surface-default-primary)] w-full">
<div
className="mx-auto flex max-w-[1920px] flex-col
gap-[var(--space-1000)]
px-[var(--space-400)]
py-[var(--space-1000)]
md:gap-[var(--space-600)]
md:px-[var(--space-800)]
md:py-[var(--space-600)]
lg:gap-[var(--space-1500)]
lg:px-[var(--space-1800)]
lg:py-[var(--space-2400)]"
>
<div
className="flex w-full flex-col
gap-[var(--space-800)]
md:gap-[var(--space-1200)]
lg:gap-[var(--space-1500)]"
>
<Logo size="footer" wordmark />
<div
className="flex w-full flex-col
gap-[var(--space-1200)]
md:flex-row md:items-start md:justify-between md:gap-0"
>
<div
className="order-2 flex flex-col
gap-[var(--space-1200)]
md:order-1 md:max-w-[min(100%,334px)]
lg:max-w-[min(100%,334px)]
lg:gap-[var(--space-1800)]"
>
<div
className="flex flex-col
gap-[var(--space-400)]"
>
<div className={orgNameClass}>{t("organization.name")}</div>
<a
href={`mailto:${t("organization.email")}`}
className={`inline-flex w-fit max-w-full ${bodyTextClass} ${linkFocusClass} p-2 -m-2 cursor-pointer`}
>
{t("organization.email")}
</a>
</div>
<div
className="flex flex-col
gap-[var(--space-400)]"
>
<a
href={t("social.bluesky.url")}
className={`group inline-flex w-fit max-w-full items-center gap-[var(--space-150)] ${linkFocusClass} p-2 -m-2 cursor-pointer`}
aria-label={t("social.bluesky.ariaLabel")}
>
{/* eslint-disable-next-line @next/next/no-img-element -- social logo */}
<img
src={getAssetPath(ASSETS.BLUESKY_LOGO)}
alt=""
width={24}
height={22}
className="h-[21px] w-[24px] flex-shrink-0 transition-transform group-hover:scale-110"
/>
<div className={bodyTextClass}>{t("social.bluesky.label")}</div>
</a>
<a
href={t("social.gitea.url")}
className={`group inline-flex w-fit max-w-full items-center gap-[var(--space-150)] ${linkFocusClass} p-2 -m-2 cursor-pointer`}
aria-label={t("social.gitea.ariaLabel")}
>
{/* eslint-disable-next-line @next/next/no-img-element -- social icon */}
<img
src={getAssetPath(ASSETS.GITEA_ICON)}
alt=""
width={22}
height={22}
className="h-5 w-[22px] flex-shrink-0 grayscale transition-transform group-hover:scale-110"
/>
<div className={bodyTextClass}>{t("social.gitea.label")}</div>
</a>
<a
href={t("social.mastodon.url")}
className={`group inline-flex w-fit max-w-full items-center gap-[var(--space-150)] ${linkFocusClass} p-2 -m-2 cursor-pointer`}
aria-label={t("social.mastodon.ariaLabel")}
>
{/* eslint-disable-next-line @next/next/no-img-element -- social icon */}
<img
src={getAssetPath(ASSETS.MASTODON_LOGO)}
alt=""
width={22}
height={22}
className="h-5 w-[22px] flex-shrink-0 grayscale transition-transform group-hover:scale-110"
/>
<div className={bodyTextClass}>{t("social.mastodon.label")}</div>
</a>
</div>
</div>
{/* Figma 18411:62937: column on the right, labels text-right at md+. Mobile stays start-aligned. */}
<nav
aria-label={tChrome("footerAriaLabel")}
className="order-1 flex w-full max-w-full flex-col
items-start
gap-[var(--space-800)]
md:order-2 md:ml-auto md:w-auto md:items-end md:text-right
md:gap-[var(--space-800)]"
>
<Link href="/use-cases" className={primaryLinkClass}>
{t("navigation.useCases")}
</Link>
<Link href="/learn" className={primaryLinkClass}>
{t("navigation.learn")}
</Link>
<Link href="/about" className={primaryLinkClass}>
{t("navigation.about")}
</Link>
</nav>
</div>
</div>
<Divider />
<div
className="flex w-full flex-col
gap-[var(--space-800)]
text-[var(--color-content-default-primary)]
md:flex-row md:items-start md:justify-between md:gap-[var(--space-1000)]
md:whitespace-nowrap
md:text-x-small-paragraph
lg:text-small-paragraph"
>
<p
className="w-full text-small-paragraph
text-[var(--color-content-default-secondary)]
md:w-auto
md:text-x-small-paragraph
lg:text-small-paragraph"
>
<a
href={t("license.href")}
rel="license"
className={`${linkFocusClass} p-2 -m-2 cursor-pointer`}
>
{t("license.label")}
</a>
</p>
<div
className="flex w-full min-w-0 flex-col flex-wrap
gap-[var(--space-800)]
text-small-paragraph
text-[var(--color-content-default-primary)]
md:max-w-[min(100%,396px)]
md:flex-row md:flex-nowrap md:content-center md:items-center md:justify-end
md:gap-[var(--space-1000)]
md:text-x-small-paragraph
lg:max-w-none
lg:gap-10
lg:text-small-paragraph"
>
<Link href={t("legal.privacyPolicyHref")} className={legalLinkClass}>
{t("legal.privacyPolicy")}
</Link>
<Link href={t("legal.termsOfServiceHref")} className={legalLinkClass}>
{t("legal.termsOfService")}
</Link>
<Link href={t("legal.cookiesSettingsHref")} className={legalLinkClass}>
{t("legal.cookiesSettings")}
</Link>
</div>
</div>
</div>
</footer>
</>
);
});
Footer.displayName = "Footer";
export default Footer;