initial commit
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
/* ============================================================================
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user