Files
sharper.ly/apps/web/app/routes/what-we-do.tsx
T
2026-09-11 02:46:56 +02:00

213 lines
6.7 KiB
TypeScript

/* ============================================================================
WHAT WE DO - /en/what-we-do and /ar/what-we-do
----------------------------------------------------------------------------
Section 2 of docs/SHARPER_Website_Draft_Section_EN_AR.docx: the participatory
approach, the four connected areas of work, the values that guide them and
the four principles the approach rests on.
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.
========================================================================= */
import { Card } from "~/components/ui/Card";
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 {
InfoPanel,
PageHero,
PageSection,
Prose,
SectionHeader,
ValueChips,
} from "./_page-kit/sections";
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),
},
]),
});
}
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;
return (
<>
<PageHero
title={hero.headline ?? copy.heroHeadline}
eyebrow={hero.eyebrow ?? t("nav.sections.aboutAndStrategy")}
lead={hero.subheading ?? copy.lead}
{...(hero.media ? { media: hero.media } : {})}
/>
{/* THE FOUR AREAS OF WORK */}
<PageSection
aria-labelledby="wwd-workstreams-heading"
className="pt-0 md:pt-0"
>
<div className="flex flex-col gap-2xl">
<SectionHeader
id="wwd-workstreams-heading"
title={copy.workstreamsHeading}
lead={intro ? undefined : copy.workstreamsIntro}
/>
{intro ? (
<Prose>
<RichText markdown={intro} />
</Prose>
) : null}
<ul className="grid list-none grid-cols-1 gap-lg md:grid-cols-2">
{workstreams.map((workstream, index) => (
<li key={workstream.title}>
<InfoPanel
title={workstream.title}
eyebrow={formatNumber(index + 1, locale, {
minimumIntegerDigits: 2,
})}
>
{workstream.description}
</InfoPanel>
</li>
))}
</ul>
<ValueChips values={values} label={copy.valuesLabel} />
</div>
</PageSection>
{/* HOW WE WORK */}
<PageSection aria-labelledby="wwd-principles-heading">
<div className="flex flex-col gap-lg">
<SectionHeader
id="wwd-principles-heading"
title={copy.principlesHeading}
/>
{loaderData.principles.length > 0 ? (
<ul className="grid list-none grid-cols-1 gap-lg md:grid-cols-2">
{loaderData.principles.map((principle) => (
<li key={principle.title}>
<InfoPanel title={principle.title}>
{principle.description}
</InfoPanel>
</li>
))}
</ul>
) : (
<Prose>
<p className="body-md text-[var(--text-secondary)]">
{copy.principles}
</p>
</Prose>
)}
{richTextBlocks.map((block) => (
<Prose key={block.id}>
<RichText markdown={block.body} />
</Prose>
))}
</div>
</PageSection>
<PageSection>
<Card.CTA
headline={t("nav.sections.activities")}
body={copy.lead}
action={{
href: sectionPath("activities", locale),
label: t("actions.seeActivities"),
}}
/>
</PageSection>
</>
);
}