273 lines
9.2 KiB
TypeScript
273 lines
9.2 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import Rule from "../../../../components/cards/Rule";
|
|
import Button from "../../../../components/buttons/Button";
|
|
import InlineTextButton from "../../../../components/buttons/InlineTextButton";
|
|
import { useMessages, useTranslation } from "../../../../contexts/MessagesContext";
|
|
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
|
import { useCreateFlow } from "../../context/CreateFlowContext";
|
|
import { useCreateFlowLgUp } from "../../hooks/useCreateFlowLgUp";
|
|
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
|
import {
|
|
CREATE_FLOW_MD_UP_GRID_CELL_CLASS,
|
|
CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS,
|
|
} from "../../components/createFlowLayoutTokens";
|
|
import {
|
|
getAssetPath,
|
|
vectorMarkPath,
|
|
} from "../../../../../lib/assetUtils";
|
|
import { methodSectionsPinsForHydratedSelections } from "../../../../../lib/create/publishedDocumentToCreateFlowState";
|
|
import { CREATE_ROUTES, createFlowStepPath } from "../../utils/createFlowPaths";
|
|
import { CREATE_FLOW_REVIEW_RETURN_QUERY_KEY } from "../../utils/flowSteps";
|
|
import type { CreateFlowStep } from "../../types";
|
|
|
|
function firstId(ids: readonly string[] | undefined): string | undefined {
|
|
return ids?.find((id) => id.length > 0);
|
|
}
|
|
|
|
function chipLabel(
|
|
id: string | undefined,
|
|
rows: readonly { id: string; label: string }[] | undefined,
|
|
snapshots: readonly { id: string; label: string }[] | undefined,
|
|
): string | undefined {
|
|
if (!id) return undefined;
|
|
const fromSnap = snapshots?.find((row) => row.id === id)?.label.trim();
|
|
if (fromSnap) return fromSnap;
|
|
return rows?.find((row) => row.id === id)?.label;
|
|
}
|
|
|
|
function reviewSizeValue(
|
|
id: string | undefined,
|
|
sizes: readonly { id: string; label: string }[],
|
|
peopleTemplate: string,
|
|
): string | undefined {
|
|
if (!id) return undefined;
|
|
const row = sizes.find((item) => item.id === id);
|
|
if (!row) return undefined;
|
|
if (id === "oneMember") return row.label;
|
|
return peopleTemplate.replace("{label}", row.label);
|
|
}
|
|
|
|
function CommunityReviewAnswerRow({
|
|
label,
|
|
value,
|
|
editLabel,
|
|
onEdit,
|
|
}: {
|
|
label: string;
|
|
value: string;
|
|
editLabel: string;
|
|
onEdit: () => void;
|
|
}) {
|
|
return (
|
|
<div className="flex w-full items-start justify-between gap-[var(--space-400)] py-[var(--space-300)]">
|
|
<div className="flex min-w-0 flex-1 flex-col gap-[var(--space-100)]">
|
|
<p className="text-small-label text-[color:var(--color-content-default-secondary)]">
|
|
{label}
|
|
</p>
|
|
<p className="text-medium-paragraph text-[color:var(--color-content-default-primary)]">
|
|
{value}
|
|
</p>
|
|
</div>
|
|
<InlineTextButton onClick={onEdit}>{editLabel}</InlineTextButton>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/** Create Community review — Figma `19706:12135` (`/create/review`; two columns from `lg:`; column caps in `createFlowLayoutTokens`). */
|
|
export function CommunityReviewScreen() {
|
|
const router = useRouter();
|
|
const lgUp = useCreateFlowLgUp();
|
|
const messages = useMessages();
|
|
const t = useTranslation("create.community.review");
|
|
const { state, updateState } = useCreateFlow();
|
|
/**
|
|
* Server layout has an empty context; the client layout may already hold a
|
|
* named draft. Defer empty-vs-congrats until after mount so the first paint
|
|
* matches SSR (`null`) instead of swapping HeaderLockup titles.
|
|
*/
|
|
const [reviewReady, setReviewReady] = useState(false);
|
|
useEffect(() => {
|
|
setReviewReady(true);
|
|
}, []);
|
|
|
|
/**
|
|
* If the user picked **Customize** from a template before finishing community
|
|
* stage, we pinned `pendingTemplateAction` so this screen can skip itself
|
|
* and `replace` to `core-values`. **Use without changes** does not skip
|
|
* identity: if they land here with that pin, send them to `community-name`
|
|
* and leave the pin so name → description → photo stay in the skip list.
|
|
*
|
|
* Ref guard covers React 18 StrictMode's double-mount in dev so we don't
|
|
* fire `router.replace` twice on the same transition.
|
|
*/
|
|
const firedRedirectRef = useRef(false);
|
|
useEffect(() => {
|
|
if (firedRedirectRef.current) return;
|
|
const pending = state.pendingTemplateAction;
|
|
if (!pending) return;
|
|
if (pending.mode !== "customize") {
|
|
firedRedirectRef.current = true;
|
|
router.replace(createFlowStepPath("community-name"));
|
|
return;
|
|
}
|
|
const target = createFlowStepPath("core-values");
|
|
firedRedirectRef.current = true;
|
|
const pinMerge = {
|
|
methodSectionsPinCommitted: {
|
|
...state.methodSectionsPinCommitted,
|
|
...methodSectionsPinsForHydratedSelections(state),
|
|
},
|
|
};
|
|
updateState({ pendingTemplateAction: undefined, ...pinMerge });
|
|
router.replace(target);
|
|
}, [
|
|
router,
|
|
state.pendingTemplateAction,
|
|
state.methodSectionsPinCommitted,
|
|
state.selectedCommunicationMethodIds,
|
|
state.selectedMembershipMethodIds,
|
|
state.selectedDecisionApproachIds,
|
|
state.selectedConflictManagementIds,
|
|
updateState,
|
|
]);
|
|
|
|
const cardTitle =
|
|
typeof state.title === "string" && state.title.trim().length > 0
|
|
? state.title.trim()
|
|
: "";
|
|
/**
|
|
* No placeholder fallback: if the user skipped `community-context`, leave
|
|
* the card description off rather than render sample copy as real user data.
|
|
*/
|
|
const cardDescription =
|
|
typeof state.communityContext === "string" &&
|
|
state.communityContext.trim().length > 0
|
|
? state.communityContext.trim()
|
|
: undefined;
|
|
|
|
const avatarUrl =
|
|
typeof state.communityAvatarUrl === "string" &&
|
|
state.communityAvatarUrl.trim().length > 0
|
|
? state.communityAvatarUrl.trim()
|
|
: null;
|
|
|
|
const structure = messages.create.community.communityStructure;
|
|
const sizes = messages.create.community.communitySize.communitySizes;
|
|
const kindValue = chipLabel(
|
|
firstId(state.selectedOrganizationTypeIds),
|
|
structure.organizationTypes,
|
|
state.communityStructureChipSnapshots?.organizationTypes,
|
|
);
|
|
const sizeValue = reviewSizeValue(
|
|
firstId(state.selectedCommunitySizeIds),
|
|
sizes,
|
|
t("rows.sizePeople"),
|
|
);
|
|
const maturityValue = chipLabel(
|
|
firstId(state.selectedMaturityIds),
|
|
structure.maturityOptions,
|
|
state.communityStructureChipSnapshots?.maturity,
|
|
);
|
|
|
|
const goEdit = (step: CreateFlowStep) => {
|
|
router.push(
|
|
createFlowStepPath(step, {
|
|
[CREATE_FLOW_REVIEW_RETURN_QUERY_KEY]: "review",
|
|
}),
|
|
);
|
|
};
|
|
|
|
if (state.pendingTemplateAction || !reviewReady) {
|
|
return null;
|
|
}
|
|
|
|
if (!cardTitle) {
|
|
return (
|
|
<CreateFlowStepShell
|
|
variant="centeredNarrow"
|
|
contentTopBelowMd="space-1400"
|
|
>
|
|
<div
|
|
className={`flex flex-col items-start gap-6 ${CREATE_FLOW_MD_UP_GRID_CELL_CLASS}`}
|
|
>
|
|
<CreateFlowHeaderLockup
|
|
title={t("empty.title")}
|
|
description={t("empty.description")}
|
|
/>
|
|
<Button
|
|
buttonType="filled"
|
|
palette="default"
|
|
size="xsmall"
|
|
href={CREATE_ROUTES.createFirstStep}
|
|
>
|
|
{t("empty.startLabel")}
|
|
</Button>
|
|
</div>
|
|
</CreateFlowStepShell>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<CreateFlowStepShell
|
|
variant="wideGridLoosePadding"
|
|
contentTopBelowMd="space-1400"
|
|
>
|
|
<div
|
|
className={`flex w-full min-w-0 flex-col items-center gap-6 lg:mx-auto lg:w-full lg:grid lg:grid-cols-2 lg:items-center lg:justify-items-center lg:gap-x-[var(--space-1200)] lg:gap-y-6 ${CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS}`}
|
|
>
|
|
<div
|
|
className={`flex flex-col justify-center lg:min-h-[212px] ${CREATE_FLOW_MD_UP_GRID_CELL_CLASS}`}
|
|
>
|
|
<CreateFlowHeaderLockup
|
|
title={t("header.title")}
|
|
description={t("header.description")}
|
|
/>
|
|
</div>
|
|
<div className={`flex flex-col ${CREATE_FLOW_MD_UP_GRID_CELL_CLASS}`}>
|
|
<Rule
|
|
title={cardTitle}
|
|
description={cardDescription}
|
|
size={lgUp ? "L" : "M"}
|
|
expanded={false}
|
|
backgroundColor="bg-[var(--color-teal-teal50,#c9fef9)]"
|
|
logoUrl={
|
|
avatarUrl ?? getAssetPath(vectorMarkPath("mutual-aid"))
|
|
}
|
|
logoAlt={cardTitle}
|
|
className="rounded-[24px]"
|
|
/>
|
|
<div className="mt-[var(--space-600)] flex w-full flex-col divide-y divide-[var(--color-border-default-secondary)]">
|
|
{kindValue ? (
|
|
<CommunityReviewAnswerRow
|
|
label={t("rows.kind")}
|
|
value={kindValue}
|
|
editLabel={t("rows.edit")}
|
|
onEdit={() => goEdit("community-structure")}
|
|
/>
|
|
) : null}
|
|
{sizeValue ? (
|
|
<CommunityReviewAnswerRow
|
|
label={t("rows.size")}
|
|
value={sizeValue}
|
|
editLabel={t("rows.edit")}
|
|
onEdit={() => goEdit("community-size")}
|
|
/>
|
|
) : null}
|
|
{maturityValue ? (
|
|
<CommunityReviewAnswerRow
|
|
label={t("rows.maturity")}
|
|
value={maturityValue}
|
|
editLabel={t("rows.edit")}
|
|
onEdit={() => goEdit("community-structure")}
|
|
/>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CreateFlowStepShell>
|
|
);
|
|
}
|