267 lines
8.8 KiB
TypeScript
267 lines
8.8 KiB
TypeScript
/* ============================================================================
|
|
ABOUT - /en/about and /ar/about
|
|
----------------------------------------------------------------------------
|
|
Section 1 of docs/SHARPER_Website_Draft_Section_EN_AR.docx: who SHARP-ER is,
|
|
the three consortium members and their roles, the three focus areas, and the
|
|
SHARP I / II legacy the project builds on.
|
|
|
|
The draft copy is the fallback; every block here is overridden field by
|
|
field by api::about-page.about-page as soon as an editor fills it in.
|
|
========================================================================= */
|
|
|
|
import { Link } from "react-router";
|
|
|
|
import { ArrowIcon, Button } from "~/components/ui/Button";
|
|
import { Card } from "~/components/ui/Card";
|
|
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,
|
|
Prose,
|
|
SectionHeader,
|
|
} from "./_page-kit/sections";
|
|
|
|
export async function loader({ params }: Route.LoaderArgs) {
|
|
const locale = routeLocale(params);
|
|
|
|
const page = await loadSingle<CmsAboutPage>("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 ?? "",
|
|
role: 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) },
|
|
]),
|
|
});
|
|
}
|
|
|
|
export default function About({ loaderData }: Route.ComponentProps) {
|
|
const { locale, hero, body, richTextBlocks, legacy } = loaderData;
|
|
const t = createTranslator(locale);
|
|
const copy = pageCopy(locale).about;
|
|
const whatWeDoCopy = pageCopy(locale).whatWeDo;
|
|
|
|
const pillars = loaderData.pillars.length > 0 ? loaderData.pillars : copy.pillars;
|
|
const consortium =
|
|
loaderData.consortium.length > 0
|
|
? loaderData.consortium
|
|
: copy.consortium.map((member) => ({
|
|
id: member.name,
|
|
name: member.name,
|
|
role: member.role,
|
|
website: null,
|
|
}));
|
|
|
|
return (
|
|
<>
|
|
<PageHero
|
|
title={hero.headline ?? copy.heroHeadline}
|
|
eyebrow={hero.eyebrow ?? t("nav.sections.aboutAndStrategy")}
|
|
lead={hero.subheading ?? copy.lead}
|
|
{...(hero.media ? { media: hero.media } : {})}
|
|
/>
|
|
|
|
<PageSection className="pt-0 md:pt-0">
|
|
<Prose>
|
|
{body ? (
|
|
<RichText markdown={body} />
|
|
) : (
|
|
<p className="body-md text-[var(--text-secondary)]">{copy.body}</p>
|
|
)}
|
|
|
|
{richTextBlocks.map((block) => (
|
|
<RichText key={block.id} markdown={block.body} />
|
|
))}
|
|
|
|
<p className="body-sm-bold text-[var(--text-accent)]">{copy.fundingNote}</p>
|
|
</Prose>
|
|
</PageSection>
|
|
|
|
{/* WHERE WE FOCUS */}
|
|
<PageSection aria-labelledby="about-pillars-heading">
|
|
<div className="flex flex-col gap-2xl">
|
|
<SectionHeader id="about-pillars-heading" title={copy.pillarsHeading} />
|
|
|
|
<ul className="grid list-none grid-cols-1 gap-lg md:grid-cols-3">
|
|
{pillars.map((pillar) => (
|
|
<li key={pillar.title}>
|
|
<InfoPanel title={pillar.title}>{pillar.description}</InfoPanel>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
</PageSection>
|
|
|
|
{/* THE CONSORTIUM */}
|
|
<PageSection aria-labelledby="about-consortium-heading">
|
|
<div className="flex flex-col gap-2xl">
|
|
<SectionHeader
|
|
id="about-consortium-heading"
|
|
title={copy.consortiumHeading}
|
|
/>
|
|
|
|
<ul className="grid list-none grid-cols-1 gap-lg md:grid-cols-3">
|
|
{consortium.map((member) => (
|
|
<li key={member.id}>
|
|
<InfoPanel title={member.name}>
|
|
<span className="block">{member.role}</span>
|
|
{member.website ? (
|
|
<a
|
|
href={member.website}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="mt-xs inline-block body-sm-bold text-[var(--text-link)] underline underline-offset-2 hover:text-[var(--text-link-hover)]"
|
|
>
|
|
{member.name}
|
|
</a>
|
|
) : null}
|
|
</InfoPanel>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
</PageSection>
|
|
|
|
{/* SHARP I AND II */}
|
|
<PageSection aria-labelledby="about-legacy-heading">
|
|
<div className="flex flex-col gap-lg">
|
|
<SectionHeader
|
|
id="about-legacy-heading"
|
|
title={legacy.headline ?? copy.legacyHeading}
|
|
/>
|
|
|
|
<Prose>
|
|
{legacy.body ? (
|
|
<RichText markdown={legacy.body} />
|
|
) : (
|
|
<p className="body-md text-[var(--text-secondary)]">
|
|
{copy.legacyBody}
|
|
</p>
|
|
)}
|
|
</Prose>
|
|
|
|
{/* The draft marks the legacy page as "[link: …]" with no target yet,
|
|
so the button appears only once the CMS supplies a real one. */}
|
|
{legacy.link ? (
|
|
legacy.link.isExternal ? (
|
|
<Button
|
|
as="a"
|
|
href={legacy.link.href}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
variant="secondary"
|
|
iconEnd={<ArrowIcon />}
|
|
className="self-start"
|
|
>
|
|
{legacy.link.label}
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
as={Link}
|
|
to={legacy.link.href}
|
|
prefetch="intent"
|
|
variant="secondary"
|
|
iconEnd={<ArrowIcon />}
|
|
className="self-start"
|
|
>
|
|
{legacy.link.label}
|
|
</Button>
|
|
)
|
|
) : null}
|
|
</div>
|
|
</PageSection>
|
|
|
|
<PageSection>
|
|
<Card.CTA
|
|
headline={whatWeDoCopy.heroHeadline}
|
|
body={whatWeDoCopy.lead}
|
|
action={{
|
|
href: sectionPath("whatWeDo", locale),
|
|
label: t("actions.learnWhatWeDo"),
|
|
}}
|
|
/>
|
|
</PageSection>
|
|
</>
|
|
);
|
|
}
|