Completed template

This commit is contained in:
adilallo
2026-03-02 22:12:50 -07:00
parent d811b87b12
commit 3e3d2881f5
103 changed files with 1410 additions and 622 deletions
+1 -1
View File
@@ -25,7 +25,7 @@ const VALID_STEPS: CreateFlowStep[] = [
/**
* Dynamic route handler for create flow steps
*
*
* Handles all flow steps via dynamic routing: /create/[step]
* Validates step exists and renders appropriate template (placeholder for now)
*/
+196
View File
@@ -0,0 +1,196 @@
"use client";
import { useState, useEffect } from "react";
import { useMediaQuery } from "../../hooks/useMediaQuery";
import HeaderLockup from "../../components/type/HeaderLockup";
import CommunityRuleDocument from "../../components/sections/CommunityRuleDocument";
import type { CommunityRuleDocumentSection } from "../../components/sections/CommunityRuleDocument/CommunityRuleDocument.types";
import Alert from "../../components/modals/Alert";
const TITLE = "Mutual Aid Mondays";
const DESCRIPTION =
"Mutual Aid Monday is a grassroots community in Denver, founded in November 2020 by Kelsang Virya, dedicated to supporting neighbors experiencing homelessness.";
const TOAST_TITLE = "This is what folks see when you share your CommunityRule";
const TOAST_DESCRIPTION =
"Your group can use this document as an operating manual.";
const SOLIDARITY_BODY =
"Food Not Bombs is not a charity. It is a project of solidarity. Charity is vertical. It moves from those who have to those who have not and maintains the hierarchy between them. Solidarity is horizontal. It moves between equals who recognize that our liberation is bound together. We do not help the poor. We share resources among community members because access to food is a human right rather than a privilege of wealth.";
/** Static sections for the completed Community Rule document (placeholder data). */
const COMPLETED_RULE_SECTIONS: CommunityRuleDocumentSection[] = [
{
categoryName: "Values",
entries: [
{ title: "Solidarity Forever", body: SOLIDARITY_BODY },
{
title: "Shared Leadership",
body: "We operate without bosses or managers. This does not mean we are disorganized. It means we are self-organized. Authority in this chapter is temporary and task-specific rather than permanent or personal. We believe the people doing the work should make the decisions about that work. By distributing responsibility we prevent burnout and ensure the movement survives beyond any single leader.",
},
{
title: "Organizing Offline",
body: "We use digital tools to coordinate but we build power in the physical world. An algorithm cannot cook a meal and a group chat cannot look someone in the eye. We prioritize face-to-face connection and resist the pull of digital metrics.",
},
{
title: "Circular Food Systems",
body: "We intervene in the ecological crisis by addressing food waste and food recovery. We redirect surplus food to where it is needed and model a circular economy at the scale of our communities.",
},
],
},
{
categoryName: "Communication",
entries: [
{
title: "Signal",
body: "We use Signal for sensitive coordination. Encrypted messaging helps protect our members and our plans from surveillance.",
},
],
},
{
categoryName: "Membership",
entries: [
{
title: "Open Admission",
body: "Anyone who shares our values and is willing to contribute is welcome. We do not require applications or approval processes for general participation.",
},
],
},
{
categoryName: "Decision-making",
entries: [
{
title: "Lazy Consensus",
body: "We use lazy consensus for most decisions: proposals move forward unless someone raises a blocking concern. This keeps us moving without requiring everyone to approve every detail.",
},
{
title: "Modified Consensus",
body: "For larger or more consequential decisions we use modified consensus, with clear timelines and a fallback to a supermajority vote if needed.",
},
],
},
{
categoryName: "Conflict management",
entries: [
{
title: "Code of Conduct",
body: "We have a code of conduct that sets expectations for behavior and outlines how we address harm.",
},
{
title: "Restorative Justice",
body: "When conflict arises we prioritize restoration and learning over punishment. We use facilitated circles and other restorative practices where appropriate.",
},
],
},
];
/**
* Completed create flow page.
* Figma: 20907-213286 (main), 18002-28017 (toast).
*/
export default function CompletedPage() {
const [isMounted, setIsMounted] = useState(false);
const [toastDismissed, setToastDismissed] = useState(false);
const isMdOrLarger = useMediaQuery("(min-width: 640px)");
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect -- intentional: defer layout breakpoint until after mount to prevent flash
setIsMounted(true);
}, []);
const showDesktopLayout = !isMounted || isMdOrLarger;
if (showDesktopLayout) {
return (
<div className="flex h-full min-h-0 w-full flex-1 flex-col overflow-hidden">
<div className="flex min-h-0 flex-1 overflow-hidden bg-[var(--color-teal-teal50,#c9fef9)] px-5 md:px-12">
<div className="grid h-full max-w-[1280px] grid-cols-2 shrink-0 gap-[var(--measures-spacing-1200,48px)] min-h-0 min-w-0 w-full">
{/* Left column: community title + header, centered, does not scroll */}
<div className="flex min-w-0 flex-col justify-center overflow-hidden py-8">
<HeaderLockup
title={TITLE}
description={DESCRIPTION}
justification="left"
size="L"
palette="inverse"
/>
</div>
{/* Right column: Community Rule document — this column scrolls independently; padding inside scroll so content isn't clipped */}
<div className="scrollbar-hide relative flex min-h-0 min-w-0 flex-col overflow-x-hidden overflow-y-auto">
{/* Soft fade at top: gradient wash only (no blur) so no sharp cutoff line */}
<div
className="sticky top-0 z-10 h-5 shrink-0 pointer-events-none bg-gradient-to-b from-[var(--color-teal-teal50,#c9fef9)]/55 from-0% via-[var(--color-teal-teal50,#c9fef9)]/20 via-50% to-transparent"
aria-hidden
/>
<div className="py-8 min-w-0">
<CommunityRuleDocument
sections={COMPLETED_RULE_SECTIONS}
className="min-w-0"
/>
</div>
</div>
</div>
</div>
{!toastDismissed && (
<div
className="fixed bottom-0 left-0 right-0 z-10 w-full"
role="status"
aria-live="polite"
>
<Alert
type="toast"
status="default"
title={TOAST_TITLE}
description={TOAST_DESCRIPTION}
hasLeadingIcon
hasBodyText
onClose={() => setToastDismissed(true)}
className="w-full"
/>
</div>
)}
</div>
);
}
return (
<>
<div className="w-full flex flex-col items-center px-5 min-w-0 bg-[var(--color-teal-teal50,#c9fef9)] py-8">
<div className="flex flex-col gap-4 w-full max-w-[639px]">
<HeaderLockup
title={TITLE}
description={DESCRIPTION}
justification="left"
size="M"
palette="inverse"
/>
<CommunityRuleDocument
sections={COMPLETED_RULE_SECTIONS}
useCardStyle
className="w-full p-4"
/>
</div>
</div>
{!toastDismissed && (
<div
className="fixed bottom-0 left-0 right-0 z-10 w-full"
role="status"
aria-live="polite"
>
<Alert
type="toast"
status="default"
title={TOAST_TITLE}
description={TOAST_DESCRIPTION}
hasLeadingIcon
hasBodyText
onClose={() => setToastDismissed(true)}
className="w-full"
/>
</div>
)}
</>
);
}
+3 -5
View File
@@ -16,7 +16,7 @@ interface CreateFlowProviderProps {
/**
* Provider component for Create Flow state management
*
*
* This is a basic implementation that will be expanded in CR-56
* with full navigation logic, state persistence, and validation.
*/
@@ -25,9 +25,7 @@ export function CreateFlowProvider({
initialStep = null,
}: CreateFlowProviderProps) {
const [state, setState] = useState<CreateFlowState>({});
const [currentStep] = useState<CreateFlowStep | null>(
initialStep,
);
const [currentStep] = useState<CreateFlowStep | null>(initialStep);
const updateState = (updates: Partial<CreateFlowState>) => {
setState((prevState) => ({
@@ -51,7 +49,7 @@ export function CreateFlowProvider({
/**
* Hook to access Create Flow context
*
*
* @throws Error if used outside CreateFlowProvider
* @returns CreateFlowContextValue
*/
+2 -4
View File
@@ -32,9 +32,7 @@ const FINAL_REVIEW_CATEGORIES: Category[] = [
},
{
name: "Membership",
chipOptions: [
{ id: "m1", label: "Open Admission", state: "unselected" },
],
chipOptions: [{ id: "m1", label: "Open Admission", state: "unselected" }],
},
{
name: "Decision-making",
@@ -70,7 +68,7 @@ export default function FinalReviewPage() {
if (showDesktopLayout) {
return (
<div className="w-full max-w-[1280px] shrink-0 px-5 md:px-16">
<div className="w-full max-w-[1280px] shrink-0 px-5 md:px-12">
<div className="flex w-full flex-col gap-4 min-w-0 sm:grid sm:grid-cols-2 sm:gap-[var(--measures-spacing-1200,48px)]">
<div className="min-w-0 flex flex-col justify-center">
<HeaderLockup
+1 -1
View File
@@ -6,7 +6,7 @@ import NumberedList from "../../components/type/NumberedList";
/**
* Informational page for the create flow
*
*
* Displays information about the create flow process using HeaderLockup and NumberedList components.
* Responsive sizing: uses L/M for HeaderLockup and M/S for NumberedList based on 640px breakpoint.
*/
+52 -24
View File
@@ -10,7 +10,7 @@ import type { CreateFlowStep } from "./types";
/**
* Layout for the Create Rule Flow
*
*
* Provides a full-screen layout without the root layout's TopNav/Footer.
* This layout wraps all create flow pages and provides the CreateFlowContext.
* Includes the create flow-specific TopNav and Footer components.
@@ -59,7 +59,10 @@ function CreateFlowLayoutContent({ children }: { children: ReactNode }) {
const previousStep = getPreviousStep();
const handleNext = () => {
if (typeof document !== "undefined" && document.activeElement instanceof HTMLElement) {
if (
typeof document !== "undefined" &&
document.activeElement instanceof HTMLElement
) {
document.activeElement.blur();
}
if (nextStep) {
@@ -68,7 +71,10 @@ function CreateFlowLayoutContent({ children }: { children: ReactNode }) {
};
const handleBack = () => {
if (typeof document !== "undefined" && document.activeElement instanceof HTMLElement) {
if (
typeof document !== "undefined" &&
document.activeElement instanceof HTMLElement
) {
document.activeElement.blur();
}
if (previousStep) {
@@ -76,30 +82,52 @@ function CreateFlowLayoutContent({ children }: { children: ReactNode }) {
}
};
const isCompletedStep = currentStep === "completed";
return (
<div className="min-h-screen bg-black flex flex-col">
<CreateFlowTopNav />
<main className="flex-1 flex items-center justify-center overflow-auto">
<div
className={`bg-black flex flex-col ${isCompletedStep ? "h-screen overflow-hidden" : "min-h-screen"}`}
>
<CreateFlowTopNav
hasShare={isCompletedStep}
hasExport={isCompletedStep}
hasEdit={isCompletedStep}
loggedIn={isCompletedStep}
onEdit={
isCompletedStep
? () => router.push("/create/final-review")
: undefined
}
buttonPalette={isCompletedStep ? "inverse" : undefined}
className={
isCompletedStep ? "!bg-[var(--color-teal-teal50,#c9fef9)]" : undefined
}
/>
<main
className={`flex-1 flex min-h-0 justify-center ${isCompletedStep ? "items-stretch overflow-hidden" : "items-center overflow-auto"}`}
>
{children}
</main>
<CreateFlowFooter
secondButton={
nextStep ? (
<Button
buttonType="filled"
palette="default"
size="xsmall"
className="md:!text-[14px] md:!leading-[16px] !text-[12px] !leading-[14px] !px-[var(--spacing-measures-spacing-200,8px)] md:!px-[var(--spacing-measures-spacing-250,10px)] !py-[var(--spacing-measures-spacing-200,8px)] md:!py-[var(--spacing-measures-spacing-250,10px)]"
onClick={handleNext}
>
{currentStep === "final-review"
? "Finalize CommunityRule"
: "Next"}
</Button>
) : null
}
onBackClick={previousStep ? handleBack : undefined}
/>
{!isCompletedStep && (
<CreateFlowFooter
secondButton={
nextStep ? (
<Button
buttonType="filled"
palette="default"
size="xsmall"
className="md:!text-[14px] md:!leading-[16px] !text-[12px] !leading-[14px] !px-[var(--spacing-measures-spacing-200,8px)] md:!px-[var(--spacing-measures-spacing-250,10px)] !py-[var(--spacing-measures-spacing-200,8px)] md:!py-[var(--spacing-measures-spacing-250,10px)]"
onClick={handleNext}
>
{currentStep === "final-review"
? "Finalize CommunityRule"
: "Next"}
</Button>
) : null
}
onBackClick={previousStep ? handleBack : undefined}
/>
)}
</div>
);
}
+19 -10
View File
@@ -7,7 +7,7 @@ import MultiSelect from "../../components/controls/MultiSelect";
/**
* Select page for the create flow
*
*
* Displays selection options using HeaderLockup and MultiSelect components.
* Responsive layout: two-column at 640px+, single column below 640px.
* Responsive sizing: uses L/M for HeaderLockup and S for MultiSelect based on 640px breakpoint.
@@ -44,9 +44,12 @@ export default function SelectPage() {
setCommunitySizeOptions((prev) =>
prev.map((opt) =>
opt.id === chipId
? { ...opt, state: opt.state === "Selected" ? "Unselected" : "Selected" }
: opt
)
? {
...opt,
state: opt.state === "Selected" ? "Unselected" : "Selected",
}
: opt,
),
);
};
@@ -54,9 +57,12 @@ export default function SelectPage() {
setOrganizationTypeOptions((prev) =>
prev.map((opt) =>
opt.id === chipId
? { ...opt, state: opt.state === "Selected" ? "Unselected" : "Selected" }
: opt
)
? {
...opt,
state: opt.state === "Selected" ? "Unselected" : "Selected",
}
: opt,
),
);
};
@@ -64,9 +70,12 @@ export default function SelectPage() {
setGovernanceStyleOptions((prev) =>
prev.map((opt) =>
opt.id === chipId
? { ...opt, state: opt.state === "Selected" ? "Unselected" : "Selected" }
: opt
)
? {
...opt,
state: opt.state === "Selected" ? "Unselected" : "Selected",
}
: opt,
),
);
};
+1 -1
View File
@@ -7,7 +7,7 @@ import TextInput from "../../components/controls/TextInput";
/**
* Text page for the create flow
*
*
* Displays a text input field for user input using HeaderLockup and TextInput components.
* Responsive sizing: uses L/M for HeaderLockup and medium/small for TextInput based on 640px breakpoint.
*/