Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/app/AppRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import ProposalCreation from "../pages/proposals/ProposalCreation";
import MyGovernance from "../pages/MyGovernance";
import ProposalDrafts from "../pages/proposals/ProposalDrafts";
import ProposalDraft from "../pages/proposals/ProposalDraft";
import PublicDraft from "../pages/proposals/PublicDraft";
import PublicDrafts from "../pages/proposals/PublicDrafts";
import FullHistory from "../pages/human-nodes/FullHistory";
import Landing from "../pages/Landing";
import Paper from "../pages/Paper";
Expand Down Expand Up @@ -92,6 +94,8 @@ const AppRoutes: React.FC = () => {
<Route path="courts/:id" element={<Courtroom />} />
<Route path="cm" element={<CMPanel />} />
<Route path="proposals" element={<Proposals />} />
<Route path="proposals/public-drafts" element={<PublicDrafts />} />
<Route path="proposals/public-drafts/:id" element={<PublicDraft />} />
<Route path="proposals/drafts" element={<ProposalDrafts />} />
<Route path="proposals/drafts/:id" element={<ProposalDraft />} />
<Route path="proposals/new" element={<ProposalCreation />} />
Expand Down
79 changes: 48 additions & 31 deletions src/app/auth/AuthContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
getPolkadotAccounts,
signPolkadotMessage,
} from "@/lib/polkadotExtension";
import { governanceIdentityStatuses } from "@/lib/humanNodesUi";

type AuthState = {
enabled: boolean;
Expand Down Expand Up @@ -210,7 +211,8 @@ export function useAuth(): AuthContextValue {
export function AuthSidebarPanel() {
const auth = useAuth();
const [activityState, setActivityState] = useState<{
governorActive: boolean;
governor: boolean;
activeGovernor: boolean;
humanNodeActive: boolean;
} | null>(null);

Expand All @@ -227,12 +229,17 @@ export function AuthSidebarPanel() {
const profile = await apiHuman(address);
if (!active) return;
setActivityState({
governorActive: profile.governorActive,
governor: profile.governor,
activeGovernor: profile.governorActive,
humanNodeActive: profile.humanNodeActive,
});
} catch {
if (!active) return;
setActivityState({ governorActive: false, humanNodeActive: false });
setActivityState({
governor: false,
activeGovernor: false,
humanNodeActive: false,
});
}
};

Expand All @@ -255,9 +262,15 @@ export function AuthSidebarPanel() {
const humanNodeActive = Boolean(
auth.authenticated && activityState?.humanNodeActive,
);
const governorActive = Boolean(
auth.authenticated && activityState?.governorActive,
const governor = Boolean(auth.authenticated && activityState?.governor);
const activeGovernor = Boolean(
auth.authenticated && activityState?.activeGovernor,
);
const identityStatuses = governanceIdentityStatuses({
governor,
activeGovernor,
humanNode: humanNodeActive,
});

const gateError =
auth.authenticated && !auth.eligible
Expand All @@ -274,32 +287,36 @@ export function AuthSidebarPanel() {
<span className="sidebar__authKicker">Wallet</span>
<span className="sidebar__authValue">{addressLabel}</span>
</div>
<div className="sidebar__authRow">
<span className="sidebar__authKicker">Human node</span>
<span
className={
humanNodeActive
? "sidebar__authValue sidebar__authValue--ok"
: "sidebar__authValue sidebar__authValue--warn"
}
title={auth.gateReason}
>
{humanNodeActive ? "Active" : "Not active"}
</span>
</div>
<div className="sidebar__authRow">
<span className="sidebar__authKicker">Governor</span>
<span
className={
governorActive
? "sidebar__authValue sidebar__authValue--ok"
: "sidebar__authValue sidebar__authValue--warn"
}
title={auth.gateReason}
>
{governorActive ? "Active" : "Not active"}
</span>
</div>
{(
[
["humanNode", auth.gateReason],
[
"governor",
"Governor status is earned through the Vortex tier system.",
],
[
"activeGovernor",
"Active Governor status reflects completed governing thresholds for the current era.",
],
] as const
).map(([key, title]) => {
const status = identityStatuses[key];
return (
<div className="sidebar__authRow" key={key}>
<span className="sidebar__authKicker">{status.label}</span>
<span
className={
status.active
? "sidebar__authValue sidebar__authValue--ok"
: "sidebar__authValue sidebar__authValue--warn"
}
title={title}
>
{status.value}
</span>
</div>
);
})}

{auth.lastError ? (
<div className="sidebar__authError" role="status">
Expand Down
19 changes: 19 additions & 0 deletions src/components/GovernanceStatusPills.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { governanceIdentityStatuses } from "@/lib/humanNodesUi";
import { StatusPill } from "./StatusPill";

type GovernanceStatusPillsProps = {
governor: boolean;
activeGovernor: boolean;
humanNode: boolean;
};

export function GovernanceStatusPills(props: GovernanceStatusPillsProps) {
const statuses = governanceIdentityStatuses(props);
return (
<div className="flex flex-col items-center gap-2 text-sm lg:items-end">
{Object.values(statuses).map((status) => (
<StatusPill key={status.label} {...status} />
))}
</div>
);
}
2 changes: 2 additions & 0 deletions src/components/ProposalPageHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export function ProposalPageHeader({
if (!proposalId) return stageLinks;
return buildProposalStageLinks({
canonicalRoute: status?.canonicalRoute,
draftRoute: status?.draftHistory?.route ?? stageLinks?.draft,
liveStage,
proposalId,
routeOverrides: stageLinks,
Expand All @@ -72,6 +73,7 @@ export function ProposalPageHeader({
showFormationStage,
stageLinks,
status?.canonicalRoute,
status?.draftHistory?.route,
]);

return (
Expand Down
4 changes: 4 additions & 0 deletions src/components/ProposalStageBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ function withSnapshotStage(href: string, stage: ProposalStage): string {

type BuildProposalStageLinksInput = {
canonicalRoute?: string;
draftRoute?: string;
liveStage: ProposalStage;
proposalId: string;
routeOverrides?: Partial<Record<ProposalStage, string>>;
Expand All @@ -63,6 +64,7 @@ type BuildProposalStageLinksInput = {

export function buildProposalStageLinks({
canonicalRoute,
draftRoute,
liveStage,
proposalId,
routeOverrides,
Expand All @@ -71,6 +73,8 @@ export function buildProposalStageLinks({
const liveIndex = stageProgressIndex(liveStage);
const links: Partial<Record<ProposalStage, string>> = {};

if (draftRoute) links.draft = draftRoute;

for (const stage of stageOrder) {
if (stage === "draft") continue;
if (stage === "build" && !showFormationStage && liveStage !== "build") {
Expand Down
6 changes: 6 additions & 0 deletions src/components/StageChip.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@
white-space: nowrap;
}

.stage-chip--draft {
--stage-chip-bg: var(--control-glass-bg);
--stage-chip-border: var(--surface-glass-border);
--stage-chip-text: var(--text);
}

.stage-chip--proposal-pool {
--stage-chip-bg: #fff1dc;
--stage-chip-border: rgba(176, 102, 25, 0.2);
Expand Down
1 change: 1 addition & 0 deletions src/components/StageChip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import "./StageChip.css";

const chipClasses: Record<StageChipKind, string> = {
draft: "stage-chip--draft",
proposal_pool: "stage-chip--proposal-pool",
chamber_vote: "stage-chip--chamber-vote",
citizen_veto: "stage-chip--citizen-veto",
Expand Down
72 changes: 72 additions & 0 deletions src/lib/apiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,13 @@ import type {
GetInvisionResponse,
GetMyGovernanceResponse,
GetProposalDraftsResponse,
GetPublicProposalDraftsResponse,
GetProposalsResponse,
GetProposalTimelineResponse,
HumanNodeProfileDto,
ProposalDraftDetailDto,
PublicProposalDraftKindDto,
PublicProposalDraftSortDto,
ProposalThreadDetailDto,
ProposalThreadDto,
ProposalThreadListDto,
Expand Down Expand Up @@ -800,6 +803,39 @@ export async function apiProposalDraft(
return await apiGet<ProposalDraftDetailDto>(`/api/proposals/drafts/${id}`);
}

export async function apiPublicProposalDrafts(input?: {
q?: string;
chamber?: string;
author?: string;
initiative?: string;
proposalPath?: PublicProposalDraftKindDto;
sort?: PublicProposalDraftSortDto;
cursor?: string;
limit?: number;
}): Promise<GetPublicProposalDraftsResponse> {
const params = new URLSearchParams();
if (input?.q) params.set("q", input.q);
if (input?.chamber) params.set("chamber", input.chamber);
if (input?.author) params.set("author", input.author);
if (input?.initiative) params.set("initiative", input.initiative);
if (input?.proposalPath) params.set("proposalPath", input.proposalPath);
if (input?.sort) params.set("sort", input.sort);
if (input?.cursor) params.set("cursor", input.cursor);
if (input?.limit) params.set("limit", String(input.limit));
const qs = params.toString();
return await apiGet<GetPublicProposalDraftsResponse>(
`/api/proposals/public-drafts?${qs}`,
);
}

export async function apiPublicProposalDraft(
id: string,
): Promise<ProposalDraftDetailDto> {
return await apiGet<ProposalDraftDetailDto>(
`/api/proposals/public-drafts/${encodeURIComponent(id)}`,
);
}

export type ProposalDraftFormPayload = {
templateId?: "project" | "system";
presetId?: string;
Expand Down Expand Up @@ -883,6 +919,42 @@ export async function apiProposalDraftDelete(input: {
});
}

export async function apiProposalDraftPublish(input: {
draftId: string;
idempotencyKey?: string;
}): Promise<{
ok: true;
type: "proposal.draft.publish";
draftId: string;
revision: number;
publicUrl: string;
publishedAt: string;
updatedAt: string;
}> {
return await apiCommand({
type: "proposal.draft.publish",
payload: { draftId: input.draftId },
idempotencyKey: input.idempotencyKey,
});
}

export async function apiProposalDraftUnpublish(input: {
draftId: string;
idempotencyKey?: string;
}): Promise<{
ok: true;
type: "proposal.draft.unpublish";
draftId: string;
unpublished: boolean;
updatedAt: string;
}> {
return await apiCommand({
type: "proposal.draft.unpublish",
payload: { draftId: input.draftId },
idempotencyKey: input.idempotencyKey,
});
}

export async function apiProposalSubmitToPool(input: {
draftId: string;
idempotencyKey?: string;
Expand Down
43 changes: 38 additions & 5 deletions src/lib/humanNodesUi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ export type HumanNodesTierFilter =
| "legate"
| "consul"
| "citizen";
export type HumanNodesStatusFilter = "all" | "governor" | "human" | "inactive";
export type HumanNodesStatusFilter =
| "all"
| "governor"
| "active-governor"
| "human"
| "inactive";
export type HumanNodesCmRange = "all" | "0-50" | "50-200" | "200+";

export type HumanNodesFilters = {
Expand All @@ -25,6 +30,32 @@ export type HumanNodesFilters = {
tierFilter: HumanNodesTierFilter;
};

export type GovernanceIdentityState = {
governor: boolean;
activeGovernor: boolean;
humanNode: boolean;
};

export function governanceIdentityStatuses(state: GovernanceIdentityState) {
return {
governor: {
label: "Governor",
value: state.governor ? "Active" : "Not active",
active: state.governor,
},
activeGovernor: {
label: "Active governor",
value: state.activeGovernor ? "Active" : "Not active",
active: state.activeGovernor,
},
humanNode: {
label: "Human node",
value: state.humanNode ? "Active" : "Not active",
active: state.humanNode,
},
} as const;
}

export const DEFAULT_HUMAN_NODES_FILTERS: HumanNodesFilters = {
sortBy: "acm-desc",
tierFilter: "all",
Expand Down Expand Up @@ -70,10 +101,12 @@ export function filterHumanNodes(input: {
statusFilter === "all"
? true
: statusFilter === "governor"
? node.active.governorActive
: statusFilter === "human"
? node.active.humanNodeActive
: !node.active.governorActive && !node.active.humanNodeActive;
? node.active.governor
: statusFilter === "active-governor"
? node.active.governorActive
: statusFilter === "human"
? node.active.humanNodeActive
: !node.active.governorActive && !node.active.humanNodeActive;
const acmValue = node.cmTotals?.acm ?? node.acm ?? 0;
const matchesRange =
cmRange === "all"
Expand Down
Loading
Loading