/* ============================================================================ ABOUT - /en/about and /ar/about ---------------------------------------------------------------------------- The redesigned About page ("About", Claude Design draft), carried onto the project's own design system. Five bands, alternating the two dark surfaces the way the draft alternates #0B0B0A and #1D1D1B: Hero eyebrow, headline and lead on the sunken surface, with the two drifting washes behind it. Facts a four-cell seam grid: the run, the coverage, the lead partner and the funder. Nationwide the approved body prose beside the three focus areas. Consortium unboxed rows for the three members. The header carries the EU funding line. Legacy the SHARP I and II panel with the accent start-edge, and the call to action into What We Do. WHAT THE DRAFT SHIPS THAT THIS PAGE DOES NOT -------------------------------------------- * Nothing was cut for sourcing: every fact, role and sentence the draft prints is in section 1 of docs/SHARPER_Website_Draft_Section_EN_AR.docx or in the project brief. What the draft does NOT print - a figure of any kind - stays absent, because the draft marks every statistic "TBD". * The draft's coloured hover glow (a green box-shadow under a hovered card) is deliberately dropped; the tiles only rise. Every block is the FALLBACK: api::about-page.about-page overrides it field by field as soon as an editor fills it in. MOTION: classes from _page-kit/motion.css - the staggered hero entrance, the drifting washes, the scroll-driven band reveals and the hover lift. There is no Arabic frame in the draft, so /ar is this same DOM under `dir="rtl"`: every inline edge below is logical (ps-/pe-/start-/end-, or an explicit `border-inline-start-*`), never left/right. ========================================================================= */ import { Link } from "react-router"; import { ArrowIcon, Button } from "~/components/ui/Button"; import { createTranslator, sectionPath } from "~/i18n"; import { buildBreadcrumbJsonLd, buildMeta } from "~/lib/seo"; import type { Route } from "./+types/about"; import { isRichTextBlock, type CmsAboutPage, } from "./_page-kit/cms-types"; import { list, loadSingle, toImage } from "./_page-kit/cms.server"; import { pageCopy } from "./_page-kit/copy"; import { routeLocale } from "./_page-kit/locale"; import { RichText } from "./_page-kit/rich-text"; import { InfoPanel, PageHero, PageSection, SectionHeader, } from "./_page-kit/sections"; import "./_page-kit/motion.css"; export async function loader({ params }: Route.LoaderArgs) { const locale = routeLocale(params); const page = await loadSingle("about-page", { locale, populate: { hero: { populate: { media: true, ctas: true } }, consortium: { populate: { partner: { populate: { logo: true } } } }, pillars: true, legacyLink: true, blocks: { on: { "blocks.rich-text": { populate: "*" } } }, seo: { populate: { ogImage: true } }, }, }); return { locale, hero: { eyebrow: page?.hero?.eyebrow ?? null, headline: page?.hero?.headline ?? null, subheading: page?.hero?.subheading ?? null, media: toImage(page?.hero?.media, "large") ?? null, }, body: page?.body ?? null, richTextBlocks: list(page?.blocks) .filter(isRichTextBlock) .map((block) => ({ id: block.id, body: block.body })), pillars: list(page?.pillars).map((pillar) => ({ title: pillar.title, description: pillar.description ?? "", })), consortium: list(page?.consortium) .filter((member) => member.partner !== null && member.partner !== undefined) .map((member) => ({ id: member.partner?.documentId ?? String(member.id ?? member.partner?.id), name: member.partner?.name ?? "", /* The chip and the line under the name are separate fields, so a CMS-authored member reads exactly like the bundled copy. */ role: member.role ?? null, description: member.roleDescription ?? member.partner?.description ?? "", website: member.partner?.website ?? null, })), legacy: { headline: page?.legacyHeadline ?? null, body: page?.legacyBody ?? null, link: page?.legacyLink ? { label: page.legacyLink.label, href: page.legacyLink.href, isExternal: page.legacyLink.isExternal ?? false, } : null, }, seo: { title: page?.seo?.metaTitle ?? null, description: page?.seo?.metaDescription ?? null, image: toImage(page?.seo?.ogImage, "large") ?? null, noIndex: page?.seo?.noIndex ?? false, }, }; } export function meta({ loaderData, matches }: Route.MetaArgs) { const root = matches[0].loaderData; const locale = loaderData?.locale ?? root.locale; const t = createTranslator(locale); const copy = pageCopy(locale).about; return buildMeta({ locale, pathname: root.pathname, siteUrl: root.siteUrl, title: loaderData?.seo.title ?? t("nav.sections.about"), description: loaderData?.seo.description ?? copy.lead, ...(loaderData?.seo.image ? { image: { url: loaderData.seo.image.src, alt: loaderData.seo.image.alt } } : {}), noIndex: loaderData?.seo.noIndex ?? false, jsonLd: buildBreadcrumbJsonLd(root.siteUrl, [ { name: t("nav.sections.home"), pathname: sectionPath("home", locale) }, { name: t("nav.sections.about"), pathname: sectionPath("about", locale) }, ]), }); } /** * The accent start-edge the draft paints down the left of its closing panel. * Written as arbitrary properties rather than `border-s-4`: the edge needs its * own colour as well as its own width, and `border-inline-start-*` is the only * spelling that mirrors in Arabic without an `rtl:` override. */ const ACCENT_EDGE = "[border-inline-start-color:var(--border-accent)] [border-inline-start-width:4px]"; /** One member of the consortium, however it reached the page. */ interface ConsortiumMember { id: string; name: string; /** The role label - `consortium.role` in the CMS, or the bundled copy's. */ role: string | null; description: string; website: string | null; } export default function About({ loaderData }: Route.ComponentProps) { const { locale, hero, body, richTextBlocks, legacy } = loaderData; const t = createTranslator(locale); const copy = pageCopy(locale).about; const pillars = loaderData.pillars.length > 0 ? loaderData.pillars : copy.pillars; const consortium: readonly ConsortiumMember[] = loaderData.consortium.length > 0 ? loaderData.consortium.map((member) => ({ id: member.id, name: member.name, role: member.role, description: member.description, website: member.website, })) : copy.consortium.map((member) => ({ id: member.name, name: member.name, role: member.role, description: member.description, website: null, })); return ( <> {/* THE FACTS BAND - four cells divided by a one-pixel seam, which is the band's own background showing between them. Facts, not figures: the run, the coverage, the lead partner and the funder, each written out in the approved draft. */}
{copy.facts.map((fact) => (
{fact.label}
{fact.value}
))}
{/* A NATIONWIDE PROJECT, DELIVERED LOCALLY - the approved body prose on the inline-start side, the three focus areas stacked beside it. The band is the raised dark the draft paints (#1d1d1b) and has to say so: the document ground is now the sunken near-black. */}
{body ? ( ) : ( /* Two paragraphs, as the draft sets them - the run and the consortium first, then what the project does. */ copy.body.map((paragraph) => (

{paragraph}

)) )} {richTextBlocks.map((block) => ( ))}
{/* The draft prints no heading over this column; the heading exists for the document outline and for screen readers. */}

{copy.pillarsHeading}

    {pillars.map((pillar) => (
  • {pillar.description}
  • ))}
{/* THE CONSORTIUM */} {/* The draft draws no rule between the raised band above and this one - the change of surface is the seam - so this band carries none. */}
{/* EU visibility, as the draft draws it beside this heading. The emblem itself is carried by the navigation and the footer. */}

{t("site.euStatement")}

    {consortium.map((member) => (
  • {member.role ? (

    {member.role}

    ) : null}

    {member.website ? ( {member.name} ) : ( member.name )}

    {member.description}

  • ))}
{/* SHARP I AND II, AND THE WAY ON TO WHAT WE DO */}
{legacy.body ? ( ) : (

{copy.legacyBody}

)}
); }