Update create flow pages
This commit is contained in:
@@ -42,6 +42,10 @@ Use `npx prisma studio` to inspect the database.
|
||||
|
||||
Set `NEXT_PUBLIC_ENABLE_BACKEND_SYNC=true` in `.env` so **signed-in** users can persist create-flow drafts to Postgres via **Save & Exit** and so **anonymous** progress can be **uploaded after magic-link sign-in** from the save-progress exit modal. Without it, server **PUT** `/api/drafts/me` is skipped; anonymous work stays in **browser `localStorage`**, but after sign-in with a `?syncDraft=1` return URL the app still **merges that local draft into the in-memory create flow** (no server write) so you can continue and publish.
|
||||
|
||||
### Create flow URLs (custom wizard)
|
||||
|
||||
The **custom** create-rule wizard lives under **`/create/…`**. The header links to **`/create`**, which redirects to the first step. **Semantic** URL segments (e.g. `community-name`, `community-size`) match Figma intent; order is **`FLOW_STEP_ORDER`** in `app/create/utils/flowSteps.ts`, with UI from **`app/create/[screenId]/page.tsx`** and **`CREATE_FLOW_SCREEN_REGISTRY`** for Figma traceability. **Figma** stages: **Create Community** (through `review`), **Create Custom CommunityRule** (`cards`–`right-rail`), **Review and complete** (`confirm-stakeholders`–`completed`). **`/create/review-template/[slug]`** is a template **preview** only. Full tables and persistence are in **[docs/create-flow.md](docs/create-flow.md)**; engineering tracking: Linear **CR-89** / Ticket 17 in [docs/backend-linear-tickets.md](docs/backend-linear-tickets.md).
|
||||
|
||||
## Frontend / tests
|
||||
|
||||
See [docs/TESTING_GUIDE.md](docs/TESTING_GUIDE.md) and the root [README.md](README.md).
|
||||
|
||||
@@ -49,10 +49,10 @@ export interface TextAreaProps extends Omit<
|
||||
className?: string;
|
||||
rows?: number;
|
||||
/**
|
||||
* Whether to show hint text below textarea (Figma prop).
|
||||
* Hint below the textarea: `true` shows placeholder copy, or pass a string (e.g. character count).
|
||||
* @default false
|
||||
*/
|
||||
textHint?: boolean;
|
||||
textHint?: boolean | string;
|
||||
/**
|
||||
* Whether to show form header (label and help icon) above textarea (Figma prop).
|
||||
* @default true
|
||||
|
||||
@@ -9,7 +9,7 @@ import TextInput from "../../controls/TextInput";
|
||||
import ContentLockup from "../../type/ContentLockup";
|
||||
import { requestMagicLink } from "../../../../lib/create/api";
|
||||
import { safeInternalPath } from "../../../../lib/safeInternalPath";
|
||||
import { setTransferPendingFlag } from "../../../create/anonymousDraftStorage";
|
||||
import { setTransferPendingFlag } from "../../../create/utils/anonymousDraftStorage";
|
||||
|
||||
/** Mail icon for login modal (inline SVG; same pattern as InfoMessageBox ExclamationIconInline). */
|
||||
function MailIconInline() {
|
||||
|
||||
@@ -197,7 +197,7 @@ const TopNavContainer = memo<TopNavProps>(
|
||||
size={buttonSize}
|
||||
buttonType={buttonType}
|
||||
palette={palette}
|
||||
onClick={() => router.push("/create/informational")}
|
||||
onClick={() => router.push("/create")}
|
||||
ariaLabel={t("ariaLabels.createNewRule")}
|
||||
>
|
||||
{renderAvatarGroup(containerSize, avatarSize)}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useCreateFlowNavigation } from "./hooks/useCreateFlowNavigation";
|
||||
import { useCreateFlowExit } from "./hooks/useCreateFlowExit";
|
||||
import CreateFlowTopNav from "../components/utility/CreateFlowTopNav";
|
||||
import { getStepIndex } from "./utils/flowSteps";
|
||||
import { createFlowStepUsesCenteredTextLayout } from "./utils/createFlowScreenRegistry";
|
||||
import CreateFlowFooter from "../components/utility/CreateFlowFooter";
|
||||
import Button from "../components/buttons/Button";
|
||||
import { buildPublishPayload } from "../../lib/create/buildPublishPayload";
|
||||
@@ -33,8 +34,8 @@ import {
|
||||
useCreateFlowDraftSaveBanner,
|
||||
} from "./context/CreateFlowDraftSaveBannerContext";
|
||||
|
||||
/** First step where Save & Exit is offered (after informational + name / `text`). */
|
||||
const SAVE_EXIT_FROM_STEP_INDEX = getStepIndex("select");
|
||||
/** First step where Save & Exit is offered (first Create Community select per Figma). */
|
||||
const SAVE_EXIT_FROM_STEP_INDEX = getStepIndex("community-size");
|
||||
|
||||
function CreateFlowSessionShell({ children }: { children: ReactNode }) {
|
||||
const [sessionUser, setSessionUser] = useState<
|
||||
@@ -211,7 +212,7 @@ function CreateFlowLayoutContent({
|
||||
variant: "saveProgress",
|
||||
nextPath:
|
||||
returnToTemplateReview ??
|
||||
`${pathname ?? "/create/informational"}?syncDraft=1`,
|
||||
`${pathname ?? "/create"}?syncDraft=1`,
|
||||
backdropVariant: "blurredYellow",
|
||||
});
|
||||
return;
|
||||
@@ -236,7 +237,7 @@ function CreateFlowLayoutContent({
|
||||
? "items-start justify-center overflow-y-auto"
|
||||
: "items-start justify-center overflow-y-auto md:items-center";
|
||||
|
||||
const isTextStep = currentStep === "text";
|
||||
const isTextStep = createFlowStepUsesCenteredTextLayout(currentStep);
|
||||
const mainMaxMdJustify =
|
||||
isTextStep && !isCompletedStep && !isRightRailStep
|
||||
? "max-md:justify-center"
|
||||
|
||||
@@ -6,9 +6,9 @@ import {
|
||||
clearAnonymousCreateFlowStorage,
|
||||
hasTransferPendingFlag,
|
||||
readAnonymousCreateFlowState,
|
||||
} from "./anonymousDraftStorage";
|
||||
} from "./utils/anonymousDraftStorage";
|
||||
import { useCreateFlow } from "./context/CreateFlowContext";
|
||||
import { isValidStep } from "./utils/flowSteps";
|
||||
import { parseCreateFlowScreenFromPathname } from "./utils/flowSteps";
|
||||
import { saveDraftToServer } from "../../lib/create/api";
|
||||
import messages from "../../messages/en/index";
|
||||
|
||||
@@ -56,8 +56,8 @@ export function PostLoginDraftTransfer({
|
||||
return;
|
||||
}
|
||||
|
||||
const segment = pathname?.split("/").pop() ?? "";
|
||||
const step = isValidStep(segment) ? segment : undefined;
|
||||
const step =
|
||||
parseCreateFlowScreenFromPathname(pathname ?? null) ?? undefined;
|
||||
const payload = {
|
||||
...local,
|
||||
...(step ? { currentStep: step } : {}),
|
||||
@@ -100,8 +100,8 @@ export function PostLoginDraftTransfer({
|
||||
return;
|
||||
}
|
||||
|
||||
const segment = pathname?.split("/").pop() ?? "";
|
||||
const step = isValidStep(segment) ? segment : undefined;
|
||||
const step =
|
||||
parseCreateFlowScreenFromPathname(pathname ?? null) ?? undefined;
|
||||
const payload = {
|
||||
...local,
|
||||
...(step ? { currentStep: step } : {}),
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
clearAnonymousCreateFlowStorage,
|
||||
hasTransferPendingFlag,
|
||||
readAnonymousCreateFlowState,
|
||||
} from "./anonymousDraftStorage";
|
||||
} from "./utils/anonymousDraftStorage";
|
||||
import { useCreateFlow } from "./context/CreateFlowContext";
|
||||
import { fetchDraftFromServer } from "../../lib/create/api";
|
||||
import messages from "../../messages/en/index";
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import { notFound } from "next/navigation";
|
||||
import { use } from "react";
|
||||
import { CreateFlowScreenView } from "../screens/CreateFlowScreenView";
|
||||
import { isValidStep } from "../utils/flowSteps";
|
||||
import type { CreateFlowStep } from "../types";
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ screenId: string }>;
|
||||
}
|
||||
|
||||
export default function CreateFlowScreenPage({ params }: PageProps) {
|
||||
const { screenId: raw } = use(params);
|
||||
|
||||
if (!isValidStep(raw)) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const screenId = raw as CreateFlowStep;
|
||||
return <CreateFlowScreenView screenId={screenId} />;
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { notFound } from "next/navigation";
|
||||
import { use } from "react";
|
||||
import { VALID_STEPS } from "../utils/flowSteps";
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ step: string }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamic route handler for create flow steps
|
||||
*
|
||||
* Handles all flow steps via dynamic routing: /create/[step]
|
||||
* Validates step exists and renders appropriate template (placeholder for now)
|
||||
*/
|
||||
export default function CreateFlowStepPage({ params }: PageProps) {
|
||||
const { step } = use(params);
|
||||
|
||||
// Validate step exists
|
||||
if (!(VALID_STEPS as readonly string[]).includes(step)) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
// Placeholder content - templates will be implemented in CR-51-55
|
||||
return (
|
||||
<div className="flex flex-1 max-md:items-start max-md:justify-start md:items-center md:justify-center">
|
||||
<div className="text-center">
|
||||
<h1 className="text-white text-2xl font-bold mb-4">
|
||||
Create Flow Step: {step}
|
||||
</h1>
|
||||
<p className="text-gray-400">
|
||||
Template implementation coming in CR-51 through CR-55
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
clearLegacyCreateFlowKeysOnce,
|
||||
readAnonymousCreateFlowState,
|
||||
writeAnonymousCreateFlowState,
|
||||
} from "../anonymousDraftStorage";
|
||||
} from "../utils/anonymousDraftStorage";
|
||||
|
||||
const CreateFlowContext = createContext<CreateFlowContextValue | null>(null);
|
||||
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useCallback } from "react";
|
||||
import type { CreateFlowStep } from "../types";
|
||||
import { getNextStep, getPreviousStep, isValidStep } from "../utils/flowSteps";
|
||||
import {
|
||||
getNextStep,
|
||||
getPreviousStep,
|
||||
parseCreateFlowScreenFromPathname,
|
||||
} from "../utils/flowSteps";
|
||||
|
||||
/**
|
||||
* Options passed to navigation handlers (e.g. for blur before navigate)
|
||||
@@ -20,8 +24,7 @@ const blurActiveElement = (): void => {
|
||||
/**
|
||||
* Hook for Create Rule Flow navigation.
|
||||
*
|
||||
* Must be used within the create flow (pathname like /create/[step]).
|
||||
* Uses the current step from the URL and provides type-safe navigation.
|
||||
* Resolves the active step from `/create/{screenId}` via {@link parseCreateFlowScreenFromPathname} (flowSteps).
|
||||
*/
|
||||
export function useCreateFlowNavigation(): {
|
||||
currentStep: CreateFlowStep | null;
|
||||
@@ -36,9 +39,7 @@ export function useCreateFlowNavigation(): {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
|
||||
const currentStep = (pathname?.split("/").pop() ??
|
||||
null) as CreateFlowStep | null;
|
||||
const validStep = isValidStep(currentStep) ? currentStep : null;
|
||||
const validStep = parseCreateFlowScreenFromPathname(pathname ?? null);
|
||||
|
||||
const nextStep = getNextStep(validStep);
|
||||
const previousStep = getPreviousStep(validStep);
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { FIRST_STEP } from "./utils/flowSteps";
|
||||
|
||||
/** `/create` redirects to the first wizard step (Figma frame 1). */
|
||||
export default function CreateIndexPage() {
|
||||
redirect(`/create/${FIRST_STEP}`);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import type { CreateFlowStep } from "../types";
|
||||
import { InformationalScreen } from "./informational/InformationalScreen";
|
||||
import { CreateFlowTextFieldScreen } from "./text/CreateFlowTextFieldScreen";
|
||||
import { CommunitySizeSelectScreen } from "./select/CommunitySizeSelectScreen";
|
||||
import { CommunityStructureSelectScreen } from "./select/CommunityStructureSelectScreen";
|
||||
import { ConfirmStakeholdersScreen } from "./select/ConfirmStakeholdersScreen";
|
||||
import { CommunityUploadScreen } from "./upload/CommunityUploadScreen";
|
||||
import { CommunityReviewScreen } from "./review/CommunityReviewScreen";
|
||||
import { FinalReviewScreen } from "./review/FinalReviewScreen";
|
||||
import { CardsScreen } from "./card/CardsScreen";
|
||||
import { RightRailScreen } from "./right-rail/RightRailScreen";
|
||||
import { CompletedScreen } from "./completed/CompletedScreen";
|
||||
|
||||
/**
|
||||
* Renders the create-flow screen for a validated `screenId` (URL segment under /create/).
|
||||
*/
|
||||
export function CreateFlowScreenView({
|
||||
screenId,
|
||||
}: {
|
||||
screenId: CreateFlowStep;
|
||||
}): ReactNode {
|
||||
switch (screenId) {
|
||||
case "informational":
|
||||
return <InformationalScreen />;
|
||||
case "community-name":
|
||||
return (
|
||||
<CreateFlowTextFieldScreen
|
||||
messageNamespace="create.communityName"
|
||||
stateField="title"
|
||||
maxLength={48}
|
||||
/>
|
||||
);
|
||||
case "community-size":
|
||||
return <CommunitySizeSelectScreen />;
|
||||
case "community-context":
|
||||
return (
|
||||
<CreateFlowTextFieldScreen
|
||||
messageNamespace="create.communityContext"
|
||||
stateField="communityContext"
|
||||
maxLength={2000}
|
||||
/>
|
||||
);
|
||||
case "community-structure":
|
||||
return <CommunityStructureSelectScreen />;
|
||||
case "community-upload":
|
||||
return <CommunityUploadScreen />;
|
||||
case "community-reflection":
|
||||
return (
|
||||
<CreateFlowTextFieldScreen
|
||||
messageNamespace="create.communityReflection"
|
||||
stateField="communityReflection"
|
||||
maxLength={2000}
|
||||
/>
|
||||
);
|
||||
case "review":
|
||||
return <CommunityReviewScreen />;
|
||||
case "cards":
|
||||
return <CardsScreen />;
|
||||
case "right-rail":
|
||||
return <RightRailScreen />;
|
||||
case "confirm-stakeholders":
|
||||
return <ConfirmStakeholdersScreen />;
|
||||
case "final-review":
|
||||
return <FinalReviewScreen />;
|
||||
case "completed":
|
||||
return <CompletedScreen />;
|
||||
default: {
|
||||
const _exhaustive: never = screenId;
|
||||
return _exhaustive;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback, useMemo } from "react";
|
||||
import { useMessages } from "../../contexts/MessagesContext";
|
||||
import { useCreateFlow } from "../context/CreateFlowContext";
|
||||
import { useCreateFlowMdUp } from "../hooks/useCreateFlowMdUp";
|
||||
import { CreateFlowHeaderLockup } from "../components/CreateFlowHeaderLockup";
|
||||
import CardStack from "../../components/utility/CardStack";
|
||||
import Create from "../../components/modals/Create";
|
||||
import TextArea from "../../components/controls/TextArea";
|
||||
import { CreateFlowStepShell } from "../components/CreateFlowStepShell";
|
||||
import { useMessages } from "../../../contexts/MessagesContext";
|
||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||
import CardStack from "../../../components/utility/CardStack";
|
||||
import Create from "../../../components/modals/Create";
|
||||
import TextArea from "../../../components/controls/TextArea";
|
||||
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
||||
|
||||
const IN_PERSON_CARD_ID = "in-person-meetings";
|
||||
const SIGNAL_CARD_ID = "signal";
|
||||
@@ -37,10 +37,6 @@ const COMMUNICATION_CARD_ORDER = [
|
||||
"7",
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Section with heading + info icon and an editable TextArea.
|
||||
* This variant uses TextArea only (no TextInput); design is "Add Signal" / "Add Video Meetings".
|
||||
*/
|
||||
function CreateModalSection({
|
||||
title,
|
||||
value: _value,
|
||||
@@ -75,7 +71,6 @@ function CreateModalSection({
|
||||
);
|
||||
}
|
||||
|
||||
/** Body for any "Add platform" modal: three editable sections (TextArea only). */
|
||||
function AddPlatformModalContent({
|
||||
platformCardId,
|
||||
}: {
|
||||
@@ -133,8 +128,7 @@ function isAddPlatformCard(cardId: string | null): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/** Create flow card stack step: compact grid with optional expand to full list. */
|
||||
export default function CardsPage() {
|
||||
export function CardsScreen() {
|
||||
const m = useMessages();
|
||||
const comm = m.create.communication;
|
||||
const mdUp = useCreateFlowMdUp();
|
||||
@@ -1,20 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import CommunityRuleDocument from "../../components/sections/CommunityRuleDocument";
|
||||
import type { CommunityRuleDocumentSection } from "../../components/sections/CommunityRuleDocument/CommunityRuleDocument.types";
|
||||
import Alert from "../../components/modals/Alert";
|
||||
import { useMessages } from "../../contexts/MessagesContext";
|
||||
import { parseDocumentSectionsForDisplay } from "../../../lib/create/buildPublishPayload";
|
||||
import { readLastPublishedRule } from "../../../lib/create/lastPublishedRule";
|
||||
import { useCreateFlowMdUp } from "../hooks/useCreateFlowMdUp";
|
||||
import { CreateFlowHeaderLockup } from "../components/CreateFlowHeaderLockup";
|
||||
import CommunityRuleDocument from "../../../components/sections/CommunityRuleDocument";
|
||||
import type { CommunityRuleDocumentSection } from "../../../components/sections/CommunityRuleDocument/CommunityRuleDocument.types";
|
||||
import Alert from "../../../components/modals/Alert";
|
||||
import { useMessages } from "../../../contexts/MessagesContext";
|
||||
import { parseDocumentSectionsForDisplay } from "../../../../lib/create/buildPublishPayload";
|
||||
import { readLastPublishedRule } from "../../../../lib/create/lastPublishedRule";
|
||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||
|
||||
/**
|
||||
* Completed create flow page.
|
||||
* Figma: 20907-213286 (main), 18002-28017 (toast).
|
||||
*/
|
||||
export default function CompletedPage() {
|
||||
export function CompletedScreen() {
|
||||
const mdUp = useCreateFlowMdUp();
|
||||
const m = useMessages();
|
||||
const completed = m.create.completed;
|
||||
@@ -40,7 +36,6 @@ export default function CompletedPage() {
|
||||
if (!stored) return;
|
||||
const parsed = parseDocumentSectionsForDisplay(stored.document);
|
||||
if (parsed.length === 0) return;
|
||||
// One-shot hydration from client-only storage after mount.
|
||||
queueMicrotask(() => {
|
||||
setDocumentSections(parsed);
|
||||
setHeaderTitle(stored.title);
|
||||
+7
-12
@@ -1,18 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import NumberedList from "../../components/type/NumberedList";
|
||||
import { useTranslation } from "../../contexts/MessagesContext";
|
||||
import { useCreateFlowMdUp } from "../hooks/useCreateFlowMdUp";
|
||||
import { CreateFlowHeaderLockup } from "../components/CreateFlowHeaderLockup";
|
||||
import { CreateFlowStepShell } from "../components/CreateFlowStepShell";
|
||||
import NumberedList from "../../../components/type/NumberedList";
|
||||
import { useTranslation } from "../../../contexts/MessagesContext";
|
||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
||||
|
||||
/**
|
||||
* Informational page for the create flow
|
||||
*
|
||||
* Displays information about the create flow process using HeaderLockup and NumberedList components.
|
||||
* Lockup sizing via `CreateFlowHeaderLockup`. NumberedList: S / M by breakpoint.
|
||||
*/
|
||||
export default function InformationalPage() {
|
||||
/** Create Community — frame 1 (Figma 20094-16005). */
|
||||
export function InformationalScreen() {
|
||||
const mdUp = useCreateFlowMdUp();
|
||||
const t = useTranslation("create.informational");
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import RuleCard from "../../components/cards/RuleCard";
|
||||
import { useTranslation } from "../../contexts/MessagesContext";
|
||||
import { CreateFlowHeaderLockup } from "../components/CreateFlowHeaderLockup";
|
||||
import { useCreateFlowMdUp } from "../hooks/useCreateFlowMdUp";
|
||||
import { CreateFlowStepShell } from "../components/CreateFlowStepShell";
|
||||
import RuleCard from "../../../components/cards/RuleCard";
|
||||
import { useTranslation } from "../../../contexts/MessagesContext";
|
||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
||||
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
||||
|
||||
/** Mid-flow review step (after upload, before cards). */
|
||||
export default function ReviewPage() {
|
||||
/** Create Community — frame 8 (Figma 19706-12135); URL segment `review`. */
|
||||
export function CommunityReviewScreen() {
|
||||
const mdUp = useCreateFlowMdUp();
|
||||
const t = useTranslation("create.review");
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import RuleCard from "../../components/cards/RuleCard";
|
||||
import type { Category } from "../../components/cards/RuleCard/RuleCard.types";
|
||||
import { useMessages, useTranslation } from "../../contexts/MessagesContext";
|
||||
import { useCreateFlow } from "../context/CreateFlowContext";
|
||||
import { useCreateFlowMdUp } from "../hooks/useCreateFlowMdUp";
|
||||
import RuleCard from "../../../components/cards/RuleCard";
|
||||
import type { Category } from "../../../components/cards/RuleCard/RuleCard.types";
|
||||
import { useMessages, useTranslation } from "../../../contexts/MessagesContext";
|
||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
||||
import {
|
||||
CREATE_FLOW_REVIEW_RULE_CARD_LAYOUT_CLASS,
|
||||
CreateFlowLockupCardStepShell,
|
||||
} from "../components/CreateFlowLockupCardStepShell";
|
||||
} from "../../components/CreateFlowLockupCardStepShell";
|
||||
|
||||
function buildFinalReviewCategories(
|
||||
rows: { name: string; chips: string[] }[],
|
||||
@@ -24,11 +24,7 @@ function buildFinalReviewCategories(
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Final review step (right before completed).
|
||||
* Figma: 20907-212767 (full-size), 20976-220705 (below `md`).
|
||||
*/
|
||||
export default function FinalReviewPage() {
|
||||
export function FinalReviewScreen() {
|
||||
const { state } = useCreateFlow();
|
||||
const mdUp = useCreateFlowMdUp();
|
||||
const t = useTranslation("create.finalReview");
|
||||
@@ -1,19 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback, useMemo } from "react";
|
||||
import DecisionMakingSidebar from "../../components/utility/DecisionMakingSidebar";
|
||||
import CardStack from "../../components/utility/CardStack";
|
||||
import type { InfoMessageBoxItem } from "../../components/utility/InfoMessageBox/InfoMessageBox.types";
|
||||
import type { CardStackItem } from "../../components/utility/CardStack/CardStack.types";
|
||||
import { useMessages } from "../../contexts/MessagesContext";
|
||||
import { useCreateFlow } from "../context/CreateFlowContext";
|
||||
import { useCreateFlowMdUp } from "../hooks/useCreateFlowMdUp";
|
||||
import DecisionMakingSidebar from "../../../components/utility/DecisionMakingSidebar";
|
||||
import CardStack from "../../../components/utility/CardStack";
|
||||
import type { InfoMessageBoxItem } from "../../../components/utility/InfoMessageBox/InfoMessageBox.types";
|
||||
import type { CardStackItem } from "../../../components/utility/CardStack/CardStack.types";
|
||||
import { useMessages } from "../../../contexts/MessagesContext";
|
||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
||||
|
||||
/**
|
||||
* Right Rail step of the create flow.
|
||||
* Two-column layout (sidebar + card stack) at 640+, single column at 320-639.
|
||||
*/
|
||||
export default function RightRailPage() {
|
||||
export function RightRailScreen() {
|
||||
const m = useMessages();
|
||||
const rr = m.create.rightRail;
|
||||
const mdUp = useCreateFlowMdUp();
|
||||
@@ -81,9 +77,7 @@ export default function RightRailPage() {
|
||||
<div className="flex h-full min-h-0 w-full flex-1 flex-col overflow-hidden md:h-full">
|
||||
<div className="flex min-h-0 flex-1 overflow-hidden px-5 max-md:overflow-y-auto md:px-12">
|
||||
<div className="mx-auto grid h-auto min-h-0 w-full max-w-[1280px] shrink-0 grid-cols-1 gap-6 min-w-0 max-md:pt-[var(--space-800)] max-md:pb-8 md:h-full md:grid-cols-2 md:gap-12 md:pb-8">
|
||||
<div
|
||||
className="flex min-w-0 flex-col items-stretch justify-start overflow-hidden md:justify-center"
|
||||
>
|
||||
<div className="flex min-w-0 flex-col items-stretch justify-start overflow-hidden md:justify-center">
|
||||
<DecisionMakingSidebar
|
||||
title={rr.sidebar.title}
|
||||
description={sidebarDescription}
|
||||
@@ -0,0 +1,171 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo, useEffect, type Dispatch, type SetStateAction } from "react";
|
||||
import MultiSelect from "../../../components/controls/MultiSelect";
|
||||
import type { ChipOption } from "../../../components/controls/MultiSelect/MultiSelect.types";
|
||||
import { useMessages, useTranslation } from "../../../contexts/MessagesContext";
|
||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
||||
|
||||
function createListCustomHandlers(
|
||||
setList: Dispatch<SetStateAction<ChipOption[]>>,
|
||||
confirmState: "Unselected" | "Selected",
|
||||
onInteraction?: () => void,
|
||||
) {
|
||||
const touch = () => onInteraction?.();
|
||||
return {
|
||||
onAddClick: () => {
|
||||
touch();
|
||||
setList((prev) => [
|
||||
...prev,
|
||||
{ id: crypto.randomUUID(), label: "", state: "Custom" },
|
||||
]);
|
||||
},
|
||||
onCustomChipConfirm: (chipId: string, value: string) => {
|
||||
touch();
|
||||
setList((prev) =>
|
||||
prev.map((opt) =>
|
||||
opt.id === chipId
|
||||
? { ...opt, label: value, state: confirmState }
|
||||
: opt,
|
||||
),
|
||||
);
|
||||
},
|
||||
onCustomChipClose: (chipId: string) => {
|
||||
touch();
|
||||
setList((prev) => prev.filter((o) => o.id !== chipId));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function chipRowsFromLabels(
|
||||
rows: readonly { label: string }[],
|
||||
): ChipOption[] {
|
||||
return rows.map((row, i) => ({
|
||||
id: String(i + 1),
|
||||
label: row.label,
|
||||
state: "Unselected" as const,
|
||||
}));
|
||||
}
|
||||
|
||||
function selectedIdsFromOptions(options: ChipOption[]): string[] {
|
||||
return options
|
||||
.filter((o) => o.state === "Selected")
|
||||
.map((o) => o.id);
|
||||
}
|
||||
|
||||
/** Create Community — frame 3 (Figma 20094-18244). */
|
||||
export function CommunitySizeSelectScreen() {
|
||||
const m = useMessages();
|
||||
const { markCreateFlowInteraction, updateState, state } = useCreateFlow();
|
||||
const mdUp = useCreateFlowMdUp();
|
||||
const t = useTranslation("create.communitySize");
|
||||
|
||||
const [communitySizeOptions, setCommunitySizeOptions] = useState<
|
||||
ChipOption[]
|
||||
>(() => {
|
||||
const base = chipRowsFromLabels(m.create.communitySize.communitySizes);
|
||||
const selected = new Set(state.selectedCommunitySizeIds ?? []);
|
||||
return base.map((opt) => ({
|
||||
...opt,
|
||||
state: selected.has(opt.id) ? ("Selected" as const) : ("Unselected" as const),
|
||||
}));
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const selected = new Set(state.selectedCommunitySizeIds ?? []);
|
||||
setCommunitySizeOptions((prev) =>
|
||||
prev.map((opt) =>
|
||||
opt.state === "Custom"
|
||||
? opt
|
||||
: {
|
||||
...opt,
|
||||
state: selected.has(opt.id)
|
||||
? ("Selected" as const)
|
||||
: ("Unselected" as const),
|
||||
},
|
||||
),
|
||||
);
|
||||
}, [state.selectedCommunitySizeIds]);
|
||||
|
||||
const communityCustomHandlers = useMemo(
|
||||
() =>
|
||||
createListCustomHandlers(
|
||||
setCommunitySizeOptions,
|
||||
"Unselected",
|
||||
markCreateFlowInteraction,
|
||||
),
|
||||
[markCreateFlowInteraction],
|
||||
);
|
||||
|
||||
const persistSelection = (next: ChipOption[]) => {
|
||||
markCreateFlowInteraction();
|
||||
setCommunitySizeOptions(next);
|
||||
updateState({
|
||||
selectedCommunitySizeIds: selectedIdsFromOptions(next),
|
||||
});
|
||||
};
|
||||
|
||||
const handleCommunitySizeClick = (chipId: string) => {
|
||||
const next: ChipOption[] = communitySizeOptions.map((opt) =>
|
||||
opt.id === chipId
|
||||
? {
|
||||
...opt,
|
||||
state:
|
||||
opt.state === "Selected"
|
||||
? ("Unselected" as const)
|
||||
: ("Selected" as const),
|
||||
}
|
||||
: opt,
|
||||
);
|
||||
persistSelection(next);
|
||||
};
|
||||
|
||||
const multiLabel = t("multiSelect.label");
|
||||
const addText = t("multiSelect.addButtonText");
|
||||
|
||||
const multiSelectBlock = (
|
||||
<MultiSelect
|
||||
label={multiLabel}
|
||||
size="S"
|
||||
options={communitySizeOptions}
|
||||
onChipClick={handleCommunitySizeClick}
|
||||
{...communityCustomHandlers}
|
||||
addButton={true}
|
||||
addButtonText={addText}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<CreateFlowStepShell
|
||||
variant="centeredNarrow"
|
||||
contentTopBelowMd="space-1400"
|
||||
>
|
||||
{mdUp ? (
|
||||
<div className="flex w-full max-w-[1280px] items-center justify-center gap-[var(--measures-spacing-1200,48px)]">
|
||||
<div className="flex max-w-[640px] min-h-px min-w-px flex-[1_0_0] flex-col items-start justify-center gap-[var(--measures-spacing-200,8px)] py-[12px]">
|
||||
<CreateFlowHeaderLockup
|
||||
title={t("header.title")}
|
||||
description={t("header.description")}
|
||||
justification="left"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex max-w-[640px] min-h-px min-w-px flex-[1_0_0] flex-col items-start gap-[var(--measures-spacing-800,32px)]">
|
||||
{multiSelectBlock}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex w-full max-w-[640px] flex-col items-start gap-[var(--measures-spacing-400,16px)]">
|
||||
<CreateFlowHeaderLockup
|
||||
title={t("header.title")}
|
||||
description={t("header.description")}
|
||||
justification="left"
|
||||
/>
|
||||
{multiSelectBlock}
|
||||
</div>
|
||||
)}
|
||||
</CreateFlowStepShell>
|
||||
);
|
||||
}
|
||||
+93
-73
@@ -3,16 +3,17 @@
|
||||
import {
|
||||
useState,
|
||||
useMemo,
|
||||
useEffect,
|
||||
type Dispatch,
|
||||
type SetStateAction,
|
||||
} from "react";
|
||||
import MultiSelect from "../../components/controls/MultiSelect";
|
||||
import type { ChipOption } from "../../components/controls/MultiSelect/MultiSelect.types";
|
||||
import { useMessages, useTranslation } from "../../contexts/MessagesContext";
|
||||
import { useCreateFlow } from "../context/CreateFlowContext";
|
||||
import { useCreateFlowMdUp } from "../hooks/useCreateFlowMdUp";
|
||||
import { CreateFlowHeaderLockup } from "../components/CreateFlowHeaderLockup";
|
||||
import { CreateFlowStepShell } from "../components/CreateFlowStepShell";
|
||||
import MultiSelect from "../../../components/controls/MultiSelect";
|
||||
import type { ChipOption } from "../../../components/controls/MultiSelect/MultiSelect.types";
|
||||
import { useMessages, useTranslation } from "../../../contexts/MessagesContext";
|
||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
||||
|
||||
function createListCustomHandlers(
|
||||
setList: Dispatch<SetStateAction<ChipOption[]>>,
|
||||
@@ -55,40 +56,60 @@ function chipRowsFromLabels(
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Select page for the create flow
|
||||
*
|
||||
* Displays selection options using HeaderLockup and MultiSelect components.
|
||||
* Responsive layout: two-column at `md` and up, single column below (see `--breakpoint-md` in `app/tailwind.css`).
|
||||
* Lockup sizing via `CreateFlowHeaderLockup`. MultiSelect stays `S`.
|
||||
*/
|
||||
export default function SelectPage() {
|
||||
const m = useMessages();
|
||||
const { markCreateFlowInteraction } = useCreateFlow();
|
||||
const mdUp = useCreateFlowMdUp();
|
||||
const t = useTranslation("create.select");
|
||||
function applySavedSelection(
|
||||
options: ChipOption[],
|
||||
saved: string[] | undefined,
|
||||
): ChipOption[] {
|
||||
const selected = new Set(saved ?? []);
|
||||
return options.map((opt) =>
|
||||
opt.state === "Custom"
|
||||
? opt
|
||||
: {
|
||||
...opt,
|
||||
state: selected.has(opt.id)
|
||||
? ("Selected" as const)
|
||||
: ("Unselected" as const),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const [communitySizeOptions, setCommunitySizeOptions] = useState<
|
||||
ChipOption[]
|
||||
>(() => chipRowsFromLabels(m.create.select.communitySizes));
|
||||
/** Create Community — frame 5 (Figma 20094-41317). */
|
||||
export function CommunityStructureSelectScreen() {
|
||||
const m = useMessages();
|
||||
const { markCreateFlowInteraction, updateState, state } = useCreateFlow();
|
||||
const mdUp = useCreateFlowMdUp();
|
||||
const t = useTranslation("create.communityStructure");
|
||||
|
||||
const [organizationTypeOptions, setOrganizationTypeOptions] = useState<
|
||||
ChipOption[]
|
||||
>(() => chipRowsFromLabels(m.create.select.organizationTypes));
|
||||
>(() =>
|
||||
applySavedSelection(
|
||||
chipRowsFromLabels(m.create.communityStructure.organizationTypes),
|
||||
state.selectedOrganizationTypeIds,
|
||||
),
|
||||
);
|
||||
|
||||
const [governanceStyleOptions, setGovernanceStyleOptions] = useState<
|
||||
ChipOption[]
|
||||
>(() => chipRowsFromLabels(m.create.select.governanceStyles));
|
||||
|
||||
const communityCustomHandlers = useMemo(
|
||||
() =>
|
||||
createListCustomHandlers(
|
||||
setCommunitySizeOptions,
|
||||
"Unselected",
|
||||
markCreateFlowInteraction,
|
||||
),
|
||||
[markCreateFlowInteraction],
|
||||
>(() =>
|
||||
applySavedSelection(
|
||||
chipRowsFromLabels(m.create.communityStructure.governanceStyles),
|
||||
state.selectedGovernanceStyleIds,
|
||||
),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setOrganizationTypeOptions((prev) =>
|
||||
applySavedSelection(prev, state.selectedOrganizationTypeIds),
|
||||
);
|
||||
}, [state.selectedOrganizationTypeIds]);
|
||||
|
||||
useEffect(() => {
|
||||
setGovernanceStyleOptions((prev) =>
|
||||
applySavedSelection(prev, state.selectedGovernanceStyleIds),
|
||||
);
|
||||
}, [state.selectedGovernanceStyleIds]);
|
||||
|
||||
const organizationCustomHandlers = useMemo(
|
||||
() =>
|
||||
createListCustomHandlers(
|
||||
@@ -108,46 +129,54 @@ export default function SelectPage() {
|
||||
[markCreateFlowInteraction],
|
||||
);
|
||||
|
||||
const handleCommunitySizeClick = (chipId: string) => {
|
||||
const persistOrg = (next: ChipOption[]) => {
|
||||
markCreateFlowInteraction();
|
||||
setCommunitySizeOptions((prev) =>
|
||||
prev.map((opt) =>
|
||||
opt.id === chipId
|
||||
? {
|
||||
...opt,
|
||||
state: opt.state === "Selected" ? "Unselected" : "Selected",
|
||||
}
|
||||
: opt,
|
||||
),
|
||||
);
|
||||
setOrganizationTypeOptions(next);
|
||||
updateState({
|
||||
selectedOrganizationTypeIds: next
|
||||
.filter((o) => o.state === "Selected")
|
||||
.map((o) => o.id),
|
||||
});
|
||||
};
|
||||
|
||||
const persistGov = (next: ChipOption[]) => {
|
||||
markCreateFlowInteraction();
|
||||
setGovernanceStyleOptions(next);
|
||||
updateState({
|
||||
selectedGovernanceStyleIds: next
|
||||
.filter((o) => o.state === "Selected")
|
||||
.map((o) => o.id),
|
||||
});
|
||||
};
|
||||
|
||||
const handleOrganizationTypeClick = (chipId: string) => {
|
||||
markCreateFlowInteraction();
|
||||
setOrganizationTypeOptions((prev) =>
|
||||
prev.map((opt) =>
|
||||
opt.id === chipId
|
||||
? {
|
||||
...opt,
|
||||
state: opt.state === "Selected" ? "Unselected" : "Selected",
|
||||
}
|
||||
: opt,
|
||||
),
|
||||
const next: ChipOption[] = organizationTypeOptions.map((opt) =>
|
||||
opt.id === chipId
|
||||
? {
|
||||
...opt,
|
||||
state:
|
||||
opt.state === "Selected"
|
||||
? ("Unselected" as const)
|
||||
: ("Selected" as const),
|
||||
}
|
||||
: opt,
|
||||
);
|
||||
persistOrg(next);
|
||||
};
|
||||
|
||||
const handleGovernanceStyleClick = (chipId: string) => {
|
||||
markCreateFlowInteraction();
|
||||
setGovernanceStyleOptions((prev) =>
|
||||
prev.map((opt) =>
|
||||
opt.id === chipId
|
||||
? {
|
||||
...opt,
|
||||
state: opt.state === "Selected" ? "Unselected" : "Selected",
|
||||
}
|
||||
: opt,
|
||||
),
|
||||
const next: ChipOption[] = governanceStyleOptions.map((opt) =>
|
||||
opt.id === chipId
|
||||
? {
|
||||
...opt,
|
||||
state:
|
||||
opt.state === "Selected"
|
||||
? ("Unselected" as const)
|
||||
: ("Selected" as const),
|
||||
}
|
||||
: opt,
|
||||
);
|
||||
persistGov(next);
|
||||
};
|
||||
|
||||
const multiLabel = t("multiSelect.label");
|
||||
@@ -155,15 +184,6 @@ export default function SelectPage() {
|
||||
|
||||
const multiSelectBlock = (
|
||||
<>
|
||||
<MultiSelect
|
||||
label={multiLabel}
|
||||
size="S"
|
||||
options={communitySizeOptions}
|
||||
onChipClick={handleCommunitySizeClick}
|
||||
{...communityCustomHandlers}
|
||||
addButton={true}
|
||||
addButtonText={addText}
|
||||
/>
|
||||
<MultiSelect
|
||||
label={multiLabel}
|
||||
size="S"
|
||||
+8
-12
@@ -1,19 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import MultiSelect from "../../components/controls/MultiSelect";
|
||||
import Alert from "../../components/modals/Alert";
|
||||
import type { ChipOption } from "../../components/controls/MultiSelect/MultiSelect.types";
|
||||
import { useTranslation } from "../../contexts/MessagesContext";
|
||||
import { useCreateFlow } from "../context/CreateFlowContext";
|
||||
import { CreateFlowHeaderLockup } from "../components/CreateFlowHeaderLockup";
|
||||
import { CreateFlowStepShell } from "../components/CreateFlowStepShell";
|
||||
import MultiSelect from "../../../components/controls/MultiSelect";
|
||||
import Alert from "../../../components/modals/Alert";
|
||||
import type { ChipOption } from "../../../components/controls/MultiSelect/MultiSelect.types";
|
||||
import { useTranslation } from "../../../contexts/MessagesContext";
|
||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
||||
|
||||
/**
|
||||
* Confirm stakeholders step — stacked lockup + MultiSelect (not split columns).
|
||||
* Figma: 21104-46594.
|
||||
*/
|
||||
export default function ConfirmStakeholdersPage() {
|
||||
export function ConfirmStakeholdersScreen() {
|
||||
const { markCreateFlowInteraction } = useCreateFlow();
|
||||
const t = useTranslation("create.confirmStakeholders");
|
||||
const [toastDismissed, setToastDismissed] = useState(false);
|
||||
@@ -1,36 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import TextInput from "../../components/controls/TextInput";
|
||||
import { useTranslation } from "../../contexts/MessagesContext";
|
||||
import { useCreateFlow } from "../context/CreateFlowContext";
|
||||
import { useCreateFlowMdUp } from "../hooks/useCreateFlowMdUp";
|
||||
import { CreateFlowHeaderLockup } from "../components/CreateFlowHeaderLockup";
|
||||
import { CreateFlowStepShell } from "../components/CreateFlowStepShell";
|
||||
import TextInput from "../../../components/controls/TextInput";
|
||||
import { useTranslation } from "../../../contexts/MessagesContext";
|
||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
||||
import type { CreateFlowTextStateField } from "../../types";
|
||||
|
||||
type Props = {
|
||||
messageNamespace: string;
|
||||
stateField: CreateFlowTextStateField;
|
||||
maxLength: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Text page for the create flow
|
||||
*
|
||||
* Displays a text input field for user input using HeaderLockup and TextInput components.
|
||||
* Lockup sizing via `CreateFlowHeaderLockup`. TextInput: small / medium by breakpoint.
|
||||
* Below `md`, this step stays vertically centered in the main area (see `CreateFlowLayoutClient`).
|
||||
* Shared narrow-column + TextInput pattern for Create Community text frames.
|
||||
*/
|
||||
export default function TextPage() {
|
||||
export function CreateFlowTextFieldScreen({
|
||||
messageNamespace,
|
||||
stateField,
|
||||
maxLength,
|
||||
}: Props) {
|
||||
const { markCreateFlowInteraction, updateState, state } = useCreateFlow();
|
||||
const mdUp = useCreateFlowMdUp();
|
||||
const t = useTranslation("create.text");
|
||||
const [value, setValue] = useState(() =>
|
||||
typeof state.title === "string" ? state.title : "",
|
||||
);
|
||||
const t = useTranslation(messageNamespace);
|
||||
|
||||
const readFromState = (): string => {
|
||||
const raw = state[stateField];
|
||||
return typeof raw === "string" ? raw : "";
|
||||
};
|
||||
|
||||
const [value, setValue] = useState(() => readFromState());
|
||||
|
||||
useEffect(() => {
|
||||
const incoming = state.title;
|
||||
if (typeof incoming !== "string" || incoming.length === 0) return;
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- sync controlled field when context hydrates from server/local
|
||||
const incoming = readFromState();
|
||||
if (incoming.length === 0) return;
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- sync when context hydrates from server/local
|
||||
setValue((prev) => (prev === "" ? incoming : prev));
|
||||
}, [state.title]);
|
||||
}, [state, stateField]);
|
||||
|
||||
const maxLength = 48;
|
||||
const characterCount = value.length;
|
||||
const hint = t("characterCountTemplate")
|
||||
.replace("{current}", String(characterCount))
|
||||
@@ -52,7 +62,7 @@ export default function TextPage() {
|
||||
const v = e.target.value;
|
||||
setValue(v);
|
||||
markCreateFlowInteraction();
|
||||
updateState({ title: v });
|
||||
updateState({ [stateField]: v } as Record<string, string>);
|
||||
}}
|
||||
inputSize={mdUp ? "medium" : "small"}
|
||||
formHeader={false}
|
||||
@@ -1,27 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import Upload from "../../components/controls/Upload";
|
||||
import { useTranslation } from "../../contexts/MessagesContext";
|
||||
import { useCreateFlow } from "../context/CreateFlowContext";
|
||||
import { useCreateFlowMdUp } from "../hooks/useCreateFlowMdUp";
|
||||
import { CreateFlowHeaderLockup } from "../components/CreateFlowHeaderLockup";
|
||||
import { CreateFlowStepShell } from "../components/CreateFlowStepShell";
|
||||
import Upload from "../../../components/controls/Upload";
|
||||
import { useTranslation } from "../../../contexts/MessagesContext";
|
||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
||||
|
||||
/**
|
||||
* Upload page for the create flow
|
||||
*
|
||||
* Displays upload functionality using HeaderLockup and Upload components.
|
||||
* Responsive layout: centered at `md` and up, left-aligned below.
|
||||
* Lockup sizing via `CreateFlowHeaderLockup`.
|
||||
*/
|
||||
export default function UploadPage() {
|
||||
/** Create Community — frame 6 (Figma 20094-41524). */
|
||||
export function CommunityUploadScreen() {
|
||||
const { markCreateFlowInteraction } = useCreateFlow();
|
||||
const mdUp = useCreateFlowMdUp();
|
||||
const t = useTranslation("create.upload");
|
||||
const t = useTranslation("create.communityUpload");
|
||||
|
||||
const handleUploadClick = () => {
|
||||
markCreateFlowInteraction();
|
||||
// TODO: Handle upload button click (e.g. open file picker)
|
||||
};
|
||||
|
||||
return (
|
||||
+25
-5
@@ -6,13 +6,17 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* Valid step IDs for the create rule flow
|
||||
* Valid step IDs for the create rule flow (URL segment after `/create/`).
|
||||
* Create Community order matches Figma; `review` closes that stage per design.
|
||||
*/
|
||||
export type CreateFlowStep =
|
||||
| "informational"
|
||||
| "text"
|
||||
| "select"
|
||||
| "upload"
|
||||
| "community-name"
|
||||
| "community-size"
|
||||
| "community-context"
|
||||
| "community-structure"
|
||||
| "community-upload"
|
||||
| "community-reflection"
|
||||
| "review"
|
||||
| "cards"
|
||||
| "right-rail"
|
||||
@@ -20,6 +24,13 @@ export type CreateFlowStep =
|
||||
| "final-review"
|
||||
| "completed";
|
||||
|
||||
/** String keys used by generic text-field steps for `CreateFlowState`. */
|
||||
export type CreateFlowTextStateField =
|
||||
| "title"
|
||||
| "summary"
|
||||
| "communityContext"
|
||||
| "communityReflection";
|
||||
|
||||
/**
|
||||
* Flow state for inputs across create-flow steps.
|
||||
* Validated on `PUT /api/drafts/me` via `createFlowStateSchema` (Zod + JSON safety checks).
|
||||
@@ -28,6 +39,15 @@ export type CreateFlowStep =
|
||||
export interface CreateFlowState {
|
||||
title?: string;
|
||||
summary?: string;
|
||||
/** Additional copy fields for multi-step Create Community text frames (Figma). */
|
||||
communityContext?: string;
|
||||
communityReflection?: string;
|
||||
/** Selected chip ids from `community-size` (MultiSelect). */
|
||||
selectedCommunitySizeIds?: string[];
|
||||
/** Selected chip ids from `community-structure` (organization types). */
|
||||
selectedOrganizationTypeIds?: string[];
|
||||
/** Selected chip ids from `community-structure` (governance styles). */
|
||||
selectedGovernanceStyleIds?: string[];
|
||||
currentStep?: CreateFlowStep;
|
||||
/** Section drafts; structure will tighten as steps persist real shapes. */
|
||||
sections?: Record<string, unknown>[];
|
||||
@@ -51,7 +71,7 @@ export interface CreateFlowContextValue {
|
||||
clearState: () => void;
|
||||
/**
|
||||
* True after the user edits any template control (pages use local state until wired to `state`).
|
||||
* Drives Save & Exit visibility together with `hasCreateFlowUserInput(state)`.
|
||||
* Drives Save & Exit visibility together with hasCreateFlowUserInput (utils/hasCreateFlowUserInput.ts).
|
||||
*/
|
||||
interactionTouched: boolean;
|
||||
markCreateFlowInteraction: () => void;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { CreateFlowState } from "./types";
|
||||
import type { CreateFlowState } from "../types";
|
||||
|
||||
/** Anonymous in-progress create flow (local only until magic-link transfer). */
|
||||
export const CREATE_FLOW_ANONYMOUS_KEY = "create-flow-anonymous" as const;
|
||||
@@ -0,0 +1,123 @@
|
||||
import type { CreateFlowStep } from "../types";
|
||||
|
||||
/**
|
||||
* Figma layout families for the create flow (not encoded in the URL).
|
||||
* Registry and `app/create/screens/` are organized by these kinds.
|
||||
*/
|
||||
export type CreateFlowLayoutKind =
|
||||
| "informational"
|
||||
| "text"
|
||||
| "select"
|
||||
| "upload"
|
||||
| "review"
|
||||
| "card"
|
||||
| "right-rail"
|
||||
| "completed";
|
||||
|
||||
export interface CreateFlowScreenDefinition {
|
||||
layoutKind: CreateFlowLayoutKind;
|
||||
/** Figma node id (file Community-Rule-System), dev mode. */
|
||||
figmaNodeId: string;
|
||||
/**
|
||||
* Namespace for `useTranslation`, e.g. `create.communityName`.
|
||||
* Not all screens use i18n the same way (e.g. card step uses `useMessages` elsewhere).
|
||||
*/
|
||||
messageNamespace: string;
|
||||
/** Match legacy `text` step: main area vertically centered below `md`. */
|
||||
centeredBodyBelowMd: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registry: **distinct URL (`CreateFlowStep`) → Figma + layout**.
|
||||
* Source of truth for product order remains `FLOW_STEP_ORDER` in `flowSteps.ts`.
|
||||
*/
|
||||
export const CREATE_FLOW_SCREEN_REGISTRY: Record<
|
||||
CreateFlowStep,
|
||||
CreateFlowScreenDefinition
|
||||
> = {
|
||||
informational: {
|
||||
layoutKind: "informational",
|
||||
figmaNodeId: "20094-16005",
|
||||
messageNamespace: "create.informational",
|
||||
centeredBodyBelowMd: false,
|
||||
},
|
||||
"community-name": {
|
||||
layoutKind: "text",
|
||||
figmaNodeId: "20094-18187",
|
||||
messageNamespace: "create.communityName",
|
||||
centeredBodyBelowMd: true,
|
||||
},
|
||||
"community-size": {
|
||||
layoutKind: "select",
|
||||
figmaNodeId: "20094-18244",
|
||||
messageNamespace: "create.communitySize",
|
||||
centeredBodyBelowMd: false,
|
||||
},
|
||||
"community-context": {
|
||||
layoutKind: "text",
|
||||
figmaNodeId: "20094-41243",
|
||||
messageNamespace: "create.communityContext",
|
||||
centeredBodyBelowMd: true,
|
||||
},
|
||||
"community-structure": {
|
||||
layoutKind: "select",
|
||||
figmaNodeId: "20094-41317",
|
||||
messageNamespace: "create.communityStructure",
|
||||
centeredBodyBelowMd: false,
|
||||
},
|
||||
"community-upload": {
|
||||
layoutKind: "upload",
|
||||
figmaNodeId: "20094-41524",
|
||||
messageNamespace: "create.communityUpload",
|
||||
centeredBodyBelowMd: false,
|
||||
},
|
||||
"community-reflection": {
|
||||
layoutKind: "text",
|
||||
figmaNodeId: "20097-14948",
|
||||
messageNamespace: "create.communityReflection",
|
||||
centeredBodyBelowMd: true,
|
||||
},
|
||||
review: {
|
||||
layoutKind: "review",
|
||||
figmaNodeId: "19706-12135",
|
||||
messageNamespace: "create.review",
|
||||
centeredBodyBelowMd: false,
|
||||
},
|
||||
cards: {
|
||||
layoutKind: "card",
|
||||
figmaNodeId: "TBD-cards",
|
||||
messageNamespace: "create.communication",
|
||||
centeredBodyBelowMd: false,
|
||||
},
|
||||
"right-rail": {
|
||||
layoutKind: "right-rail",
|
||||
figmaNodeId: "TBD-right-rail",
|
||||
messageNamespace: "create.rightRail",
|
||||
centeredBodyBelowMd: false,
|
||||
},
|
||||
"confirm-stakeholders": {
|
||||
layoutKind: "select",
|
||||
figmaNodeId: "21104-46594",
|
||||
messageNamespace: "create.confirmStakeholders",
|
||||
centeredBodyBelowMd: false,
|
||||
},
|
||||
"final-review": {
|
||||
layoutKind: "review",
|
||||
figmaNodeId: "20907-212767",
|
||||
messageNamespace: "create.finalReview",
|
||||
centeredBodyBelowMd: false,
|
||||
},
|
||||
completed: {
|
||||
layoutKind: "completed",
|
||||
figmaNodeId: "20907-213286",
|
||||
messageNamespace: "create.completed",
|
||||
centeredBodyBelowMd: false,
|
||||
},
|
||||
};
|
||||
|
||||
export function createFlowStepUsesCenteredTextLayout(
|
||||
step: CreateFlowStep | null,
|
||||
): boolean {
|
||||
if (!step) return false;
|
||||
return CREATE_FLOW_SCREEN_REGISTRY[step].centeredBodyBelowMd;
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
* Step definitions and helpers for the Create Rule Flow
|
||||
*
|
||||
* Single source of truth for step order and navigation helpers.
|
||||
* Order matches Figma Create Community (frames 1–8) then later stages.
|
||||
*/
|
||||
|
||||
import type { CreateFlowStep } from "../types";
|
||||
@@ -11,9 +12,12 @@ import type { CreateFlowStep } from "../types";
|
||||
*/
|
||||
export const FLOW_STEP_ORDER: readonly CreateFlowStep[] = [
|
||||
"informational",
|
||||
"text",
|
||||
"select",
|
||||
"upload",
|
||||
"community-name",
|
||||
"community-size",
|
||||
"community-context",
|
||||
"community-structure",
|
||||
"community-upload",
|
||||
"community-reflection",
|
||||
"review",
|
||||
"cards",
|
||||
"right-rail",
|
||||
@@ -75,3 +79,23 @@ export function isValidStep(
|
||||
(VALID_STEPS as readonly string[]).includes(step)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses `/create/{screenId}` (and optional trailing segments) from pathname.
|
||||
* Returns null for non-wizard paths (e.g. `/create/review-template/...`).
|
||||
*/
|
||||
export function parseCreateFlowScreenFromPathname(
|
||||
pathname: string | null,
|
||||
): CreateFlowStep | null {
|
||||
if (!pathname || pathname.length === 0) return null;
|
||||
if (pathname.includes("/create/review-template/")) return null;
|
||||
|
||||
const parts = pathname.split("/").filter(Boolean);
|
||||
const createIdx = parts.indexOf("create");
|
||||
if (createIdx === -1 || createIdx >= parts.length - 1) return null;
|
||||
|
||||
const segment = parts[createIdx + 1];
|
||||
if (segment === "review-template") return null;
|
||||
|
||||
return isValidStep(segment) ? segment : null;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { CreateFlowState } from "./types";
|
||||
import type { CreateFlowState } from "../types";
|
||||
|
||||
const IGNORED_KEYS = new Set<string>(["currentStep"]);
|
||||
|
||||
@@ -190,7 +190,7 @@ Optional: **Docker image deploy** using the repo [Dockerfile](Dockerfile)—admi
|
||||
**Implementation:**
|
||||
|
||||
1. **Hydration:** **Done:** [SignedInDraftHydration](app/create/SignedInDraftHydration.tsx) + [messages/en/create/draftHydration.json](messages/en/create/draftHydration.json); skips `?syncDraft=1` / transfer-pending (PostLogin owns that). Wired in [layout](app/create/layout.tsx).
|
||||
2. **Conflict:** **Done:** If `create-flow-anonymous` and server draft are both non-empty, `window.confirm` (OK = account draft, Cancel = browser copy); documented on [anonymousDraftStorage](app/create/anonymousDraftStorage.ts). Newer-`updatedAt` client compare remains optional.
|
||||
2. **Conflict:** **Done:** If `create-flow-anonymous` and server draft are both non-empty, `window.confirm` (OK = account draft, Cancel = browser copy); documented on [anonymousDraftStorage](app/create/utils/anonymousDraftStorage.ts). Newer-`updatedAt` client compare remains optional.
|
||||
3. **Save failures (API surface):** **Done (CR-76):** [saveDraftToServer](lib/create/api.ts) returns `SaveDraftResult` with parsed API `message`; wired in [useCreateFlowExit](app/create/hooks/useCreateFlowExit.ts) and [PostLoginDraftTransfer](app/create/PostLoginDraftTransfer.tsx).
|
||||
4. **Save failures (UX):** **Done (CR-76):** Dismissible banner with server `message` (no second confirm to leave); post-login transfer shows reason; unit tests in `tests/unit/saveDraftToServer.test.ts`. Retry/backoff remains optional.
|
||||
5. **Tests:** `saveDraftToServer` unit tests; [draftHydrationUtils](lib/create/draftHydrationUtils.ts) unit tests. Playwright against Next standalone + route mocks for `/api/auth/session` was flaky here; cover hydration with **manual QA** (signed in + sync on + server draft) or add a future E2E with a dedicated auth fixture.
|
||||
@@ -210,7 +210,7 @@ Optional: **Docker image deploy** using the repo [Dockerfile](Dockerfile)—admi
|
||||
|
||||
**Goal:** Completing the flow persists a **PublishedRule** via existing [publishRule](lib/create/api.ts).
|
||||
|
||||
**Context:** [lib/create/api.ts](lib/create/api.ts) already wraps `POST /api/rules`. UI on [app/create/final-review/page.tsx](app/create/final-review/page.tsx) or [completed/page.tsx](app/create/completed/page.tsx) must call it with `{ title, summary?, document }` derived from `CreateFlowState`.
|
||||
**Context:** [lib/create/api.ts](lib/create/api.ts) already wraps `POST /api/rules`. UI on the `final-review` / `completed` steps (see [app/create/screens/CreateFlowScreenView.tsx](app/create/screens/CreateFlowScreenView.tsx) and `app/create/screens/`) must call it with `{ title, summary?, document }` derived from `CreateFlowState`.
|
||||
|
||||
**Implementation:**
|
||||
|
||||
@@ -258,7 +258,7 @@ Optional: **Docker image deploy** using the repo [Dockerfile](Dockerfile)—admi
|
||||
|
||||
**Goal:** Home or create entry surfaces use live template data instead of only static i18n JSON.
|
||||
|
||||
**Context:** [RuleStack.view.tsx](app/components/sections/RuleStack/RuleStack.view.tsx) and [app/create/[step]/page.tsx](app/create/[step]/page.tsx) placeholders reference future template work (CR-51–55).
|
||||
**Context:** [RuleStack.view.tsx](app/components/sections/RuleStack/RuleStack.view.tsx) and create entry surfaces reference future template work. Wizard URLs are static segments under `app/create/`; see [`docs/create-flow.md`](create-flow.md) and **Ticket 17** for the canonical custom flow.
|
||||
|
||||
**Implementation:**
|
||||
|
||||
@@ -271,7 +271,7 @@ Optional: **Docker image deploy** using the repo [Dockerfile](Dockerfile)—admi
|
||||
- [ ] Changing a template row in Prisma Studio reflects after refresh (or revalidate).
|
||||
- [ ] No layout shift regression on LCP-critical pages (use skeletons).
|
||||
|
||||
**Files:** [app/components/sections/RuleStack/](app/components/sections/RuleStack/), [app/create/[step]/page.tsx](app/create/[step]/page.tsx) or related, possibly new `lib/templates/fetchTemplates.ts`.
|
||||
**Files:** [app/components/sections/RuleStack/](app/components/sections/RuleStack/), create-flow entry routes under [app/create/](app/create/), possibly new `lib/templates/fetchTemplates.ts`.
|
||||
|
||||
**Follow-up:** **Ticket 16** — dynamic recommendations from authoring spreadsheets and create-flow answers.
|
||||
|
||||
@@ -305,6 +305,37 @@ Optional: **Docker image deploy** using the repo [Dockerfile](Dockerfile)—admi
|
||||
|
||||
---
|
||||
|
||||
## Ticket 17 — Canon custom create-rule wizard (routes, resume, progress) + docs
|
||||
|
||||
**Depends on:** none for documentation; soft optional **CR-73**, **CR-76**, **CR-77** for payload/resume/publish alignment.
|
||||
|
||||
**Goal:** Establish the **official custom** create-rule flow (ordered steps, URLs, persistence, entry points, **Figma three-stage framing**) in repo docs and close gaps between that spec and the implementation (routing clutter, progress UI, step source of truth, resume vs URL).
|
||||
|
||||
**Context:** Step order lives in [`app/create/utils/flowSteps.ts`](app/create/utils/flowSteps.ts). Wizard screens render from [`app/create/[screenId]/page.tsx`](app/create/[screenId]/page.tsx) plus [`CREATE_FLOW_SCREEN_REGISTRY`](app/create/utils/createFlowScreenRegistry.ts) (Figma node + layout family per slug). [`docs/create-flow.md`](create-flow.md) is the **canonical** URL/persistence summary: **Create Community** (eight semantic steps ending at `review`) → **Create Custom CommunityRule** → **Review and complete**. **Full create-from-template** will likely use **additional route(s)** when product defines it; **`/create/review-template/[slug]`** remains auxiliary preview only. **Template → `final-review` or mid-wizard prefill** is **out of scope** here (future ticket); `/create/informational?template=` is a **no-op** until then.
|
||||
|
||||
**Implementation:**
|
||||
|
||||
1. Keep [`docs/create-flow.md`](create-flow.md) in sync with product/Figma (stage ↔ step mapping, future template routes).
|
||||
2. ~~Remove legacy [`app/create/[step]/page.tsx`](app/create/[step]/page.tsx)~~ — replaced by [`app/create/[screenId]/page.tsx`](app/create/[screenId]/page.tsx) with real screens; unknown slugs `notFound()`.
|
||||
3. Unify **step source of truth**: URL via [`useCreateFlowNavigation`](app/create/hooks/useCreateFlowNavigation.ts) vs unused [`CreateFlowContext`](app/create/context/CreateFlowContext.tsx) `currentStep` — pick one model; align [`useCreateFlowExit`](app/create/hooks/useCreateFlowExit.ts) / draft payload if needed.
|
||||
4. **Resume:** After [`SignedInDraftHydration`](app/create/SignedInDraftHydration.tsx), decide redirect to `/create/${state.currentStep}` vs stay on current URL; test or document.
|
||||
5. Wire [`CreateFlowFooter`](app/components/utility/CreateFlowFooter/) `ProportionBar` to step progress from `FLOW_STEP_ORDER` (and `review-template` / `completed` exceptions per design); optional **two-level progress** (stage + step within stage) when design specifies.
|
||||
6. When Figma hands off, surface **stage labels** in create shell (top nav, footer, or step chrome) using the mapping in `create-flow.md`.
|
||||
|
||||
**Acceptance criteria:**
|
||||
|
||||
- [ ] [`docs/create-flow.md`](create-flow.md) matches shipped behavior or lists known gaps, including **Figma three-stage** mapping and **future template route** note.
|
||||
- [ ] No misleading dynamic step placeholder for valid wizard URLs.
|
||||
- [ ] Footer progress reflects step index **or** doc/issue records a deliberate deferral with design sign-off.
|
||||
- [ ] Hydration + `currentStep` behavior is verified (redirect vs stay).
|
||||
- [ ] `?template=` documented as deferred; no implied “template customize → full wizard” parity.
|
||||
|
||||
**Files:** [`docs/create-flow.md`](create-flow.md), [`app/create/`](app/create/), [`app/components/utility/CreateFlowFooter/`](app/components/utility/CreateFlowFooter/), optionally [`docs/backend-roadmap.md`](backend-roadmap.md) §12 cross-links.
|
||||
|
||||
**Linear:** [CR-89](https://linear.app/community-rule/issue/CR-89/product-canon-custom-create-rule-wizard-routes-resume-progress-repo) (**Backlog**). **Parallel** to templates (7–8) and publish (6); not part of **CR-72 → CR-83**.
|
||||
|
||||
---
|
||||
|
||||
## Ticket 9 — Persist web vitals outside `.next` (prefer external RUM)
|
||||
|
||||
**Depends on:** none (orthogonal).
|
||||
@@ -509,14 +540,15 @@ Optional: **Docker image deploy** using the repo [Dockerfile](Dockerfile)—admi
|
||||
| 14 | 14 | Session lifecycle + cleanup |
|
||||
| 15 | 15 | Profile + account (Figma profile) |
|
||||
| 16 | 16 | Template matrix + xlsx ingestion |
|
||||
| 17 | 17 | Canon create-flow (custom path) |
|
||||
|
||||
Tickets **10–11** can be deferred without blocking the core “auth + drafts + publish + templates” vertical slice. **Ticket 16** is also **deferrable** until after **7–8** (flat template list + UI); it adds **spreadsheet-driven** recommendations and facet APIs. **Tickets 13–14** are parallel to that chain (**CR-73** / **CR-75** prerequisites are **Done** — **CR-84** / **CR-85** are unblocked), not sequential after CR-83. **Ticket 15** is also **parallel** (blocked by **publish (CR-77)** once session/auth are shipped); Linear: **CR-86**.
|
||||
Tickets **10–11** can be deferred without blocking the core “auth + drafts + publish + templates” vertical slice. **Ticket 16** is also **deferrable** until after **7–8** (flat template list + UI); it adds **spreadsheet-driven** recommendations and facet APIs. **Ticket 17** (**[CR-89](https://linear.app/community-rule/issue/CR-89/product-canon-custom-create-rule-wizard-routes-resume-progress-repo)**) canonizes the **custom** wizard in [`docs/create-flow.md`](create-flow.md) and tracks UX/code alignment (progress bar, resume URL, `[step]` cleanup); **parallel** to publish and templates. **Tickets 13–14** are parallel to that chain (**CR-73** / **CR-75** prerequisites are **Done** — **CR-84** / **CR-85** are unblocked), not sequential after CR-83. **Ticket 15** is also **parallel** (blocked by **publish (CR-77)** once session/auth are shipped); Linear: **CR-86**.
|
||||
|
||||
---
|
||||
|
||||
## Linear (Community-rule team)
|
||||
|
||||
**Main chain:** **CR-72 → CR-83** (each blocks the next). **Parallel:** **CR-84** (**CR-73** Done — ready to pick up), **CR-85** (**CR-75** Done — ready to pick up), **CR-86** / Ticket 15 (blocked by **CR-77** publish only; **CR-75** Done), **CR-88** / Ticket 16 (template matrix + `.xlsx` ingestion — after **CR-78**/**CR-79**), not in the CR-72–83 sequence.
|
||||
**Main chain:** **CR-72 → CR-83** (each blocks the next). **Parallel:** **CR-84** (**CR-73** Done — ready to pick up), **CR-85** (**CR-75** Done — ready to pick up), **CR-86** / Ticket 15 (blocked by **CR-77** publish only; **CR-75** Done), **CR-88** / Ticket 16 (template matrix + `.xlsx` ingestion — after **CR-78**/**CR-79**), **CR-89** / Ticket 17 (canon create-flow + implementation gaps), not in the CR-72–83 sequence.
|
||||
|
||||
| Doc ticket | Linear | Title (short) |
|
||||
| ---------: | --------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- |
|
||||
@@ -536,6 +568,7 @@ Tickets **10–11** can be deferred without blocking the core “auth + drafts +
|
||||
| 14 | [CR-85](https://linear.app/community-rule/issue/CR-85/backend-custom-session-lifecycle-cleanup-invalidation-policy) | Session lifecycle + cleanup |
|
||||
| 15 | [CR-86](https://linear.app/community-rule/issue/CR-86/backend-profile-dashboard-account-figma-profile) | Profile + account (Figma 22143:900069) |
|
||||
| 16 | [CR-88](https://linear.app/community-rule/issue/CR-88/backend-template-recommendation-matrix-xlsx-sheets-ingestion) | Template matrix + xlsx ingestion |
|
||||
| 17 | [CR-89](https://linear.app/community-rule/issue/CR-89/product-canon-custom-create-rule-wizard-routes-resume-progress-repo) | Canon create-flow (custom wizard + docs) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ Temporary working notes for building the backend. Safe to delete once the stack
|
||||
- **Next.js 16** single repo ([`package.json`](package.json)).
|
||||
- **PostgreSQL + Prisma**: schema and migrations under `prisma/`; product APIs under `app/api/*` (health, auth/magic-link, session, drafts, rules, templates, web-vitals).
|
||||
- **Server modules** in `lib/server/` (db, session, mail, rate limiting, etc.).
|
||||
- **Create flow:** **Anonymous** users mirror in-progress state to **`create-flow-anonymous`** in `localStorage`; **Exit** opens the save-progress magic-link modal; after verify, [`PostLoginDraftTransfer`](app/create/PostLoginDraftTransfer.tsx) can **PUT** `/api/drafts/me` when **`NEXT_PUBLIC_ENABLE_BACKEND_SYNC=true`**. **Signed-in** users start a **fresh** in-memory session per “Create rule”; **Save & Exit** (from `select` onward) **PUT**s when sync is on. **Log in** from the marketing header uses the global modal ([`AuthModalProvider`](app/contexts/AuthModalContext.tsx)); **`/login`** remains for verify errors and deep links.
|
||||
- **Create flow:** **Anonymous** users mirror in-progress state to **`create-flow-anonymous`** in `localStorage`; **Exit** opens the save-progress magic-link modal; after verify, [`PostLoginDraftTransfer`](app/create/PostLoginDraftTransfer.tsx) can **PUT** `/api/drafts/me` when **`NEXT_PUBLIC_ENABLE_BACKEND_SYNC=true`**. **Signed-in** users get a **fresh** in-memory session per “Create rule” entry, but with sync on the layout may **hydrate** from **`GET /api/drafts/me`** via [`SignedInDraftHydration`](app/create/SignedInDraftHydration.tsx); **Save & Exit** (from `community-size` onward) **PUT**s when sync is on. **Log in** from the marketing header uses the global modal ([`AuthModalProvider`](app/contexts/AuthModalContext.tsx)); **`/login`** remains for verify errors and deep links. **Step order and URLs:** [`docs/create-flow.md`](docs/create-flow.md) and [`app/create/utils/flowSteps.ts`](app/create/utils/flowSteps.ts).
|
||||
- **Web vitals** [`app/api/web-vitals/route.ts`](app/api/web-vitals/route.ts) still use **file-based** storage under `.next` (not suitable for multi-instance production).
|
||||
- **CI:** [`.gitea/workflows/ci.yaml`](.gitea/workflows/ci.yaml) (build, test, lint, `prisma validate`); no in-repo production deploy definition.
|
||||
|
||||
@@ -138,7 +138,7 @@ Match the current API behavior; tighten as product evolves:
|
||||
|
||||
**Backend behavior already in the repo:** Steps **5–10** match implemented Route Handlers and middleware (`lib/server/*`). **Step 11** (web vitals) is **not** production-ready (files under `.next`); treat as follow-up work aligned with §7.
|
||||
|
||||
**Product / frontend still open (not only “backend exists”):** Sign-in UI, wiring publish from the create flow, template seed + UI consumption (flat list first), **spreadsheet-driven template recommendations** (Ticket 16 / [CR-88](https://linear.app/community-rule/issue/CR-88/backend-template-recommendation-matrix-xlsx-sheets-ingestion) — after v1 templates), **profile / my rules dashboard** (Ticket 15)—see §12 and [docs/backend-linear-tickets.md](backend-linear-tickets.md).
|
||||
**Product / frontend still open (not only “backend exists”):** Sign-in UI, wiring publish from the create flow, template seed + UI consumption (flat list first), **canon create-flow alignment** (Ticket 17 / [CR-89](https://linear.app/community-rule/issue/CR-89/product-canon-custom-create-rule-wizard-routes-resume-progress-repo) — progress bar, resume URL, `[step]` cleanup; spec in [`docs/create-flow.md`](create-flow.md)), **spreadsheet-driven template recommendations** (Ticket 16 / [CR-88](https://linear.app/community-rule/issue/CR-88/backend-template-recommendation-matrix-xlsx-sheets-ingestion) — after v1 templates), **profile / my rules dashboard** (Ticket 15)—see §12 and [docs/backend-linear-tickets.md](backend-linear-tickets.md).
|
||||
|
||||
---
|
||||
|
||||
@@ -218,7 +218,7 @@ npm run dev
|
||||
|
||||
## 12. Frontend hook-up
|
||||
|
||||
**Step 1.** **Anonymous** create flow: in-progress state is stored in **`create-flow-anonymous`** (`localStorage`). **Signed-in** “Create rule” does **not** auto-load server drafts yet (profile “open draft” is future).
|
||||
**Step 1.** **Anonymous** create flow: in-progress state is stored in **`create-flow-anonymous`** (`localStorage`). **Signed-in** users: when **`NEXT_PUBLIC_ENABLE_BACKEND_SYNC=true`**, the create layout may **hydrate** in-memory flow state from **`GET /api/drafts/me`** once per session ([`SignedInDraftHydration`](../app/create/SignedInDraftHydration.tsx)), including conflict handling if anonymous storage also has data. Without sync, signed-in progress stays **in memory** until **Save & Exit** (no automatic server read on entry). **Canonical wizard step order, URLs, and Figma product stages** (**Create Community** → **Create Custom CommunityRule** → **Review and complete**) are documented in [`docs/create-flow.md`](create-flow.md). The route **`/create/review-template/[slug]`** is an **auxiliary** template preview (not a numbered wizard step); a **full create-from-template** path will likely be **separate route(s)** when defined. **Prefilling the wizard or landing on `final-review` from a template** is **not** shipped yet — see **[CR-89](https://linear.app/community-rule/issue/CR-89/product-canon-custom-create-rule-wizard-routes-resume-progress-repo)** / Ticket 17 in [docs/backend-linear-tickets.md](backend-linear-tickets.md).
|
||||
|
||||
**Step 2.** Set `NEXT_PUBLIC_ENABLE_BACKEND_SYNC=true` to enable **PUT** on **Save & Exit** and after **magic-link transfer** from the save-progress exit modal.
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
# Create rule flow (custom wizard) — canonical reference
|
||||
|
||||
Product/engineering reference for the **custom** “Create rule” experience: URL order, persistence, and entry points. **Implementation work** to align code with this doc (progress bar, resume redirects, etc.) is tracked in Linear **[CR-89](https://linear.app/community-rule/issue/CR-89/product-canon-custom-create-rule-wizard-routes-resume-progress-repo)** and [docs/backend-linear-tickets.md](backend-linear-tickets.md) **Ticket 17**.
|
||||
|
||||
---
|
||||
|
||||
## Product stages (Figma)
|
||||
|
||||
The Figma **Create Community** sequence is the **source of truth** for the first segment of the wizard (eight frames). After **`review`**, the flow continues with **Create Custom CommunityRule** and **Review and complete** stages. The shipped URL sequence in [`FLOW_STEP_ORDER`](../app/create/utils/flowSteps.ts) **follows that trajectory**; stages are a **product** slice of that linear order, not separate routers today.
|
||||
|
||||
| Stage (Figma) | Purpose (summary) | `CreateFlowStep` values (in order) |
|
||||
| --- | --- | --- |
|
||||
| **Create Community** | Intro, naming, size, context, structure, upload, reflection, then community review. | `informational` → `community-name` → `community-size` → `community-context` → `community-structure` → `community-upload` → `community-reflection` → `review` |
|
||||
| **Create Custom CommunityRule** | Author the CommunityRule content and structure. | `cards` → `right-rail` |
|
||||
| **Review and complete** | Stakeholders, final card, publish, success. | `confirm-stakeholders` → `final-review` → `completed` |
|
||||
|
||||
Treat these stages as the **canonical product sections** when adding chrome (e.g. stage headers, progress copy), breaking work across teams, or reusing flows in other surfaces. **Layout kind** is **not** encoded in the URL; it lives in [`CREATE_FLOW_SCREEN_REGISTRY`](../app/create/utils/createFlowScreenRegistry.ts) (Figma node id + `layoutKind` per step). Figma defines eight layout kinds: **informational**, **text**, **select**, **upload**, **review**, **card**, **right-rail**, **completed** — `CreateFlowLayoutKind` and [`app/create/screens/`](../app/create/screens/) mirror that list (one folder per kind; multiple steps may share a kind, e.g. several **select** screens).
|
||||
|
||||
**Create from template (future):** A full **template-driven** create path is **not** finalized; it will likely live on **additional route(s)** (and may reuse these stages where it overlaps the custom trajectory). Today, **`/create/review-template/[slug]`** is only an auxiliary **preview** in the create shell; it is **not** a Figma stage and not the final template-create entry. See **Out of scope** in [CR-89](https://linear.app/community-rule/issue/CR-89/product-canon-custom-create-rule-wizard-routes-resume-progress-repo).
|
||||
|
||||
---
|
||||
|
||||
## Step order and URLs
|
||||
|
||||
Order is defined in code by [`FLOW_STEP_ORDER`](../app/create/utils/flowSteps.ts) and the [`CreateFlowStep`](../app/create/types.ts) type. Wizard steps use a **single dynamic route**: [`app/create/[screenId]/page.tsx`](../app/create/[screenId]/page.tsx), which validates `screenId` and renders [`CreateFlowScreenView`](../app/create/screens/CreateFlowScreenView.tsx). Implementation files are grouped under [`app/create/screens/`](../app/create/screens/) by Figma **layout kind** (subfolders: informational, text, select, upload, review, card, right-rail, completed). **`/create`** redirects to the first step.
|
||||
|
||||
| Order | Figma stage | Step ID (`screenId`) | Path |
|
||||
| ----: | ----------- | -------------------- | ---- |
|
||||
| 1 | Create Community | `informational` | `/create/informational` |
|
||||
| 2 | Create Community | `community-name` | `/create/community-name` |
|
||||
| 3 | Create Community | `community-size` | `/create/community-size` |
|
||||
| 4 | Create Community | `community-context` | `/create/community-context` |
|
||||
| 5 | Create Community | `community-structure` | `/create/community-structure` |
|
||||
| 6 | Create Community | `community-upload` | `/create/community-upload` |
|
||||
| 7 | Create Community | `community-reflection` | `/create/community-reflection` |
|
||||
| 8 | Create Community (review frame) | `review` | `/create/review` |
|
||||
| 9 | Create Custom CommunityRule | `cards` | `/create/cards` |
|
||||
| 10 | Create Custom CommunityRule | `right-rail` | `/create/right-rail` |
|
||||
| 11 | Review and complete | `confirm-stakeholders` | `/create/confirm-stakeholders` |
|
||||
| 12 | Review and complete | `final-review` | `/create/final-review` |
|
||||
| 13 | Review and complete | `completed` | `/create/completed` |
|
||||
|
||||
**Primary entry:** marketing header “Create rule” navigates to **`/create`**, which redirects to **`/create/informational`** (see [`TopNav.container.tsx`](../app/components/navigation/TopNav/TopNav.container.tsx)).
|
||||
|
||||
Active step for chrome and navigation is resolved from the pathname via [`parseCreateFlowScreenFromPathname`](../app/create/utils/flowSteps.ts) inside [`useCreateFlowNavigation`](../app/create/hooks/useCreateFlowNavigation.ts).
|
||||
|
||||
---
|
||||
|
||||
## Auxiliary route (not a wizard step or Figma stage)
|
||||
|
||||
| Path | Purpose |
|
||||
| --- | --- |
|
||||
| `/create/review-template/[slug]` | Template preview in the create shell; uses the same layout/footer chrome as other create pages but **is not** part of `FLOW_STEP_ORDER` **or** the three Figma stages above. |
|
||||
|
||||
From that page, **Customize** currently navigates to `/create/informational?template=<slug>`. The **`template` query parameter is reserved**; the informational step **does not** yet read it to prefill `CreateFlowState`. **Starting the wizard from a template at `final-review` or any mid-flow step** is **out of scope** until a dedicated product ticket ships. A **full create-from-template** experience will **likely use separate route(s)** when product and eng define it (may still align conceptually with the same three stages where behavior overlaps the custom path).
|
||||
|
||||
---
|
||||
|
||||
## Persistence and exit
|
||||
|
||||
| Mode | Where progress lives | Save & Exit / server draft |
|
||||
| --- | --- | --- |
|
||||
| **Anonymous** | `localStorage` key **`create-flow-anonymous`** | **Exit** opens save-progress magic link; after verify, optional **PUT** `/api/drafts/me` when `NEXT_PUBLIC_ENABLE_BACKEND_SYNC=true` (see Tickets 4–5 in [backend-linear-tickets.md](backend-linear-tickets.md)). |
|
||||
| **Signed-in** | In-memory React state in **`CreateFlowContext`** | **Save & Exit** from the **`community-size`** step onward (step index ≥ `community-size`) may **PUT** `/api/drafts/me` when sync is on. **Sign out** is on profile, not in the create top nav. |
|
||||
|
||||
Details and edge cases (conflict confirm, banners, `?syncDraft=1`) match **Ticket 4**, **Ticket 5**, and [`docs/backend-roadmap.md`](backend-roadmap.md) §12.
|
||||
|
||||
---
|
||||
|
||||
## Known implementation gaps (tracked on CR-89)
|
||||
|
||||
- **URL vs `currentStep` in saved draft:** hydration may merge server JSON without redirecting to `state.currentStep`; confirm product behavior and fix or document.
|
||||
- **Footer progress:** `ProportionBar` is not yet driven by step index vs `FLOW_STEP_ORDER`.
|
||||
- **Inner “text/select shells”:** deferred until Create Community is stable; screens use **`CreateFlowStepShell`** only for Stage 1.
|
||||
|
||||
---
|
||||
|
||||
## Related docs
|
||||
|
||||
- [docs/backend-roadmap.md](backend-roadmap.md) §12 — Frontend hook-up
|
||||
- [docs/backend-linear-tickets.md](backend-linear-tickets.md) — Tickets 4, 5, 6, 17
|
||||
@@ -50,11 +50,20 @@ export function buildPublishPayload(
|
||||
return { ok: false, error: "missingCommunityName" };
|
||||
}
|
||||
|
||||
let summary: string | undefined;
|
||||
if (typeof state.summary === "string") {
|
||||
const t = state.summary.trim();
|
||||
if (t.length > 0) summary = t;
|
||||
}
|
||||
const firstNonEmpty = (...candidates: unknown[]): string | undefined => {
|
||||
for (const c of candidates) {
|
||||
if (typeof c !== "string") continue;
|
||||
const t = c.trim();
|
||||
if (t.length > 0) return t;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
let summary = firstNonEmpty(
|
||||
state.summary,
|
||||
state.communityContext,
|
||||
state.communityReflection,
|
||||
);
|
||||
|
||||
let sections = parseSectionsFromCreateFlowState(state);
|
||||
if (sections.length === 0) {
|
||||
|
||||
@@ -29,6 +29,11 @@ export const createFlowStateSchema = z
|
||||
.object({
|
||||
title: z.string().max(500).optional(),
|
||||
summary: z.string().max(8000).optional(),
|
||||
communityContext: z.string().max(8000).optional(),
|
||||
communityReflection: z.string().max(8000).optional(),
|
||||
selectedCommunitySizeIds: z.array(z.string()).optional(),
|
||||
selectedOrganizationTypeIds: z.array(z.string()).optional(),
|
||||
selectedGovernanceStyleIds: z.array(z.string()).optional(),
|
||||
currentStep: createFlowStepSchema.optional(),
|
||||
sections: z.array(z.unknown()).optional(),
|
||||
stakeholders: z.array(z.unknown()).optional(),
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"title": "Tell us more about your community",
|
||||
"description": "Share context that will help shape your CommunityRule.",
|
||||
"placeholder": "Describe your community",
|
||||
"characterCountTemplate": "{current}/{max}"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"title": "What is your community called?",
|
||||
"description": "This will be the name of your community",
|
||||
"placeholder": "Enter your community name",
|
||||
"characterCountTemplate": "{current}/{max}"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"title": "Anything else we should know?",
|
||||
"description": "Optional details before you review your progress.",
|
||||
"placeholder": "Add notes (optional)",
|
||||
"characterCountTemplate": "{current}/{max}"
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"header": {
|
||||
"title": "How large is your community?",
|
||||
"description": "Choose the size that best matches your group."
|
||||
},
|
||||
"multiSelect": {
|
||||
"label": "Label",
|
||||
"addButtonText": "Add organization type"
|
||||
},
|
||||
"communitySizes": [
|
||||
{ "label": "1 member" },
|
||||
{ "label": "2-10 members" },
|
||||
{ "label": "10-24 members" },
|
||||
{ "label": "24-64 members" },
|
||||
{ "label": "64-128 members" },
|
||||
{ "label": "125-1000 members" },
|
||||
{ "label": "1000+ members" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"header": {
|
||||
"title": "How is your community organized?",
|
||||
"description": "Select the options that best describe your group."
|
||||
},
|
||||
"multiSelect": {
|
||||
"label": "Label",
|
||||
"addButtonText": "Add organization type"
|
||||
},
|
||||
"organizationTypes": [
|
||||
{ "label": "Non-profit" },
|
||||
{ "label": "For-profit" },
|
||||
{ "label": "Community" },
|
||||
{ "label": "Educational" }
|
||||
],
|
||||
"governanceStyles": [
|
||||
{ "label": "Democratic" },
|
||||
{ "label": "Consensus" },
|
||||
{ "label": "Hierarchical" },
|
||||
{ "label": "Flat" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"title": "How should conflicts be resolved?",
|
||||
"description": "Upload supporting materials or examples that help describe how your community handles conflict."
|
||||
}
|
||||
+12
-6
@@ -20,9 +20,12 @@ import navigation from "./navigation.json";
|
||||
import metadata from "./metadata.json";
|
||||
import communication from "./create/communication.json";
|
||||
import createInformational from "./create/informational.json";
|
||||
import createText from "./create/text.json";
|
||||
import createSelect from "./create/select.json";
|
||||
import createUpload from "./create/upload.json";
|
||||
import createCommunityName from "./create/communityName.json";
|
||||
import createCommunitySize from "./create/communitySize.json";
|
||||
import createCommunityContext from "./create/communityContext.json";
|
||||
import createCommunityStructure from "./create/communityStructure.json";
|
||||
import createCommunityUpload from "./create/communityUpload.json";
|
||||
import createCommunityReflection from "./create/communityReflection.json";
|
||||
import createReview from "./create/review.json";
|
||||
import createConfirmStakeholders from "./create/confirmStakeholders.json";
|
||||
import createFinalReview from "./create/finalReview.json";
|
||||
@@ -58,9 +61,12 @@ export default {
|
||||
create: {
|
||||
communication,
|
||||
informational: createInformational,
|
||||
text: createText,
|
||||
select: createSelect,
|
||||
upload: createUpload,
|
||||
communityName: createCommunityName,
|
||||
communitySize: createCommunitySize,
|
||||
communityContext: createCommunityContext,
|
||||
communityStructure: createCommunityStructure,
|
||||
communityUpload: createCommunityUpload,
|
||||
communityReflection: createCommunityReflection,
|
||||
review: createReview,
|
||||
confirmStakeholders: createConfirmStakeholders,
|
||||
finalReview: createFinalReview,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import CardsPage from "../../app/create/cards/page";
|
||||
import { CardsScreen } from "../../app/create/screens/card/CardsScreen";
|
||||
|
||||
export default {
|
||||
title: "Pages/Create Flow/Cards",
|
||||
component: CardsPage,
|
||||
component: CardsScreen,
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
docs: {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import CompletedPage from "../../app/create/completed/page";
|
||||
import { CompletedScreen } from "../../app/create/screens/completed/CompletedScreen";
|
||||
|
||||
export default {
|
||||
title: "Pages/Create Flow/Completed",
|
||||
component: CompletedPage,
|
||||
component: CompletedScreen,
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
docs: {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import ConfirmStakeholdersPage from "../../app/create/confirm-stakeholders/page";
|
||||
import { ConfirmStakeholdersScreen } from "../../app/create/screens/select/ConfirmStakeholdersScreen";
|
||||
|
||||
export default {
|
||||
title: "Pages/Create Flow/Confirm stakeholders",
|
||||
component: ConfirmStakeholdersPage,
|
||||
component: ConfirmStakeholdersScreen,
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
docs: {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import FinalReviewPage from "../../app/create/final-review/page";
|
||||
import { FinalReviewScreen } from "../../app/create/screens/review/FinalReviewScreen";
|
||||
|
||||
export default {
|
||||
title: "Pages/Create Flow/Final review",
|
||||
component: FinalReviewPage,
|
||||
component: FinalReviewScreen,
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
docs: {
|
||||
|
||||
@@ -1,35 +1,9 @@
|
||||
import InformationalPage from "../../app/create/informational/page";
|
||||
import { InformationalScreen } from "../../app/create/screens/informational/InformationalScreen";
|
||||
|
||||
export default {
|
||||
title: "Pages/Create Flow/Informational",
|
||||
component: InformationalPage,
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"Create flow entry: HeaderLockup + NumberedList. Responsive L/M and M/S at 640px.",
|
||||
},
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="min-h-screen bg-black flex items-center justify-center">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
tags: ["autodocs"],
|
||||
title: "Pages/Create/Informational",
|
||||
component: InformationalScreen,
|
||||
parameters: { layout: "fullscreen" },
|
||||
};
|
||||
|
||||
export const Desktop = {
|
||||
parameters: {
|
||||
viewport: { defaultViewport: "desktop" },
|
||||
},
|
||||
};
|
||||
|
||||
export const Mobile = {
|
||||
parameters: {
|
||||
viewport: { defaultViewport: "mobile1" },
|
||||
},
|
||||
};
|
||||
export const Default = {};
|
||||
|
||||
@@ -1,39 +1,9 @@
|
||||
import ReviewPage from "../../app/create/review/page";
|
||||
import { CommunityReviewScreen } from "../../app/create/screens/review/CommunityReviewScreen";
|
||||
|
||||
export default {
|
||||
title: "Pages/Create Flow/Review",
|
||||
component: ReviewPage,
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"Mid-flow review step (after upload). 640px+: HeaderLockup left (L), RuleCard right (L, collapsed). Below 640px: single column with HeaderLockup M and RuleCard M. Figma: 19688-13891, 19706-12120.",
|
||||
},
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="min-h-screen bg-black flex items-center justify-center">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
tags: ["autodocs"],
|
||||
title: "Pages/Create/Review",
|
||||
component: CommunityReviewScreen,
|
||||
parameters: { layout: "fullscreen" },
|
||||
};
|
||||
|
||||
export const Desktop = {
|
||||
parameters: {
|
||||
viewport: {
|
||||
defaultViewport: "desktop",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Mobile = {
|
||||
parameters: {
|
||||
viewport: {
|
||||
defaultViewport: "mobile1",
|
||||
},
|
||||
},
|
||||
};
|
||||
export const Default = {};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import RightRailPage from "../../app/create/right-rail/page";
|
||||
import { RightRailScreen } from "../../app/create/screens/right-rail/RightRailScreen";
|
||||
|
||||
export default {
|
||||
title: "Pages/Create Flow/Right rail",
|
||||
component: RightRailPage,
|
||||
component: RightRailScreen,
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
docs: {
|
||||
|
||||
@@ -1,35 +1,9 @@
|
||||
import SelectPage from "../../app/create/select/page";
|
||||
import { CommunitySizeSelectScreen } from "../../app/create/screens/select/CommunitySizeSelectScreen";
|
||||
|
||||
export default {
|
||||
title: "Pages/Create Flow/Select",
|
||||
component: SelectPage,
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"Multi-select template: two columns at 640px+, stacked below. MultiSelect with add → custom chip.",
|
||||
},
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="min-h-screen bg-black flex items-center justify-center">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
tags: ["autodocs"],
|
||||
title: "Pages/Create/CommunitySize",
|
||||
component: CommunitySizeSelectScreen,
|
||||
parameters: { layout: "fullscreen" },
|
||||
};
|
||||
|
||||
export const Desktop = {
|
||||
parameters: {
|
||||
viewport: { defaultViewport: "desktop" },
|
||||
},
|
||||
};
|
||||
|
||||
export const Mobile = {
|
||||
parameters: {
|
||||
viewport: { defaultViewport: "mobile1" },
|
||||
},
|
||||
};
|
||||
export const Default = {};
|
||||
|
||||
@@ -1,35 +1,15 @@
|
||||
import TextPage from "../../app/create/text/page";
|
||||
import { CreateFlowTextFieldScreen } from "../../app/create/screens/text/CreateFlowTextFieldScreen";
|
||||
|
||||
export default {
|
||||
title: "Pages/Create Flow/Text",
|
||||
component: TextPage,
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"Community name step: HeaderLockup + TextInput. Responsive sizing at 640px.",
|
||||
},
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="min-h-screen bg-black flex items-center justify-center">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
tags: ["autodocs"],
|
||||
title: "Pages/Create/CommunityName",
|
||||
component: CreateFlowTextFieldScreen,
|
||||
parameters: { layout: "fullscreen" },
|
||||
};
|
||||
|
||||
export const Desktop = {
|
||||
parameters: {
|
||||
viewport: { defaultViewport: "desktop" },
|
||||
},
|
||||
};
|
||||
|
||||
export const Mobile = {
|
||||
parameters: {
|
||||
viewport: { defaultViewport: "mobile1" },
|
||||
export const Default = {
|
||||
args: {
|
||||
messageNamespace: "create.communityName",
|
||||
stateField: "title",
|
||||
maxLength: 48,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,35 +1,9 @@
|
||||
import UploadPage from "../../app/create/upload/page";
|
||||
import { CommunityUploadScreen } from "../../app/create/screens/upload/CommunityUploadScreen";
|
||||
|
||||
export default {
|
||||
title: "Pages/Create Flow/Upload",
|
||||
component: UploadPage,
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"Upload step: HeaderLockup + Upload control. Centered lockup at 640px+.",
|
||||
},
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="min-h-screen bg-black flex items-center justify-center">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
tags: ["autodocs"],
|
||||
title: "Pages/Create/CommunityUpload",
|
||||
component: CommunityUploadScreen,
|
||||
parameters: { layout: "fullscreen" },
|
||||
};
|
||||
|
||||
export const Desktop = {
|
||||
parameters: {
|
||||
viewport: { defaultViewport: "desktop" },
|
||||
},
|
||||
};
|
||||
|
||||
export const Mobile = {
|
||||
parameters: {
|
||||
viewport: { defaultViewport: "mobile1" },
|
||||
},
|
||||
};
|
||||
export const Default = {};
|
||||
|
||||
@@ -31,10 +31,10 @@ vi.mock("../../lib/create/api", () => ({
|
||||
requestMagicLink: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../app/create/anonymousDraftStorage", async (importOriginal) => {
|
||||
vi.mock("../../app/create/utils/anonymousDraftStorage", async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<
|
||||
typeof import("../../app/create/anonymousDraftStorage")
|
||||
typeof import("../../app/create/utils/anonymousDraftStorage")
|
||||
>();
|
||||
return {
|
||||
...actual,
|
||||
@@ -43,7 +43,7 @@ vi.mock("../../app/create/anonymousDraftStorage", async (importOriginal) => {
|
||||
});
|
||||
|
||||
import { requestMagicLink } from "../../lib/create/api";
|
||||
import { setTransferPendingFlag } from "../../app/create/anonymousDraftStorage";
|
||||
import { setTransferPendingFlag } from "../../app/create/utils/anonymousDraftStorage";
|
||||
|
||||
function LoginTrigger() {
|
||||
const { openLogin, closeLogin } = useAuthModal();
|
||||
@@ -57,7 +57,7 @@ function LoginTrigger() {
|
||||
onClick={() =>
|
||||
openLogin({
|
||||
variant: "saveProgress",
|
||||
nextPath: "/create/select?syncDraft=1",
|
||||
nextPath: "/create/community-size?syncDraft=1",
|
||||
})
|
||||
}
|
||||
>
|
||||
@@ -143,7 +143,7 @@ describe("AuthModalProvider (header overlay)", () => {
|
||||
await waitFor(() => {
|
||||
expect(requestMagicLink).toHaveBeenCalledWith(
|
||||
"guest@example.com",
|
||||
"/create/select?syncDraft=1",
|
||||
"/create/community-size?syncDraft=1",
|
||||
);
|
||||
});
|
||||
expect(setTransferPendingFlag).toHaveBeenCalled();
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { renderWithProviders as render, screen } from "../utils/test-utils";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import CompletedPage from "../../app/create/completed/page";
|
||||
import { CompletedScreen } from "../../app/create/screens/completed/CompletedScreen";
|
||||
|
||||
describe("CompletedPage", () => {
|
||||
describe("CompletedScreen", () => {
|
||||
it("renders without crashing", () => {
|
||||
render(<CompletedPage />);
|
||||
render(<CompletedScreen />);
|
||||
expect(screen.getByRole("heading", { level: 1 })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders HeaderLockup with expected title", () => {
|
||||
render(<CompletedPage />);
|
||||
render(<CompletedScreen />);
|
||||
expect(
|
||||
screen.getByRole("heading", {
|
||||
name: "Mutual Aid Mondays",
|
||||
@@ -19,7 +19,7 @@ describe("CompletedPage", () => {
|
||||
});
|
||||
|
||||
it("renders HeaderLockup with expected description", () => {
|
||||
render(<CompletedPage />);
|
||||
render(<CompletedScreen />);
|
||||
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,
|
||||
@@ -28,7 +28,7 @@ describe("CompletedPage", () => {
|
||||
});
|
||||
|
||||
it("renders Community Rule document with section labels", () => {
|
||||
render(<CompletedPage />);
|
||||
render(<CompletedScreen />);
|
||||
expect(screen.getByText("Values")).toBeInTheDocument();
|
||||
expect(screen.getByText("Communication")).toBeInTheDocument();
|
||||
expect(screen.getByText("Membership")).toBeInTheDocument();
|
||||
@@ -37,7 +37,7 @@ describe("CompletedPage", () => {
|
||||
});
|
||||
|
||||
it("renders document entry titles", () => {
|
||||
render(<CompletedPage />);
|
||||
render(<CompletedScreen />);
|
||||
expect(screen.getByText("Solidarity Forever")).toBeInTheDocument();
|
||||
expect(screen.getByText("Shared Leadership")).toBeInTheDocument();
|
||||
expect(screen.getByText("Organizing Offline")).toBeInTheDocument();
|
||||
@@ -45,7 +45,7 @@ describe("CompletedPage", () => {
|
||||
});
|
||||
|
||||
it("renders toast alert when page loads", () => {
|
||||
render(<CompletedPage />);
|
||||
render(<CompletedScreen />);
|
||||
expect(
|
||||
screen.getByText(
|
||||
"This is what folks see when you share your CommunityRule",
|
||||
@@ -59,7 +59,7 @@ describe("CompletedPage", () => {
|
||||
});
|
||||
|
||||
it("renders toast with role status", () => {
|
||||
render(<CompletedPage />);
|
||||
render(<CompletedScreen />);
|
||||
const statusRegions = screen.getAllByRole("status");
|
||||
expect(statusRegions.length).toBeGreaterThanOrEqual(1);
|
||||
expect(
|
||||
|
||||
@@ -2,11 +2,11 @@ import { describe, it, expect } from "vitest";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { renderWithProviders as render, screen } from "../utils/test-utils";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import ConfirmStakeholdersPage from "../../app/create/confirm-stakeholders/page";
|
||||
import { ConfirmStakeholdersScreen } from "../../app/create/screens/select/ConfirmStakeholdersScreen";
|
||||
|
||||
describe("ConfirmStakeholdersPage", () => {
|
||||
describe("ConfirmStakeholdersScreen", () => {
|
||||
it("renders title and description", () => {
|
||||
render(<ConfirmStakeholdersPage />);
|
||||
render(<ConfirmStakeholdersScreen />);
|
||||
expect(
|
||||
screen.getByRole("heading", {
|
||||
name: /Do other stakeholders need to be involved/i,
|
||||
@@ -20,7 +20,7 @@ describe("ConfirmStakeholdersPage", () => {
|
||||
});
|
||||
|
||||
it("renders Add stakeholder control", () => {
|
||||
render(<ConfirmStakeholdersPage />);
|
||||
render(<ConfirmStakeholdersScreen />);
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Add stakeholder" }),
|
||||
).toBeInTheDocument();
|
||||
@@ -28,7 +28,7 @@ describe("ConfirmStakeholdersPage", () => {
|
||||
|
||||
it("shows draft toast and can dismiss it", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ConfirmStakeholdersPage />);
|
||||
render(<ConfirmStakeholdersScreen />);
|
||||
expect(
|
||||
screen.getByText(/Congratulations! You've drafted your CommunityRule!/i),
|
||||
).toBeInTheDocument();
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
waitFor,
|
||||
} from "../utils/test-utils";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import FinalReviewPage from "../../app/create/final-review/page";
|
||||
import { FinalReviewScreen } from "../../app/create/screens/review/FinalReviewScreen";
|
||||
import { useCreateFlow } from "../../app/create/context/CreateFlowContext";
|
||||
|
||||
const FALLBACK_CARD_TITLE = "Your community";
|
||||
@@ -24,17 +24,17 @@ function FinalReviewWithFlowState({
|
||||
useLayoutEffect(() => {
|
||||
replaceState({ title, ...(summary !== undefined ? { summary } : {}) });
|
||||
}, [replaceState, title, summary]);
|
||||
return <FinalReviewPage />;
|
||||
return <FinalReviewScreen />;
|
||||
}
|
||||
|
||||
describe("FinalReviewPage", () => {
|
||||
describe("FinalReviewScreen", () => {
|
||||
it("renders without crashing", () => {
|
||||
render(<FinalReviewPage />);
|
||||
render(<FinalReviewScreen />);
|
||||
expect(screen.getByRole("heading", { level: 1 })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders lockup title", () => {
|
||||
render(<FinalReviewPage />);
|
||||
render(<FinalReviewScreen />);
|
||||
expect(
|
||||
screen.getByRole("heading", {
|
||||
name: "Review your CommunityRule",
|
||||
@@ -43,7 +43,7 @@ describe("FinalReviewPage", () => {
|
||||
});
|
||||
|
||||
it("renders lockup description", () => {
|
||||
render(<FinalReviewPage />);
|
||||
render(<FinalReviewScreen />);
|
||||
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,
|
||||
@@ -52,12 +52,12 @@ describe("FinalReviewPage", () => {
|
||||
});
|
||||
|
||||
it("renders RuleCard with fallback title when context has no name", () => {
|
||||
render(<FinalReviewPage />);
|
||||
render(<FinalReviewScreen />);
|
||||
expect(screen.getByText(FALLBACK_CARD_TITLE)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders RuleCard with fallback description when context has no summary", () => {
|
||||
render(<FinalReviewPage />);
|
||||
render(<FinalReviewScreen />);
|
||||
expect(
|
||||
screen.getByText(new RegExp(FALLBACK_CARD_DESCRIPTION_SNIPPET, "i")),
|
||||
).toBeInTheDocument();
|
||||
@@ -76,7 +76,7 @@ describe("FinalReviewPage", () => {
|
||||
});
|
||||
|
||||
it("renders RuleCard as a button (card is interactive)", () => {
|
||||
render(<FinalReviewPage />);
|
||||
render(<FinalReviewScreen />);
|
||||
const buttons = screen.getAllByRole("button");
|
||||
expect(buttons.length).toBeGreaterThanOrEqual(1);
|
||||
expect(
|
||||
@@ -85,7 +85,7 @@ describe("FinalReviewPage", () => {
|
||||
});
|
||||
|
||||
it("renders expanded RuleCard with category labels", () => {
|
||||
render(<FinalReviewPage />);
|
||||
render(<FinalReviewScreen />);
|
||||
expect(screen.getByText("Values")).toBeInTheDocument();
|
||||
expect(screen.getByText("Communication")).toBeInTheDocument();
|
||||
expect(screen.getByText("Membership")).toBeInTheDocument();
|
||||
@@ -94,7 +94,7 @@ describe("FinalReviewPage", () => {
|
||||
});
|
||||
|
||||
it("renders category chips", () => {
|
||||
render(<FinalReviewPage />);
|
||||
render(<FinalReviewScreen />);
|
||||
expect(screen.getByText("Consciousness")).toBeInTheDocument();
|
||||
expect(screen.getByText("Signal")).toBeInTheDocument();
|
||||
expect(screen.getByText("Open Admission")).toBeInTheDocument();
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { renderWithProviders as render, screen } from "../utils/test-utils";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import InformationalPage from "../../app/create/informational/page";
|
||||
import { InformationalScreen } from "../../app/create/screens/informational/InformationalScreen";
|
||||
|
||||
describe("InformationalPage", () => {
|
||||
describe("InformationalScreen", () => {
|
||||
it("renders without crashing", () => {
|
||||
render(<InformationalPage />);
|
||||
render(<InformationalScreen />);
|
||||
expect(
|
||||
screen.getByRole("heading", {
|
||||
name: "How CommunityRule helps groups like yours",
|
||||
@@ -14,7 +14,7 @@ describe("InformationalPage", () => {
|
||||
});
|
||||
|
||||
it("renders lockup description", () => {
|
||||
render(<InformationalPage />);
|
||||
render(<InformationalScreen />);
|
||||
expect(
|
||||
screen.getByText(
|
||||
/This flow will give you recommendations to improve your community/i,
|
||||
@@ -23,7 +23,7 @@ describe("InformationalPage", () => {
|
||||
});
|
||||
|
||||
it("renders first numbered list item title", () => {
|
||||
render(<InformationalPage />);
|
||||
render(<InformationalScreen />);
|
||||
expect(
|
||||
screen.getByText("Tell us about your organization"),
|
||||
).toBeInTheDocument();
|
||||
|
||||
@@ -32,10 +32,10 @@ vi.mock("../../lib/create/api", () => ({
|
||||
requestMagicLink: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../app/create/anonymousDraftStorage", async (importOriginal) => {
|
||||
vi.mock("../../app/create/utils/anonymousDraftStorage", async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<
|
||||
typeof import("../../app/create/anonymousDraftStorage")
|
||||
typeof import("../../app/create/utils/anonymousDraftStorage")
|
||||
>();
|
||||
return {
|
||||
...actual,
|
||||
@@ -44,7 +44,7 @@ vi.mock("../../app/create/anonymousDraftStorage", async (importOriginal) => {
|
||||
});
|
||||
|
||||
import { requestMagicLink } from "../../lib/create/api";
|
||||
import { setTransferPendingFlag } from "../../app/create/anonymousDraftStorage";
|
||||
import { setTransferPendingFlag } from "../../app/create/utils/anonymousDraftStorage";
|
||||
|
||||
function renderLoginForm() {
|
||||
return renderWithProviders(
|
||||
@@ -119,7 +119,7 @@ describe("LoginForm", () => {
|
||||
<Suspense fallback={null}>
|
||||
<LoginForm
|
||||
variant="saveProgress"
|
||||
magicLinkNextPath="/create/select?syncDraft=1"
|
||||
magicLinkNextPath="/create/community-size?syncDraft=1"
|
||||
/>
|
||||
</Suspense>,
|
||||
);
|
||||
@@ -133,7 +133,7 @@ describe("LoginForm", () => {
|
||||
await waitFor(() => {
|
||||
expect(requestMagicLink).toHaveBeenCalledWith(
|
||||
"save@example.com",
|
||||
"/create/select?syncDraft=1",
|
||||
"/create/community-size?syncDraft=1",
|
||||
);
|
||||
});
|
||||
expect(setTransferPendingFlag).toHaveBeenCalled();
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { renderWithProviders as render, screen } from "../utils/test-utils";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import ReviewPage from "../../app/create/review/page";
|
||||
import { CommunityReviewScreen } from "../../app/create/screens/review/CommunityReviewScreen";
|
||||
|
||||
describe("ReviewPage", () => {
|
||||
describe("CommunityReviewScreen", () => {
|
||||
it("renders without crashing", () => {
|
||||
render(<ReviewPage />);
|
||||
render(<CommunityReviewScreen />);
|
||||
expect(screen.getByRole("heading", { level: 1 })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders HeaderLockup with expected title", () => {
|
||||
render(<ReviewPage />);
|
||||
render(<CommunityReviewScreen />);
|
||||
expect(
|
||||
screen.getByRole("heading", {
|
||||
name: "Your community is added - congrats!",
|
||||
@@ -19,7 +19,7 @@ describe("ReviewPage", () => {
|
||||
});
|
||||
|
||||
it("renders HeaderLockup with expected description", () => {
|
||||
render(<ReviewPage />);
|
||||
render(<CommunityReviewScreen />);
|
||||
expect(
|
||||
screen.getByText(
|
||||
/In the next section, we'll go through membership, decision-making, conflict resolution, and community values and create a custom operating manual for your organization based on the specifics you just shared./i,
|
||||
@@ -28,12 +28,12 @@ describe("ReviewPage", () => {
|
||||
});
|
||||
|
||||
it("renders RuleCard with title", () => {
|
||||
render(<ReviewPage />);
|
||||
render(<CommunityReviewScreen />);
|
||||
expect(screen.getByText("Mutual Aid Mondays")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders RuleCard with description", () => {
|
||||
render(<ReviewPage />);
|
||||
render(<CommunityReviewScreen />);
|
||||
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,
|
||||
@@ -42,7 +42,7 @@ describe("ReviewPage", () => {
|
||||
});
|
||||
|
||||
it("renders RuleCard as a button (card is interactive)", () => {
|
||||
render(<ReviewPage />);
|
||||
render(<CommunityReviewScreen />);
|
||||
const buttons = screen.getAllByRole("button");
|
||||
expect(buttons.length).toBeGreaterThanOrEqual(1);
|
||||
expect(
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { renderWithProviders as render, screen } from "../utils/test-utils";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import SelectPage from "../../app/create/select/page";
|
||||
import { CommunitySizeSelectScreen } from "../../app/create/screens/select/CommunitySizeSelectScreen";
|
||||
|
||||
describe("SelectPage", () => {
|
||||
describe("CommunitySizeSelectScreen", () => {
|
||||
it("renders HeaderLockup title", () => {
|
||||
render(<SelectPage />);
|
||||
render(<CommunitySizeSelectScreen />);
|
||||
expect(
|
||||
screen.getByRole("heading", {
|
||||
name: "What is your community called?",
|
||||
name: "How large is your community?",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders MultiSelect add control", () => {
|
||||
render(<SelectPage />);
|
||||
render(<CommunitySizeSelectScreen />);
|
||||
const addButtons = screen.getAllByRole("button", {
|
||||
name: "Add organization type",
|
||||
});
|
||||
@@ -22,8 +22,8 @@ describe("SelectPage", () => {
|
||||
});
|
||||
|
||||
it("renders preset chip labels", () => {
|
||||
render(<SelectPage />);
|
||||
render(<CommunitySizeSelectScreen />);
|
||||
expect(screen.getByText("1 member")).toBeInTheDocument();
|
||||
expect(screen.getByText("Non-profit")).toBeInTheDocument();
|
||||
expect(screen.getByText("2-10 members")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { renderWithProviders as render, screen } from "../utils/test-utils";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import TextPage from "../../app/create/text/page";
|
||||
import { CreateFlowTextFieldScreen } from "../../app/create/screens/text/CreateFlowTextFieldScreen";
|
||||
|
||||
describe("TextPage", () => {
|
||||
describe("CreateFlowTextFieldScreen (community name)", () => {
|
||||
it("renders main heading", () => {
|
||||
render(<TextPage />);
|
||||
render(
|
||||
<CreateFlowTextFieldScreen
|
||||
messageNamespace="create.communityName"
|
||||
stateField="title"
|
||||
maxLength={48}
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
screen.getByRole("heading", {
|
||||
name: "What is your community called?",
|
||||
@@ -14,7 +20,13 @@ describe("TextPage", () => {
|
||||
});
|
||||
|
||||
it("renders description and text field", () => {
|
||||
render(<TextPage />);
|
||||
render(
|
||||
<CreateFlowTextFieldScreen
|
||||
messageNamespace="create.communityName"
|
||||
stateField="title"
|
||||
maxLength={48}
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
screen.getByText("This will be the name of your community"),
|
||||
).toBeInTheDocument();
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { renderWithProviders as render, screen } from "../utils/test-utils";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import UploadPage from "../../app/create/upload/page";
|
||||
import { CommunityUploadScreen } from "../../app/create/screens/upload/CommunityUploadScreen";
|
||||
|
||||
describe("UploadPage", () => {
|
||||
describe("CommunityUploadScreen", () => {
|
||||
it("renders HeaderLockup", () => {
|
||||
render(<UploadPage />);
|
||||
render(<CommunityUploadScreen />);
|
||||
expect(
|
||||
screen.getByRole("heading", {
|
||||
name: "How should conflicts be resolved?",
|
||||
@@ -14,7 +14,7 @@ describe("UploadPage", () => {
|
||||
});
|
||||
|
||||
it("renders Upload control and helper copy", () => {
|
||||
render(<UploadPage />);
|
||||
render(<CommunityUploadScreen />);
|
||||
expect(screen.getByRole("button", { name: "Upload" })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/Add images, PDFs, and other files to the policy/i),
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from "../utils/test-utils";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, test, expect, afterEach } from "vitest";
|
||||
import CardsPage from "../../app/create/cards/page";
|
||||
import { CardsScreen } from "../../app/create/screens/card/CardsScreen";
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
@@ -15,7 +15,7 @@ afterEach(() => {
|
||||
describe("Create flow cards page", () => {
|
||||
test("clicking a card opens the Create modal", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<CardsPage />);
|
||||
render(<CardsScreen />);
|
||||
|
||||
const signalCards = screen.getAllByRole("button", {
|
||||
name: /Signal: Encrypted messaging/,
|
||||
@@ -29,7 +29,7 @@ describe("Create flow cards page", () => {
|
||||
});
|
||||
|
||||
test("renders without error", () => {
|
||||
render(<CardsPage />);
|
||||
render(<CardsScreen />);
|
||||
|
||||
expect(
|
||||
screen.getByText(
|
||||
@@ -39,7 +39,7 @@ describe("Create flow cards page", () => {
|
||||
});
|
||||
|
||||
test("renders HeaderLockup and CardStack content", () => {
|
||||
render(<CardsPage />);
|
||||
render(<CardsScreen />);
|
||||
|
||||
expect(
|
||||
screen.getByText(
|
||||
@@ -53,7 +53,7 @@ describe("Create flow cards page", () => {
|
||||
|
||||
test("toggle expands and shows Show less", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<CardsPage />);
|
||||
render(<CardsScreen />);
|
||||
|
||||
const toggle = screen.getByRole("button", {
|
||||
name: "See all communication approaches",
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from "../utils/test-utils";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, test, expect, afterEach } from "vitest";
|
||||
import RightRailPage from "../../app/create/right-rail/page";
|
||||
import { RightRailScreen } from "../../app/create/screens/right-rail/RightRailScreen";
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
@@ -14,7 +14,7 @@ afterEach(() => {
|
||||
|
||||
describe("Create flow right-rail page", () => {
|
||||
test("renders without error", () => {
|
||||
render(<RightRailPage />);
|
||||
render(<RightRailScreen />);
|
||||
|
||||
expect(
|
||||
screen.getByRole("heading", {
|
||||
@@ -24,7 +24,7 @@ describe("Create flow right-rail page", () => {
|
||||
});
|
||||
|
||||
test("renders sidebar description with add link", () => {
|
||||
render(<RightRailPage />);
|
||||
render(<RightRailScreen />);
|
||||
|
||||
const description = screen.getByText((content, element) => {
|
||||
if (element?.tagName !== "P") return false;
|
||||
@@ -39,7 +39,7 @@ describe("Create flow right-rail page", () => {
|
||||
});
|
||||
|
||||
test("renders message box with title and checkboxes", () => {
|
||||
render(<RightRailPage />);
|
||||
render(<RightRailScreen />);
|
||||
|
||||
const region = screen.getByRole("region", {
|
||||
name: "Consider defining approaches to steward key resources:",
|
||||
@@ -65,7 +65,7 @@ describe("Create flow right-rail page", () => {
|
||||
});
|
||||
|
||||
test("renders card stack with See all decision approaches toggle", () => {
|
||||
render(<RightRailPage />);
|
||||
render(<RightRailScreen />);
|
||||
|
||||
expect(
|
||||
screen.getByRole("button", { name: "See all decision approaches" }),
|
||||
@@ -73,7 +73,7 @@ describe("Create flow right-rail page", () => {
|
||||
});
|
||||
|
||||
test("renders recommended approach cards", () => {
|
||||
render(<RightRailPage />);
|
||||
render(<RightRailScreen />);
|
||||
|
||||
expect(
|
||||
screen.getByRole("button", {
|
||||
@@ -94,7 +94,7 @@ describe("Create flow right-rail page", () => {
|
||||
|
||||
test("toggle expands and shows Show less", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<RightRailPage />);
|
||||
render(<RightRailScreen />);
|
||||
|
||||
const toggle = screen.getByRole("button", {
|
||||
name: "See all decision approaches",
|
||||
@@ -108,7 +108,7 @@ describe("Create flow right-rail page", () => {
|
||||
|
||||
test("expanded view shows Label cards", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<RightRailPage />);
|
||||
render(<RightRailScreen />);
|
||||
|
||||
const toggle = screen.getByRole("button", {
|
||||
name: "See all decision approaches",
|
||||
@@ -121,7 +121,7 @@ describe("Create flow right-rail page", () => {
|
||||
|
||||
test("clicking a card toggles selection", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<RightRailPage />);
|
||||
render(<RightRailScreen />);
|
||||
|
||||
const mediationCard = screen.getByRole("button", {
|
||||
name: /Mediation: Collaborative work to reach a resolution/,
|
||||
@@ -133,7 +133,7 @@ describe("Create flow right-rail page", () => {
|
||||
|
||||
test("message box checkboxes are interactive", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<RightRailPage />);
|
||||
render(<RightRailScreen />);
|
||||
|
||||
const amendCheckbox = screen.getByRole("checkbox", {
|
||||
name: "Amend your CommunityRule",
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
} from "../utils/test-utils";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, test, expect, afterEach, beforeEach } from "vitest";
|
||||
import TemplatesPage from "../../app/(marketing)/templates/page";
|
||||
import TemplatesPageClient from "../../app/(marketing)/templates/TemplatesPageClient";
|
||||
import { testRouter } from "../mocks/navigation";
|
||||
import { GOVERNANCE_TEMPLATE_CATALOG } from "../../lib/templates/governanceTemplateCatalog";
|
||||
|
||||
@@ -19,7 +19,9 @@ afterEach(() => {
|
||||
|
||||
describe("Templates page (/templates)", () => {
|
||||
test("renders title, intro, and full catalog", () => {
|
||||
render(<TemplatesPage />);
|
||||
render(
|
||||
<TemplatesPageClient initialGridEntries={GOVERNANCE_TEMPLATE_CATALOG} />,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByRole("heading", { name: "Templates", level: 1 }),
|
||||
@@ -35,7 +37,9 @@ describe("Templates page (/templates)", () => {
|
||||
|
||||
test("each template card navigates to review flow for its slug", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<TemplatesPage />);
|
||||
render(
|
||||
<TemplatesPageClient initialGridEntries={GOVERNANCE_TEMPLATE_CATALOG} />,
|
||||
);
|
||||
|
||||
const consensusCard = screen.getByText("Consensus").closest("div");
|
||||
await user.click(consensusCard);
|
||||
|
||||
@@ -34,7 +34,7 @@ describe("flowSteps", () => {
|
||||
});
|
||||
|
||||
it("isValidStep reflects FLOW_STEP_ORDER membership", () => {
|
||||
expect(isValidStep("select")).toBe(true);
|
||||
expect(isValidStep("community-size")).toBe(true);
|
||||
expect(isValidStep("confirm-stakeholders")).toBe(true);
|
||||
expect(isValidStep("nope")).toBe(false);
|
||||
expect(isValidStep(null)).toBe(false);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { hasCreateFlowUserInput } from "../../app/create/hasCreateFlowUserInput";
|
||||
import { hasCreateFlowUserInput } from "../../app/create/utils/hasCreateFlowUserInput";
|
||||
|
||||
describe("hasCreateFlowUserInput", () => {
|
||||
it("returns false for empty state", () => {
|
||||
@@ -7,7 +7,9 @@ describe("hasCreateFlowUserInput", () => {
|
||||
});
|
||||
|
||||
it("ignores currentStep alone", () => {
|
||||
expect(hasCreateFlowUserInput({ currentStep: "text" })).toBe(false);
|
||||
expect(hasCreateFlowUserInput({ currentStep: "informational" })).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("returns true for non-empty title", () => {
|
||||
|
||||
Reference in New Issue
Block a user