From aa5dbc8088f9f5399e5728346ce6ceb4abafa2ca Mon Sep 17 00:00:00 2001 From: Cyber-preacher Date: Wed, 15 Apr 2026 17:22:34 +0400 Subject: [PATCH 1/2] Add proposal deliberation UI --- src/app/App.tsx | 4 +- .../discussions/ThreadPrimitives.tsx | 398 ++++++++++++++++++ src/lib/apiClient.ts | 164 +++++--- src/pages/proposals/ProposalChamber.tsx | 3 + src/pages/proposals/ProposalChamberVeto.tsx | 3 + src/pages/proposals/ProposalCitizenVeto.tsx | 3 + src/pages/proposals/ProposalDeliberation.tsx | 395 +++++++++++++++++ src/pages/proposals/ProposalFinished.tsx | 3 + src/pages/proposals/ProposalFormation.tsx | 3 + src/pages/proposals/ProposalPP.tsx | 3 + src/pages/proposals/ProposalReferendum.tsx | 3 + src/pages/proposals/Proposals.tsx | 18 +- src/types/api.ts | 52 +++ 13 files changed, 992 insertions(+), 60 deletions(-) create mode 100644 src/components/discussions/ThreadPrimitives.tsx create mode 100644 src/pages/proposals/ProposalDeliberation.tsx diff --git a/src/app/App.tsx b/src/app/App.tsx index 00b5f74..4dacfef 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -4,12 +4,12 @@ import { AuthProvider } from "@/app/auth/AuthContext"; import AppRoutes from "./AppRoutes"; const ScrollToTopOnRouteChange: React.FC = () => { - const { pathname, search } = useLocation(); + const { pathname } = useLocation(); useEffect(() => { document.title = "Vortex Sim"; window.scrollTo({ top: 0, left: 0, behavior: "auto" }); - }, [pathname, search]); + }, [pathname]); return null; }; diff --git a/src/components/discussions/ThreadPrimitives.tsx b/src/components/discussions/ThreadPrimitives.tsx new file mode 100644 index 0000000..ab87dd7 --- /dev/null +++ b/src/components/discussions/ThreadPrimitives.tsx @@ -0,0 +1,398 @@ +import type { FormEventHandler, ReactNode } from "react"; + +import { AddressInline } from "@/components/AddressInline"; +import { Surface } from "@/components/Surface"; +import { Button } from "@/components/primitives/button"; +import { Input } from "@/components/primitives/input"; +import { Select } from "@/components/primitives/select"; +import { formatDateTime } from "@/lib/dateTime"; + +type DiscussionOption = { + value: TValue; + label: string; +}; + +export type DiscussionStatusOption = + DiscussionOption; + +type DiscussionThreadPermissions = { + canReply?: boolean; + canTransition?: boolean; +}; + +type DiscussionThreadListItem< + TCategory extends string = string, + TStatus extends string = string, +> = { + id: string; + category: TCategory; + status: TStatus; + title: string; + body: string; + replies: number; + updatedAt: string; + permissions: DiscussionThreadPermissions; +}; + +type DiscussionThreadDetailItem< + TCategory extends string = string, + TStatus extends string = string, +> = DiscussionThreadListItem & { + authorAddress: string; + createdAt: string; +}; + +type DiscussionThreadMessage = { + id: string; + authorAddress: string; + body: string; + createdAt: string; +}; + +const textareaClassName = + "min-h-[96px] w-full resize-y rounded-xl border border-border bg-panel-alt px-3 py-2 text-sm text-text shadow-[var(--shadow-control)] focus-visible:ring-2 focus-visible:ring-[color:var(--primary-dim)] focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-60"; + +type ThreadCategoryFilterProps = { + options: Array>; + value: TValue; + onChange: (value: TValue) => void; +}; + +export function ThreadCategoryFilter({ + options, + value, + onChange, +}: ThreadCategoryFilterProps) { + return ( +
+ {options.map((option) => ( + + ))} +
+ ); +} + +type ThreadComposerProps = { + categoryOptions: Array>; + categoryValue: TCategory; + onCategoryChange: (value: TCategory) => void; + title: string; + onTitleChange: (value: string) => void; + body: string; + onBodyChange: (value: string) => void; + onSubmit: FormEventHandler; + canCreate: boolean; + busy: boolean; + disabledMessage?: string | null; + titlePlaceholder?: string; + bodyPlaceholder?: string; + submitLabel?: string; + busyLabel?: string; +}; + +export function ThreadComposer({ + categoryOptions, + categoryValue, + onCategoryChange, + title, + onTitleChange, + body, + onBodyChange, + onSubmit, + canCreate, + busy, + disabledMessage, + titlePlaceholder = "Thread title", + bodyPlaceholder = "Write the opening post", + submitLabel = "Start thread", + busyLabel = "Posting...", +}: ThreadComposerProps) { + return ( +
+
+ + onTitleChange(event.target.value)} + placeholder={titlePlaceholder} + disabled={!canCreate || busy} + /> +
+