Files
community-rule/app/components/cards/TemplateReviewCard/TemplateChipDetailModal.tsx
T
2026-04-29 07:20:16 -06:00

355 lines
10 KiB
TypeScript

"use client";
import { useMemo } from "react";
import Create from "../../modals/Create";
import Chip from "../../controls/Chip";
import InputLabel from "../../type/InputLabel";
import ContentLockup from "../../type/ContentLockup";
import ModalTextAreaField from "../../../(app)/create/components/ModalTextAreaField";
import { useMessages, useTranslation } from "../../../contexts/MessagesContext";
import type { TemplateChipDetail } from "../../../../lib/create/templateReviewMapping";
export interface TemplateChipDetailModalProps {
isOpen: boolean;
onClose: () => void;
detail: TemplateChipDetail | null;
}
/**
* Read-only mirror of the custom-rule per-chip modals. Shows the exact text
* from `messages/en/create/customRule/*.json` for the matched preset — never
* the template `body` placeholder. When no preset is found for the chip label,
* the modal surfaces a clear "details not available" note rather than falling
* back to seed copy.
*/
export function TemplateChipDetailModal({
isOpen,
onClose,
detail,
}: TemplateChipDetailModalProps) {
const m = useMessages();
const t = useTranslation("create.templateReview.chipDetailModal");
const resolved = useMemo(() => resolveChipContent(detail, m), [detail, m]);
return (
<Create
isOpen={isOpen}
onClose={onClose}
backdropVariant="blurredYellow"
headerContent={
<div className="bg-[var(--color-surface-default-primary)] px-[24px] py-[12px] shrink-0">
<ContentLockup
title={resolved?.title ?? ""}
description={resolved?.subtitle ?? ""}
variant="modal"
alignment="left"
/>
</div>
}
showBackButton={false}
showNextButton
onNext={onClose}
nextButtonText={t("closeButton")}
ariaLabel={resolved?.title || "Template entry details"}
>
<div className="flex flex-col gap-[var(--measures-spacing-600,24px)] pb-2">
{resolved?.body ?? (
<p className="font-inter text-[14px] leading-[20px] text-[color:var(--color-content-default-secondary,#a3a3a3)]">
{t("fallback.bodyLabel")}
</p>
)}
</div>
</Create>
);
}
type ResolvedChipContent = {
title: string;
subtitle: string;
body: React.ReactNode;
};
function resolveChipContent(
detail: TemplateChipDetail | null,
m: ReturnType<typeof useMessages>,
): ResolvedChipContent | null {
if (!detail) return null;
const title = detail.chipLabel;
switch (detail.groupKey) {
case "coreValues": {
const cv = m.create.customRule.coreValues;
const preset = findCoreValuePreset(cv.values, detail.chipLabel);
if (!preset) return noPresetFallback(title);
return {
title,
subtitle: cv.detailModal.subtitle,
body: (
<>
<ModalTextAreaField
label={cv.detailModal.meaningLabel}
value={preset.meaning}
onChange={noop}
disabled
rows={4}
/>
<ModalTextAreaField
label={cv.detailModal.signalsLabel}
value={preset.signals}
onChange={noop}
disabled
rows={4}
/>
</>
),
};
}
case "communication": {
const comm = m.create.customRule.communication;
const preset = findMethodByLabel(comm.methods, detail.chipLabel);
if (!preset) return noPresetFallback(title);
return {
title,
subtitle: preset.supportText,
body: (
<>
<ModalTextAreaField
label={comm.sectionHeadings.corePrinciple}
value={preset.sections.corePrinciple}
onChange={noop}
disabled
rows={6}
/>
<ModalTextAreaField
label={comm.sectionHeadings.logisticsAdmin}
value={preset.sections.logisticsAdmin}
onChange={noop}
disabled
rows={6}
/>
<ModalTextAreaField
label={comm.sectionHeadings.codeOfConduct}
value={preset.sections.codeOfConduct}
onChange={noop}
disabled
rows={6}
/>
</>
),
};
}
case "membership": {
const mem = m.create.customRule.membership;
const preset = findMethodByLabel(mem.methods, detail.chipLabel);
if (!preset) return noPresetFallback(title);
return {
title,
subtitle: preset.supportText,
body: (
<>
<ModalTextAreaField
label={mem.sectionHeadings.eligibility}
value={preset.sections.eligibility}
onChange={noop}
disabled
rows={6}
/>
<ModalTextAreaField
label={mem.sectionHeadings.joiningProcess}
value={preset.sections.joiningProcess}
onChange={noop}
disabled
rows={6}
/>
<ModalTextAreaField
label={mem.sectionHeadings.expectations}
value={preset.sections.expectations}
onChange={noop}
disabled
rows={6}
/>
</>
),
};
}
case "decisionApproaches": {
const da = m.create.customRule.decisionApproaches;
const preset = findMethodByLabel(da.methods, detail.chipLabel);
if (!preset) return noPresetFallback(title);
return {
title,
subtitle: preset.supportText,
body: (
<>
<ModalTextAreaField
label={da.sectionHeadings.corePrinciple}
value={preset.sections.corePrinciple}
onChange={noop}
disabled
rows={4}
/>
<ReadOnlyScopeField
label={da.sectionHeadings.applicableScope}
scopes={preset.sections.applicableScope}
/>
<ModalTextAreaField
label={da.sectionHeadings.stepByStepInstructions}
value={preset.sections.stepByStepInstructions}
onChange={noop}
disabled
rows={4}
/>
<ReadOnlyValueField
label={da.sectionHeadings.consensusLevel}
value={`${preset.sections.consensusLevel}%`}
/>
<ModalTextAreaField
label={da.sectionHeadings.objectionsDeadlocks}
value={preset.sections.objectionsDeadlocks}
onChange={noop}
disabled
rows={4}
/>
</>
),
};
}
case "conflictManagement": {
const cm = m.create.customRule.conflictManagement;
const preset = findMethodByLabel(cm.methods, detail.chipLabel);
if (!preset) return noPresetFallback(title);
return {
title,
subtitle: preset.supportText,
body: (
<>
<ModalTextAreaField
label={cm.sectionHeadings.corePrinciple}
value={preset.sections.corePrinciple}
onChange={noop}
disabled
rows={4}
/>
<ReadOnlyScopeField
label={cm.sectionHeadings.applicableScope}
scopes={preset.sections.applicableScope}
/>
<ModalTextAreaField
label={cm.sectionHeadings.processProtocol}
value={preset.sections.processProtocol}
onChange={noop}
disabled
rows={4}
/>
<ModalTextAreaField
label={cm.sectionHeadings.restorationFallbacks}
value={preset.sections.restorationFallbacks}
onChange={noop}
disabled
rows={4}
/>
</>
),
};
}
default:
return noPresetFallback(title);
}
}
function noPresetFallback(title: string): ResolvedChipContent {
return { title, subtitle: "", body: null };
}
function noop() {
/* read-only */
}
/**
* Minimal read-only Applicable Scope row — locked chips shown as "selected"
* without the "+ Add" affordance.
*/
function ReadOnlyScopeField({
label,
scopes,
}: {
label: string;
scopes: readonly string[];
}) {
return (
<div className="flex flex-col gap-2">
<InputLabel label={label} helpIcon size="s" palette="default" />
<div className="flex flex-wrap items-center gap-2">
{scopes.map((scope) => (
<Chip
key={scope}
label={scope}
state="selected"
palette="default"
size="s"
disabled
/>
))}
</div>
</div>
);
}
function ReadOnlyValueField({
label,
value,
}: {
label: string;
value: string;
}) {
return (
<div className="flex flex-col gap-2">
<InputLabel label={label} helpIcon size="s" palette="default" />
<span className="font-inter text-[16px] font-medium leading-[20px] text-[color:var(--color-content-default-primary)]">
{value}
</span>
</div>
);
}
/** Case-insensitive, trim-tolerant method lookup by `label`. */
function findMethodByLabel<T extends { label: string }>(
methods: readonly T[],
label: string,
): T | undefined {
const normalized = label.trim().toLowerCase();
return methods.find((m) => m.label.trim().toLowerCase() === normalized);
}
type CoreValuePreset = { label: string; meaning: string; signals: string };
function findCoreValuePreset(
values: readonly unknown[],
label: string,
): CoreValuePreset | undefined {
const normalized = label.trim().toLowerCase();
for (const v of values) {
if (
v &&
typeof v === "object" &&
"label" in v &&
typeof (v as CoreValuePreset).label === "string" &&
(v as CoreValuePreset).label.trim().toLowerCase() === normalized
) {
const preset = v as Partial<CoreValuePreset>;
return {
label: preset.label ?? label,
meaning: preset.meaning ?? "",
signals: preset.signals ?? "",
};
}
}
return undefined;
}