FCI theming refresh, contact identity fields, Amplify secrets fix

- Remap globals.css tokens to FCI brand palette (Eggplant #801d7f,
  Spring Pea #96bc33, Seed Grant #679038, Squash #c9ad2d, FCI gray
  #4b5657). Existing leaf-* / clay-* class names preserved.
- Switch body font to Open Sans (FCI's free fallback for Museo Sans).
  Headings keep Fraunces.
- Add contact identity (first name, last name, email) as readonly
  fields at the top of Stage 0. /api/data fetches via APIv4
  Contact.get with email_primary.email join; values flow through
  FormDataPayload.contact and into the form's evalState so the
  readonly renderer displays them. Draft restore re-applies them so
  a stale local draft can't override.
- amplify.yml: fetch Amplify Secrets from SSM Parameter Store when
  they don't arrive as build-shell env vars (the common failure mode
  behind "Refusing to run in production without CIVI_*"). Adds a
  length-only diagnostic echo and a hard-fail guard so a missing
  required var stops the build with a clear message instead of
  bundling empty strings and crashing the SSR Lambda at runtime.
This commit is contained in:
Joel Brock
2026-05-20 16:46:44 -07:00
parent 8caa851bcc
commit f0530ed337
7 changed files with 209 additions and 81 deletions
+29 -9
View File
@@ -76,13 +76,22 @@ export function EngagementForm({ config, cid, cs }: EngagementFormProps) {
(useWatch({ control, name: "current_stage" }) as string | undefined) ?? "";
// State map passed to the conditional engine and to FieldRenderer for
// readonly display values. Only fields referenced by visibility rules or
// by readonly displays need to be here — currently just current_stage,
// which gates every Stage 15 visibleWhen rule and is shown as a readonly
// field in the Stage 0 header.
// readonly display values. Includes:
// - current_stage: gates every Stage 15 visibleWhen rule and renders
// as a readonly field in Stage 0.
// - contact_first_name / contact_last_name / contact_email: readonly
// identity fields in Stage 0, sourced from /api/data (the cid contact).
// Static after load, so we read straight from load.data.contact rather
// than subscribing through react-hook-form.
const readyContact = load.kind === "ready" ? load.data.contact : undefined;
const evalState = useMemo(
() => ({ current_stage: currentStageValue }),
[currentStageValue],
() => ({
current_stage: currentStageValue,
contact_first_name: readyContact?.firstName ?? "",
contact_last_name: readyContact?.lastName ?? "",
contact_email: readyContact?.email ?? "",
}),
[currentStageValue, readyContact],
);
// ── Initial load ───────────────────────────────────────────────────────
@@ -107,17 +116,28 @@ export function EngagementForm({ config, cid, cs }: EngagementFormProps) {
const data: FormDataPayload = await res.json();
if (cancelled) return;
// Build defaults: server prefill + current stage. Then check if a
// local draft exists newer than the server data; if so, layer it on top.
// Build defaults: server prefill + current stage + readonly contact
// identity. Then check if a local draft exists newer than the server
// data; if so, layer it on top. Contact-identity fields are never
// user-editable, so a draft can't override them — they're force-set
// again after the draft merge below.
const contactDefaults: Record<string, unknown> = {
contact_first_name: data.contact?.firstName ?? "",
contact_last_name: data.contact?.lastName ?? "",
contact_email: data.contact?.email ?? "",
};
const defaults: Record<string, unknown> = {
[config.stageField]: data.currentStage,
...contactDefaults,
...data.prefill,
};
const draft = loadDraft(cid);
if (draft && Object.keys(draft.values).length > 0) {
Object.assign(defaults, draft.values);
// Re-set the stage from server (draft can never override the org's actual stage).
// Re-set the stage and contact identity from server (drafts can
// never override the org's actual stage nor the cid's contact).
defaults[config.stageField] = data.currentStage;
Object.assign(defaults, contactDefaults);
setDraftRestored(true);
setDraftSavedAt(draft.savedAt);
}