Mirrors the form's IA and Field Almanac aesthetic; same auth (cid/cs
checksum) so the org owner who can fill the form can also view its
history.
New routes:
- GET /api/report — verifies checksum, resolves the org via the
Primary Contact relationship, fires Contact.get + Activity.get +
OptionValue.get in parallel. For every form field with a civiField,
walks all the org's Check-in (organizing) activities and collects
every non-empty value into a sorted-DESC history list. Returns
ReportPayload (orgName, currentStage, activities, fieldHistory,
options). Has stub-mode payload for env-less local dev.
- /report — page entry; same layout shell (SiteHeader + SiteFooter,
3xl page width). Eyebrow "Activity report - Co-op organizing".
ReportView component:
- ReportContextHeader: large org name, progress dots + uppercase
"Current stage" eyebrow + the Civi option *label* on its own line
at display-font xl/2xl leaf-800 (matches the form's header). Below
it a 3-up stat band: total check-ins, fields tracked, date span.
- One accordion card per stage section, in stage-rank order. Only
sections that have at least one field-with-entries render — past,
current, or "future-with-data" all welcome; truly empty stages stay
hidden so the page is calm.
- Same journey rail (md+) and mobile stem (md-) with past =
check-filled-leaf, current = filled-leaf-with-ring, future = dashed
hollow ring; solid leaf line vs dashed muted between markers.
- Within each card: divide-y rows. Field label and help on the left,
most-recent value on the right in display-font lg leaf-800, dated
beneath with an "{N} earlier entries" disclosure that expands a
small vertical timeline (date on left, value on right).
- FormattedValue handles currency (Intl), percent, number (tabular
nums), date (long, timezone-safe for YYYY-MM-DD), boolean (Yes/No),
select/readonly (resolved via option group), multiselect (handles
array or delimited string), file (filename), text-like (as-is).
- Loading / empty / error states match the form's treatments.
types/form.ts: new FieldHistoryEntry, ActivitySummary, ReportPayload.
The fieldHistory map keys by FieldConfig.name and only includes fields
that have at least one non-empty entry.
78 lines
2.5 KiB
TypeScript
78 lines
2.5 KiB
TypeScript
import { Suspense } from "react";
|
|
import { ReportView } from "@/components/ReportView";
|
|
import { SiteHeader, SiteFooter } from "@/components/SiteChrome";
|
|
import { formConfig } from "@/config/form";
|
|
|
|
interface PageProps {
|
|
searchParams: Promise<{ cid?: string; cs?: string }>;
|
|
}
|
|
|
|
export const metadata = {
|
|
title: "Activity report — Food Co-op Initiative",
|
|
robots: { index: false, follow: false },
|
|
};
|
|
|
|
export default async function ReportPage({ searchParams }: PageProps) {
|
|
const { cid, cs } = await searchParams;
|
|
|
|
return (
|
|
<>
|
|
<a
|
|
href="#main"
|
|
className="sr-only focus:not-sr-only focus:absolute focus:left-4 focus:top-4 focus:z-50 focus:rounded focus:bg-paper focus:px-3 focus:py-2 focus:text-ink focus:shadow"
|
|
>
|
|
Skip to content
|
|
</a>
|
|
<SiteHeader />
|
|
<main id="main" className="flex-1">
|
|
<div className="mx-auto max-w-3xl px-4 py-10 sm:px-6 sm:py-14">
|
|
<ReportIntro />
|
|
{!cid || !cs ? (
|
|
<MissingLinkParams />
|
|
) : (
|
|
<Suspense fallback={null}>
|
|
<ReportView config={formConfig} cid={cid} cs={cs} />
|
|
</Suspense>
|
|
)}
|
|
</div>
|
|
</main>
|
|
<SiteFooter />
|
|
</>
|
|
);
|
|
}
|
|
|
|
function ReportIntro() {
|
|
return (
|
|
<header className="mb-10 max-w-2xl">
|
|
<p className="text-[11px] uppercase tracking-[0.18em] text-leaf-700">
|
|
Activity report · Co-op organizing
|
|
</p>
|
|
<h1 className="mt-2 font-display text-[40px] font-normal leading-[1.05] tracking-tight text-ink sm:text-[52px]">
|
|
What we know so far
|
|
</h1>
|
|
<p className="mt-4 max-w-prose text-[17px] leading-relaxed text-ink-soft">
|
|
A read-only summary of every value your co-op has shared through past
|
|
check-ins, grouped by stage. The most recent entry sits at the top of
|
|
each row; expand a row to see how a number or note has changed over
|
|
time.
|
|
</p>
|
|
<div className="mt-6 h-px bg-rule" />
|
|
</header>
|
|
);
|
|
}
|
|
|
|
function MissingLinkParams() {
|
|
return (
|
|
<div role="alert" className="rounded-lg border-2 border-clay-200 bg-clay-100/30 px-6 py-7">
|
|
<h2 className="font-display text-xl font-medium text-clay-700">
|
|
This link is missing required information.
|
|
</h2>
|
|
<p className="mt-3 leading-relaxed text-ink-soft">
|
|
Open the report using the personalized link from your email. If you no
|
|
longer have it, please contact your engagement coordinator and ask for
|
|
a fresh link.
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|