Skip to content

Commit 8926c3b

Browse files
Harden deploy proposal surfaces and mobile UX (#39)
phase-82: harden deploy proposal surfaces and mobile
1 parent 8fde8e4 commit 8926c3b

28 files changed

+792
-354
lines changed

src/components/AttachmentList.tsx

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Surface } from "@/components/Surface";
66
export type AttachmentItem = {
77
id: string;
88
title: ReactNode;
9+
href?: string;
910
actionLabel?: ReactNode;
1011
};
1112

@@ -39,12 +40,20 @@ export function AttachmentList({
3940
<span className="min-w-0 flex-1 [overflow-wrap:anywhere] break-words">
4041
{file.title}
4142
</span>
42-
<button
43-
type="button"
44-
className="shrink-0 text-sm font-semibold text-primary"
45-
>
46-
{file.actionLabel ?? "View"}
47-
</button>
43+
{file.href ? (
44+
<a
45+
href={file.href}
46+
target="_blank"
47+
rel="noreferrer"
48+
className="shrink-0 text-sm font-semibold text-primary"
49+
>
50+
{file.actionLabel ?? "View"}
51+
</a>
52+
) : (
53+
<span className="shrink-0 text-sm font-semibold text-muted">
54+
{file.actionLabel ?? "Attached"}
55+
</span>
56+
)}
4857
</Surface>
4958
))}
5059
</ul>

src/components/ProposalStageBar.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import React from "react";
22
import { HintLabel } from "@/components/Hint";
33

4-
export type ProposalStage = "draft" | "pool" | "vote" | "build" | "passed";
4+
export type ProposalStage =
5+
| "draft"
6+
| "pool"
7+
| "vote"
8+
| "build"
9+
| "passed"
10+
| "failed";
511

612
type ProposalStageBarProps = {
713
current: ProposalStage;
@@ -36,14 +42,17 @@ export const ProposalStageBar: React.FC<ProposalStageBarProps> = ({
3642
render: <HintLabel termId="formation">Formation</HintLabel>,
3743
},
3844
{ key: "passed", label: "Passed" },
45+
{ key: "failed", label: "Failed" },
3946
];
4047
const stages = allStages.filter(
4148
(stage) =>
4249
stage.key !== "build" || showFormationStage || current === "build",
4350
);
4451

4552
return (
46-
<div className={["flex gap-2", className].filter(Boolean).join(" ")}>
53+
<div
54+
className={["flex flex-wrap gap-2", className].filter(Boolean).join(" ")}
55+
>
4756
{stages.map((stage) => {
4857
const active = stage.key === current;
4958
const activeClasses =
@@ -55,12 +64,14 @@ export const ProposalStageBar: React.FC<ProposalStageBarProps> = ({
5564
? "bg-[var(--accent)] text-[var(--accent-foreground)]"
5665
: stage.key === "build"
5766
? "bg-[var(--accent-warm)] text-[var(--text)]"
58-
: "bg-[color:var(--ok)]/20 text-[color:var(--ok)]";
67+
: stage.key === "passed"
68+
? "bg-[color:var(--ok)]/20 text-[color:var(--ok)]"
69+
: "bg-[color:var(--danger)]/12 text-[color:var(--danger)]";
5970
return (
6071
<div
6172
key={stage.key}
6273
className={[
63-
"flex-1 rounded-full px-3 py-2 text-center text-xs font-semibold transition",
74+
"min-w-0 basis-[calc(50%-0.25rem)] rounded-full px-3 py-2 text-center text-xs leading-tight font-semibold transition sm:flex-1 sm:basis-0",
6475
active
6576
? activeClasses
6677
: "border border-border bg-panel-alt [background-image:var(--card-grad)] bg-cover bg-no-repeat text-muted",

src/components/StageChip.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const chipClasses: Record<StageChipKind, string> = {
1414
chamber_vote: "bg-[color:var(--accent)]/15 text-[var(--accent)]",
1515
formation: "bg-[color:var(--primary)]/12 text-primary",
1616
passed: "bg-[color:var(--ok)]/20 text-[color:var(--ok)]",
17+
failed: "bg-[color:var(--danger)]/12 text-[color:var(--danger)]",
1718
thread: "bg-panel-alt text-muted",
1819
courts: "bg-[color:var(--accent-warm)]/15 text-[var(--accent-warm)]",
1920
faction: "bg-panel-alt text-muted",

src/components/VoteButton.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,13 @@ export function VoteButton({
5151
const title =
5252
titleProp ??
5353
(gated
54-
? !auth.authenticated
55-
? "Connect and verify to interact."
56-
: auth.gateReason
57-
? `Not eligible: ${auth.gateReason}`
58-
: "Not eligible to interact."
54+
? auth.loading
55+
? "Checking wallet status…"
56+
: !auth.authenticated
57+
? "Connect and verify to interact."
58+
: auth.gateReason
59+
? `Not eligible: ${auth.gateReason}`
60+
: "Not eligible to interact."
5961
: undefined);
6062

6163
return (

src/components/primitives/tabs.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export function Tabs({
2121
return (
2222
<div
2323
className={cn(
24-
"inline-flex rounded-full border border-border bg-panel [background-image:var(--card-grad)] p-1 shadow-(--shadow-control) ring-1 ring-(--glass-border) ring-inset",
24+
"flex max-w-full flex-wrap rounded-full border border-border bg-panel [background-image:var(--card-grad)] p-1 shadow-(--shadow-control) ring-1 ring-(--glass-border) ring-inset",
2525
className,
2626
)}
2727
{...props}

src/lib/apiClient.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type {
1111
CourtCaseDetailDto,
1212
FactionDto,
1313
FormationProposalPageDto,
14+
ProposalFinishedPageDto,
1415
GetFactionsResponse,
1516
GetChamberResponse,
1617
GetChambersResponse,
@@ -656,10 +657,8 @@ export async function apiProposalFormationPage(
656657

657658
export async function apiProposalFinishedPage(
658659
id: string,
659-
): Promise<FormationProposalPageDto> {
660-
return await apiGet<FormationProposalPageDto>(
661-
`/api/proposals/${id}/finished`,
662-
);
660+
): Promise<ProposalFinishedPageDto> {
661+
return await apiGet<ProposalFinishedPageDto>(`/api/proposals/${id}/finished`);
663662
}
664663

665664
export async function apiCourts(): Promise<GetCourtsResponse> {

src/pages/MyGovernance.tsx

Lines changed: 47 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -229,16 +229,6 @@ const MyGovernance: React.FC = () => {
229229
}, []);
230230

231231
const eraActivity = gov?.eraActivity;
232-
const status: { label: GoverningStatus; termId: string } = gov?.rollup
233-
? {
234-
label: gov.rollup.status,
235-
termId: governingStatusTermId(gov.rollup.status),
236-
}
237-
: governingStatusForProgress(
238-
eraActivity?.completed ?? 0,
239-
eraActivity?.required ?? 0,
240-
);
241-
242232
const timeLeftValue = useMemo(() => {
243233
const targetMs = clock?.nextEraAt
244234
? toTimestampMs(clock.nextEraAt, NaN)
@@ -254,6 +244,37 @@ const MyGovernance: React.FC = () => {
254244
return chambers.filter((chamber) => gov.myChamberIds.includes(chamber.id));
255245
}, [gov, chambers]);
256246

247+
if (gov === null || chambers === null) {
248+
return (
249+
<div className="flex flex-col gap-6">
250+
<PageHint pageId="my-governance" />
251+
<Surface
252+
variant="panelAlt"
253+
radius="2xl"
254+
shadow="tile"
255+
className={cn(
256+
"px-5 py-4 text-sm text-muted",
257+
loadError ? "text-destructive" : undefined,
258+
)}
259+
>
260+
{loadError
261+
? `My governance unavailable: ${formatLoadError(loadError)}`
262+
: "Loading…"}
263+
</Surface>
264+
</div>
265+
);
266+
}
267+
268+
const status: { label: GoverningStatus; termId: string } = gov?.rollup
269+
? {
270+
label: gov.rollup.status,
271+
termId: governingStatusTermId(gov.rollup.status),
272+
}
273+
: governingStatusForProgress(
274+
eraActivity?.completed ?? 0,
275+
eraActivity?.required ?? 0,
276+
);
277+
257278
const tierProgress = gov?.tier ?? null;
258279
const currentTier = (tierProgress?.tier as TierKey | undefined) ?? "Nominee";
259280
const nextTier = (tierProgress?.nextTier as TierKey | null) ?? null;
@@ -390,21 +411,6 @@ const MyGovernance: React.FC = () => {
390411
return (
391412
<div className="flex flex-col gap-6">
392413
<PageHint pageId="my-governance" />
393-
{gov === null || chambers === null ? (
394-
<Surface
395-
variant="panelAlt"
396-
radius="2xl"
397-
shadow="tile"
398-
className={cn(
399-
"px-5 py-4 text-sm text-muted",
400-
loadError ? "text-destructive" : undefined,
401-
)}
402-
>
403-
{loadError
404-
? `My governance unavailable: ${formatLoadError(loadError)}`
405-
: "Loading…"}
406-
</Surface>
407-
) : null}
408414
<Card>
409415
<CardHeader className="pb-2">
410416
<CardTitle>
@@ -414,6 +420,15 @@ const MyGovernance: React.FC = () => {
414420
</CardTitle>
415421
</CardHeader>
416422
<CardContent className="space-y-4">
423+
<Surface
424+
variant="panelAlt"
425+
radius="2xl"
426+
shadow="tile"
427+
className="px-4 py-3 text-sm text-muted"
428+
>
429+
This tracks opportunities that occurred during the current era, even
430+
if those votes are already closed.
431+
</Surface>
417432
<div className="grid gap-3 sm:grid-cols-2">
418433
{[
419434
{ label: "Era", value: eraActivity?.era ?? "—" },
@@ -443,11 +458,11 @@ const MyGovernance: React.FC = () => {
443458
key: "required",
444459
label: (
445460
<HintLabel termId="governing_threshold">
446-
Required actions
461+
Era participation
447462
</HintLabel>
448463
),
449464
value: eraActivity
450-
? `${eraActivity.completed} / ${eraActivity.required} completed`
465+
? `${eraActivity.completed} / ${eraActivity.required} completed this era`
451466
: "—",
452467
},
453468
{
@@ -480,7 +495,7 @@ const MyGovernance: React.FC = () => {
480495
className="flex h-full flex-col items-center justify-center px-3 py-3 text-center"
481496
>
482497
<Kicker align="center" className="text-[0.7rem]">
483-
{act.label}
498+
{act.label} this era
484499
</Kicker>
485500
<p className="text-base font-semibold text-text">
486501
{act.done} / {act.required}
@@ -552,7 +567,7 @@ const MyGovernance: React.FC = () => {
552567
);
553568
return (
554569
<div key={key} className="space-y-2">
555-
<div className="flex items-center justify-between gap-3">
570+
<div className="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between sm:gap-3">
556571
<p className="text-sm font-semibold text-text">
557572
{requirementLabel[key]}
558573
</p>
@@ -595,7 +610,7 @@ const MyGovernance: React.FC = () => {
595610
return (
596611
<div
597612
key={key}
598-
className="flex items-center justify-between gap-4 px-3 py-3"
613+
className="flex flex-col gap-3 px-3 py-3 sm:flex-row sm:items-center sm:justify-between sm:gap-4"
599614
>
600615
<p className="text-sm leading-snug font-semibold text-text">
601616
{requirementLabel[key]}
@@ -769,7 +784,7 @@ const MyGovernance: React.FC = () => {
769784
shadow="tile"
770785
className="space-y-2 px-4 py-3"
771786
>
772-
<div className="flex items-center justify-between">
787+
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
773788
<p className="text-sm font-semibold text-text">
774789
{chamber.chamberTitle}
775790
</p>
@@ -813,7 +828,7 @@ const MyGovernance: React.FC = () => {
813828
shadow="tile"
814829
className="space-y-3 p-4"
815830
>
816-
<div className="flex items-start justify-between gap-3">
831+
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
817832
<div>
818833
<Kicker>
819834
{chamberLabel(item.chamberId, chambers)}

src/pages/chambers/Chamber.tsx

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import type {
2828
ChamberThreadDto,
2929
ChamberThreadMessageDto,
3030
GetChamberResponse,
31+
GetMyGovernanceResponse,
3132
} from "@/types/api";
3233
import {
3334
apiChamber,
@@ -79,7 +80,8 @@ const Chamber: React.FC = () => {
7980
const [chatMessage, setChatMessage] = useState("");
8081
const [chatError, setChatError] = useState<string | null>(null);
8182
const [chatBusy, setChatBusy] = useState(false);
82-
const [myChamberIds, setMyChamberIds] = useState<string[]>([]);
83+
const [myGovernance, setMyGovernance] =
84+
useState<GetMyGovernanceResponse | null>(null);
8385
const [chatPeers, setChatPeers] = useState<ChamberChatPeerDto[]>([]);
8486
const [chatSignalError, setChatSignalError] = useState<string | null>(null);
8587

@@ -142,18 +144,18 @@ const Chamber: React.FC = () => {
142144

143145
useEffect(() => {
144146
if (!address) {
145-
setMyChamberIds([]);
147+
setMyGovernance(null);
146148
return;
147149
}
148150
let active = true;
149151
(async () => {
150152
try {
151153
const res = await apiMyGovernance();
152154
if (!active) return;
153-
setMyChamberIds(res.myChamberIds ?? []);
155+
setMyGovernance(res);
154156
} catch {
155157
if (!active) return;
156-
setMyChamberIds([]);
158+
setMyGovernance(null);
157159
}
158160
})();
159161
return () => {
@@ -238,10 +240,15 @@ const Chamber: React.FC = () => {
238240
const canWrite = useMemo(() => {
239241
if (!address || !id) return false;
240242
if (id === "general") {
241-
return isMember || myChamberIds.length > 0;
243+
return (
244+
isMember ||
245+
(myGovernance?.delegation.chambers ?? []).some(
246+
(item) => item.chamberId === "general",
247+
)
248+
);
242249
}
243250
return isMember;
244-
}, [address, id, isMember, myChamberIds.length]);
251+
}, [address, id, isMember, myGovernance]);
245252

246253
const appendChatMessage = useCallback((message: ChamberChatMessageDto) => {
247254
if (chatMessageIdsRef.current.has(message.id)) return;
@@ -944,7 +951,7 @@ const Chamber: React.FC = () => {
944951
</Card>
945952

946953
<Card>
947-
<CardHeader className="flex items-center justify-between pb-3">
954+
<CardHeader className="flex flex-col gap-3 pb-3 sm:flex-row sm:items-center sm:justify-between">
948955
<div>
949956
<Kicker>Governors</Kicker>
950957
<CardTitle>Chamber roster</CardTitle>
@@ -986,7 +993,7 @@ const Chamber: React.FC = () => {
986993
{gov.mcm.toLocaleString()}
987994
</p>
988995
{gov.delegateeAddress ? (
989-
<div className="mt-1 flex items-center gap-1 text-xs text-muted">
996+
<div className="mt-1 flex flex-wrap items-center gap-1 text-xs text-muted">
990997
<span>Delegates to</span>
991998
<AddressInline
992999
address={gov.delegateeAddress}

0 commit comments

Comments
 (0)