/* ============================================================================ 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 the three members as role / name / deliverable panels, under a header row that 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 chip - `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.description} {member.website ? ( {member.name} ) : null}
  • ))}
{/* SHARP I AND II, AND THE WAY ON TO WHAT WE DO */}
{legacy.body ? ( ) : (

{copy.legacyBody}

)}
{/* The draft marks the legacy page as "[link: …]" with no target yet, so this second button appears only once the CMS gives it a real one. */} {legacy.link ? ( legacy.link.isExternal ? ( ) : ( ) ) : null}
); }