Final review template

This commit is contained in:
adilallo
2026-03-01 21:44:15 -07:00
parent 0799636c78
commit d811b87b12
4 changed files with 194 additions and 23 deletions
+108 -8
View File
@@ -1,25 +1,125 @@
"use client";
import { useState, useEffect } from "react";
import { useMediaQuery } from "../../hooks/useMediaQuery";
import HeaderLockup from "../../components/type/HeaderLockup";
import RuleCard from "../../components/cards/RuleCard";
import type { Category } from "../../components/cards/RuleCard/RuleCard.types";
const TITLE = "Review your CommunityRule";
const DESCRIPTION =
"Here's what other people will see. Make sure everything looks good before you finalize everything. Once the rule is finalized, you must use one of your decision-making mechanisms to edit it again.";
const RULE_CARD_TITLE = "Mutual Aid Mondays";
const RULE_CARD_DESCRIPTION =
"Mutual Aid Monday is a grassroots community in Denver, founded in November 2020 by Kelsang Virya, dedicated to supporting neighbors experiencing homelessness.";
/** Static categories for final review (read-only display). */
const FINAL_REVIEW_CATEGORIES: Category[] = [
{
name: "Values",
chipOptions: [
{ id: "v1", label: "Consciousness", state: "unselected" },
{ id: "v2", label: "Ecology", state: "unselected" },
{ id: "v3", label: "Abundance", state: "unselected" },
{ id: "v4", label: "Art", state: "unselected" },
{ id: "v5", label: "Decisiveness", state: "unselected" },
],
},
{
name: "Communication",
chipOptions: [{ id: "c1", label: "Signal", state: "unselected" }],
},
{
name: "Membership",
chipOptions: [
{ id: "m1", label: "Open Admission", state: "unselected" },
],
},
{
name: "Decision-making",
chipOptions: [
{ id: "d1", label: "Lazy Consensus", state: "unselected" },
{ id: "d2", label: "Modified Consensus", state: "unselected" },
],
},
{
name: "Conflict management",
chipOptions: [
{ id: "cf1", label: "Code of Conduct", state: "unselected" },
{ id: "cf2", label: "Restorative Justice", state: "unselected" },
],
},
];
/**
* Final review step (right before completed).
* Figma: 20907-212767
* Figma: 20907-212767 (full-size), 20976-220705 (small breakpoint).
*/
export default function FinalReviewPage() {
const [isMounted, setIsMounted] = useState(false);
const isMdOrLarger = useMediaQuery("(min-width: 640px)");
// Avoid flash: only use breakpoint after mount so SSR and first paint use same layout (desktop).
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect -- intentional: defer layout breakpoint until after mount to prevent flash
setIsMounted(true);
}, []);
const showDesktopLayout = !isMounted || isMdOrLarger;
if (showDesktopLayout) {
return (
<div className="w-full max-w-[1280px] shrink-0 px-5 md:px-16">
<div className="flex w-full flex-col gap-4 min-w-0 sm:grid sm:grid-cols-2 sm:gap-[var(--measures-spacing-1200,48px)]">
<div className="min-w-0 flex flex-col justify-center">
<HeaderLockup
title={TITLE}
description={DESCRIPTION}
justification="left"
size="L"
/>
</div>
<div className="min-w-0 w-full flex flex-col items-stretch">
<RuleCard
title={RULE_CARD_TITLE}
description={RULE_CARD_DESCRIPTION}
size="L"
expanded={true}
backgroundColor="bg-[#c9fef9]"
logoUrl="/assets/Vector_MutualAid.svg"
logoAlt={RULE_CARD_TITLE}
categories={FINAL_REVIEW_CATEGORIES}
className="rounded-[24px] !max-w-full !w-full min-w-0"
onClick={() => {}}
/>
</div>
</div>
</div>
);
}
return (
<div className="w-full flex flex-col items-center px-[var(--spacing-measures-spacing-500,20px)] md:px-[64px]">
<div className="flex flex-col gap-[48px] items-center w-full max-w-[640px]">
<div className="w-full flex flex-col items-center px-5 min-w-0">
<div className="flex flex-col gap-4 w-full max-w-[639px]">
<HeaderLockup
title="Review before submitting"
description="One last look at your CommunityRule before you complete. Submit when you're ready."
justification={isMdOrLarger ? "center" : "left"}
size={isMdOrLarger ? "L" : "M"}
title={TITLE}
description={DESCRIPTION}
justification="left"
size="M"
/>
<RuleCard
title={RULE_CARD_TITLE}
description={RULE_CARD_DESCRIPTION}
size="L"
expanded={true}
backgroundColor="bg-[#c9fef9]"
logoUrl="/assets/Vector_MutualAid.svg"
logoAlt={RULE_CARD_TITLE}
categories={FINAL_REVIEW_CATEGORIES}
className="w-full rounded-[12px] p-4"
onClick={() => {}}
/>
{/* Content area: summary or checklist can be added per Figma */}
</div>
</div>
);
+3 -1
View File
@@ -92,7 +92,9 @@ function CreateFlowLayoutContent({ children }: { children: ReactNode }) {
className="md:!text-[14px] md:!leading-[16px] !text-[12px] !leading-[14px] !px-[var(--spacing-measures-spacing-200,8px)] md:!px-[var(--spacing-measures-spacing-250,10px)] !py-[var(--spacing-measures-spacing-200,8px)] md:!py-[var(--spacing-measures-spacing-250,10px)]"
onClick={handleNext}
>
Next
{currentStep === "final-review"
? "Finalize CommunityRule"
: "Next"}
</Button>
) : null
}
+69
View File
@@ -0,0 +1,69 @@
import React from "react";
import { describe, it, expect } from "vitest";
import { renderWithProviders as render, screen } from "../utils/test-utils";
import "@testing-library/jest-dom/vitest";
import FinalReviewPage from "../../app/create/final-review/page";
describe("FinalReviewPage", () => {
it("renders without crashing", () => {
render(<FinalReviewPage />);
expect(screen.getByRole("heading", { level: 1 })).toBeInTheDocument();
});
it("renders HeaderLockup with expected title", () => {
render(<FinalReviewPage />);
expect(
screen.getByRole("heading", {
name: "Review your CommunityRule",
}),
).toBeInTheDocument();
});
it("renders HeaderLockup with expected description", () => {
render(<FinalReviewPage />);
expect(
screen.getByText(
/Here's what other people will see. Make sure everything looks good before you finalize everything. Once the rule is finalized, you must use one of your decision-making mechanisms to edit it again./i,
),
).toBeInTheDocument();
});
it("renders RuleCard with title", () => {
render(<FinalReviewPage />);
expect(screen.getByText("Mutual Aid Mondays")).toBeInTheDocument();
});
it("renders RuleCard with description", () => {
render(<FinalReviewPage />);
expect(
screen.getByText(
/Mutual Aid Monday is a grassroots community in Denver, founded in November 2020 by Kelsang Virya, dedicated to supporting neighbors experiencing homelessness./i,
),
).toBeInTheDocument();
});
it("renders RuleCard as a button (card is interactive)", () => {
render(<FinalReviewPage />);
const buttons = screen.getAllByRole("button");
expect(buttons.length).toBeGreaterThanOrEqual(1);
expect(
buttons.some((el) => el.textContent?.includes("Mutual Aid Mondays")),
).toBe(true);
});
it("renders expanded RuleCard with category labels", () => {
render(<FinalReviewPage />);
expect(screen.getByText("Values")).toBeInTheDocument();
expect(screen.getByText("Communication")).toBeInTheDocument();
expect(screen.getByText("Membership")).toBeInTheDocument();
expect(screen.getByText("Decision-making")).toBeInTheDocument();
expect(screen.getByText("Conflict management")).toBeInTheDocument();
});
it("renders category chips", () => {
render(<FinalReviewPage />);
expect(screen.getByText("Consciousness")).toBeInTheDocument();
expect(screen.getByText("Signal")).toBeInTheDocument();
expect(screen.getByText("Open Admission")).toBeInTheDocument();
});
});
+14 -14
View File
@@ -9,24 +9,24 @@ import { vi, describe, test, expect, afterEach } from "vitest";
import React from "react";
import Page from "../../app/(marketing)/page";
// Mock next/dynamic to return components synchronously in tests
// Mock next/dynamic so dynamically loaded components render after the import resolves
vi.mock("next/dynamic", () => {
const React = require("react");
return {
default: (importFn, options) => {
// In tests, resolve the dynamic import immediately and return the component
let Component = null;
importFn().then((mod) => {
Component = mod.default || mod;
});
// Return a synchronous wrapper that uses the mocked component
return (props) => {
// Use the mocked component directly once resolved
if (Component) {
return <Component {...props} />;
function DynamicWrapper(props) {
const [Component, setComponent] = React.useState(null);
React.useEffect(() => {
importFn().then((mod) =>
setComponent(() => mod.default || mod),
);
}, []);
if (!Component) {
return options?.loading ? options.loading() : null;
}
// Fallback: return the loading placeholder if component not ready
return options?.loading ? options.loading() : null;
};
return <Component {...props} />;
}
return DynamicWrapper;
},
};
});