2026-09-11 02:46:56 +02:00
|
|
|
/* ============================================================================
|
|
|
|
|
WHAT WE DO - /en/what-we-do and /ar/what-we-do
|
|
|
|
|
----------------------------------------------------------------------------
|
2026-09-16 20:32:56 +02:00
|
|
|
The redesigned What We Do page ("What-We-Do", Claude Design draft), carried
|
|
|
|
|
onto the project's own design system. Four bands:
|
|
|
|
|
|
|
|
|
|
Hero eyebrow, headline and the one-line lead, on the sunken
|
|
|
|
|
surface with the two drifting washes behind it.
|
|
|
|
|
Areas the four connected areas of work as a seam grid, each cell
|
|
|
|
|
led by its numeral.
|
|
|
|
|
Guidance the five values as a centred chip row, then the four
|
|
|
|
|
principles as bar-topped tiles.
|
|
|
|
|
Activities the closing panel with the accent start-edge.
|
|
|
|
|
|
|
|
|
|
WHAT THE DRAFT SHIPS THAT THIS PAGE DOES NOT
|
|
|
|
|
--------------------------------------------
|
|
|
|
|
* The draft gives each principle an explanatory line ("Local organisations
|
|
|
|
|
lead the design and delivery of activities", "Solutions are built to
|
|
|
|
|
outlast the project's own timeline", "Institutional change is supported
|
|
|
|
|
step by step, not imposed"). None of those sentences is in an approved
|
|
|
|
|
document, so the tiles carry only the clause the approved "How we work"
|
|
|
|
|
sentence states, which is what the draft's own titles carry too.
|
|
|
|
|
* The draft's coloured hover glow is deliberately dropped.
|
2026-09-11 02:46:56 +02:00
|
|
|
|
|
|
|
|
Overridden field by field by api::what-we-do-page.what-we-do-page; the draft
|
|
|
|
|
copy below is what renders until an editor fills that in.
|
2026-09-16 20:32:56 +02:00
|
|
|
|
|
|
|
|
Arabic is this same DOM under `dir="rtl"`, so every inline edge is logical.
|
2026-09-11 02:46:56 +02:00
|
|
|
========================================================================= */
|
|
|
|
|
|
2026-09-16 20:32:56 +02:00
|
|
|
import { Link } from "react-router";
|
|
|
|
|
|
|
|
|
|
import { ArrowIcon, Button } from "~/components/ui/Button";
|
2026-09-11 02:46:56 +02:00
|
|
|
import { createTranslator, formatNumber, sectionPath } from "~/i18n";
|
|
|
|
|
import { buildBreadcrumbJsonLd, buildMeta } from "~/lib/seo";
|
|
|
|
|
|
|
|
|
|
import type { Route } from "./+types/what-we-do";
|
|
|
|
|
import { isRichTextBlock, type CmsWhatWeDoPage } from "./_page-kit/cms-types";
|
|
|
|
|
import {
|
|
|
|
|
list,
|
|
|
|
|
loadSingle,
|
|
|
|
|
toImage,
|
|
|
|
|
toStringList,
|
|
|
|
|
} 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 {
|
|
|
|
|
PageHero,
|
|
|
|
|
PageSection,
|
|
|
|
|
Prose,
|
|
|
|
|
SectionHeader,
|
|
|
|
|
ValueChips,
|
|
|
|
|
} from "./_page-kit/sections";
|
|
|
|
|
|
2026-09-16 20:32:56 +02:00
|
|
|
import "./_page-kit/motion.css";
|
|
|
|
|
|
2026-09-11 02:46:56 +02:00
|
|
|
export async function loader({ params }: Route.LoaderArgs) {
|
|
|
|
|
const locale = routeLocale(params);
|
|
|
|
|
|
|
|
|
|
const page = await loadSingle<CmsWhatWeDoPage>("what-we-do-page", {
|
|
|
|
|
locale,
|
|
|
|
|
populate: {
|
|
|
|
|
hero: { populate: { media: true, ctas: true } },
|
|
|
|
|
workstreams: { populate: { icon: true } },
|
|
|
|
|
principles: 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,
|
|
|
|
|
},
|
|
|
|
|
intro: page?.intro ?? null,
|
|
|
|
|
richTextBlocks: list(page?.blocks)
|
|
|
|
|
.filter(isRichTextBlock)
|
|
|
|
|
.map((block) => ({ id: block.id, body: block.body })),
|
|
|
|
|
workstreams: list(page?.workstreams).map((workstream) => ({
|
|
|
|
|
title: workstream.title,
|
|
|
|
|
description: workstream.description ?? "",
|
|
|
|
|
icon: toImage(workstream.icon, "thumbnail") ?? null,
|
|
|
|
|
})),
|
|
|
|
|
principles: list(page?.principles).map((principle) => ({
|
|
|
|
|
title: principle.title,
|
|
|
|
|
description: principle.description ?? "",
|
|
|
|
|
})),
|
|
|
|
|
guidingValues: toStringList(page?.guidingValues),
|
|
|
|
|
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).whatWeDo;
|
|
|
|
|
|
|
|
|
|
return buildMeta({
|
|
|
|
|
locale,
|
|
|
|
|
pathname: root.pathname,
|
|
|
|
|
siteUrl: root.siteUrl,
|
|
|
|
|
title: loaderData?.seo.title ?? t("nav.sections.whatWeDo"),
|
|
|
|
|
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.whatWeDo"),
|
|
|
|
|
pathname: sectionPath("whatWeDo", locale),
|
|
|
|
|
},
|
|
|
|
|
]),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-16 20:32:56 +02:00
|
|
|
/** See the note on the same constant in about.tsx. */
|
|
|
|
|
const ACCENT_EDGE =
|
|
|
|
|
"[border-inline-start-color:var(--border-accent)] [border-inline-start-width:4px]";
|
|
|
|
|
|
2026-09-11 02:46:56 +02:00
|
|
|
export default function WhatWeDo({ loaderData }: Route.ComponentProps) {
|
|
|
|
|
const { locale, hero, intro, richTextBlocks } = loaderData;
|
|
|
|
|
const t = createTranslator(locale);
|
|
|
|
|
const copy = pageCopy(locale).whatWeDo;
|
|
|
|
|
|
|
|
|
|
const workstreams =
|
|
|
|
|
loaderData.workstreams.length > 0
|
|
|
|
|
? loaderData.workstreams
|
|
|
|
|
: copy.workstreams.map((workstream) => ({ ...workstream, icon: null }));
|
|
|
|
|
|
|
|
|
|
const values =
|
|
|
|
|
loaderData.guidingValues.length > 0 ? loaderData.guidingValues : copy.values;
|
|
|
|
|
|
2026-09-16 20:32:56 +02:00
|
|
|
/* An editor's principles carry a line of their own; the bundled fallback is
|
|
|
|
|
the four clauses of the approved sentence and nothing more. */
|
|
|
|
|
const principles =
|
|
|
|
|
loaderData.principles.length > 0
|
|
|
|
|
? loaderData.principles
|
|
|
|
|
: copy.principleTitles.map((title) => ({ title, description: "" }));
|
|
|
|
|
|
2026-09-11 02:46:56 +02:00
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<PageHero
|
2026-09-16 20:32:56 +02:00
|
|
|
className="border-b border-[var(--border-subtle)] bg-[var(--surface-sunken)] [--layout-hero-bottom:clamp(2.75rem,6vw,4.25rem)]"
|
2026-09-11 02:46:56 +02:00
|
|
|
title={hero.headline ?? copy.heroHeadline}
|
2026-09-16 20:32:56 +02:00
|
|
|
eyebrow={hero.eyebrow ?? t("nav.sections.whatWeDo")}
|
2026-09-11 02:46:56 +02:00
|
|
|
lead={hero.subheading ?? copy.lead}
|
2026-09-16 20:32:56 +02:00
|
|
|
ambient={[{ top: -240, from: "end", offset: "-120px", width: 700, height: 560, durationSeconds: 22 }]}
|
|
|
|
|
leadTone="primary"
|
|
|
|
|
reveal
|
2026-09-11 02:46:56 +02:00
|
|
|
{...(hero.media ? { media: hero.media } : {})}
|
|
|
|
|
/>
|
|
|
|
|
|
2026-09-16 20:32:56 +02:00
|
|
|
{/* THE FOUR AREAS OF WORK - one seam grid, the cells raised out of the
|
|
|
|
|
band, each led by its numeral (localised, so the Arabic page numbers
|
|
|
|
|
its cells in its own conventions). */}
|
2026-09-11 02:46:56 +02:00
|
|
|
<PageSection
|
|
|
|
|
aria-labelledby="wwd-workstreams-heading"
|
2026-09-16 20:32:56 +02:00
|
|
|
className="bg-[var(--surface-sunken)]"
|
2026-09-11 02:46:56 +02:00
|
|
|
>
|
|
|
|
|
<div className="flex flex-col gap-2xl">
|
2026-09-16 20:32:56 +02:00
|
|
|
{/* The draft prints the heading alone - the approved intro sentence
|
|
|
|
|
IS this heading - so no lead is drawn under it. */}
|
2026-09-11 02:46:56 +02:00
|
|
|
<SectionHeader
|
2026-09-16 20:32:56 +02:00
|
|
|
className="sh-reveal max-w-[var(--layout-prose-max)]"
|
2026-09-11 02:46:56 +02:00
|
|
|
id="wwd-workstreams-heading"
|
|
|
|
|
title={copy.workstreamsHeading}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{intro ? (
|
|
|
|
|
<Prose>
|
|
|
|
|
<RichText markdown={intro} />
|
|
|
|
|
</Prose>
|
|
|
|
|
) : null}
|
|
|
|
|
|
2026-09-16 20:32:56 +02:00
|
|
|
<ul className="grid list-none grid-cols-1 gap-[1px] overflow-hidden rounded-card border border-[var(--border-subtle)] bg-[var(--border-subtle)] sm:grid-cols-2 lg:grid-cols-4">
|
2026-09-11 02:46:56 +02:00
|
|
|
{workstreams.map((workstream, index) => (
|
2026-09-16 20:32:56 +02:00
|
|
|
<li
|
|
|
|
|
key={workstream.title}
|
|
|
|
|
className="sh-reveal flex min-h-[16.25rem] flex-col gap-sm bg-[var(--surface-card)] p-lg transition-colors duration-[var(--motion-duration-base)] hover:bg-[var(--surface-muted)]"
|
|
|
|
|
>
|
|
|
|
|
<p
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
className="heading-h2 text-[var(--text-accent)]"
|
2026-09-11 02:46:56 +02:00
|
|
|
>
|
2026-09-16 20:32:56 +02:00
|
|
|
{formatNumber(index + 1, locale, { minimumIntegerDigits: 2 })}
|
|
|
|
|
</p>
|
|
|
|
|
{/* The draft sets an area title a step above body - the same
|
|
|
|
|
size it gives the consortium names on About. */}
|
|
|
|
|
<h3 className="heading-h3 text-[var(--text-primary)]">
|
|
|
|
|
{workstream.title}
|
|
|
|
|
</h3>
|
|
|
|
|
<p className="body-md text-[var(--text-secondary)]">
|
2026-09-11 02:46:56 +02:00
|
|
|
{workstream.description}
|
2026-09-16 20:32:56 +02:00
|
|
|
</p>
|
2026-09-11 02:46:56 +02:00
|
|
|
</li>
|
|
|
|
|
))}
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
</PageSection>
|
|
|
|
|
|
2026-09-16 20:32:56 +02:00
|
|
|
{/* WHAT GUIDES US, AND HOW WE WORK - the one band the draft raises off
|
|
|
|
|
the near-black ground, so it names its surface explicitly. */}
|
|
|
|
|
<PageSection
|
|
|
|
|
aria-labelledby="wwd-principles-heading"
|
|
|
|
|
band
|
|
|
|
|
>
|
|
|
|
|
<div className="flex flex-col gap-2xl">
|
|
|
|
|
{/* The draft's chips rest as a plain outline and only take the accent
|
|
|
|
|
fill under the pointer; the shared component rests filled, so the
|
|
|
|
|
resting and hover paints are restated here until it carries the
|
|
|
|
|
draft's own treatment. */}
|
|
|
|
|
<ValueChips
|
|
|
|
|
className="sh-reveal"
|
|
|
|
|
align="center"
|
|
|
|
|
values={values}
|
|
|
|
|
label={copy.valuesLabel}
|
2026-09-11 02:46:56 +02:00
|
|
|
/>
|
|
|
|
|
|
2026-09-16 20:32:56 +02:00
|
|
|
<div className="flex flex-col gap-2xl border-t border-[var(--border-default)] pt-2xl">
|
|
|
|
|
{/* The draft draws the four principles straight under the rule,
|
|
|
|
|
with no heading over them; the heading stays in the outline. */}
|
|
|
|
|
<h2 id="wwd-principles-heading" className="sr-only">
|
|
|
|
|
{copy.principlesHeading}
|
|
|
|
|
</h2>
|
|
|
|
|
|
|
|
|
|
<ul className="grid list-none grid-cols-1 gap-lg sm:grid-cols-2 lg:grid-cols-4">
|
|
|
|
|
{principles.map((principle) => (
|
|
|
|
|
<li
|
|
|
|
|
key={principle.title}
|
|
|
|
|
className="sh-reveal flex flex-col gap-sm"
|
|
|
|
|
>
|
|
|
|
|
{/* The short accent rule the draft draws over each tile,
|
|
|
|
|
drawn on from the inline start. Decorative. */}
|
|
|
|
|
<span
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
className="sh-bar-grow block h-[3px] w-[44px] bg-[var(--surface-accent)]"
|
|
|
|
|
/>
|
|
|
|
|
<h3 className="body-md-bold text-[var(--text-primary)]">
|
|
|
|
|
{principle.title}
|
|
|
|
|
</h3>
|
|
|
|
|
{principle.description ? (
|
|
|
|
|
<p className="body-sm text-[var(--text-secondary)]">
|
|
|
|
|
{principle.description}
|
|
|
|
|
</p>
|
|
|
|
|
) : null}
|
2026-09-11 02:46:56 +02:00
|
|
|
</li>
|
|
|
|
|
))}
|
|
|
|
|
</ul>
|
|
|
|
|
|
2026-09-16 20:32:56 +02:00
|
|
|
{richTextBlocks.map((block) => (
|
|
|
|
|
<Prose key={block.id}>
|
|
|
|
|
<RichText markdown={block.body} />
|
|
|
|
|
</Prose>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
2026-09-11 02:46:56 +02:00
|
|
|
</div>
|
|
|
|
|
</PageSection>
|
|
|
|
|
|
2026-09-16 20:32:56 +02:00
|
|
|
{/* THE WAY ON TO THE ACTIVITIES */}
|
|
|
|
|
<PageSection
|
|
|
|
|
aria-labelledby="wwd-activities-heading"
|
|
|
|
|
className="bg-[var(--surface-sunken)]"
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
className={`sh-reveal flex flex-wrap items-center justify-between gap-xl rounded-card border border-[var(--border-subtle)] bg-[var(--surface-raised)] p-lg md:p-xl ${ACCENT_EDGE}`}
|
|
|
|
|
>
|
|
|
|
|
<h2
|
|
|
|
|
id="wwd-activities-heading"
|
|
|
|
|
className="heading-h2 max-w-[39rem] text-[var(--text-primary)]"
|
|
|
|
|
>
|
|
|
|
|
{copy.activitiesCtaHeading}
|
|
|
|
|
</h2>
|
|
|
|
|
|
|
|
|
|
<Button
|
|
|
|
|
as={Link}
|
|
|
|
|
to={sectionPath("activities", locale)}
|
|
|
|
|
prefetch="intent"
|
|
|
|
|
size="lg"
|
|
|
|
|
iconEnd={<ArrowIcon />}
|
|
|
|
|
>
|
|
|
|
|
{t("actions.seeActivities")}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
2026-09-11 02:46:56 +02:00
|
|
|
</PageSection>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|