Create Community stage implemented
This commit is contained in:
@@ -92,7 +92,7 @@ export interface TextAreaViewProps {
|
|||||||
handleChange: (_e: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
handleChange: (_e: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
||||||
handleFocus: (_e: React.FocusEvent<HTMLTextAreaElement>) => void;
|
handleFocus: (_e: React.FocusEvent<HTMLTextAreaElement>) => void;
|
||||||
handleBlur: (_e: React.FocusEvent<HTMLTextAreaElement>) => void;
|
handleBlur: (_e: React.FocusEvent<HTMLTextAreaElement>) => void;
|
||||||
textHint?: boolean;
|
textHint?: boolean | string;
|
||||||
formHeader?: boolean;
|
formHeader?: boolean;
|
||||||
showHelpIcon?: boolean;
|
showHelpIcon?: boolean;
|
||||||
appearance?: "default" | "embedded";
|
appearance?: "default" | "embedded";
|
||||||
|
|||||||
@@ -78,13 +78,13 @@ export const TextAreaView = forwardRef<HTMLTextAreaElement, TextAreaViewProps>(
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{textHint && (
|
{textHint ? (
|
||||||
<div className="flex items-start relative shrink-0 w-full">
|
<div className="flex items-start relative shrink-0 w-full">
|
||||||
<p className="flex-[1_0_0] font-inter font-normal leading-[16px] min-h-px min-w-px relative text-[color:var(--color-content-default-tertiary,#b4b4b4)] text-[length:var(--sizing-300,12px)]">
|
<p className="flex-[1_0_0] font-inter font-normal leading-[16px] min-h-px min-w-px relative text-[color:var(--color-content-default-tertiary,#b4b4b4)] text-[length:var(--sizing-300,12px)]">
|
||||||
Hint text here
|
{typeof textHint === "string" ? textHint : "Hint text here"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,12 +5,20 @@ import UploadView from "./Upload.view";
|
|||||||
import type { UploadProps } from "./Upload.types";
|
import type { UploadProps } from "./Upload.types";
|
||||||
|
|
||||||
const UploadContainer = memo<UploadProps>(
|
const UploadContainer = memo<UploadProps>(
|
||||||
({ active = true, label, showHelpIcon = true, onClick, className = "" }) => {
|
({
|
||||||
|
active = true,
|
||||||
|
label,
|
||||||
|
showHelpIcon = true,
|
||||||
|
hintText = "Add image from your device",
|
||||||
|
onClick,
|
||||||
|
className = "",
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<UploadView
|
<UploadView
|
||||||
active={active}
|
active={active}
|
||||||
label={label}
|
label={label}
|
||||||
showHelpIcon={showHelpIcon}
|
showHelpIcon={showHelpIcon}
|
||||||
|
hintText={hintText}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={className}
|
className={className}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ export interface UploadProps {
|
|||||||
* @default true
|
* @default true
|
||||||
*/
|
*/
|
||||||
showHelpIcon?: boolean;
|
showHelpIcon?: boolean;
|
||||||
|
/**
|
||||||
|
* Copy beside the upload button (Figma Flow — Upload `20094:41524`).
|
||||||
|
* @default "Add image from your device"
|
||||||
|
*/
|
||||||
|
hintText?: string;
|
||||||
/**
|
/**
|
||||||
* Callback when upload button is clicked
|
* Callback when upload button is clicked
|
||||||
*/
|
*/
|
||||||
@@ -29,6 +34,7 @@ export interface UploadViewProps {
|
|||||||
active: boolean;
|
active: boolean;
|
||||||
label?: string;
|
label?: string;
|
||||||
showHelpIcon: boolean;
|
showHelpIcon: boolean;
|
||||||
|
hintText: string;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
className: string;
|
className: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ function UploadView({
|
|||||||
active = true,
|
active = true,
|
||||||
label,
|
label,
|
||||||
showHelpIcon = true,
|
showHelpIcon = true,
|
||||||
|
hintText,
|
||||||
onClick,
|
onClick,
|
||||||
className = "",
|
className = "",
|
||||||
}: UploadViewProps) {
|
}: UploadViewProps) {
|
||||||
@@ -54,7 +55,7 @@ function UploadView({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={`${buttonBgClass} flex gap-[var(--measures-spacing-150,6px)] items-center justify-center overflow-clip p-[var(--measures-spacing-300,12px)] rounded-[var(--measures-radius-full,9999px)] shrink-0 hover:opacity-80 transition-opacity`}
|
className={`${buttonBgClass} flex gap-[var(--measures-spacing-150,6px)] items-center justify-center overflow-clip px-[var(--space-400,16px)] py-[var(--measures-spacing-300,12px)] rounded-[var(--measures-radius-full,9999px)] shrink-0 hover:opacity-80 transition-opacity`}
|
||||||
aria-label="Upload"
|
aria-label="Upload"
|
||||||
>
|
>
|
||||||
{/* Upload icon */}
|
{/* Upload icon */}
|
||||||
@@ -105,9 +106,7 @@ function UploadView({
|
|||||||
<div
|
<div
|
||||||
className={`flex flex-[1_0_0] flex-col font-inter font-normal h-[32px] justify-center leading-[0] min-h-px min-w-px relative text-[length:var(--sizing-350,14px)] ${descriptionTextColor}`}
|
className={`flex flex-[1_0_0] flex-col font-inter font-normal h-[32px] justify-center leading-[0] min-h-px min-w-px relative text-[length:var(--sizing-350,14px)] ${descriptionTextColor}`}
|
||||||
>
|
>
|
||||||
<p className="leading-[20px] whitespace-pre-wrap">
|
<p className="leading-[20px] whitespace-pre-wrap">{hintText}</p>
|
||||||
Add images, PDFs, and other files to the policy
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
|
import { normalizeProportionBarVariant } from "../../../../lib/propNormalization";
|
||||||
import { ProportionBarView } from "./ProportionBar.view";
|
import { ProportionBarView } from "./ProportionBar.view";
|
||||||
import type { ProportionBarProps } from "./ProportionBar.types";
|
import type { ProportionBarProps } from "./ProportionBar.types";
|
||||||
|
|
||||||
const ProportionBarContainer = memo<ProportionBarProps>(
|
const ProportionBarContainer = memo<ProportionBarProps>(
|
||||||
({ progress = "3-2", className = "" }) => {
|
({ progress = "3-2", className = "", variant: variantProp }) => {
|
||||||
|
const variant = normalizeProportionBarVariant(variantProp);
|
||||||
const barClasses = `h-[8px] relative w-full`;
|
const barClasses = `h-[8px] relative w-full`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -13,6 +15,7 @@ const ProportionBarContainer = memo<ProportionBarProps>(
|
|||||||
progress={progress}
|
progress={progress}
|
||||||
className={className}
|
className={className}
|
||||||
barClasses={barClasses}
|
barClasses={barClasses}
|
||||||
|
variant={variant}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { ProportionBarVariantValue } from "../../../../lib/propNormalization";
|
||||||
|
|
||||||
export type ProportionBarState =
|
export type ProportionBarState =
|
||||||
| "1-0"
|
| "1-0"
|
||||||
| "1-1"
|
| "1-1"
|
||||||
@@ -12,13 +14,20 @@ export type ProportionBarState =
|
|||||||
| "3-1"
|
| "3-1"
|
||||||
| "3-2";
|
| "3-2";
|
||||||
|
|
||||||
|
export type ProportionBarVariant = ProportionBarVariantValue;
|
||||||
|
|
||||||
export interface ProportionBarProps {
|
export interface ProportionBarProps {
|
||||||
progress?: ProportionBarState;
|
progress?: ProportionBarState;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
/**
|
||||||
|
* `segmented` (Figma: create-flow footer): pill-shaped partial fills inside each segment.
|
||||||
|
*/
|
||||||
|
variant?: ProportionBarVariant;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProportionBarViewProps {
|
export interface ProportionBarViewProps {
|
||||||
progress: ProportionBarState;
|
progress: ProportionBarState;
|
||||||
className: string;
|
className: string;
|
||||||
barClasses: string;
|
barClasses: string;
|
||||||
|
variant: "default" | "segmented";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ export function ProportionBarView({
|
|||||||
progress,
|
progress,
|
||||||
className,
|
className,
|
||||||
barClasses,
|
barClasses,
|
||||||
|
variant,
|
||||||
}: ProportionBarViewProps) {
|
}: ProportionBarViewProps) {
|
||||||
// Proportion bar type
|
// Proportion bar type
|
||||||
const [fullSegments, partialSegment] = progress.split("-").map(Number);
|
const [fullSegments, partialSegment] = progress.split("-").map(Number);
|
||||||
|
const segmented = variant === "segmented";
|
||||||
// Calculate total progress:
|
// Calculate total progress:
|
||||||
// - For 1-X: first section is (X+1)/6 filled
|
// - For 1-X: first section is (X+1)/6 filled
|
||||||
// - For 2-X: first section full, second section X/3 filled
|
// - For 2-X: first section full, second section X/3 filled
|
||||||
@@ -58,7 +60,11 @@ export function ProportionBarView({
|
|||||||
<div className="flex-1 h-full relative">
|
<div className="flex-1 h-full relative">
|
||||||
{fullSegments === 1 ? (
|
{fullSegments === 1 ? (
|
||||||
<div
|
<div
|
||||||
className="absolute inset-y-0 left-0 bg-[var(--color-content-default-brand-primary)] rounded-l-[var(--radius-full)]"
|
className={`absolute inset-y-0 left-0 bg-[var(--color-content-default-brand-primary)] rounded-l-[var(--radius-full)] ${
|
||||||
|
segmented && partialSegment < 5
|
||||||
|
? "rounded-r-[var(--radius-full)]"
|
||||||
|
: ""
|
||||||
|
}`.trim()}
|
||||||
style={{ width: `${((partialSegment + 1) / 6) * 100}%` }}
|
style={{ width: `${((partialSegment + 1) / 6) * 100}%` }}
|
||||||
/>
|
/>
|
||||||
) : fullSegments >= 2 ? (
|
) : fullSegments >= 2 ? (
|
||||||
@@ -70,7 +76,11 @@ export function ProportionBarView({
|
|||||||
{fullSegments === 2 ? (
|
{fullSegments === 2 ? (
|
||||||
partialSegment > 0 ? (
|
partialSegment > 0 ? (
|
||||||
<div
|
<div
|
||||||
className="absolute inset-y-0 left-0 bg-[var(--color-content-default-brand-primary)]"
|
className={`absolute inset-y-0 left-0 bg-[var(--color-content-default-brand-primary)] ${
|
||||||
|
segmented
|
||||||
|
? "rounded-l-[var(--radius-full)] rounded-r-[var(--radius-full)]"
|
||||||
|
: ""
|
||||||
|
}`.trim()}
|
||||||
style={{ width: `${(partialSegment / 3) * 100}%` }}
|
style={{ width: `${(partialSegment / 3) * 100}%` }}
|
||||||
/>
|
/>
|
||||||
) : null
|
) : null
|
||||||
@@ -84,8 +94,12 @@ export function ProportionBarView({
|
|||||||
{fullSegments === 3 && partialSegment > 0 ? (
|
{fullSegments === 3 && partialSegment > 0 ? (
|
||||||
<div
|
<div
|
||||||
className={`absolute inset-y-0 left-0 bg-[var(--color-content-default-brand-primary)] ${
|
className={`absolute inset-y-0 left-0 bg-[var(--color-content-default-brand-primary)] ${
|
||||||
partialSegment >= 3 ? "rounded-r-[var(--radius-full)]" : ""
|
segmented
|
||||||
}`}
|
? "rounded-l-[var(--radius-full)] rounded-r-[var(--radius-full)]"
|
||||||
|
: partialSegment >= 3
|
||||||
|
? "rounded-r-[var(--radius-full)]"
|
||||||
|
: ""
|
||||||
|
}`.trim()}
|
||||||
style={{ width: `${Math.min((partialSegment / 3) * 100, 100)}%` }}
|
style={{ width: `${Math.min((partialSegment / 3) * 100, 100)}%` }}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
export type HeaderLockupJustificationValue =
|
export type HeaderLockupJustificationValue =
|
||||||
| "left"
|
| "left"
|
||||||
| "center"
|
| "center"
|
||||||
@@ -16,9 +18,9 @@ export interface HeaderLockupProps {
|
|||||||
*/
|
*/
|
||||||
title: string;
|
title: string;
|
||||||
/**
|
/**
|
||||||
* Description text (optional)
|
* Description (optional). String for plain copy, or ReactNode for rich inline content (e.g. linked words).
|
||||||
*/
|
*/
|
||||||
description?: string;
|
description?: ReactNode;
|
||||||
/**
|
/**
|
||||||
* Text justification. Accepts both PascalCase (Figma) and lowercase (codebase).
|
* Text justification. Accepts both PascalCase (Figma) and lowercase (codebase).
|
||||||
* Figma uses PascalCase, codebase uses lowercase - both are supported.
|
* Figma uses PascalCase, codebase uses lowercase - both are supported.
|
||||||
@@ -38,7 +40,7 @@ export interface HeaderLockupProps {
|
|||||||
|
|
||||||
export interface HeaderLockupViewProps {
|
export interface HeaderLockupViewProps {
|
||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: ReactNode;
|
||||||
justification: "left" | "center";
|
justification: "left" | "center";
|
||||||
size: "L" | "M";
|
size: "L" | "M";
|
||||||
palette: "default" | "inverse";
|
palette: "default" | "inverse";
|
||||||
|
|||||||
@@ -43,17 +43,18 @@ function HeaderLockupView({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
{description && (
|
{description != null &&
|
||||||
<p
|
!(typeof description === "string" && description.length === 0) && (
|
||||||
className={`font-inter font-normal max-w-[640px] overflow-hidden relative shrink-0 ${descriptionColorClass} text-ellipsis w-full whitespace-pre-wrap ${
|
<p
|
||||||
isLeft ? "" : "text-center"
|
className={`font-inter font-normal max-w-[640px] overflow-hidden relative shrink-0 ${descriptionColorClass} text-ellipsis w-full whitespace-pre-wrap ${
|
||||||
} ${
|
isLeft ? "" : "text-center"
|
||||||
isL ? "text-[18px] leading-[1.3]" : "text-[14px] leading-[20px]"
|
} ${
|
||||||
}`}
|
isL ? "text-[18px] leading-[1.3]" : "text-[14px] leading-[20px]"
|
||||||
>
|
}`}
|
||||||
{description}
|
>
|
||||||
</p>
|
{description}
|
||||||
)}
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,20 @@ import { CreateFlowFooterView } from "./CreateFlowFooter.view";
|
|||||||
import type { CreateFlowFooterProps } from "./CreateFlowFooter.types";
|
import type { CreateFlowFooterProps } from "./CreateFlowFooter.types";
|
||||||
|
|
||||||
const CreateFlowFooterContainer = memo<CreateFlowFooterProps>(
|
const CreateFlowFooterContainer = memo<CreateFlowFooterProps>(
|
||||||
({ secondButton, progressBar = true, onBackClick, className = "" }) => {
|
({
|
||||||
|
secondButton,
|
||||||
|
progressBar = true,
|
||||||
|
proportionBarProgress,
|
||||||
|
proportionBarVariant,
|
||||||
|
onBackClick,
|
||||||
|
className = "",
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<CreateFlowFooterView
|
<CreateFlowFooterView
|
||||||
secondButton={secondButton}
|
secondButton={secondButton}
|
||||||
progressBar={progressBar}
|
progressBar={progressBar}
|
||||||
|
proportionBarProgress={proportionBarProgress}
|
||||||
|
proportionBarVariant={proportionBarVariant}
|
||||||
onBackClick={onBackClick}
|
onBackClick={onBackClick}
|
||||||
className={className}
|
className={className}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
import type {
|
||||||
|
ProportionBarState,
|
||||||
|
ProportionBarVariant,
|
||||||
|
} from "../../progress/ProportionBar/ProportionBar.types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Type definitions for CreateFlowFooter component
|
* Type definitions for CreateFlowFooter component
|
||||||
*
|
*
|
||||||
@@ -13,6 +18,16 @@ export interface CreateFlowFooterProps {
|
|||||||
* @default true
|
* @default true
|
||||||
*/
|
*/
|
||||||
progressBar?: boolean;
|
progressBar?: boolean;
|
||||||
|
/**
|
||||||
|
* `ProportionBar` state when the bar is shown (driven by create-flow step).
|
||||||
|
* @default "1-0"
|
||||||
|
*/
|
||||||
|
proportionBarProgress?: ProportionBarState;
|
||||||
|
/**
|
||||||
|
* `ProportionBar` layout variant (Figma create-flow footer uses `segmented`).
|
||||||
|
* @default "default"
|
||||||
|
*/
|
||||||
|
proportionBarVariant?: ProportionBarVariant;
|
||||||
/**
|
/**
|
||||||
* Callback function for Back button click
|
* Callback function for Back button click
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { normalizeProportionBarVariant } from "../../../../lib/propNormalization";
|
||||||
import ProportionBar from "../../progress/ProportionBar";
|
import ProportionBar from "../../progress/ProportionBar";
|
||||||
import Button from "../../buttons/Button";
|
import Button from "../../buttons/Button";
|
||||||
import type { CreateFlowFooterProps } from "./CreateFlowFooter.types";
|
import type { CreateFlowFooterProps } from "./CreateFlowFooter.types";
|
||||||
@@ -5,9 +6,14 @@ import type { CreateFlowFooterProps } from "./CreateFlowFooter.types";
|
|||||||
export function CreateFlowFooterView({
|
export function CreateFlowFooterView({
|
||||||
secondButton,
|
secondButton,
|
||||||
progressBar = true,
|
progressBar = true,
|
||||||
|
proportionBarProgress = "1-0",
|
||||||
|
proportionBarVariant: proportionBarVariantProp,
|
||||||
onBackClick,
|
onBackClick,
|
||||||
className = "",
|
className = "",
|
||||||
}: CreateFlowFooterProps) {
|
}: CreateFlowFooterProps) {
|
||||||
|
const proportionBarVariant = normalizeProportionBarVariant(
|
||||||
|
proportionBarVariantProp,
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<footer
|
<footer
|
||||||
className={`bg-black w-full ${className}`}
|
className={`bg-black w-full ${className}`}
|
||||||
@@ -17,7 +23,10 @@ export function CreateFlowFooterView({
|
|||||||
{/* Progress Bar - Top */}
|
{/* Progress Bar - Top */}
|
||||||
{progressBar && (
|
{progressBar && (
|
||||||
<div className="px-[var(--spacing-measures-spacing-500,20px)] md:px-[var(--spacing-measures-spacing-1200,48px)] pt-[var(--spacing-measures-spacing-300,12px)]">
|
<div className="px-[var(--spacing-measures-spacing-500,20px)] md:px-[var(--spacing-measures-spacing-1200,48px)] pt-[var(--spacing-measures-spacing-300,12px)]">
|
||||||
<ProportionBar progress="1-0" />
|
<ProportionBar
|
||||||
|
progress={proportionBarProgress}
|
||||||
|
variant={proportionBarVariant}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -12,12 +12,20 @@ import { CreateFlowProvider, useCreateFlow } from "./context/CreateFlowContext";
|
|||||||
import { useCreateFlowNavigation } from "./hooks/useCreateFlowNavigation";
|
import { useCreateFlowNavigation } from "./hooks/useCreateFlowNavigation";
|
||||||
import { useCreateFlowExit } from "./hooks/useCreateFlowExit";
|
import { useCreateFlowExit } from "./hooks/useCreateFlowExit";
|
||||||
import CreateFlowTopNav from "../components/utility/CreateFlowTopNav";
|
import CreateFlowTopNav from "../components/utility/CreateFlowTopNav";
|
||||||
import { getStepIndex } from "./utils/flowSteps";
|
import { getNextStep, getStepIndex } from "./utils/flowSteps";
|
||||||
|
import { getProportionBarProgressForCreateFlowStep } from "./utils/createFlowProportionProgress";
|
||||||
import { createFlowStepUsesCenteredTextLayout } from "./utils/createFlowScreenRegistry";
|
import { createFlowStepUsesCenteredTextLayout } from "./utils/createFlowScreenRegistry";
|
||||||
import CreateFlowFooter from "../components/utility/CreateFlowFooter";
|
import CreateFlowFooter from "../components/utility/CreateFlowFooter";
|
||||||
import Button from "../components/buttons/Button";
|
import Button from "../components/buttons/Button";
|
||||||
import { buildPublishPayload } from "../../lib/create/buildPublishPayload";
|
import { buildPublishPayload } from "../../lib/create/buildPublishPayload";
|
||||||
import { fetchAuthSession, publishRule } from "../../lib/create/api";
|
import { isValidCreateFlowSaveEmail } from "../../lib/create/isValidCreateFlowSaveEmail";
|
||||||
|
import {
|
||||||
|
fetchAuthSession,
|
||||||
|
publishRule,
|
||||||
|
requestMagicLink,
|
||||||
|
} from "../../lib/create/api";
|
||||||
|
import { safeInternalPath } from "../../lib/safeInternalPath";
|
||||||
|
import { setTransferPendingFlag } from "./utils/anonymousDraftStorage";
|
||||||
import { writeLastPublishedRule } from "../../lib/create/lastPublishedRule";
|
import { writeLastPublishedRule } from "../../lib/create/lastPublishedRule";
|
||||||
import {
|
import {
|
||||||
fetchTemplateBySlug,
|
fetchTemplateBySlug,
|
||||||
@@ -25,7 +33,7 @@ import {
|
|||||||
} from "../../lib/create/fetchTemplates";
|
} from "../../lib/create/fetchTemplates";
|
||||||
import messages from "../../messages/en/index";
|
import messages from "../../messages/en/index";
|
||||||
import { useAuthModal } from "../contexts/AuthModalContext";
|
import { useAuthModal } from "../contexts/AuthModalContext";
|
||||||
import { useTranslation } from "../contexts/MessagesContext";
|
import { useMessages, useTranslation } from "../contexts/MessagesContext";
|
||||||
import { PostLoginDraftTransfer } from "./PostLoginDraftTransfer";
|
import { PostLoginDraftTransfer } from "./PostLoginDraftTransfer";
|
||||||
import { SignedInDraftHydration } from "./SignedInDraftHydration";
|
import { SignedInDraftHydration } from "./SignedInDraftHydration";
|
||||||
import Alert from "../components/modals/Alert";
|
import Alert from "../components/modals/Alert";
|
||||||
@@ -35,7 +43,7 @@ import {
|
|||||||
} from "./context/CreateFlowDraftSaveBannerContext";
|
} from "./context/CreateFlowDraftSaveBannerContext";
|
||||||
|
|
||||||
/** First step where Save & Exit is offered (first Create Community select per Figma). */
|
/** First step where Save & Exit is offered (first Create Community select per Figma). */
|
||||||
const SAVE_EXIT_FROM_STEP_INDEX = getStepIndex("community-size");
|
const SAVE_EXIT_FROM_STEP_INDEX = getStepIndex("community-structure");
|
||||||
|
|
||||||
function CreateFlowSessionShell({ children }: { children: ReactNode }) {
|
function CreateFlowSessionShell({ children }: { children: ReactNode }) {
|
||||||
const [sessionUser, setSessionUser] = useState<
|
const [sessionUser, setSessionUser] = useState<
|
||||||
@@ -78,7 +86,10 @@ function CreateFlowLayoutContent({
|
|||||||
sessionUser: { id: string; email: string } | null | undefined;
|
sessionUser: { id: string; email: string } | null | undefined;
|
||||||
sessionResolved: boolean;
|
sessionResolved: boolean;
|
||||||
}) {
|
}) {
|
||||||
const tFooter = useTranslation("create.footer");
|
const { create } = useMessages();
|
||||||
|
const footer = create.footer;
|
||||||
|
const communitySaveMessages = create.communitySave;
|
||||||
|
const tLogin = useTranslation("pages.login");
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const { openLogin } = useAuthModal();
|
const { openLogin } = useAuthModal();
|
||||||
@@ -89,7 +100,7 @@ function CreateFlowLayoutContent({
|
|||||||
goToNextStep,
|
goToNextStep,
|
||||||
goToPreviousStep,
|
goToPreviousStep,
|
||||||
} = useCreateFlowNavigation();
|
} = useCreateFlowNavigation();
|
||||||
const { state, clearState } = useCreateFlow();
|
const { state, clearState, updateState } = useCreateFlow();
|
||||||
const { draftSaveBannerMessage, setDraftSaveBannerMessage } =
|
const { draftSaveBannerMessage, setDraftSaveBannerMessage } =
|
||||||
useCreateFlowDraftSaveBanner();
|
useCreateFlowDraftSaveBanner();
|
||||||
const [publishBannerMessage, setPublishBannerMessage] = useState<
|
const [publishBannerMessage, setPublishBannerMessage] = useState<
|
||||||
@@ -100,6 +111,13 @@ function CreateFlowLayoutContent({
|
|||||||
string | null
|
string | null
|
||||||
>(null);
|
>(null);
|
||||||
const [isApplyingTemplate, setIsApplyingTemplate] = useState(false);
|
const [isApplyingTemplate, setIsApplyingTemplate] = useState(false);
|
||||||
|
const [communitySaveMagicLinkSubmitting, setCommunitySaveMagicLinkSubmitting] =
|
||||||
|
useState(false);
|
||||||
|
const [communitySaveMagicLinkError, setCommunitySaveMagicLinkError] = useState<
|
||||||
|
string | null
|
||||||
|
>(null);
|
||||||
|
const [communitySaveMagicLinkSuccess, setCommunitySaveMagicLinkSuccess] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
const templateReviewMatch = pathname?.match(
|
const templateReviewMatch = pathname?.match(
|
||||||
/\/create\/review-template\/([^/?#]+)/,
|
/\/create\/review-template\/([^/?#]+)/,
|
||||||
@@ -222,6 +240,51 @@ function CreateFlowLayoutContent({
|
|||||||
await runAuthenticatedExit(opts);
|
await runAuthenticatedExit(opts);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentStep !== "community-save") {
|
||||||
|
setCommunitySaveMagicLinkError(null);
|
||||||
|
setCommunitySaveMagicLinkSuccess(false);
|
||||||
|
setCommunitySaveMagicLinkSubmitting(false);
|
||||||
|
}
|
||||||
|
}, [currentStep]);
|
||||||
|
|
||||||
|
const handleCommunitySaveMagicLinkSubmit = useCallback(async () => {
|
||||||
|
setCommunitySaveMagicLinkError(null);
|
||||||
|
setCommunitySaveMagicLinkSuccess(false);
|
||||||
|
const raw = state.communitySaveEmail;
|
||||||
|
const trimmed = typeof raw === "string" ? raw.trim().toLowerCase() : "";
|
||||||
|
if (!isValidCreateFlowSaveEmail(trimmed)) return;
|
||||||
|
|
||||||
|
setCommunitySaveMagicLinkSubmitting(true);
|
||||||
|
try {
|
||||||
|
const stepAfterSave = getNextStep("community-save");
|
||||||
|
const segment = stepAfterSave ?? "review";
|
||||||
|
const rawNext = `/create/${segment}?syncDraft=1`;
|
||||||
|
const nextPath = safeInternalPath(rawNext);
|
||||||
|
const result = await requestMagicLink(trimmed, nextPath);
|
||||||
|
if (result.ok === false) {
|
||||||
|
if (result.retryAfterMs != null && result.retryAfterMs > 0) {
|
||||||
|
const seconds = Math.ceil(result.retryAfterMs / 1000);
|
||||||
|
setCommunitySaveMagicLinkError(
|
||||||
|
tLogin("errors.rateLimited").replace("{seconds}", String(seconds)),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setCommunitySaveMagicLinkError(
|
||||||
|
result.error || tLogin("errors.generic"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTransferPendingFlag();
|
||||||
|
updateState({ communitySaveEmail: trimmed });
|
||||||
|
setCommunitySaveMagicLinkSuccess(true);
|
||||||
|
} catch {
|
||||||
|
setCommunitySaveMagicLinkError(tLogin("errors.network"));
|
||||||
|
} finally {
|
||||||
|
setCommunitySaveMagicLinkSubmitting(false);
|
||||||
|
}
|
||||||
|
}, [state.communitySaveEmail, tLogin, updateState]);
|
||||||
|
|
||||||
const isCompletedStep = currentStep === "completed";
|
const isCompletedStep = currentStep === "completed";
|
||||||
const isRightRailStep = currentStep === "right-rail";
|
const isRightRailStep = currentStep === "right-rail";
|
||||||
const isFinalReviewStep = currentStep === "final-review";
|
const isFinalReviewStep = currentStep === "final-review";
|
||||||
@@ -250,14 +313,23 @@ function CreateFlowLayoutContent({
|
|||||||
const saveDraftOnExit =
|
const saveDraftOnExit =
|
||||||
Boolean(sessionUser) && stepIdx >= SAVE_EXIT_FROM_STEP_INDEX;
|
Boolean(sessionUser) && stepIdx >= SAVE_EXIT_FROM_STEP_INDEX;
|
||||||
|
|
||||||
const hasErrorOverlays =
|
const proportionBarProgress = getProportionBarProgressForCreateFlowStep(
|
||||||
|
currentStep,
|
||||||
|
);
|
||||||
|
|
||||||
|
const footerPrimaryButtonClass =
|
||||||
|
"md:!text-[14px] md:!leading-[16px] !text-[12px] !leading-[14px] !px-[var(--spacing-measures-spacing-200,8px)] md:!px-[var(--spacing-measures-spacing-250,10px)] !py-[var(--spacing-measures-spacing-200,8px)] md:!py-[var(--spacing-measures-spacing-250,10px)]";
|
||||||
|
|
||||||
|
const hasTopOverlays =
|
||||||
Boolean(draftSaveBannerMessage) ||
|
Boolean(draftSaveBannerMessage) ||
|
||||||
Boolean(publishBannerMessage) ||
|
Boolean(publishBannerMessage) ||
|
||||||
Boolean(templateReviewApplyError);
|
Boolean(templateReviewApplyError) ||
|
||||||
|
Boolean(communitySaveMagicLinkError) ||
|
||||||
|
Boolean(communitySaveMagicLinkSuccess);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex h-screen min-h-0 flex-col overflow-hidden bg-black">
|
<div className="relative flex h-screen min-h-0 flex-col overflow-hidden bg-black">
|
||||||
{hasErrorOverlays ? (
|
{hasTopOverlays ? (
|
||||||
<div
|
<div
|
||||||
className="pointer-events-none fixed left-0 right-0 top-0 z-[200] flex flex-col gap-2 px-[var(--spacing-measures-spacing-500,20px)] pt-[var(--spacing-measures-spacing-300,12px)] md:px-[var(--measures-spacing-1800,64px)]"
|
className="pointer-events-none fixed left-0 right-0 top-0 z-[200] flex flex-col gap-2 px-[var(--spacing-measures-spacing-500,20px)] pt-[var(--spacing-measures-spacing-300,12px)] md:px-[var(--measures-spacing-1800,64px)]"
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
@@ -298,6 +370,30 @@ function CreateFlowLayoutContent({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
{communitySaveMagicLinkError ? (
|
||||||
|
<div className="pointer-events-auto mx-auto w-full max-w-[960px]">
|
||||||
|
<Alert
|
||||||
|
type="banner"
|
||||||
|
status="danger"
|
||||||
|
title={communitySaveMessages.magicLinkErrorTitle}
|
||||||
|
description={communitySaveMagicLinkError}
|
||||||
|
onClose={() => setCommunitySaveMagicLinkError(null)}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{communitySaveMagicLinkSuccess ? (
|
||||||
|
<div className="pointer-events-auto mx-auto w-full max-w-[960px]">
|
||||||
|
<Alert
|
||||||
|
type="banner"
|
||||||
|
status="positive"
|
||||||
|
title={communitySaveMessages.magicLinkSuccessTitle}
|
||||||
|
description={communitySaveMessages.magicLinkSuccessDescription}
|
||||||
|
onClose={() => setCommunitySaveMagicLinkSuccess(false)}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
@@ -334,6 +430,8 @@ function CreateFlowLayoutContent({
|
|||||||
<CreateFlowFooter
|
<CreateFlowFooter
|
||||||
className="shrink-0"
|
className="shrink-0"
|
||||||
progressBar={!isTemplateReviewRoute && !isFinalReviewStep}
|
progressBar={!isTemplateReviewRoute && !isFinalReviewStep}
|
||||||
|
proportionBarProgress={proportionBarProgress}
|
||||||
|
proportionBarVariant="segmented"
|
||||||
secondButton={
|
secondButton={
|
||||||
isTemplateReviewRoute ? (
|
isTemplateReviewRoute ? (
|
||||||
<div className="flex flex-shrink-0 items-center gap-3 md:gap-4">
|
<div className="flex flex-shrink-0 items-center gap-3 md:gap-4">
|
||||||
@@ -367,13 +465,101 @@ function CreateFlowLayoutContent({
|
|||||||
{messages.create.templateReview.footer.customize}
|
{messages.create.templateReview.footer.customize}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
) : currentStep === "community-name" && nextStep ? (
|
||||||
|
<div className="flex flex-shrink-0 items-center gap-3 md:gap-4">
|
||||||
|
<Button
|
||||||
|
buttonType="outline"
|
||||||
|
palette="inverse"
|
||||||
|
size="xsmall"
|
||||||
|
disabled={isPublishing}
|
||||||
|
className={footerPrimaryButtonClass}
|
||||||
|
onClick={() => {
|
||||||
|
goToNextStep();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{footer.next}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
buttonType="filled"
|
||||||
|
palette="default"
|
||||||
|
size="xsmall"
|
||||||
|
disabled={isPublishing}
|
||||||
|
className={footerPrimaryButtonClass}
|
||||||
|
onClick={() => {
|
||||||
|
goToNextStep();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{footer.confirmName}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : currentStep === "community-save" && nextStep ? (
|
||||||
|
<div className="flex flex-shrink-0 items-center gap-3 md:gap-4">
|
||||||
|
<Button
|
||||||
|
buttonType="outline"
|
||||||
|
palette="default"
|
||||||
|
size="xsmall"
|
||||||
|
disabled={isPublishing}
|
||||||
|
className={footerPrimaryButtonClass}
|
||||||
|
onClick={() => {
|
||||||
|
goToNextStep();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{footer.saveLater}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
buttonType="filled"
|
||||||
|
palette="default"
|
||||||
|
size="xsmall"
|
||||||
|
disabled={
|
||||||
|
isPublishing ||
|
||||||
|
communitySaveMagicLinkSubmitting ||
|
||||||
|
communitySaveMagicLinkSuccess ||
|
||||||
|
!isValidCreateFlowSaveEmail(state.communitySaveEmail)
|
||||||
|
}
|
||||||
|
className={footerPrimaryButtonClass}
|
||||||
|
onClick={() => {
|
||||||
|
void handleCommunitySaveMagicLinkSubmit();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{communitySaveMagicLinkSubmitting
|
||||||
|
? footer.submitEmailSending
|
||||||
|
: footer.submitEmail}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : currentStep === "review" && nextStep ? (
|
||||||
|
<div className="flex flex-shrink-0 items-center gap-3 md:gap-4">
|
||||||
|
<Button
|
||||||
|
buttonType="outline"
|
||||||
|
palette="default"
|
||||||
|
size="xsmall"
|
||||||
|
disabled={isPublishing}
|
||||||
|
className={footerPrimaryButtonClass}
|
||||||
|
onClick={() => {
|
||||||
|
goToNextStep();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{footer.createCustom}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
buttonType="filled"
|
||||||
|
palette="default"
|
||||||
|
size="xsmall"
|
||||||
|
disabled={isPublishing}
|
||||||
|
className={footerPrimaryButtonClass}
|
||||||
|
onClick={() => {
|
||||||
|
router.push("/templates");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{footer.createFromTemplate}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
) : nextStep ? (
|
) : nextStep ? (
|
||||||
<Button
|
<Button
|
||||||
buttonType="filled"
|
buttonType="filled"
|
||||||
palette="default"
|
palette="default"
|
||||||
size="xsmall"
|
size="xsmall"
|
||||||
disabled={isPublishing}
|
disabled={isPublishing}
|
||||||
className="md:!text-[14px] md:!leading-[16px] !text-[12px] !leading-[14px] !px-[var(--spacing-measures-spacing-200,8px)] md:!px-[var(--spacing-measures-spacing-250,10px)] !py-[var(--spacing-measures-spacing-200,8px)] md:!py-[var(--spacing-measures-spacing-250,10px)]"
|
className={footerPrimaryButtonClass}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (currentStep === "final-review") {
|
if (currentStep === "final-review") {
|
||||||
void handleFinalize();
|
void handleFinalize();
|
||||||
@@ -385,10 +571,16 @@ function CreateFlowLayoutContent({
|
|||||||
{currentStep === "final-review"
|
{currentStep === "final-review"
|
||||||
? isPublishing
|
? isPublishing
|
||||||
? messages.create.publish.finalizeButtonPublishing
|
? messages.create.publish.finalizeButtonPublishing
|
||||||
: tFooter("finalizeCommunityRule")
|
: footer.finalizeCommunityRule
|
||||||
: currentStep === "confirm-stakeholders"
|
: currentStep === "confirm-stakeholders"
|
||||||
? tFooter("confirmStakeholders")
|
? footer.confirmStakeholders
|
||||||
: tFooter("next")}
|
: currentStep === "community-context"
|
||||||
|
? footer.confirmDescription
|
||||||
|
: currentStep === "community-structure"
|
||||||
|
? footer.confirmDetails
|
||||||
|
: currentStep === "community-size"
|
||||||
|
? footer.confirmMembers
|
||||||
|
: footer.next}
|
||||||
</Button>
|
</Button>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { notFound } from "next/navigation";
|
import { notFound, useRouter } from "next/navigation";
|
||||||
import { use } from "react";
|
import { use, useEffect } from "react";
|
||||||
import { CreateFlowScreenView } from "../screens/CreateFlowScreenView";
|
import { CreateFlowScreenView } from "../screens/CreateFlowScreenView";
|
||||||
import { isValidStep } from "../utils/flowSteps";
|
import { isValidStep } from "../utils/flowSteps";
|
||||||
import type { CreateFlowStep } from "../types";
|
import type { CreateFlowStep } from "../types";
|
||||||
@@ -12,6 +12,17 @@ interface PageProps {
|
|||||||
|
|
||||||
export default function CreateFlowScreenPage({ params }: PageProps) {
|
export default function CreateFlowScreenPage({ params }: PageProps) {
|
||||||
const { screenId: raw } = use(params);
|
const { screenId: raw } = use(params);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (raw === "community-reflection") {
|
||||||
|
router.replace("/create/community-save");
|
||||||
|
}
|
||||||
|
}, [raw, router]);
|
||||||
|
|
||||||
|
if (raw === "community-reflection") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isValidStep(raw)) {
|
if (!isValidStep(raw)) {
|
||||||
notFound();
|
notFound();
|
||||||
|
|||||||
@@ -3,10 +3,14 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { CreateFlowHeaderLockup } from "./CreateFlowHeaderLockup";
|
import { CreateFlowHeaderLockup } from "./CreateFlowHeaderLockup";
|
||||||
import { CreateFlowStepShell } from "./CreateFlowStepShell";
|
import { CreateFlowStepShell } from "./CreateFlowStepShell";
|
||||||
|
import {
|
||||||
|
CREATE_FLOW_MD_UP_GRID_CELL_CLASS,
|
||||||
|
CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS,
|
||||||
|
} from "./createFlowLayoutTokens";
|
||||||
|
|
||||||
/** Shared `RuleCard` / template card chrome: width + radius; padding comes from `RuleCard` (L+expanded = 24px). */
|
/** Shared `RuleCard` / template card chrome: width + radius; padding comes from `RuleCard` (L+expanded = 24px). */
|
||||||
export const CREATE_FLOW_REVIEW_RULE_CARD_LAYOUT_CLASS =
|
export const CREATE_FLOW_REVIEW_RULE_CARD_LAYOUT_CLASS =
|
||||||
"w-full min-w-0 rounded-[12px] md:rounded-[24px] md:!max-w-full md:!w-full";
|
"w-full min-w-0 rounded-[12px] md:rounded-[24px] md:max-w-[640px]";
|
||||||
|
|
||||||
type CreateFlowLockupCardStepShellProps = {
|
type CreateFlowLockupCardStepShellProps = {
|
||||||
lockupTitle: string;
|
lockupTitle: string;
|
||||||
@@ -14,10 +18,7 @@ type CreateFlowLockupCardStepShellProps = {
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/** Final-review layout: `wideGrid`, two columns from `md:`, column widths from `createFlowLayoutTokens`. */
|
||||||
* Final-review-style create-flow step: `wideGrid` shell, two-column grid at `md+`,
|
|
||||||
* left `CreateFlowHeaderLockup` (vertically centered in column), right column for card content.
|
|
||||||
*/
|
|
||||||
export function CreateFlowLockupCardStepShell({
|
export function CreateFlowLockupCardStepShell({
|
||||||
lockupTitle,
|
lockupTitle,
|
||||||
lockupDescription,
|
lockupDescription,
|
||||||
@@ -25,15 +26,23 @@ export function CreateFlowLockupCardStepShell({
|
|||||||
}: CreateFlowLockupCardStepShellProps) {
|
}: CreateFlowLockupCardStepShellProps) {
|
||||||
return (
|
return (
|
||||||
<CreateFlowStepShell variant="wideGrid" contentTopBelowMd="space-800">
|
<CreateFlowStepShell variant="wideGrid" contentTopBelowMd="space-800">
|
||||||
<div className="flex w-full min-w-0 flex-col gap-4 md:grid md:grid-cols-2 md:gap-[var(--measures-spacing-1200,48px)]">
|
<div
|
||||||
<div className="flex min-w-0 flex-col justify-start md:justify-center">
|
className={`mx-auto flex w-full min-w-0 flex-col gap-4 md:grid md:w-full md:grid-cols-2 md:justify-items-center md:gap-[var(--measures-spacing-1200,48px)] ${CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`flex min-w-0 flex-col justify-start md:justify-center ${CREATE_FLOW_MD_UP_GRID_CELL_CLASS}`}
|
||||||
|
>
|
||||||
<CreateFlowHeaderLockup
|
<CreateFlowHeaderLockup
|
||||||
title={lockupTitle}
|
title={lockupTitle}
|
||||||
description={lockupDescription}
|
description={lockupDescription}
|
||||||
justification="left"
|
justification="left"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex min-w-0 w-full flex-col items-stretch">{children}</div>
|
<div
|
||||||
|
className={`flex min-w-0 flex-col items-stretch ${CREATE_FLOW_MD_UP_GRID_CELL_CLASS}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CreateFlowStepShell>
|
</CreateFlowStepShell>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export type CreateFlowStepShellVariant =
|
|||||||
| "wideGridLoosePadding"
|
| "wideGridLoosePadding"
|
||||||
| "bare";
|
| "bare";
|
||||||
|
|
||||||
/** Top padding below `md` between top nav and step content (semantic space tokens). */
|
/** Semantic top padding below create-flow top nav (applied at all breakpoints; name is legacy). */
|
||||||
export type CreateFlowContentTopBelowMd = "none" | "space-1400" | "space-800";
|
export type CreateFlowContentTopBelowMd = "none" | "space-1400" | "space-800";
|
||||||
|
|
||||||
const outerByVariant: Record<CreateFlowStepShellVariant, string> = {
|
const outerByVariant: Record<CreateFlowStepShellVariant, string> = {
|
||||||
@@ -17,22 +17,24 @@ const outerByVariant: Record<CreateFlowStepShellVariant, string> = {
|
|||||||
"flex w-full min-w-0 flex-col items-center px-5 md:px-16",
|
"flex w-full min-w-0 flex-col items-center px-5 md:px-16",
|
||||||
centeredNarrowBottomPad:
|
centeredNarrowBottomPad:
|
||||||
"flex w-full min-w-0 flex-col items-center px-5 pb-28 md:px-[var(--measures-spacing-1800,64px)] md:pb-32",
|
"flex w-full min-w-0 flex-col items-center px-5 pb-28 md:px-[var(--measures-spacing-1800,64px)] md:pb-32",
|
||||||
wideGrid: "w-full min-w-0 max-w-[1280px] shrink-0 px-5 md:px-12",
|
/** Wide two-column steps; 1328px = two 640px columns + 48px gutter. */
|
||||||
|
wideGrid: "w-full min-w-0 max-w-[1328px] shrink-0 px-5 md:px-12",
|
||||||
|
/** Create Community review + card grid (Figma Flow — Review `19706:12135`): max width 1440. */
|
||||||
wideGridLoosePadding:
|
wideGridLoosePadding:
|
||||||
"w-full min-w-0 max-w-[1280px] shrink-0 px-5 md:px-16",
|
"w-full min-w-0 max-w-[1440px] shrink-0 px-5 md:px-16",
|
||||||
bare: "w-full min-w-0",
|
bare: "w-full min-w-0",
|
||||||
};
|
};
|
||||||
|
|
||||||
const contentTopBelowMdClass: Record<CreateFlowContentTopBelowMd, string> = {
|
const contentTopBelowMdClass: Record<CreateFlowContentTopBelowMd, string> = {
|
||||||
none: "",
|
none: "",
|
||||||
"space-1400": "max-md:pt-[var(--space-1400)]",
|
"space-1400": "pt-[var(--space-1400)]",
|
||||||
"space-800": "max-md:pt-[var(--space-800)]",
|
"space-800": "pt-[var(--space-800)]",
|
||||||
};
|
};
|
||||||
|
|
||||||
interface CreateFlowStepShellProps {
|
interface CreateFlowStepShellProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
variant?: CreateFlowStepShellVariant;
|
variant?: CreateFlowStepShellVariant;
|
||||||
/** Padding-top below `md` only; `text` step uses `none`. */
|
/** Top spacing below top chrome (`CreateFlowTextFieldScreen` defaults to `space-1400`). */
|
||||||
contentTopBelowMd?: CreateFlowContentTopBelowMd;
|
contentTopBelowMd?: CreateFlowContentTopBelowMd;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
/** Single column/section: full width under `md`, max 640px from `--breakpoint-md` up. */
|
||||||
|
export const CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS =
|
||||||
|
"w-full min-w-0 md:max-w-[640px]";
|
||||||
|
|
||||||
|
/** Grid cell: same cap as column max, centered when the track is wider than 640px. */
|
||||||
|
export const CREATE_FLOW_MD_UP_GRID_CELL_CLASS =
|
||||||
|
"w-full min-w-0 md:mx-auto md:max-w-[640px]";
|
||||||
|
|
||||||
|
/** Two 640px columns + `--measures-spacing-1200` (48px) gutter. */
|
||||||
|
export const CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS = "md:max-w-[1328px]";
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useMediaQuery } from "../../hooks/useMediaQuery";
|
||||||
|
|
||||||
|
/** `--breakpoint-lg` (1024px); same SSR/first-paint pattern as `useCreateFlowMdUp`. */
|
||||||
|
const CREATE_FLOW_MIN_WIDTH_LG = "(min-width: 1024px)";
|
||||||
|
|
||||||
|
/** True at viewport ≥1024px (e.g. review grid column split with Tailwind `lg:`). */
|
||||||
|
export function useCreateFlowLgUp(): boolean {
|
||||||
|
const [isMounted, setIsMounted] = useState(false);
|
||||||
|
const isLgOrLarger = useMediaQuery(CREATE_FLOW_MIN_WIDTH_LG);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect -- intentional: defer until mount for SSR/first-paint alignment
|
||||||
|
setIsMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return !isMounted || isLgOrLarger;
|
||||||
|
}
|
||||||
@@ -3,19 +3,10 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useMediaQuery } from "../../hooks/useMediaQuery";
|
import { useMediaQuery } from "../../hooks/useMediaQuery";
|
||||||
|
|
||||||
/**
|
/** `--breakpoint-md` (640px); same SSR/first-paint pattern as `useCreateFlowLgUp`. */
|
||||||
* Matches design-system `md` (`--breakpoint-md`, 640px in `app/tailwind.css`).
|
|
||||||
* Use with Tailwind `md:` / `max-md:` utilities in create-flow pages.
|
|
||||||
*/
|
|
||||||
const CREATE_FLOW_MIN_WIDTH_MD = "(min-width: 640px)";
|
const CREATE_FLOW_MIN_WIDTH_MD = "(min-width: 640px)";
|
||||||
|
|
||||||
/**
|
/** True at viewport ≥640px (pairs with Tailwind `md:` on create-flow screens). */
|
||||||
* True at or above the create-flow `md` breakpoint (desktop-oriented layout).
|
|
||||||
*
|
|
||||||
* `useMediaQuery` initializes to `false` on the server and first client render
|
|
||||||
* to avoid hydration mismatches. We combine it with a post-mount flag so the
|
|
||||||
* first paint matches the intended desktop layout until `matchMedia` runs.
|
|
||||||
*/
|
|
||||||
export function useCreateFlowMdUp(): boolean {
|
export function useCreateFlowMdUp(): boolean {
|
||||||
const [isMounted, setIsMounted] = useState(false);
|
const [isMounted, setIsMounted] = useState(false);
|
||||||
const isMdOrLarger = useMediaQuery(CREATE_FLOW_MIN_WIDTH_MD);
|
const isMdOrLarger = useMediaQuery(CREATE_FLOW_MIN_WIDTH_MD);
|
||||||
|
|||||||
@@ -15,16 +15,14 @@ import {
|
|||||||
CreateFlowLockupCardStepShell,
|
CreateFlowLockupCardStepShell,
|
||||||
} from "../../components/CreateFlowLockupCardStepShell";
|
} from "../../components/CreateFlowLockupCardStepShell";
|
||||||
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
||||||
|
import { CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS } from "../../components/createFlowLayoutTokens";
|
||||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
||||||
|
|
||||||
interface PageProps {
|
interface PageProps {
|
||||||
params: Promise<{ slug: string }>;
|
params: Promise<{ slug: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Template review route — same shell/grid as final-review; Figma `22142-898702`. */
|
||||||
* Template review: same responsive grid and RuleCard chrome as final-review;
|
|
||||||
* copy from Figma 22142-898702 (intro + dynamic card from API).
|
|
||||||
*/
|
|
||||||
export default function ReviewTemplatePage({ params }: PageProps) {
|
export default function ReviewTemplatePage({ params }: PageProps) {
|
||||||
const { slug: rawSlug } = use(params);
|
const { slug: rawSlug } = use(params);
|
||||||
const slug = decodeURIComponent(rawSlug);
|
const slug = decodeURIComponent(rawSlug);
|
||||||
@@ -75,7 +73,9 @@ export default function ReviewTemplatePage({ params }: PageProps) {
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<CreateFlowStepShell variant="wideGrid" contentTopBelowMd="space-800">
|
<CreateFlowStepShell variant="wideGrid" contentTopBelowMd="space-800">
|
||||||
<div className="flex w-full shrink-0 items-center justify-start pb-16">
|
<div
|
||||||
|
className={`flex shrink-0 items-center justify-start pb-16 ${CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}`}
|
||||||
|
>
|
||||||
<p className="text-[var(--color-content-default-secondary,#a3a3a3)]">
|
<p className="text-[var(--color-content-default-secondary,#a3a3a3)]">
|
||||||
{t("loading")}
|
{t("loading")}
|
||||||
</p>
|
</p>
|
||||||
@@ -87,7 +87,9 @@ export default function ReviewTemplatePage({ params }: PageProps) {
|
|||||||
if (error || !template) {
|
if (error || !template) {
|
||||||
return (
|
return (
|
||||||
<CreateFlowStepShell variant="wideGrid" contentTopBelowMd="space-800">
|
<CreateFlowStepShell variant="wideGrid" contentTopBelowMd="space-800">
|
||||||
<div className="flex w-full max-w-[640px] shrink-0 flex-col gap-4 pb-8">
|
<div
|
||||||
|
className={`flex shrink-0 flex-col gap-4 pb-8 ${CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}`}
|
||||||
|
>
|
||||||
<Alert
|
<Alert
|
||||||
type="banner"
|
type="banner"
|
||||||
status="danger"
|
status="danger"
|
||||||
|
|||||||
@@ -33,26 +33,31 @@ export function CreateFlowScreenView({
|
|||||||
maxLength={48}
|
maxLength={48}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "community-size":
|
case "community-structure":
|
||||||
return <CommunitySizeSelectScreen />;
|
return <CommunityStructureSelectScreen />;
|
||||||
case "community-context":
|
case "community-context":
|
||||||
return (
|
return (
|
||||||
<CreateFlowTextFieldScreen
|
<CreateFlowTextFieldScreen
|
||||||
messageNamespace="create.communityContext"
|
messageNamespace="create.communityContext"
|
||||||
stateField="communityContext"
|
stateField="communityContext"
|
||||||
maxLength={2000}
|
maxLength={48}
|
||||||
|
mainAlign="center"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "community-structure":
|
case "community-size":
|
||||||
return <CommunityStructureSelectScreen />;
|
return <CommunitySizeSelectScreen />;
|
||||||
case "community-upload":
|
case "community-upload":
|
||||||
return <CommunityUploadScreen />;
|
return <CommunityUploadScreen />;
|
||||||
case "community-reflection":
|
case "community-save":
|
||||||
return (
|
return (
|
||||||
<CreateFlowTextFieldScreen
|
<CreateFlowTextFieldScreen
|
||||||
messageNamespace="create.communityReflection"
|
messageNamespace="create.communitySave"
|
||||||
stateField="communityReflection"
|
stateField="communitySaveEmail"
|
||||||
maxLength={2000}
|
maxLength={254}
|
||||||
|
mainAlign="center"
|
||||||
|
inputType="email"
|
||||||
|
showCharacterCount={false}
|
||||||
|
headerJustification="center"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "review":
|
case "review":
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import CardStack from "../../../components/utility/CardStack";
|
|||||||
import Create from "../../../components/modals/Create";
|
import Create from "../../../components/modals/Create";
|
||||||
import TextArea from "../../../components/controls/TextArea";
|
import TextArea from "../../../components/controls/TextArea";
|
||||||
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
||||||
|
import { CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS } from "../../components/createFlowLayoutTokens";
|
||||||
|
|
||||||
const IN_PERSON_CARD_ID = "in-person-meetings";
|
const IN_PERSON_CARD_ID = "in-person-meetings";
|
||||||
const SIGNAL_CARD_ID = "signal";
|
const SIGNAL_CARD_ID = "signal";
|
||||||
@@ -210,15 +211,15 @@ export function CardsScreen() {
|
|||||||
variant="wideGridLoosePadding"
|
variant="wideGridLoosePadding"
|
||||||
contentTopBelowMd="space-800"
|
contentTopBelowMd="space-800"
|
||||||
>
|
>
|
||||||
<div className="flex w-full min-w-0 flex-col gap-6">
|
<div className="flex w-full min-w-0 flex-col items-center gap-6">
|
||||||
<div className="min-w-0">
|
<div className={CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}>
|
||||||
<CreateFlowHeaderLockup
|
<CreateFlowHeaderLockup
|
||||||
title={title}
|
title={title}
|
||||||
description={description}
|
description={description}
|
||||||
justification="center"
|
justification="center"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 w-full">
|
<div className={CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}>
|
||||||
<CardStack
|
<CardStack
|
||||||
cards={sampleCards}
|
cards={sampleCards}
|
||||||
selectedIds={selectedIds}
|
selectedIds={selectedIds}
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ import { parseDocumentSectionsForDisplay } from "../../../../lib/create/buildPub
|
|||||||
import { readLastPublishedRule } from "../../../../lib/create/lastPublishedRule";
|
import { readLastPublishedRule } from "../../../../lib/create/lastPublishedRule";
|
||||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
||||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||||
|
import {
|
||||||
|
CREATE_FLOW_MD_UP_GRID_CELL_CLASS,
|
||||||
|
CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS,
|
||||||
|
} from "../../components/createFlowLayoutTokens";
|
||||||
|
|
||||||
export function CompletedScreen() {
|
export function CompletedScreen() {
|
||||||
const mdUp = useCreateFlowMdUp();
|
const mdUp = useCreateFlowMdUp();
|
||||||
@@ -67,8 +71,12 @@ export function CompletedScreen() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex min-h-0 w-full flex-1 flex-col overflow-hidden bg-[var(--color-teal-teal50,#c9fef9)] md:h-full">
|
<div className="flex min-h-0 w-full flex-1 flex-col overflow-hidden bg-[var(--color-teal-teal50,#c9fef9)] md:h-full">
|
||||||
<div className="mx-auto grid min-h-0 w-full max-w-[1280px] grid-cols-1 gap-4 px-5 max-md:max-w-[639px] max-md:pt-[var(--space-800)] max-md:pb-8 md:h-full md:grid-cols-2 md:gap-[var(--measures-spacing-1200,48px)] md:overflow-hidden md:px-12 md:py-0">
|
<div
|
||||||
<div className="flex min-w-0 flex-col justify-start overflow-hidden md:justify-center md:pb-8">
|
className={`mx-auto grid min-h-0 w-full grid-cols-1 gap-4 px-5 max-md:max-w-[639px] max-md:pt-[var(--space-800)] max-md:pb-8 md:h-full md:grid-cols-2 md:justify-items-center md:gap-[var(--measures-spacing-1200,48px)] md:overflow-hidden md:px-12 md:py-0 ${CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`flex flex-col justify-start overflow-hidden md:justify-center md:pb-8 ${CREATE_FLOW_MD_UP_GRID_CELL_CLASS}`}
|
||||||
|
>
|
||||||
<CreateFlowHeaderLockup
|
<CreateFlowHeaderLockup
|
||||||
title={headerTitle}
|
title={headerTitle}
|
||||||
description={headerDescription}
|
description={headerDescription}
|
||||||
@@ -77,12 +85,14 @@ export function CompletedScreen() {
|
|||||||
palette="inverse"
|
palette="inverse"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="scrollbar-hide relative flex min-h-0 min-w-0 flex-col overflow-x-hidden md:overflow-y-auto">
|
<div
|
||||||
|
className={`scrollbar-hide relative flex min-h-0 flex-col overflow-x-hidden md:overflow-y-auto ${CREATE_FLOW_MD_UP_GRID_CELL_CLASS}`}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className="pointer-events-none sticky top-0 z-10 hidden h-5 shrink-0 bg-gradient-to-b from-[var(--color-teal-teal50,#c9fef9)]/55 from-0% via-[var(--color-teal-teal50,#c9fef9)]/20 via-50% to-transparent md:block"
|
className="pointer-events-none sticky top-0 z-10 hidden h-5 shrink-0 bg-gradient-to-b from-[var(--color-teal-teal50,#c9fef9)]/55 from-0% via-[var(--color-teal-teal50,#c9fef9)]/20 via-50% to-transparent md:block"
|
||||||
aria-hidden
|
aria-hidden
|
||||||
/>
|
/>
|
||||||
<div className="min-w-0 py-0 md:pb-8">
|
<div className="w-full min-w-0 py-0 md:pb-8">
|
||||||
<CommunityRuleDocument
|
<CommunityRuleDocument
|
||||||
sections={documentSections}
|
sections={documentSections}
|
||||||
useCardStyle={!mdUp}
|
useCardStyle={!mdUp}
|
||||||
|
|||||||
@@ -1,40 +1,63 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import type { ReactNode } from "react";
|
||||||
import NumberedList from "../../../components/type/NumberedList";
|
import NumberedList from "../../../components/type/NumberedList";
|
||||||
import { useTranslation } from "../../../contexts/MessagesContext";
|
import { useMessages } from "../../../contexts/MessagesContext";
|
||||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
||||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||||
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
||||||
|
import { CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS } from "../../components/createFlowLayoutTokens";
|
||||||
|
|
||||||
/** Create Community — frame 1 (Figma 20094-16005). */
|
/**
|
||||||
|
* Create Community — frame 1 (Figma [20094-16005](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=20094-16005)).
|
||||||
|
* URL: /create/informational
|
||||||
|
*/
|
||||||
export function InformationalScreen() {
|
export function InformationalScreen() {
|
||||||
const mdUp = useCreateFlowMdUp();
|
const mdUp = useCreateFlowMdUp();
|
||||||
const t = useTranslation("create.informational");
|
const copy = useMessages().create.informational;
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
title: t("steps.0.title"),
|
title: copy.steps["0"].title,
|
||||||
description: t("steps.0.description"),
|
description: copy.steps["0"].description,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("steps.1.title"),
|
title: copy.steps["1"].title,
|
||||||
description: t("steps.1.description"),
|
description: copy.steps["1"].description,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("steps.2.title"),
|
title: copy.steps["2"].title,
|
||||||
description: t("steps.2.description"),
|
description: copy.steps["2"].description,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const description: ReactNode = (
|
||||||
|
<>
|
||||||
|
{copy.descriptionLead}{" "}
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="font-inter font-normal text-[var(--color-content-default-tertiary,#b4b4b4)] underline decoration-solid underline-offset-[3px] cursor-pointer"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{copy.workshopLabel}
|
||||||
|
</a>{" "}
|
||||||
|
{copy.descriptionTrail}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CreateFlowStepShell
|
<CreateFlowStepShell
|
||||||
variant="centeredNarrow"
|
variant="centeredNarrow"
|
||||||
contentTopBelowMd="space-1400"
|
contentTopBelowMd="space-1400"
|
||||||
>
|
>
|
||||||
<div className="flex w-full max-w-[640px] flex-col items-center gap-12">
|
<div
|
||||||
|
className={`flex flex-col items-center gap-[var(--measures-spacing-1200,48px)] ${CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}`}
|
||||||
|
>
|
||||||
<CreateFlowHeaderLockup
|
<CreateFlowHeaderLockup
|
||||||
title={t("title")}
|
title={copy.title}
|
||||||
description={t("description")}
|
description={description}
|
||||||
justification="left"
|
justification="left"
|
||||||
/>
|
/>
|
||||||
<NumberedList items={items} size={mdUp ? "M" : "S"} />
|
<NumberedList items={items} size={mdUp ? "M" : "S"} />
|
||||||
|
|||||||
@@ -3,37 +3,56 @@
|
|||||||
import RuleCard from "../../../components/cards/RuleCard";
|
import RuleCard from "../../../components/cards/RuleCard";
|
||||||
import { useTranslation } from "../../../contexts/MessagesContext";
|
import { useTranslation } from "../../../contexts/MessagesContext";
|
||||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||||
|
import { useCreateFlowLgUp } from "../../hooks/useCreateFlowLgUp";
|
||||||
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
||||||
|
import {
|
||||||
|
CREATE_FLOW_MD_UP_GRID_CELL_CLASS,
|
||||||
|
CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS,
|
||||||
|
} from "../../components/createFlowLayoutTokens";
|
||||||
|
|
||||||
/** Create Community — frame 8 (Figma 19706-12135); URL segment `review`. */
|
/** Create Community review — Figma `19706:12135` (`/create/review`; two columns from `lg:`; column caps in `createFlowLayoutTokens`). */
|
||||||
export function CommunityReviewScreen() {
|
export function CommunityReviewScreen() {
|
||||||
const mdUp = useCreateFlowMdUp();
|
const lgUp = useCreateFlowLgUp();
|
||||||
const t = useTranslation("create.review");
|
const t = useTranslation("create.review");
|
||||||
|
const { state } = useCreateFlow();
|
||||||
|
|
||||||
|
const cardTitle =
|
||||||
|
typeof state.title === "string" && state.title.trim().length > 0
|
||||||
|
? state.title.trim()
|
||||||
|
: t("ruleCard.title");
|
||||||
|
const cardDescription =
|
||||||
|
typeof state.communityContext === "string" &&
|
||||||
|
state.communityContext.trim().length > 0
|
||||||
|
? state.communityContext.trim()
|
||||||
|
: t("ruleCard.description");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CreateFlowStepShell
|
<CreateFlowStepShell
|
||||||
variant="wideGridLoosePadding"
|
variant="wideGridLoosePadding"
|
||||||
contentTopBelowMd="space-1400"
|
contentTopBelowMd="space-1400"
|
||||||
>
|
>
|
||||||
<div className="flex w-full min-w-0 flex-col gap-4 md:grid md:grid-cols-2 md:gap-[var(--measures-spacing-1200,48px)]">
|
<div
|
||||||
<div className="min-w-0">
|
className={`flex w-full min-w-0 flex-col items-center gap-6 lg:mx-auto lg:w-full lg:grid lg:grid-cols-2 lg:items-center lg:justify-items-center lg:gap-x-[var(--measures-spacing-1200,48px)] lg:gap-y-6 ${CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`flex flex-col justify-center lg:min-h-[212px] ${CREATE_FLOW_MD_UP_GRID_CELL_CLASS}`}
|
||||||
|
>
|
||||||
<CreateFlowHeaderLockup
|
<CreateFlowHeaderLockup
|
||||||
title={t("header.title")}
|
title={t("header.title")}
|
||||||
description={t("header.description")}
|
description={t("header.description")}
|
||||||
justification="left"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 w-full">
|
<div className={CREATE_FLOW_MD_UP_GRID_CELL_CLASS}>
|
||||||
<RuleCard
|
<RuleCard
|
||||||
title={t("ruleCard.title")}
|
title={cardTitle}
|
||||||
description={t("ruleCard.description")}
|
description={cardDescription}
|
||||||
size={mdUp ? "L" : "M"}
|
size={lgUp ? "L" : "M"}
|
||||||
expanded={false}
|
expanded={false}
|
||||||
backgroundColor="bg-[#c9fef9]"
|
backgroundColor="bg-[var(--color-teal-teal50,#c9fef9)]"
|
||||||
logoUrl="/assets/Vector_MutualAid.svg"
|
logoUrl="/assets/Vector_MutualAid.svg"
|
||||||
logoAlt={t("ruleCard.logoAlt")}
|
logoAlt={cardTitle}
|
||||||
className="rounded-[16px]"
|
className="rounded-[24px]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ import type { CardStackItem } from "../../../components/utility/CardStack/CardSt
|
|||||||
import { useMessages } from "../../../contexts/MessagesContext";
|
import { useMessages } from "../../../contexts/MessagesContext";
|
||||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
||||||
|
import {
|
||||||
|
CREATE_FLOW_MD_UP_GRID_CELL_CLASS,
|
||||||
|
CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS,
|
||||||
|
} from "../../components/createFlowLayoutTokens";
|
||||||
|
|
||||||
export function RightRailScreen() {
|
export function RightRailScreen() {
|
||||||
const m = useMessages();
|
const m = useMessages();
|
||||||
@@ -76,8 +80,12 @@ export function RightRailScreen() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex h-full min-h-0 w-full flex-1 flex-col overflow-hidden md:h-full">
|
<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="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
|
||||||
<div className="flex min-w-0 flex-col items-stretch justify-start overflow-hidden md:justify-center">
|
className={`mx-auto grid h-auto min-h-0 w-full 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:justify-items-center md:gap-12 md:pb-8 ${CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`flex flex-col items-stretch justify-start overflow-hidden md:justify-center ${CREATE_FLOW_MD_UP_GRID_CELL_CLASS}`}
|
||||||
|
>
|
||||||
<DecisionMakingSidebar
|
<DecisionMakingSidebar
|
||||||
title={rr.sidebar.title}
|
title={rr.sidebar.title}
|
||||||
description={sidebarDescription}
|
description={sidebarDescription}
|
||||||
@@ -89,8 +97,10 @@ export function RightRailScreen() {
|
|||||||
justification={mdUp ? "left" : "center"}
|
justification={mdUp ? "left" : "center"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="scrollbar-hide relative flex min-h-0 min-w-0 flex-col overflow-x-hidden max-md:overflow-visible md:overflow-y-auto">
|
<div
|
||||||
<div className="flex min-w-0 flex-col items-center gap-6 py-0 md:pb-8">
|
className={`scrollbar-hide relative flex min-h-0 flex-col overflow-x-hidden max-md:overflow-visible md:overflow-y-auto ${CREATE_FLOW_MD_UP_GRID_CELL_CLASS}`}
|
||||||
|
>
|
||||||
|
<div className="flex w-full min-w-0 flex-col items-center gap-6 py-0 md:pb-8">
|
||||||
<CardStack
|
<CardStack
|
||||||
cards={sampleCards}
|
cards={sampleCards}
|
||||||
selectedIds={selectedIds}
|
selectedIds={selectedIds}
|
||||||
|
|||||||
@@ -1,44 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useMemo, useEffect, type Dispatch, type SetStateAction } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import MultiSelect from "../../../components/controls/MultiSelect";
|
import MultiSelect from "../../../components/controls/MultiSelect";
|
||||||
import type { ChipOption } from "../../../components/controls/MultiSelect/MultiSelect.types";
|
import type { ChipOption } from "../../../components/controls/MultiSelect/MultiSelect.types";
|
||||||
import { useMessages, useTranslation } from "../../../contexts/MessagesContext";
|
import { useMessages } from "../../../contexts/MessagesContext";
|
||||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
|
||||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||||
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
||||||
|
import { CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS } from "../../components/createFlowLayoutTokens";
|
||||||
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(
|
function chipRowsFromLabels(
|
||||||
rows: readonly { label: string }[],
|
rows: readonly { label: string }[],
|
||||||
@@ -56,17 +25,16 @@ function selectedIdsFromOptions(options: ChipOption[]): string[] {
|
|||||||
.map((o) => o.id);
|
.map((o) => o.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Create Community — frame 3 (Figma 20094-18244). */
|
/** Create Community — Figma `20094:41317`, chips only (layout tokens shared with structure select). */
|
||||||
export function CommunitySizeSelectScreen() {
|
export function CommunitySizeSelectScreen() {
|
||||||
const m = useMessages();
|
const m = useMessages();
|
||||||
|
const cs = m.create.communitySize;
|
||||||
const { markCreateFlowInteraction, updateState, state } = useCreateFlow();
|
const { markCreateFlowInteraction, updateState, state } = useCreateFlow();
|
||||||
const mdUp = useCreateFlowMdUp();
|
|
||||||
const t = useTranslation("create.communitySize");
|
|
||||||
|
|
||||||
const [communitySizeOptions, setCommunitySizeOptions] = useState<
|
const [communitySizeOptions, setCommunitySizeOptions] = useState<
|
||||||
ChipOption[]
|
ChipOption[]
|
||||||
>(() => {
|
>(() => {
|
||||||
const base = chipRowsFromLabels(m.create.communitySize.communitySizes);
|
const base = chipRowsFromLabels(cs.communitySizes);
|
||||||
const selected = new Set(state.selectedCommunitySizeIds ?? []);
|
const selected = new Set(state.selectedCommunitySizeIds ?? []);
|
||||||
return base.map((opt) => ({
|
return base.map((opt) => ({
|
||||||
...opt,
|
...opt,
|
||||||
@@ -90,16 +58,6 @@ export function CommunitySizeSelectScreen() {
|
|||||||
);
|
);
|
||||||
}, [state.selectedCommunitySizeIds]);
|
}, [state.selectedCommunitySizeIds]);
|
||||||
|
|
||||||
const communityCustomHandlers = useMemo(
|
|
||||||
() =>
|
|
||||||
createListCustomHandlers(
|
|
||||||
setCommunitySizeOptions,
|
|
||||||
"Unselected",
|
|
||||||
markCreateFlowInteraction,
|
|
||||||
),
|
|
||||||
[markCreateFlowInteraction],
|
|
||||||
);
|
|
||||||
|
|
||||||
const persistSelection = (next: ChipOption[]) => {
|
const persistSelection = (next: ChipOption[]) => {
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
setCommunitySizeOptions(next);
|
setCommunitySizeOptions(next);
|
||||||
@@ -123,18 +81,13 @@ export function CommunitySizeSelectScreen() {
|
|||||||
persistSelection(next);
|
persistSelection(next);
|
||||||
};
|
};
|
||||||
|
|
||||||
const multiLabel = t("multiSelect.label");
|
|
||||||
const addText = t("multiSelect.addButtonText");
|
|
||||||
|
|
||||||
const multiSelectBlock = (
|
const multiSelectBlock = (
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
label={multiLabel}
|
formHeader={false}
|
||||||
size="S"
|
size="M"
|
||||||
options={communitySizeOptions}
|
options={communitySizeOptions}
|
||||||
onChipClick={handleCommunitySizeClick}
|
onChipClick={handleCommunitySizeClick}
|
||||||
{...communityCustomHandlers}
|
addButton={false}
|
||||||
addButton={true}
|
|
||||||
addButtonText={addText}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -143,29 +96,22 @@ export function CommunitySizeSelectScreen() {
|
|||||||
variant="centeredNarrow"
|
variant="centeredNarrow"
|
||||||
contentTopBelowMd="space-1400"
|
contentTopBelowMd="space-1400"
|
||||||
>
|
>
|
||||||
{mdUp ? (
|
<div className="flex w-full min-w-0 flex-col items-start gap-[var(--measures-spacing-400,16px)] md:max-w-[640px] lg:max-w-[1328px] lg:flex-row lg:flex-nowrap lg:items-center lg:justify-center lg:gap-[var(--measures-spacing-1200,48px)]">
|
||||||
<div className="flex w-full max-w-[1280px] items-center justify-center gap-[var(--measures-spacing-1200,48px)]">
|
<div
|
||||||
<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]">
|
className={`flex flex-col items-start gap-[var(--measures-spacing-200,8px)] lg:flex-1 lg:justify-center lg:py-[12px] ${CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}`}
|
||||||
<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
|
<CreateFlowHeaderLockup
|
||||||
title={t("header.title")}
|
title={cs.header.title}
|
||||||
description={t("header.description")}
|
description={cs.header.description}
|
||||||
justification="left"
|
justification="left"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`flex flex-col items-start gap-[var(--measures-spacing-800,32px)] lg:flex-1 ${CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}`}
|
||||||
|
>
|
||||||
{multiSelectBlock}
|
{multiSelectBlock}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</CreateFlowStepShell>
|
</CreateFlowStepShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ import {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import MultiSelect from "../../../components/controls/MultiSelect";
|
import MultiSelect from "../../../components/controls/MultiSelect";
|
||||||
import type { ChipOption } from "../../../components/controls/MultiSelect/MultiSelect.types";
|
import type { ChipOption } from "../../../components/controls/MultiSelect/MultiSelect.types";
|
||||||
import { useMessages, useTranslation } from "../../../contexts/MessagesContext";
|
import { useMessages } from "../../../contexts/MessagesContext";
|
||||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
|
||||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||||
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
||||||
|
import { CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS } from "../../components/createFlowLayoutTokens";
|
||||||
|
|
||||||
function createListCustomHandlers(
|
function createListCustomHandlers(
|
||||||
setList: Dispatch<SetStateAction<ChipOption[]>>,
|
setList: Dispatch<SetStateAction<ChipOption[]>>,
|
||||||
@@ -73,28 +73,38 @@ function applySavedSelection(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Create Community — frame 5 (Figma 20094-41317). */
|
function selectedIdsFromOptions(options: ChipOption[]): string[] {
|
||||||
|
return options
|
||||||
|
.filter((o) => o.state === "Selected")
|
||||||
|
.map((o) => o.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create Community step 3 — Figma `20094:18244` (responsive grid + column caps via `createFlowLayoutTokens`). */
|
||||||
export function CommunityStructureSelectScreen() {
|
export function CommunityStructureSelectScreen() {
|
||||||
const m = useMessages();
|
const m = useMessages();
|
||||||
|
const cs = m.create.communityStructure;
|
||||||
const { markCreateFlowInteraction, updateState, state } = useCreateFlow();
|
const { markCreateFlowInteraction, updateState, state } = useCreateFlow();
|
||||||
const mdUp = useCreateFlowMdUp();
|
|
||||||
const t = useTranslation("create.communityStructure");
|
|
||||||
|
|
||||||
const [organizationTypeOptions, setOrganizationTypeOptions] = useState<
|
const [organizationTypeOptions, setOrganizationTypeOptions] = useState<
|
||||||
ChipOption[]
|
ChipOption[]
|
||||||
>(() =>
|
>(() =>
|
||||||
applySavedSelection(
|
applySavedSelection(
|
||||||
chipRowsFromLabels(m.create.communityStructure.organizationTypes),
|
chipRowsFromLabels(cs.organizationTypes),
|
||||||
state.selectedOrganizationTypeIds,
|
state.selectedOrganizationTypeIds,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const [governanceStyleOptions, setGovernanceStyleOptions] = useState<
|
const [scaleOptions, setScaleOptions] = useState<ChipOption[]>(() =>
|
||||||
ChipOption[]
|
|
||||||
>(() =>
|
|
||||||
applySavedSelection(
|
applySavedSelection(
|
||||||
chipRowsFromLabels(m.create.communityStructure.governanceStyles),
|
chipRowsFromLabels(cs.scaleOptions),
|
||||||
state.selectedGovernanceStyleIds,
|
state.selectedScaleIds,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const [maturityOptions, setMaturityOptions] = useState<ChipOption[]>(() =>
|
||||||
|
applySavedSelection(
|
||||||
|
chipRowsFromLabels(cs.maturityOptions),
|
||||||
|
state.selectedMaturityIds,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -105,10 +115,14 @@ export function CommunityStructureSelectScreen() {
|
|||||||
}, [state.selectedOrganizationTypeIds]);
|
}, [state.selectedOrganizationTypeIds]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setGovernanceStyleOptions((prev) =>
|
setScaleOptions((prev) => applySavedSelection(prev, state.selectedScaleIds));
|
||||||
applySavedSelection(prev, state.selectedGovernanceStyleIds),
|
}, [state.selectedScaleIds]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMaturityOptions((prev) =>
|
||||||
|
applySavedSelection(prev, state.selectedMaturityIds),
|
||||||
);
|
);
|
||||||
}, [state.selectedGovernanceStyleIds]);
|
}, [state.selectedMaturityIds]);
|
||||||
|
|
||||||
const organizationCustomHandlers = useMemo(
|
const organizationCustomHandlers = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -119,10 +133,19 @@ export function CommunityStructureSelectScreen() {
|
|||||||
),
|
),
|
||||||
[markCreateFlowInteraction],
|
[markCreateFlowInteraction],
|
||||||
);
|
);
|
||||||
const governanceCustomHandlers = useMemo(
|
const scaleCustomHandlers = useMemo(
|
||||||
() =>
|
() =>
|
||||||
createListCustomHandlers(
|
createListCustomHandlers(
|
||||||
setGovernanceStyleOptions,
|
setScaleOptions,
|
||||||
|
"Unselected",
|
||||||
|
markCreateFlowInteraction,
|
||||||
|
),
|
||||||
|
[markCreateFlowInteraction],
|
||||||
|
);
|
||||||
|
const maturityCustomHandlers = useMemo(
|
||||||
|
() =>
|
||||||
|
createListCustomHandlers(
|
||||||
|
setMaturityOptions,
|
||||||
"Unselected",
|
"Unselected",
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
),
|
),
|
||||||
@@ -132,75 +155,100 @@ export function CommunityStructureSelectScreen() {
|
|||||||
const persistOrg = (next: ChipOption[]) => {
|
const persistOrg = (next: ChipOption[]) => {
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
setOrganizationTypeOptions(next);
|
setOrganizationTypeOptions(next);
|
||||||
updateState({
|
updateState({ selectedOrganizationTypeIds: selectedIdsFromOptions(next) });
|
||||||
selectedOrganizationTypeIds: next
|
|
||||||
.filter((o) => o.state === "Selected")
|
|
||||||
.map((o) => o.id),
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const persistGov = (next: ChipOption[]) => {
|
const persistScale = (next: ChipOption[]) => {
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
setGovernanceStyleOptions(next);
|
setScaleOptions(next);
|
||||||
updateState({
|
updateState({ selectedScaleIds: selectedIdsFromOptions(next) });
|
||||||
selectedGovernanceStyleIds: next
|
};
|
||||||
.filter((o) => o.state === "Selected")
|
|
||||||
.map((o) => o.id),
|
const persistMaturity = (next: ChipOption[]) => {
|
||||||
});
|
markCreateFlowInteraction();
|
||||||
|
setMaturityOptions(next);
|
||||||
|
updateState({ selectedMaturityIds: selectedIdsFromOptions(next) });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOrganizationTypeClick = (chipId: string) => {
|
const handleOrganizationTypeClick = (chipId: string) => {
|
||||||
const next: ChipOption[] = organizationTypeOptions.map((opt) =>
|
persistOrg(
|
||||||
opt.id === chipId
|
organizationTypeOptions.map((opt) =>
|
||||||
? {
|
opt.id === chipId
|
||||||
...opt,
|
? {
|
||||||
state:
|
...opt,
|
||||||
opt.state === "Selected"
|
state:
|
||||||
? ("Unselected" as const)
|
opt.state === "Selected"
|
||||||
: ("Selected" as const),
|
? ("Unselected" as const)
|
||||||
}
|
: ("Selected" as const),
|
||||||
: opt,
|
}
|
||||||
|
: opt,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
persistOrg(next);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGovernanceStyleClick = (chipId: string) => {
|
const handleScaleClick = (chipId: string) => {
|
||||||
const next: ChipOption[] = governanceStyleOptions.map((opt) =>
|
persistScale(
|
||||||
opt.id === chipId
|
scaleOptions.map((opt) =>
|
||||||
? {
|
opt.id === chipId
|
||||||
...opt,
|
? {
|
||||||
state:
|
...opt,
|
||||||
opt.state === "Selected"
|
state:
|
||||||
? ("Unselected" as const)
|
opt.state === "Selected"
|
||||||
: ("Selected" as const),
|
? ("Unselected" as const)
|
||||||
}
|
: ("Selected" as const),
|
||||||
: opt,
|
}
|
||||||
|
: opt,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
persistGov(next);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const multiLabel = t("multiSelect.label");
|
const handleMaturityClick = (chipId: string) => {
|
||||||
const addText = t("multiSelect.addButtonText");
|
persistMaturity(
|
||||||
|
maturityOptions.map((opt) =>
|
||||||
|
opt.id === chipId
|
||||||
|
? {
|
||||||
|
...opt,
|
||||||
|
state:
|
||||||
|
opt.state === "Selected"
|
||||||
|
? ("Unselected" as const)
|
||||||
|
: ("Selected" as const),
|
||||||
|
}
|
||||||
|
: opt,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const multiSelectBlock = (
|
const multiSelectBlock = (
|
||||||
<>
|
<>
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
label={multiLabel}
|
label={cs.organizationMultiSelect.label}
|
||||||
|
showHelpIcon
|
||||||
size="S"
|
size="S"
|
||||||
options={organizationTypeOptions}
|
options={organizationTypeOptions}
|
||||||
onChipClick={handleOrganizationTypeClick}
|
onChipClick={handleOrganizationTypeClick}
|
||||||
{...organizationCustomHandlers}
|
{...organizationCustomHandlers}
|
||||||
addButton={true}
|
addButton
|
||||||
addButtonText={addText}
|
addButtonText={cs.organizationMultiSelect.addButtonText}
|
||||||
/>
|
/>
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
label={multiLabel}
|
label={cs.scaleMultiSelect.label}
|
||||||
|
showHelpIcon
|
||||||
size="S"
|
size="S"
|
||||||
options={governanceStyleOptions}
|
options={scaleOptions}
|
||||||
onChipClick={handleGovernanceStyleClick}
|
onChipClick={handleScaleClick}
|
||||||
{...governanceCustomHandlers}
|
{...scaleCustomHandlers}
|
||||||
addButton={true}
|
addButton
|
||||||
addButtonText={addText}
|
addButtonText={cs.scaleMultiSelect.addButtonText}
|
||||||
|
/>
|
||||||
|
<MultiSelect
|
||||||
|
label={cs.maturityMultiSelect.label}
|
||||||
|
showHelpIcon
|
||||||
|
size="S"
|
||||||
|
options={maturityOptions}
|
||||||
|
onChipClick={handleMaturityClick}
|
||||||
|
{...maturityCustomHandlers}
|
||||||
|
addButton
|
||||||
|
addButtonText={cs.maturityMultiSelect.addButtonText}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -210,29 +258,22 @@ export function CommunityStructureSelectScreen() {
|
|||||||
variant="centeredNarrow"
|
variant="centeredNarrow"
|
||||||
contentTopBelowMd="space-1400"
|
contentTopBelowMd="space-1400"
|
||||||
>
|
>
|
||||||
{mdUp ? (
|
<div className="flex w-full min-w-0 flex-col items-start gap-[var(--measures-spacing-400,16px)] md:max-w-[640px] lg:max-w-[1328px] lg:flex-row lg:flex-nowrap lg:items-center lg:justify-center lg:gap-[var(--measures-spacing-1200,48px)]">
|
||||||
<div className="flex w-full max-w-[1280px] items-center justify-center gap-[var(--measures-spacing-1200,48px)]">
|
<div
|
||||||
<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]">
|
className={`flex flex-col items-start gap-[var(--measures-spacing-200,8px)] lg:flex-1 lg:justify-center lg:py-[12px] ${CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}`}
|
||||||
<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
|
<CreateFlowHeaderLockup
|
||||||
title={t("header.title")}
|
title={cs.header.title}
|
||||||
description={t("header.description")}
|
description={cs.header.description}
|
||||||
justification="left"
|
justification="left"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`flex flex-col items-start gap-[var(--measures-spacing-800,32px)] lg:flex-1 ${CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}`}
|
||||||
|
>
|
||||||
{multiSelectBlock}
|
{multiSelectBlock}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</CreateFlowStepShell>
|
</CreateFlowStepShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useTranslation } from "../../../contexts/MessagesContext";
|
|||||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||||
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
||||||
|
import { CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS } from "../../components/createFlowLayoutTokens";
|
||||||
|
|
||||||
export function ConfirmStakeholdersScreen() {
|
export function ConfirmStakeholdersScreen() {
|
||||||
const { markCreateFlowInteraction } = useCreateFlow();
|
const { markCreateFlowInteraction } = useCreateFlow();
|
||||||
@@ -50,7 +51,9 @@ export function ConfirmStakeholdersScreen() {
|
|||||||
variant="centeredNarrowBottomPad"
|
variant="centeredNarrowBottomPad"
|
||||||
contentTopBelowMd="space-1400"
|
contentTopBelowMd="space-1400"
|
||||||
>
|
>
|
||||||
<div className="flex w-full max-w-[640px] flex-col items-start gap-[var(--measures-spacing-300,12px)]">
|
<div
|
||||||
|
className={`flex flex-col items-start gap-[var(--measures-spacing-300,12px)] ${CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}`}
|
||||||
|
>
|
||||||
<div className="flex w-full flex-col gap-[var(--measures-spacing-200,8px)] py-[12px]">
|
<div className="flex w-full flex-col gap-[var(--measures-spacing-200,8px)] py-[12px]">
|
||||||
<CreateFlowHeaderLockup
|
<CreateFlowHeaderLockup
|
||||||
title={t("title")}
|
title={t("title")}
|
||||||
|
|||||||
@@ -1,18 +1,30 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, type HTMLInputTypeAttribute } from "react";
|
||||||
import TextInput from "../../../components/controls/TextInput";
|
import TextInput from "../../../components/controls/TextInput";
|
||||||
|
import type { HeaderLockupJustificationValue } from "../../../components/type/HeaderLockup/HeaderLockup.types";
|
||||||
import { useTranslation } from "../../../contexts/MessagesContext";
|
import { useTranslation } from "../../../contexts/MessagesContext";
|
||||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
||||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||||
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
import {
|
||||||
|
CreateFlowStepShell,
|
||||||
|
type CreateFlowContentTopBelowMd,
|
||||||
|
} from "../../components/CreateFlowStepShell";
|
||||||
|
import { CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS } from "../../components/createFlowLayoutTokens";
|
||||||
import type { CreateFlowTextStateField } from "../../types";
|
import type { CreateFlowTextStateField } from "../../types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
messageNamespace: string;
|
messageNamespace: string;
|
||||||
stateField: CreateFlowTextStateField;
|
stateField: CreateFlowTextStateField;
|
||||||
maxLength: number;
|
maxLength: number;
|
||||||
|
/** Figma Flow — Text (`20094:41243`): main column `items-center` + horizontal padding token. */
|
||||||
|
mainAlign?: "start" | "center";
|
||||||
|
inputType?: HTMLInputTypeAttribute;
|
||||||
|
showCharacterCount?: boolean;
|
||||||
|
headerJustification?: HeaderLockupJustificationValue;
|
||||||
|
/** Top spacing under top chrome (`CreateFlowStepShell` / `CreateFlowContentTopBelowMd`). */
|
||||||
|
contentTopBelowMd?: CreateFlowContentTopBelowMd;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -22,6 +34,11 @@ export function CreateFlowTextFieldScreen({
|
|||||||
messageNamespace,
|
messageNamespace,
|
||||||
stateField,
|
stateField,
|
||||||
maxLength,
|
maxLength,
|
||||||
|
mainAlign = "start",
|
||||||
|
inputType = "text",
|
||||||
|
showCharacterCount = true,
|
||||||
|
headerJustification = "left",
|
||||||
|
contentTopBelowMd = "space-1400",
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { markCreateFlowInteraction, updateState, state } = useCreateFlow();
|
const { markCreateFlowInteraction, updateState, state } = useCreateFlow();
|
||||||
const mdUp = useCreateFlowMdUp();
|
const mdUp = useCreateFlowMdUp();
|
||||||
@@ -42,20 +59,35 @@ export function CreateFlowTextFieldScreen({
|
|||||||
}, [state, stateField]);
|
}, [state, stateField]);
|
||||||
|
|
||||||
const characterCount = value.length;
|
const characterCount = value.length;
|
||||||
const hint = t("characterCountTemplate")
|
const hint =
|
||||||
.replace("{current}", String(characterCount))
|
showCharacterCount === false
|
||||||
.replace("{max}", String(maxLength));
|
? false
|
||||||
|
: t("characterCountTemplate")
|
||||||
|
.replace("{current}", String(characterCount))
|
||||||
|
.replace("{max}", String(maxLength));
|
||||||
|
|
||||||
|
const mainItems =
|
||||||
|
mainAlign === "center" ? "items-center" : "items-start";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CreateFlowStepShell variant="centeredNarrow">
|
<CreateFlowStepShell
|
||||||
<div className="flex w-full max-w-[640px] flex-col items-start gap-[18px]">
|
variant="centeredNarrow"
|
||||||
<CreateFlowHeaderLockup
|
contentTopBelowMd={contentTopBelowMd}
|
||||||
title={t("title")}
|
>
|
||||||
description={t("description")}
|
<div
|
||||||
justification="left"
|
className={`flex flex-col gap-[18px] ${mainItems} ${CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}`}
|
||||||
/>
|
>
|
||||||
|
<div className="w-full">
|
||||||
|
<CreateFlowHeaderLockup
|
||||||
|
title={t("title")}
|
||||||
|
description={t("description")}
|
||||||
|
justification={headerJustification}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<TextInput
|
<TextInput
|
||||||
|
className="!transition-none"
|
||||||
|
type={inputType}
|
||||||
placeholder={t("placeholder")}
|
placeholder={t("placeholder")}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Upload from "../../../components/controls/Upload";
|
import Upload from "../../../components/controls/Upload";
|
||||||
import { useTranslation } from "../../../contexts/MessagesContext";
|
import { useMessages } from "../../../contexts/MessagesContext";
|
||||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
|
||||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||||
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
||||||
|
import { CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS } from "../../components/createFlowLayoutTokens";
|
||||||
|
|
||||||
/** Create Community — frame 6 (Figma 20094-41524). */
|
/** Create Community — Figma Flow — Upload `20094:41524`. */
|
||||||
export function CommunityUploadScreen() {
|
export function CommunityUploadScreen() {
|
||||||
|
const m = useMessages();
|
||||||
|
const u = m.create.communityUpload;
|
||||||
const { markCreateFlowInteraction } = useCreateFlow();
|
const { markCreateFlowInteraction } = useCreateFlow();
|
||||||
const mdUp = useCreateFlowMdUp();
|
|
||||||
const t = useTranslation("create.communityUpload");
|
|
||||||
|
|
||||||
const handleUploadClick = () => {
|
const handleUploadClick = () => {
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
@@ -22,16 +22,21 @@ export function CommunityUploadScreen() {
|
|||||||
variant="centeredNarrow"
|
variant="centeredNarrow"
|
||||||
contentTopBelowMd="space-1400"
|
contentTopBelowMd="space-1400"
|
||||||
>
|
>
|
||||||
<div className="flex w-full max-w-[640px] flex-col items-center gap-[18px]">
|
<div
|
||||||
<CreateFlowHeaderLockup
|
className={`flex flex-col items-center gap-[18px] ${CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}`}
|
||||||
title={t("title")}
|
>
|
||||||
description={t("description")}
|
<div className="w-full">
|
||||||
justification={mdUp ? "center" : "left"}
|
<CreateFlowHeaderLockup
|
||||||
/>
|
title={u.title}
|
||||||
<div className="w-full max-w-[474px]">
|
description={u.description}
|
||||||
|
justification="center"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-full">
|
||||||
<Upload
|
<Upload
|
||||||
active={true}
|
active={true}
|
||||||
showHelpIcon={true}
|
showHelpIcon={false}
|
||||||
|
hintText={u.hintText}
|
||||||
onClick={handleUploadClick}
|
onClick={handleUploadClick}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+8
-5
@@ -16,7 +16,7 @@ export type CreateFlowStep =
|
|||||||
| "community-context"
|
| "community-context"
|
||||||
| "community-structure"
|
| "community-structure"
|
||||||
| "community-upload"
|
| "community-upload"
|
||||||
| "community-reflection"
|
| "community-save"
|
||||||
| "review"
|
| "review"
|
||||||
| "cards"
|
| "cards"
|
||||||
| "right-rail"
|
| "right-rail"
|
||||||
@@ -29,7 +29,7 @@ export type CreateFlowTextStateField =
|
|||||||
| "title"
|
| "title"
|
||||||
| "summary"
|
| "summary"
|
||||||
| "communityContext"
|
| "communityContext"
|
||||||
| "communityReflection";
|
| "communitySaveEmail";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flow state for inputs across create-flow steps.
|
* Flow state for inputs across create-flow steps.
|
||||||
@@ -41,13 +41,16 @@ export interface CreateFlowState {
|
|||||||
summary?: string;
|
summary?: string;
|
||||||
/** Additional copy fields for multi-step Create Community text frames (Figma). */
|
/** Additional copy fields for multi-step Create Community text frames (Figma). */
|
||||||
communityContext?: string;
|
communityContext?: string;
|
||||||
communityReflection?: string;
|
/** Email collected on the “Save your progress” step (Figma Flow — Text `20097:14948`). */
|
||||||
|
communitySaveEmail?: string;
|
||||||
/** Selected chip ids from `community-size` (MultiSelect). */
|
/** Selected chip ids from `community-size` (MultiSelect). */
|
||||||
selectedCommunitySizeIds?: string[];
|
selectedCommunitySizeIds?: string[];
|
||||||
/** Selected chip ids from `community-structure` (organization types). */
|
/** Selected chip ids from `community-structure` (organization types). */
|
||||||
selectedOrganizationTypeIds?: string[];
|
selectedOrganizationTypeIds?: string[];
|
||||||
/** Selected chip ids from `community-structure` (governance styles). */
|
/** Selected chip ids from `community-structure` (scale). */
|
||||||
selectedGovernanceStyleIds?: string[];
|
selectedScaleIds?: string[];
|
||||||
|
/** Selected chip ids from `community-structure` (maturity). */
|
||||||
|
selectedMaturityIds?: string[];
|
||||||
currentStep?: CreateFlowStep;
|
currentStep?: CreateFlowStep;
|
||||||
/** Section drafts; structure will tighten as steps persist real shapes. */
|
/** Section drafts; structure will tighten as steps persist real shapes. */
|
||||||
sections?: Record<string, unknown>[];
|
sections?: Record<string, unknown>[];
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { CreateFlowState } from "../types";
|
import type { CreateFlowState } from "../types";
|
||||||
|
import { migrateLegacyCreateFlowState } from "../../../lib/create/migrateLegacyCreateFlowState";
|
||||||
|
|
||||||
/** Anonymous in-progress create flow (local only until magic-link transfer). */
|
/** Anonymous in-progress create flow (local only until magic-link transfer). */
|
||||||
export const CREATE_FLOW_ANONYMOUS_KEY = "create-flow-anonymous" as const;
|
export const CREATE_FLOW_ANONYMOUS_KEY = "create-flow-anonymous" as const;
|
||||||
@@ -23,8 +24,10 @@ export function readAnonymousCreateFlowState(): CreateFlowState {
|
|||||||
try {
|
try {
|
||||||
const raw = window.localStorage.getItem(CREATE_FLOW_ANONYMOUS_KEY);
|
const raw = window.localStorage.getItem(CREATE_FLOW_ANONYMOUS_KEY);
|
||||||
if (!raw) return {};
|
if (!raw) return {};
|
||||||
const parsed = JSON.parse(raw) as CreateFlowState;
|
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
||||||
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
return typeof parsed === "object" && parsed !== null
|
||||||
|
? migrateLegacyCreateFlowState(parsed)
|
||||||
|
: {};
|
||||||
} catch {
|
} catch {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import type { ProportionBarState } from "../../components/progress/ProportionBar/ProportionBar.types";
|
||||||
|
import type { CreateFlowStep } from "../types";
|
||||||
|
import { FLOW_STEP_ORDER, getStepIndex } from "./flowSteps";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One `ProportionBarState` per index in `FLOW_STEP_ORDER` (same length).
|
||||||
|
* Third Create Community step (`community-structure`) uses `1-2` per Figma.
|
||||||
|
*/
|
||||||
|
const PROPORTION_BY_STEP_INDEX: readonly ProportionBarState[] = [
|
||||||
|
"1-0", // informational
|
||||||
|
"1-1", // community-name
|
||||||
|
"1-2", // community-structure
|
||||||
|
"1-3", // community-context
|
||||||
|
"1-4", // community-size
|
||||||
|
"1-5", // community-upload
|
||||||
|
"2-0", // community-save
|
||||||
|
"2-0", // review (Figma Flow — Review `19706:12135`: same segment fill as end of Create Community)
|
||||||
|
"2-2", // cards
|
||||||
|
"3-0", // right-rail
|
||||||
|
"3-1", // confirm-stakeholders
|
||||||
|
"3-2", // final-review
|
||||||
|
"3-2", // completed
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
if (PROPORTION_BY_STEP_INDEX.length !== FLOW_STEP_ORDER.length) {
|
||||||
|
throw new Error(
|
||||||
|
"createFlowProportionProgress: PROPORTION_BY_STEP_INDEX length must match FLOW_STEP_ORDER",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getProportionBarProgressForCreateFlowStep(
|
||||||
|
step: CreateFlowStep | null | undefined,
|
||||||
|
): ProportionBarState {
|
||||||
|
const idx = getStepIndex(step);
|
||||||
|
if (idx < 0) return "1-0";
|
||||||
|
return PROPORTION_BY_STEP_INDEX[idx] ?? "1-0";
|
||||||
|
}
|
||||||
@@ -35,6 +35,7 @@ export const CREATE_FLOW_SCREEN_REGISTRY: Record<
|
|||||||
CreateFlowStep,
|
CreateFlowStep,
|
||||||
CreateFlowScreenDefinition
|
CreateFlowScreenDefinition
|
||||||
> = {
|
> = {
|
||||||
|
/** Figma: Flow — Informational (node 20094-16005). */
|
||||||
informational: {
|
informational: {
|
||||||
layoutKind: "informational",
|
layoutKind: "informational",
|
||||||
figmaNodeId: "20094-16005",
|
figmaNodeId: "20094-16005",
|
||||||
@@ -49,7 +50,7 @@ export const CREATE_FLOW_SCREEN_REGISTRY: Record<
|
|||||||
},
|
},
|
||||||
"community-size": {
|
"community-size": {
|
||||||
layoutKind: "select",
|
layoutKind: "select",
|
||||||
figmaNodeId: "20094-18244",
|
figmaNodeId: "20094-41317",
|
||||||
messageNamespace: "create.communitySize",
|
messageNamespace: "create.communitySize",
|
||||||
centeredBodyBelowMd: false,
|
centeredBodyBelowMd: false,
|
||||||
},
|
},
|
||||||
@@ -61,7 +62,7 @@ export const CREATE_FLOW_SCREEN_REGISTRY: Record<
|
|||||||
},
|
},
|
||||||
"community-structure": {
|
"community-structure": {
|
||||||
layoutKind: "select",
|
layoutKind: "select",
|
||||||
figmaNodeId: "20094-41317",
|
figmaNodeId: "20094-18244",
|
||||||
messageNamespace: "create.communityStructure",
|
messageNamespace: "create.communityStructure",
|
||||||
centeredBodyBelowMd: false,
|
centeredBodyBelowMd: false,
|
||||||
},
|
},
|
||||||
@@ -71,10 +72,10 @@ export const CREATE_FLOW_SCREEN_REGISTRY: Record<
|
|||||||
messageNamespace: "create.communityUpload",
|
messageNamespace: "create.communityUpload",
|
||||||
centeredBodyBelowMd: false,
|
centeredBodyBelowMd: false,
|
||||||
},
|
},
|
||||||
"community-reflection": {
|
"community-save": {
|
||||||
layoutKind: "text",
|
layoutKind: "text",
|
||||||
figmaNodeId: "20097-14948",
|
figmaNodeId: "20097-14948",
|
||||||
messageNamespace: "create.communityReflection",
|
messageNamespace: "create.communitySave",
|
||||||
centeredBodyBelowMd: true,
|
centeredBodyBelowMd: true,
|
||||||
},
|
},
|
||||||
review: {
|
review: {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
*
|
*
|
||||||
* Single source of truth for step order and navigation helpers.
|
* Single source of truth for step order and navigation helpers.
|
||||||
* Order matches Figma Create Community (frames 1–8) then later stages.
|
* Order matches Figma Create Community (frames 1–8) then later stages.
|
||||||
|
* `community-structure` precedes `community-context` and `community-size` (Figma frame 3 vs 5 swap).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { CreateFlowStep } from "../types";
|
import type { CreateFlowStep } from "../types";
|
||||||
@@ -13,11 +14,11 @@ import type { CreateFlowStep } from "../types";
|
|||||||
export const FLOW_STEP_ORDER: readonly CreateFlowStep[] = [
|
export const FLOW_STEP_ORDER: readonly CreateFlowStep[] = [
|
||||||
"informational",
|
"informational",
|
||||||
"community-name",
|
"community-name",
|
||||||
"community-size",
|
|
||||||
"community-context",
|
|
||||||
"community-structure",
|
"community-structure",
|
||||||
|
"community-context",
|
||||||
|
"community-size",
|
||||||
"community-upload",
|
"community-upload",
|
||||||
"community-reflection",
|
"community-save",
|
||||||
"review",
|
"review",
|
||||||
"cards",
|
"cards",
|
||||||
"right-rail",
|
"right-rail",
|
||||||
|
|||||||
@@ -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)).
|
- **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).
|
- **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.).
|
- **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 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).
|
- **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-structure` 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).
|
- **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.
|
- **CI:** [`.gitea/workflows/ci.yaml`](.gitea/workflows/ci.yaml) (build, test, lint, `prisma validate`); no in-repo production deploy definition.
|
||||||
|
|
||||||
|
|||||||
+5
-6
@@ -10,7 +10,7 @@ The Figma **Create Community** sequence is the **source of truth** for the first
|
|||||||
|
|
||||||
| Stage (Figma) | Purpose (summary) | `CreateFlowStep` values (in order) |
|
| 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 Community** | Intro, naming, structure, context, size, upload, save progress (email), then community review. | `informational` → `community-name` → `community-structure` → `community-context` → `community-size` → `community-upload` → `community-save` → `review` |
|
||||||
| **Create Custom CommunityRule** | Author the CommunityRule content and structure. | `cards` → `right-rail` |
|
| **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` |
|
| **Review and complete** | Stakeholders, final card, publish, success. | `confirm-stakeholders` → `final-review` → `completed` |
|
||||||
|
|
||||||
@@ -28,11 +28,11 @@ Order is defined in code by [`FLOW_STEP_ORDER`](../app/create/utils/flowSteps.ts
|
|||||||
| ----: | ----------- | -------------------- | ---- |
|
| ----: | ----------- | -------------------- | ---- |
|
||||||
| 1 | Create Community | `informational` | `/create/informational` |
|
| 1 | Create Community | `informational` | `/create/informational` |
|
||||||
| 2 | Create Community | `community-name` | `/create/community-name` |
|
| 2 | Create Community | `community-name` | `/create/community-name` |
|
||||||
| 3 | Create Community | `community-size` | `/create/community-size` |
|
| 3 | Create Community | `community-structure` | `/create/community-structure` |
|
||||||
| 4 | Create Community | `community-context` | `/create/community-context` |
|
| 4 | Create Community | `community-context` | `/create/community-context` |
|
||||||
| 5 | Create Community | `community-structure` | `/create/community-structure` |
|
| 5 | Create Community | `community-size` | `/create/community-size` |
|
||||||
| 6 | Create Community | `community-upload` | `/create/community-upload` |
|
| 6 | Create Community | `community-upload` | `/create/community-upload` |
|
||||||
| 7 | Create Community | `community-reflection` | `/create/community-reflection` |
|
| 7 | Create Community | `community-save` | `/create/community-save` |
|
||||||
| 8 | Create Community (review frame) | `review` | `/create/review` |
|
| 8 | Create Community (review frame) | `review` | `/create/review` |
|
||||||
| 9 | Create Custom CommunityRule | `cards` | `/create/cards` |
|
| 9 | Create Custom CommunityRule | `cards` | `/create/cards` |
|
||||||
| 10 | Create Custom CommunityRule | `right-rail` | `/create/right-rail` |
|
| 10 | Create Custom CommunityRule | `right-rail` | `/create/right-rail` |
|
||||||
@@ -61,7 +61,7 @@ From that page, **Customize** currently navigates to `/create/informational?temp
|
|||||||
| Mode | Where progress lives | Save & Exit / server draft |
|
| 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)). |
|
| **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. |
|
| **Signed-in** | In-memory React state in **`CreateFlowContext`** | **Save & Exit** from the **`community-structure`** step onward (step index ≥ `community-structure`) 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.
|
Details and edge cases (conflict confirm, banners, `?syncDraft=1`) match **Ticket 4**, **Ticket 5**, and [`docs/backend-roadmap.md`](backend-roadmap.md) §12.
|
||||||
|
|
||||||
@@ -70,7 +70,6 @@ Details and edge cases (conflict confirm, banners, `?syncDraft=1`) match **Ticke
|
|||||||
## Known implementation gaps (tracked on CR-89)
|
## 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.
|
- **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.
|
- **Inner “text/select shells”:** deferred until Create Community is stable; screens use **`CreateFlowStepShell`** only for Stage 1.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
+4
-1
@@ -1,4 +1,5 @@
|
|||||||
import type { CreateFlowState } from "../../app/create/types";
|
import type { CreateFlowState } from "../../app/create/types";
|
||||||
|
import { migrateLegacyCreateFlowState } from "./migrateLegacyCreateFlowState";
|
||||||
|
|
||||||
const jsonHeaders = { "Content-Type": "application/json" };
|
const jsonHeaders = { "Content-Type": "application/json" };
|
||||||
|
|
||||||
@@ -77,7 +78,9 @@ export async function fetchDraftFromServer(): Promise<CreateFlowState | null> {
|
|||||||
if (!data.draft?.payload || typeof data.draft.payload !== "object") {
|
if (!data.draft?.payload || typeof data.draft.payload !== "object") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return data.draft.payload as CreateFlowState;
|
return migrateLegacyCreateFlowState(
|
||||||
|
data.draft.payload as Record<string, unknown>,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const DRAFT_SAVE_NETWORK_ERROR =
|
const DRAFT_SAVE_NETWORK_ERROR =
|
||||||
|
|||||||
@@ -59,11 +59,7 @@ export function buildPublishPayload(
|
|||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
let summary = firstNonEmpty(
|
let summary = firstNonEmpty(state.summary, state.communityContext);
|
||||||
state.summary,
|
|
||||||
state.communityContext,
|
|
||||||
state.communityReflection,
|
|
||||||
);
|
|
||||||
|
|
||||||
let sections = parseSectionsFromCreateFlowState(state);
|
let sections = parseSectionsFromCreateFlowState(state);
|
||||||
if (sections.length === 0) {
|
if (sections.length === 0) {
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
const EMAIL_MAX_LEN = 254;
|
||||||
|
|
||||||
|
/** Pragmatic check for the create-flow “save progress” email field (draft + footer enablement). */
|
||||||
|
export function isValidCreateFlowSaveEmail(value: unknown): boolean {
|
||||||
|
if (typeof value !== "string") return false;
|
||||||
|
const t = value.trim();
|
||||||
|
if (t.length === 0 || t.length > EMAIL_MAX_LEN) return false;
|
||||||
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(t);
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import type { CreateFlowState } from "../../app/create/types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps pre-rename draft keys and step ids (`community-reflection` → `community-save`).
|
||||||
|
* Safe to run on any parsed draft payload before merging into context.
|
||||||
|
*/
|
||||||
|
export function migrateLegacyCreateFlowState(
|
||||||
|
raw: Record<string, unknown> | null | undefined,
|
||||||
|
): CreateFlowState {
|
||||||
|
if (!raw || typeof raw !== "object") return {};
|
||||||
|
const next: Record<string, unknown> = { ...raw };
|
||||||
|
if (typeof next.communityReflection === "string") {
|
||||||
|
if (
|
||||||
|
next.communitySaveEmail === undefined ||
|
||||||
|
next.communitySaveEmail === ""
|
||||||
|
) {
|
||||||
|
next.communitySaveEmail = next.communityReflection;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete next.communityReflection;
|
||||||
|
if (next.currentStep === "community-reflection") {
|
||||||
|
next.currentStep = "community-save";
|
||||||
|
}
|
||||||
|
return next as CreateFlowState;
|
||||||
|
}
|
||||||
@@ -852,3 +852,27 @@ export type ButtonStateValue =
|
|||||||
| "Active"
|
| "Active"
|
||||||
| "Hover"
|
| "Hover"
|
||||||
| "Disabled";
|
| "Disabled";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ProportionBar layout variant (Figma uses a segmented track in the create-flow footer).
|
||||||
|
*/
|
||||||
|
export type ProportionBarVariantValue =
|
||||||
|
| "default"
|
||||||
|
| "segmented"
|
||||||
|
| "Default"
|
||||||
|
| "Segmented";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize ProportionBar variant (Figma PascalCase vs codebase lowercase).
|
||||||
|
*/
|
||||||
|
export function normalizeProportionBarVariant(
|
||||||
|
value: string | undefined,
|
||||||
|
defaultValue: "default" | "segmented" = "default",
|
||||||
|
): "default" | "segmented" {
|
||||||
|
if (!value) return defaultValue;
|
||||||
|
const normalized = value.toLowerCase();
|
||||||
|
if (normalized === "default" || normalized === "segmented") {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,11 +29,12 @@ export const createFlowStateSchema = z
|
|||||||
.object({
|
.object({
|
||||||
title: z.string().max(500).optional(),
|
title: z.string().max(500).optional(),
|
||||||
summary: z.string().max(8000).optional(),
|
summary: z.string().max(8000).optional(),
|
||||||
communityContext: z.string().max(8000).optional(),
|
communityContext: z.string().max(48).optional(),
|
||||||
communityReflection: z.string().max(8000).optional(),
|
communitySaveEmail: z.string().max(320).optional(),
|
||||||
selectedCommunitySizeIds: z.array(z.string()).optional(),
|
selectedCommunitySizeIds: z.array(z.string()).optional(),
|
||||||
selectedOrganizationTypeIds: z.array(z.string()).optional(),
|
selectedOrganizationTypeIds: z.array(z.string()).optional(),
|
||||||
selectedGovernanceStyleIds: z.array(z.string()).optional(),
|
selectedScaleIds: z.array(z.string()).optional(),
|
||||||
|
selectedMaturityIds: z.array(z.string()).optional(),
|
||||||
currentStep: createFlowStepSchema.optional(),
|
currentStep: createFlowStepSchema.optional(),
|
||||||
sections: z.array(z.unknown()).optional(),
|
sections: z.array(z.unknown()).optional(),
|
||||||
stakeholders: z.array(z.unknown()).optional(),
|
stakeholders: z.array(z.unknown()).optional(),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"title": "Tell us more about your community",
|
"title": "Why does your community exist?",
|
||||||
"description": "Share context that will help shape your CommunityRule.",
|
"description": "Edit or change the description to match how you’d like the organization to be described to other users. Try and describe your mission, goals, and scope.",
|
||||||
"placeholder": "Describe your community",
|
"placeholder": "Describe your community",
|
||||||
"characterCountTemplate": "{current}/{max}"
|
"characterCountTemplate": "{current}/{max}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"title": "What is your community called?",
|
"title": "What is your community called?",
|
||||||
"description": "This will be the name of your community",
|
"description": "This will be the name of your community",
|
||||||
"placeholder": "Enter your community name",
|
"placeholder": "Enter community name",
|
||||||
"characterCountTemplate": "{current}/{max}"
|
"characterCountTemplate": "{current}/{max}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"title": "Anything else we should know?",
|
|
||||||
"description": "Optional details before you review your progress.",
|
|
||||||
"placeholder": "Add notes (optional)",
|
|
||||||
"characterCountTemplate": "{current}/{max}"
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"title": "Save your progress",
|
||||||
|
"description": "We need your email to save your CommunityRule progress\nand make it accessible to you later.",
|
||||||
|
"placeholder": "email@domain.com",
|
||||||
|
"characterCountTemplate": "{current}/{max}",
|
||||||
|
"magicLinkSuccessTitle": "Check your email to log in!",
|
||||||
|
"magicLinkSuccessDescription": "Your account is created, now just check your email for a magic link",
|
||||||
|
"magicLinkErrorTitle": "Could not send link"
|
||||||
|
}
|
||||||
@@ -1,19 +1,13 @@
|
|||||||
{
|
{
|
||||||
"header": {
|
"header": {
|
||||||
"title": "How large is your community?",
|
"title": "How many people will be in your community in the near term?",
|
||||||
"description": "Choose the size that best matches your group."
|
"description": "Choose how many people you think will be in your community in the next year or two. Your selection here will determine what governance patterns are recommended later in the process."
|
||||||
},
|
|
||||||
"multiSelect": {
|
|
||||||
"label": "Label",
|
|
||||||
"addButtonText": "Add organization type"
|
|
||||||
},
|
},
|
||||||
"communitySizes": [
|
"communitySizes": [
|
||||||
{ "label": "1 member" },
|
{ "label": "1 member" },
|
||||||
{ "label": "2-10 members" },
|
{ "label": "2-5 members" },
|
||||||
{ "label": "10-24 members" },
|
{ "label": "6-12 members" },
|
||||||
{ "label": "24-64 members" },
|
{ "label": "13-100 members" },
|
||||||
{ "label": "64-128 members" },
|
{ "label": "100-100,000 members" }
|
||||||
{ "label": "125-1000 members" },
|
|
||||||
{ "label": "1000+ members" }
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,38 @@
|
|||||||
{
|
{
|
||||||
"header": {
|
"header": {
|
||||||
"title": "How is your community organized?",
|
"title": "What kind of community would you like to improve?",
|
||||||
"description": "Select the options that best describe your group."
|
"description": "Choose tags the describe your community. You can also combine or add new values to the list."
|
||||||
},
|
},
|
||||||
"multiSelect": {
|
"organizationMultiSelect": {
|
||||||
"label": "Label",
|
"label": "Organization Type",
|
||||||
"addButtonText": "Add organization type"
|
"addButtonText": "Add organization type"
|
||||||
},
|
},
|
||||||
|
"scaleMultiSelect": {
|
||||||
|
"label": "Scale",
|
||||||
|
"addButtonText": "Add scale"
|
||||||
|
},
|
||||||
|
"maturityMultiSelect": {
|
||||||
|
"label": "Maturity",
|
||||||
|
"addButtonText": "Add maturity"
|
||||||
|
},
|
||||||
"organizationTypes": [
|
"organizationTypes": [
|
||||||
{ "label": "Non-profit" },
|
{ "label": "Worker’s coop" },
|
||||||
{ "label": "For-profit" },
|
{ "label": "Mutual aid" },
|
||||||
{ "label": "Community" },
|
{ "label": "Open source project" },
|
||||||
{ "label": "Educational" }
|
{ "label": "Nonprofit" },
|
||||||
|
{ "label": "For profit business" },
|
||||||
|
{ "label": "DAO" }
|
||||||
],
|
],
|
||||||
"governanceStyles": [
|
"scaleOptions": [
|
||||||
{ "label": "Democratic" },
|
{ "label": "Local" },
|
||||||
{ "label": "Consensus" },
|
{ "label": "Regional" },
|
||||||
{ "label": "Hierarchical" },
|
{ "label": "National" },
|
||||||
{ "label": "Flat" }
|
{ "label": "Global" }
|
||||||
|
],
|
||||||
|
"maturityOptions": [
|
||||||
|
{ "label": "Early stage" },
|
||||||
|
{ "label": "Growth stage" },
|
||||||
|
{ "label": "Established" },
|
||||||
|
{ "label": "Enterprise" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
"title": "How should conflicts be resolved?",
|
"title": "Add a photo to identify your group",
|
||||||
"description": "Upload supporting materials or examples that help describe how your community handles conflict."
|
"description": "This photo be used as a profile picture for your group and will be editable later. If possible, try to use a simple logo or graphic.",
|
||||||
|
"hintText": "Add image from your device"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
{
|
{
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
|
"saveLater": "Save Later",
|
||||||
|
"submitEmail": "Submit Email",
|
||||||
|
"submitEmailSending": "Sending link…",
|
||||||
|
"createCustom": "Create custom",
|
||||||
|
"createFromTemplate": "Create from template",
|
||||||
|
"confirmName": "Confirm name",
|
||||||
|
"confirmDetails": "Confirm details",
|
||||||
|
"confirmDescription": "Confirm description",
|
||||||
|
"confirmMembers": "Confirm members",
|
||||||
"finalizeCommunityRule": "Finalize CommunityRule",
|
"finalizeCommunityRule": "Finalize CommunityRule",
|
||||||
"confirmStakeholders": "Confirm Stakeholders"
|
"confirmStakeholders": "Confirm Stakeholders"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
{
|
{
|
||||||
"title": "How CommunityRule helps groups like yours",
|
"title": "How CommunityRule helps groups like yours",
|
||||||
"description": "This flow will give you recommendations to improve your community and help you put together a proposal for your group to consider. Alternatively, there is a workshop that your group can use to go through the process it together.",
|
"descriptionLead": "This flow will give you recommendations to improve your community and help you put together a proposal for your group to consider. Alternatively, there is a",
|
||||||
|
"workshopLabel": "workshop",
|
||||||
|
"descriptionTrail": "that your group can use to go through the process it together.",
|
||||||
"steps": {
|
"steps": {
|
||||||
"0": {
|
"0": {
|
||||||
"title": "Tell us about your organization",
|
"title": "Tell us about your organization",
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import createCommunitySize from "./create/communitySize.json";
|
|||||||
import createCommunityContext from "./create/communityContext.json";
|
import createCommunityContext from "./create/communityContext.json";
|
||||||
import createCommunityStructure from "./create/communityStructure.json";
|
import createCommunityStructure from "./create/communityStructure.json";
|
||||||
import createCommunityUpload from "./create/communityUpload.json";
|
import createCommunityUpload from "./create/communityUpload.json";
|
||||||
import createCommunityReflection from "./create/communityReflection.json";
|
import createCommunitySave from "./create/communitySave.json";
|
||||||
import createReview from "./create/review.json";
|
import createReview from "./create/review.json";
|
||||||
import createConfirmStakeholders from "./create/confirmStakeholders.json";
|
import createConfirmStakeholders from "./create/confirmStakeholders.json";
|
||||||
import createFinalReview from "./create/finalReview.json";
|
import createFinalReview from "./create/finalReview.json";
|
||||||
@@ -66,7 +66,7 @@ export default {
|
|||||||
communityContext: createCommunityContext,
|
communityContext: createCommunityContext,
|
||||||
communityStructure: createCommunityStructure,
|
communityStructure: createCommunityStructure,
|
||||||
communityUpload: createCommunityUpload,
|
communityUpload: createCommunityUpload,
|
||||||
communityReflection: createCommunityReflection,
|
communitySave: createCommunitySave,
|
||||||
review: createReview,
|
review: createReview,
|
||||||
confirmStakeholders: createConfirmStakeholders,
|
confirmStakeholders: createConfirmStakeholders,
|
||||||
finalReview: createFinalReview,
|
finalReview: createFinalReview,
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
argTypes: {
|
argTypes: {
|
||||||
|
variant: {
|
||||||
|
control: { type: "select" },
|
||||||
|
options: ["default", "segmented", "Default", "Segmented"],
|
||||||
|
description:
|
||||||
|
"Segmented: pill-shaped partial fills (create-flow footer / Figma).",
|
||||||
|
},
|
||||||
progress: {
|
progress: {
|
||||||
control: { type: "select" },
|
control: { type: "select" },
|
||||||
options: [
|
options: [
|
||||||
@@ -46,6 +52,27 @@ export const Default = {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const SegmentedCreateFlow = {
|
||||||
|
args: {
|
||||||
|
progress: "1-1",
|
||||||
|
variant: "segmented",
|
||||||
|
},
|
||||||
|
render: (args) => (
|
||||||
|
<div className="w-full max-w-[640px] bg-black p-4">
|
||||||
|
<ProportionBar {...args} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story:
|
||||||
|
"Matches the create-flow footer: three segments with partial fill in the first segment (`1-1` on community name).",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
backgrounds: { default: "dark" },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const AllStates = {
|
export const AllStates = {
|
||||||
args: {},
|
args: {},
|
||||||
render: (_args) => (
|
render: (_args) => (
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ function LoginTrigger() {
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
openLogin({
|
openLogin({
|
||||||
variant: "saveProgress",
|
variant: "saveProgress",
|
||||||
nextPath: "/create/community-size?syncDraft=1",
|
nextPath: "/create/community-structure?syncDraft=1",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -143,7 +143,7 @@ describe("AuthModalProvider (header overlay)", () => {
|
|||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(requestMagicLink).toHaveBeenCalledWith(
|
expect(requestMagicLink).toHaveBeenCalledWith(
|
||||||
"guest@example.com",
|
"guest@example.com",
|
||||||
"/create/community-size?syncDraft=1",
|
"/create/community-structure?syncDraft=1",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
expect(setTransferPendingFlag).toHaveBeenCalled();
|
expect(setTransferPendingFlag).toHaveBeenCalled();
|
||||||
|
|||||||
@@ -48,6 +48,22 @@ describe("CreateFlowFooter (behavioral tests)", () => {
|
|||||||
name: "Create Flow Footer",
|
name: "Create Flow Footer",
|
||||||
});
|
});
|
||||||
expect(footer).toBeInTheDocument();
|
expect(footer).toBeInTheDocument();
|
||||||
|
const bar = screen.getByRole("progressbar");
|
||||||
|
expect(bar).toHaveAttribute("aria-valuenow", String(1 / 6));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes proportionBarProgress to the progress bar", () => {
|
||||||
|
render(
|
||||||
|
<CreateFlowFooter
|
||||||
|
progressBar={true}
|
||||||
|
proportionBarProgress="1-1"
|
||||||
|
proportionBarVariant="segmented"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(screen.getByRole("progressbar")).toHaveAttribute(
|
||||||
|
"aria-valuenow",
|
||||||
|
String(2 / 6),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not render progress bar when progressBar is false", () => {
|
it("does not render progress bar when progressBar is false", () => {
|
||||||
|
|||||||
@@ -49,6 +49,22 @@ describe("HeaderLockup (behavioral tests)", () => {
|
|||||||
expect(screen.getByText("Test description")).toBeInTheDocument();
|
expect(screen.getByText("Test description")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders ReactNode description (rich inline)", () => {
|
||||||
|
render(
|
||||||
|
<HeaderLockup
|
||||||
|
title="Test Title"
|
||||||
|
description={
|
||||||
|
<>
|
||||||
|
Before <span className="underline">link</span> after
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(screen.getByText(/Before/)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("link")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/after/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it("does not render description when not provided", () => {
|
it("does not render description when not provided", () => {
|
||||||
const { container } = render(<HeaderLockup title="Test Title" />);
|
const { container } = render(<HeaderLockup title="Test Title" />);
|
||||||
const description = container.querySelector("p");
|
const description = container.querySelector("p");
|
||||||
|
|||||||
@@ -22,6 +22,13 @@ describe("InformationalScreen", () => {
|
|||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders workshop as a link (URL TBD) with underline per Figma", () => {
|
||||||
|
render(<InformationalScreen />);
|
||||||
|
const workshop = screen.getByRole("link", { name: "workshop" });
|
||||||
|
expect(workshop).toHaveAttribute("href", "#");
|
||||||
|
expect(workshop.className).toMatch(/underline/);
|
||||||
|
});
|
||||||
|
|
||||||
it("renders first numbered list item title", () => {
|
it("renders first numbered list item title", () => {
|
||||||
render(<InformationalScreen />);
|
render(<InformationalScreen />);
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ describe("LoginForm", () => {
|
|||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<LoginForm
|
<LoginForm
|
||||||
variant="saveProgress"
|
variant="saveProgress"
|
||||||
magicLinkNextPath="/create/community-size?syncDraft=1"
|
magicLinkNextPath="/create/community-structure?syncDraft=1"
|
||||||
/>
|
/>
|
||||||
</Suspense>,
|
</Suspense>,
|
||||||
);
|
);
|
||||||
@@ -133,7 +133,7 @@ describe("LoginForm", () => {
|
|||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(requestMagicLink).toHaveBeenCalledWith(
|
expect(requestMagicLink).toHaveBeenCalledWith(
|
||||||
"save@example.com",
|
"save@example.com",
|
||||||
"/create/community-size?syncDraft=1",
|
"/create/community-structure?syncDraft=1",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
expect(setTransferPendingFlag).toHaveBeenCalled();
|
expect(setTransferPendingFlag).toHaveBeenCalled();
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const config: ComponentTestSuiteConfig<ProportionBarProps> = {
|
|||||||
optionalProps: {
|
optionalProps: {
|
||||||
progress: "3-2",
|
progress: "3-2",
|
||||||
className: "custom-class",
|
className: "custom-class",
|
||||||
|
variant: "segmented",
|
||||||
},
|
},
|
||||||
primaryRole: "progressbar",
|
primaryRole: "progressbar",
|
||||||
testCases: {
|
testCases: {
|
||||||
|
|||||||
@@ -8,22 +8,14 @@ describe("CommunitySizeSelectScreen", () => {
|
|||||||
render(<CommunitySizeSelectScreen />);
|
render(<CommunitySizeSelectScreen />);
|
||||||
expect(
|
expect(
|
||||||
screen.getByRole("heading", {
|
screen.getByRole("heading", {
|
||||||
name: "How large is your community?",
|
name: "How many people will be in your community in the near term?",
|
||||||
}),
|
}),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders MultiSelect add control", () => {
|
it("renders preset size chips", () => {
|
||||||
render(<CommunitySizeSelectScreen />);
|
|
||||||
const addButtons = screen.getAllByRole("button", {
|
|
||||||
name: "Add organization type",
|
|
||||||
});
|
|
||||||
expect(addButtons.length).toBeGreaterThanOrEqual(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders preset chip labels", () => {
|
|
||||||
render(<CommunitySizeSelectScreen />);
|
render(<CommunitySizeSelectScreen />);
|
||||||
expect(screen.getByText("1 member")).toBeInTheDocument();
|
expect(screen.getByText("1 member")).toBeInTheDocument();
|
||||||
expect(screen.getByText("2-10 members")).toBeInTheDocument();
|
expect(screen.getByText("2-5 members")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ describe("CreateFlowTextFieldScreen (community name)", () => {
|
|||||||
screen.getByText("This will be the name of your community"),
|
screen.getByText("This will be the name of your community"),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
expect(
|
expect(
|
||||||
screen.getByPlaceholderText("Enter your community name"),
|
screen.getByPlaceholderText("Enter community name"),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ componentTestSuite<UploadProps>({
|
|||||||
label: "Upload",
|
label: "Upload",
|
||||||
active: true,
|
active: true,
|
||||||
showHelpIcon: true,
|
showHelpIcon: true,
|
||||||
|
hintText: "Add image from your device",
|
||||||
},
|
},
|
||||||
primaryRole: "button",
|
primaryRole: "button",
|
||||||
testCases: {
|
testCases: {
|
||||||
@@ -81,14 +82,14 @@ describe("Upload (behavioral tests)", () => {
|
|||||||
it("displays description text", () => {
|
it("displays description text", () => {
|
||||||
render(<Upload label="Upload" />);
|
render(<Upload label="Upload" />);
|
||||||
expect(
|
expect(
|
||||||
screen.getByText(/Add images, PDFs, and other files to the policy/i),
|
screen.getByText(/Add image from your device/i),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("applies active state styles correctly", () => {
|
it("applies active state styles correctly", () => {
|
||||||
render(<Upload label="Upload" active={true} />);
|
render(<Upload label="Upload" active={true} />);
|
||||||
const descriptionText = screen.getByText(
|
const descriptionText = screen.getByText(
|
||||||
/Add images, PDFs, and other files to the policy/i,
|
/Add image from your device/i,
|
||||||
);
|
);
|
||||||
const descriptionContainer = descriptionText.parentElement;
|
const descriptionContainer = descriptionText.parentElement;
|
||||||
expect(descriptionContainer).toHaveClass(
|
expect(descriptionContainer).toHaveClass(
|
||||||
@@ -99,7 +100,7 @@ describe("Upload (behavioral tests)", () => {
|
|||||||
it("applies inactive state styles correctly", () => {
|
it("applies inactive state styles correctly", () => {
|
||||||
render(<Upload label="Upload" active={false} />);
|
render(<Upload label="Upload" active={false} />);
|
||||||
const descriptionText = screen.getByText(
|
const descriptionText = screen.getByText(
|
||||||
/Add images, PDFs, and other files to the policy/i,
|
/Add image from your device/i,
|
||||||
);
|
);
|
||||||
const descriptionContainer = descriptionText.parentElement;
|
const descriptionContainer = descriptionText.parentElement;
|
||||||
expect(descriptionContainer).toHaveClass(
|
expect(descriptionContainer).toHaveClass(
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ describe("CommunityUploadScreen", () => {
|
|||||||
render(<CommunityUploadScreen />);
|
render(<CommunityUploadScreen />);
|
||||||
expect(
|
expect(
|
||||||
screen.getByRole("heading", {
|
screen.getByRole("heading", {
|
||||||
name: "How should conflicts be resolved?",
|
name: "Add a photo to identify your group",
|
||||||
}),
|
}),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
@@ -17,7 +17,9 @@ describe("CommunityUploadScreen", () => {
|
|||||||
render(<CommunityUploadScreen />);
|
render(<CommunityUploadScreen />);
|
||||||
expect(screen.getByRole("button", { name: "Upload" })).toBeInTheDocument();
|
expect(screen.getByRole("button", { name: "Upload" })).toBeInTheDocument();
|
||||||
expect(
|
expect(
|
||||||
screen.getByText(/Add images, PDFs, and other files to the policy/i),
|
screen.getByText(
|
||||||
|
/This photo be used as a profile picture for your group/i,
|
||||||
|
),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import {
|
||||||
|
CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS,
|
||||||
|
CREATE_FLOW_MD_UP_GRID_CELL_CLASS,
|
||||||
|
CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS,
|
||||||
|
} from "../../app/create/components/createFlowLayoutTokens";
|
||||||
|
|
||||||
|
describe("createFlowLayoutTokens", () => {
|
||||||
|
it("exports create-flow column and two-column max class strings", () => {
|
||||||
|
expect(CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS).toBe(
|
||||||
|
"w-full min-w-0 md:max-w-[640px]",
|
||||||
|
);
|
||||||
|
expect(CREATE_FLOW_MD_UP_GRID_CELL_CLASS).toBe(
|
||||||
|
"w-full min-w-0 md:mx-auto md:max-w-[640px]",
|
||||||
|
);
|
||||||
|
expect(CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS).toBe("md:max-w-[1328px]");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { getProportionBarProgressForCreateFlowStep } from "../../app/create/utils/createFlowProportionProgress";
|
||||||
|
|
||||||
|
describe("getProportionBarProgressForCreateFlowStep", () => {
|
||||||
|
it("uses 1-2 on community-structure (third Create Community step)", () => {
|
||||||
|
expect(getProportionBarProgressForCreateFlowStep("community-structure")).toBe(
|
||||||
|
"1-2",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("advances proportion after structure for context and size", () => {
|
||||||
|
expect(getProportionBarProgressForCreateFlowStep("community-context")).toBe(
|
||||||
|
"1-3",
|
||||||
|
);
|
||||||
|
expect(getProportionBarProgressForCreateFlowStep("community-size")).toBe(
|
||||||
|
"1-4",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses 2-0 on community-save and review (end of Create Community segment)", () => {
|
||||||
|
expect(getProportionBarProgressForCreateFlowStep("community-save")).toBe(
|
||||||
|
"2-0",
|
||||||
|
);
|
||||||
|
expect(getProportionBarProgressForCreateFlowStep("review")).toBe("2-0");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -71,6 +71,13 @@ describe("createFlowStateSchema", () => {
|
|||||||
const r = createFlowStateSchema.safeParse({ title: "x".repeat(600) });
|
const r = createFlowStateSchema.safeParse({ title: "x".repeat(600) });
|
||||||
expect(r.success).toBe(false);
|
expect(r.success).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("rejects communitySaveEmail longer than 320 chars", () => {
|
||||||
|
const r = createFlowStateSchema.safeParse({
|
||||||
|
communitySaveEmail: "x".repeat(321),
|
||||||
|
});
|
||||||
|
expect(r.success).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("putDraftBodySchema", () => {
|
describe("putDraftBodySchema", () => {
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import messages from "../../messages/en/index";
|
||||||
|
|
||||||
|
describe("create footer messages", () => {
|
||||||
|
it("exposes confirmName for the community-name footer CTA", () => {
|
||||||
|
expect(messages.create.footer.confirmName).toBe("Confirm name");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("exposes confirmDetails for the community-structure footer CTA", () => {
|
||||||
|
expect(messages.create.footer.confirmDetails).toBe("Confirm details");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("exposes confirmDescription for the community-context footer CTA", () => {
|
||||||
|
expect(messages.create.footer.confirmDescription).toBe(
|
||||||
|
"Confirm description",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("exposes confirmMembers for the community-size footer CTA", () => {
|
||||||
|
expect(messages.create.footer.confirmMembers).toBe("Confirm members");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -8,6 +8,8 @@ describe("createFlowStateHasKeys", () => {
|
|||||||
|
|
||||||
it("returns true when any key is present", () => {
|
it("returns true when any key is present", () => {
|
||||||
expect(createFlowStateHasKeys({ title: "x" })).toBe(true);
|
expect(createFlowStateHasKeys({ title: "x" })).toBe(true);
|
||||||
expect(createFlowStateHasKeys({ currentStep: "text" })).toBe(true);
|
expect(createFlowStateHasKeys({ currentStep: "community-name" })).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -46,4 +46,13 @@ describe("flowSteps", () => {
|
|||||||
// @ts-expect-error — invalid step id
|
// @ts-expect-error — invalid step id
|
||||||
expect(getStepIndex("bogus")).toBe(-1);
|
expect(getStepIndex("bogus")).toBe(-1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("places community-structure before community-context and community-size (Figma order)", () => {
|
||||||
|
expect(getStepIndex("community-structure")).toBe(2);
|
||||||
|
expect(getStepIndex("community-context")).toBe(3);
|
||||||
|
expect(getStepIndex("community-size")).toBe(4);
|
||||||
|
expect(getNextStep("community-name")).toBe("community-structure");
|
||||||
|
expect(getNextStep("community-structure")).toBe("community-context");
|
||||||
|
expect(getNextStep("community-context")).toBe("community-size");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { migrateLegacyCreateFlowState } from "../../lib/create/migrateLegacyCreateFlowState";
|
||||||
|
|
||||||
|
describe("migrateLegacyCreateFlowState", () => {
|
||||||
|
it("maps communityReflection to communitySaveEmail when save email empty", () => {
|
||||||
|
const out = migrateLegacyCreateFlowState({
|
||||||
|
title: "T",
|
||||||
|
communityReflection: "old@example.com",
|
||||||
|
});
|
||||||
|
expect(out.communitySaveEmail).toBe("old@example.com");
|
||||||
|
expect("communityReflection" in out).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not overwrite existing communitySaveEmail", () => {
|
||||||
|
const out = migrateLegacyCreateFlowState({
|
||||||
|
communityReflection: "old@example.com",
|
||||||
|
communitySaveEmail: "kept@example.com",
|
||||||
|
});
|
||||||
|
expect(out.communitySaveEmail).toBe("kept@example.com");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rewrites currentStep slug", () => {
|
||||||
|
const out = migrateLegacyCreateFlowState({
|
||||||
|
currentStep: "community-reflection",
|
||||||
|
});
|
||||||
|
expect(out.currentStep).toBe("community-save");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns empty object for nullish input", () => {
|
||||||
|
expect(migrateLegacyCreateFlowState(null)).toEqual({});
|
||||||
|
expect(migrateLegacyCreateFlowState(undefined)).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user