/* ============================================================================ APPLICATIONS AND OPPORTUNITIES - /{locale}/applications-and-opportunities ---------------------------------------------------------------------------- Draft section 8 ("Get Involved"). Two halves: 1. the three ways into the project, which are editorial and permanent - a Call for Expression of Interest, the SHARP network, and the SUQ dialogues and trainings; 2. the current calls, which come from the CMS and carry a status tag of Open / Upcoming / Closed. The draft's network card ends "Membership involves [eligibility criteria - TBD]". That sentence is NOT rendered: eligibility is a CMS field on the network entry, and the page shows it only once it exists. Nothing here invents a criterion, a deadline or a figure. Open / Upcoming / Closed is derived from `openDate` and `deadline` (see ./_events-opportunities/opportunities.ts); an editorial `opportunityStatus` of "closed" wins over the dates, for a call suspended or closed early. EMPTY BY DESIGN: with an empty CMS the three pathway cards still render - they are approved copy, not content - and the calls list shows its localised "nothing open yet" panel. ========================================================================= */ import { Link, useSearchParams } from "react-router"; import { Button } from "~/components/ui/Button"; import { Card } from "~/components/ui/Card"; import { localizedPath, sectionPath, translate, useTranslation, } from "~/i18n"; import { buildBreadcrumbJsonLd, buildMeta } from "~/lib/seo"; import type { Route } from "./+types/applications-and-opportunities"; import { getRouteCopy } from "./_events-opportunities/copy"; import { filterOpportunities, findNetworkOpportunity, OPPORTUNITY_STATUS_FILTERS, opportunityStatusKey, sortOpportunities, } from "./_events-opportunities/opportunities"; import { fetchOpportunities } from "./_events-opportunities/opportunities.server"; import { readRootSeo } from "./_events-opportunities/root-seo"; import { Notice, OpportunityCard } from "./_events-opportunities/ui"; import { buildPagination, PAGE_SIZE, parseEnum, parsePage, requireLocale, withParam, } from "./_shared/listing"; import { EmptyState, PageHeader, PageShell, Pagination, ResultSummary, TabLinks, } from "./_shared/listing-ui"; /** Anchor of the calls list - the "See current calls" card jumps to it. */ const LIST_ANCHOR = "current-calls"; export async function loader({ params, request }: Route.LoaderArgs) { const locale = requireLocale(params.lang); const url = new URL(request.url); const now = new Date(); const status = parseEnum( url.searchParams, "status", OPPORTUNITY_STATUS_FILTERS, "all", ); const { opportunities, available } = await fetchOpportunities(locale, now); const matching = sortOpportunities( filterOpportunities(opportunities, status), ); const pagination = buildPagination( parsePage(url.searchParams), PAGE_SIZE, matching.length, ); const start = (pagination.page - 1) * PAGE_SIZE; /* "Join the SHARP Network" points at the network entry when the CMS has one and at Contact when it does not - never at a URL that does not exist. */ const network = findNetworkOpportunity(opportunities); return { locale, opportunities: matching.slice(start, start + PAGE_SIZE), pagination, status, available, hasOpportunities: opportunities.length > 0, networkSlug: network?.slug ?? null, }; } export function meta({ location, matches }: Route.MetaArgs) { const root = readRootSeo(matches); const copy = getRouteCopy(root.locale); const search = new URLSearchParams(location.search); return buildMeta({ locale: root.locale, pathname: root.pathname, siteUrl: root.siteUrl, title: translate(root.locale, "nav.sections.applications"), description: copy.opportunities.metaDescription, noIndex: search.has("status") || search.has("page"), jsonLd: buildBreadcrumbJsonLd(root.siteUrl, [ { name: translate(root.locale, "nav.sections.home"), pathname: localizedPath("/", root.locale), }, { name: translate(root.locale, "nav.sections.applications"), pathname: sectionPath("applications", root.locale), }, ]), }); } export default function ApplicationsAndOpportunities({ loaderData, }: Route.ComponentProps) { const { opportunities, pagination, status, available, hasOpportunities, networkSlug, } = loaderData; const { locale, t } = useTranslation(); const copy = getRouteCopy(locale); const [searchParams] = useSearchParams(); const pathname = sectionPath("applications", locale); const href = (value: string | undefined) => `${pathname}${withParam(searchParams, "status", value)}`; const tabs = OPPORTUNITY_STATUS_FILTERS.map((value) => ({ id: value, label: value === "all" ? t("filters.all") : t(opportunityStatusKey(value)), href: href(value === "all" ? undefined : value), isCurrent: status === value, })); const pathways = [ { id: "calls", title: copy.opportunities.pathwayCallsTitle, excerpt: copy.opportunities.pathwayCallsBody, href: `#${LIST_ANCHOR}`, actionLabel: t("actions.seeCurrentCalls"), }, { id: "network", title: copy.opportunities.pathwayNetworkTitle, excerpt: copy.opportunities.pathwayNetworkBody, href: networkSlug ? `${pathname}/${encodeURIComponent(networkSlug)}` : sectionPath("contact", locale), actionLabel: t("actions.applyToJoin"), }, { id: "sessions", title: copy.opportunities.pathwaySessionsTitle, excerpt: copy.opportunities.pathwaySessionsBody, href: sectionPath("events", locale), actionLabel: t("actions.viewUpcomingSessions"), }, ]; return (

{copy.opportunities.pathwaysHeading}

    {pathways.map((pathway) => ( ))}

{copy.opportunities.listHeading}

{available ? null : ( {copy.opportunities.unavailable} )} {opportunities.length > 0 ? ( <>
    {opportunities.map((opportunity) => ( ))}
) : hasOpportunities && status !== "all" ? ( ) : ( )}
); }