Build standalone CiviCRM check-in middleware
This commit is contained in:
144
components/StageSection.tsx
Normal file
144
components/StageSection.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
"use client";
|
||||
|
||||
import { useId, useState } from "react";
|
||||
import type { StageSectionConfig } from "@/types/form";
|
||||
import type { UseFormRegister, FieldValues, FieldErrors } from "react-hook-form";
|
||||
import { FieldRenderer } from "./fields/FieldRenderer";
|
||||
import { evaluate } from "@/lib/conditional";
|
||||
|
||||
interface StageSectionProps {
|
||||
section: StageSectionConfig;
|
||||
register: UseFormRegister<FieldValues>;
|
||||
errors: FieldErrors;
|
||||
/** Live form values, used to evaluate per-field visibility rules. */
|
||||
formValues: Record<string, unknown>;
|
||||
/** Whether this is the section that matches the current org stage (for the "current" badge). */
|
||||
isCurrent: boolean;
|
||||
/** Whether the section starts open. */
|
||||
defaultOpen: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* One accordion card per stage section. Renders all fields whose individual
|
||||
* visibility rules pass. The section's own visibility is decided by the
|
||||
* parent EngagementForm; if rendered, it's visible.
|
||||
*/
|
||||
export function StageSection({
|
||||
section,
|
||||
register,
|
||||
errors,
|
||||
formValues,
|
||||
isCurrent,
|
||||
defaultOpen,
|
||||
}: StageSectionProps) {
|
||||
const [open, setOpen] = useState(defaultOpen);
|
||||
const headingId = useId();
|
||||
const panelId = useId();
|
||||
|
||||
const visibleFields = section.fields.filter((f) => evaluate(f.visibleWhen, formValues));
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-labelledby={headingId}
|
||||
className={
|
||||
"overflow-hidden rounded-xl border bg-white shadow-sm transition " +
|
||||
(isCurrent
|
||||
? "border-leaf-300 ring-1 ring-leaf-200"
|
||||
: "border-stone-200")
|
||||
}
|
||||
>
|
||||
<h2 id={headingId} className="m-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
aria-expanded={open}
|
||||
aria-controls={panelId}
|
||||
className={
|
||||
"group flex w-full items-center justify-between gap-4 px-5 py-4 text-left " +
|
||||
"focus:outline-none focus-visible:ring-2 focus-visible:ring-leaf-500/40 " +
|
||||
(isCurrent ? "bg-leaf-50" : "bg-stone-50 hover:bg-stone-100")
|
||||
}
|
||||
>
|
||||
<span className="flex items-center gap-3">
|
||||
<RankBadge rank={section.rank} isCurrent={isCurrent} />
|
||||
<span className="flex flex-col">
|
||||
<span className="text-base font-semibold text-stone-900">{section.label}</span>
|
||||
{isCurrent && (
|
||||
<span className="text-xs font-medium uppercase tracking-wide text-leaf-700">
|
||||
Current stage
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
<Chevron open={open} />
|
||||
</button>
|
||||
</h2>
|
||||
|
||||
<div
|
||||
id={panelId}
|
||||
role="region"
|
||||
aria-labelledby={headingId}
|
||||
hidden={!open}
|
||||
className="border-t border-stone-100"
|
||||
>
|
||||
<div className="px-5 py-5 sm:px-6 sm:py-6">
|
||||
{section.intro && (
|
||||
<p className="mb-5 text-sm text-stone-600 leading-relaxed">{section.intro}</p>
|
||||
)}
|
||||
{visibleFields.length === 0 ? (
|
||||
<p className="text-sm italic text-stone-500">No fields are visible at this stage.</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-x-6 gap-y-5 md:grid-cols-2">
|
||||
{visibleFields.map((f) => (
|
||||
<div
|
||||
key={f.name}
|
||||
className={f.type === "textarea" || f.type === "boolean" ? "md:col-span-2" : ""}
|
||||
>
|
||||
<FieldRenderer
|
||||
field={f}
|
||||
register={register}
|
||||
errors={errors}
|
||||
readonlyValue={formValues[f.name]}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function RankBadge({ rank, isCurrent }: { rank: number; isCurrent: boolean }) {
|
||||
return (
|
||||
<span
|
||||
aria-hidden
|
||||
className={
|
||||
"inline-flex h-9 w-9 items-center justify-center rounded-full text-sm font-semibold " +
|
||||
(isCurrent
|
||||
? "bg-leaf-600 text-white"
|
||||
: "bg-stone-200 text-stone-700")
|
||||
}
|
||||
>
|
||||
{rank}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function Chevron({ open }: { open: boolean }) {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
className={"h-5 w-5 text-stone-500 transition-transform " + (open ? "rotate-180" : "")}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M5.23 7.21a.75.75 0 011.06.02L10 11.06l3.71-3.83a.75.75 0 111.08 1.04l-4.25 4.39a.75.75 0 01-1.08 0L5.21 8.27a.75.75 0 01.02-1.06z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user