From 408e7e0c5c36ffb9d50b980df8efd6846d8a121f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ti=E1=BA=BFn=20Nguy=E1=BB=85n=20Kh=E1=BA=AFc?= Date: Wed, 26 Feb 2025 15:15:56 +1300 Subject: [PATCH] feat: simple referenda page --- panda.config.ts | 35 +- src/components/info-header.tsx | 50 +++ .../coretime/components/parachains.tsx | 196 +++++++++ .../explorer/components/statistics.tsx | 91 +--- .../components/active-referenda.tsx | 393 ++++++++++++++++++ .../components/concluded-referenda.tsx | 108 +++++ src/features/governance/utils.ts | 33 ++ src/hooks/chain.ts | 51 +++ src/routeTree.gen.ts | 83 +++- src/routes/_layout.tsx | 10 + src/routes/_layout/parachains.tsx | 10 + src/routes/_layout/referenda/concludeds.tsx | 10 + src/routes/_layout/referenda/index.tsx | 10 + src/utils.ts | 4 + 14 files changed, 1011 insertions(+), 73 deletions(-) create mode 100644 src/components/info-header.tsx create mode 100644 src/features/coretime/components/parachains.tsx create mode 100644 src/features/governance/components/active-referenda.tsx create mode 100644 src/features/governance/components/concluded-referenda.tsx create mode 100644 src/features/governance/utils.ts create mode 100644 src/routes/_layout/parachains.tsx create mode 100644 src/routes/_layout/referenda/concludeds.tsx create mode 100644 src/routes/_layout/referenda/index.tsx diff --git a/panda.config.ts b/panda.config.ts index 742871f..d664281 100644 --- a/panda.config.ts +++ b/panda.config.ts @@ -1,10 +1,12 @@ import { defineConfig } from "@pandacss/dev"; import { createPreset } from "@park-ui/panda-preset"; +import amber from "@park-ui/panda-preset/colors/amber"; import blue from "@park-ui/panda-preset/colors/blue"; import crimson from "@park-ui/panda-preset/colors/crimson"; import green from "@park-ui/panda-preset/colors/green"; import neutral from "@park-ui/panda-preset/colors/neutral"; import red from "@park-ui/panda-preset/colors/red"; +import violet from "@park-ui/panda-preset/colors/violet"; import yellow from "@park-ui/panda-preset/colors/yellow"; export default defineConfig({ @@ -20,30 +22,55 @@ export default defineConfig({ extend: { tokens: { colors: { + [red.name]: red.tokens, [green.name]: green.tokens, [blue.name]: blue.tokens, [yellow.name]: yellow.tokens, - [red.name]: red.tokens, + [amber.name]: amber.tokens, success: green.tokens, info: blue.tokens, - warning: yellow.tokens, + warning: amber.tokens, error: red.tokens, + violet: violet.tokens, }, }, semanticTokens: { colors: { + [red.name]: red.semanticTokens, [green.name]: green.semanticTokens, [blue.name]: blue.semanticTokens, [yellow.name]: yellow.semanticTokens, - [red.name]: red.semanticTokens, + [amber.name]: amber.semanticTokens, success: green.semanticTokens, info: blue.semanticTokens, - warning: yellow.semanticTokens, + warning: amber.semanticTokens, error: red.semanticTokens, + violet: violet.semanticTokens, }, }, }, }, + staticCss: { + css: [ + { + properties: { + colorPalette: [ + "success", + "info", + "warning", + "error", + "violet", + "red", + "green", + "blue", + "yellow", + "amber", + "violet", + ], + }, + }, + ], + }, include: ["./src/**/*.{js,jsx,ts,tsx}"], jsxFramework: "react", outdir: "styled-system", diff --git a/src/components/info-header.tsx b/src/components/info-header.tsx new file mode 100644 index 0000000..1539a58 --- /dev/null +++ b/src/components/info-header.tsx @@ -0,0 +1,50 @@ +import { Heading } from "./ui/heading"; +import type { PropsWithChildren, ReactNode } from "react"; +import { css, cx } from "styled-system/css"; + +type InfoHeaderProps = PropsWithChildren<{ + className?: string | undefined; +}>; + +export const InfoHeader = Object.assign( + ({ className, children }: InfoHeaderProps) => ( +
*": { + flex: 1, + padding: "0 1rem", + }, + "@media(min-width: 68rem)": { + flexDirection: "row", + "&>*:not(:first-child)": { + borderLeft: "1px solid", + }, + }, + }), + )} + > + {children} +
+ ), + { Item: InfoHeaderItem }, +); + +type InfoHeaderItemProps = PropsWithChildren<{ + title: ReactNode; +}>; + +function InfoHeaderItem({ title, children }: InfoHeaderItemProps) { + return ( +
+
+ {title} +
+
{children}
+
+ ); +} diff --git a/src/features/coretime/components/parachains.tsx b/src/features/coretime/components/parachains.tsx new file mode 100644 index 0000000..8a2ef29 --- /dev/null +++ b/src/features/coretime/components/parachains.tsx @@ -0,0 +1,196 @@ +import { QueryRenderer, useLazyLoadQuery } from "@reactive-dot/react"; +import { Suspense } from "react"; +import { Center } from "styled-system/jsx"; +import { CircularProgressIndicator } from "~/components/circular-progress-indicator"; +import { InfoHeader } from "~/components/info-header"; +import { Badge } from "~/components/ui/badge"; +import { Table } from "~/components/ui/table"; +import { useRelayChainId } from "~/hooks/chain"; + +export function Parachains() { + return ( +
+ + + }> + + builder.readStorageEntries("Paras", "ParaLifecycles", []) + } + > + {(lifecycles) => lifecycles.length.toLocaleString()} + + + + + }> + builder.readStorage("Broker", "Status", [])} + > + {(data) => data?.core_count.toLocaleString() ?? "N/A"} + + + + + + + + } + > + + +
+ ); +} + +function ParachainsTable() { + const [ + [paraLifecycles, freeOnDemandEntries, affinityOnDemandEntries], + [workloads, reservations, leases], + ] = useLazyLoadQuery([ + { + chainId: "polkadot", + query: (builder) => + builder + .readStorageEntries("Paras", "ParaLifecycles", []) + .readStorage("OnDemand", "FreeEntries", []) + .readStorageEntries("OnDemand", "AffinityEntries", []), + }, + { + chainId: "polkadot_coretime", + query: (builder) => + builder + .readStorageEntries("Broker", "Workload", []) + .readStorage("Broker", "Reservations", []) + .readStorage("Broker", "Leases", []), + }, + ]); + + const systemParaIds = new Set( + reservations + .flat() + .map((reservation) => reservation.assignment) + .filter((x) => x.type === "Task") + .map((assignment) => assignment.value), + ); + + const legacyParaIds = new Set(leases.map((lease) => lease.task)); + + const taskCoresMap = workloads + .flatMap(([[core], loads]) => + loads.map((load) => ({ core, ...load.assignment })), + ) + .filter((assignment) => assignment.type === "Task") + .reduce( + (prev, curr) => + prev.set(curr.value, (prev.get(curr.core) ?? new Set()).add(curr.core)), + new Map>(), + ); + + const onDemandCoresMap = affinityOnDemandEntries + .flatMap(([[core], entries]) => + entries.map((entry) => ({ core, paraId: entry.para_id })), + ) + .reduce( + (prev, curr) => + prev.set( + curr.paraId, + (prev.get(curr.core) ?? new Set()).add(curr.core), + ), + new Map>(), + ); + + const activeOnDemandParaIds = new Set( + freeOnDemandEntries + .map((entry) => entry.para_id) + .concat( + affinityOnDemandEntries.flatMap(([_, entries]) => + entries.map((entry) => entry.para_id), + ), + ), + ); + + const parachains = paraLifecycles + .toSorted(([[a]], [[b]]) => a - b) + .map(([[paraId]]) => { + const type = (() => { + if (systemParaIds.has(paraId)) { + return "System parachain" as const; + } + + if (legacyParaIds.has(paraId)) { + return "Legacy parachain" as const; + } + + if (taskCoresMap.has(paraId)) { + return "Parachain" as const; + } + + if (onDemandCoresMap.has(paraId)) { + return "On-demand parachain" as const; + } + + return "On-demand parachain" as const; + })(); + + return { + type, + id: paraId, + active: taskCoresMap.has(paraId) || activeOnDemandParaIds.has(paraId), + cores: Array.from( + (taskCoresMap.get(paraId) ?? new Set())?.union( + onDemandCoresMap.get(paraId) ?? new Set(), + ) ?? [], + ), + }; + }); + + return ( + + + + Id + Type + Status + Core ID + + + + {parachains.map((parachain) => ( + + {parachain.id.toLocaleString()} + { + switch (parachain.type) { + case "System parachain": + return "current"; + case "Parachain": + return "green"; + case "Legacy parachain": + return "violet"; + case "On-demand parachain": + return "blue"; + } + })()}`} + > + {parachain.type} + + + {parachain.active ? "Active" : "Inactive"} + + + {parachain.cores.length > 0 ? parachain.cores.join(", ") : "N/A"} + + + ))} + + + ); +} diff --git a/src/features/explorer/components/statistics.tsx b/src/features/explorer/components/statistics.tsx index 5536819..39cb12a 100644 --- a/src/features/explorer/components/statistics.tsx +++ b/src/features/explorer/components/statistics.tsx @@ -9,8 +9,7 @@ import { } from "@reactive-dot/react"; import { differenceInMilliseconds, formatDuration } from "date-fns"; import { useEffect, useState } from "react"; -import { css, cx } from "styled-system/css"; -import { Heading } from "~/components/ui/heading"; +import { InfoHeader } from "~/components/info-header"; export type StatisticsProps = { className?: string | undefined; @@ -22,60 +21,21 @@ export function Statistics({ className }: StatisticsProps) { ); return ( -
*": { - flex: 1, - padding: "0 1rem", - }, - "@media(min-width: 68rem)": { - flexDirection: "row", - "&>*:not(:first-child)": { - borderLeft: "1px solid", - }, - }, - }), - )} - > -
-
- Block time -
-
- -
-
-
-
- Total issuance -
-
- {useNativeTokenAmountFromPlanck(totalIssuance).toLocaleString()} -
-
+ + + + + + {useNativeTokenAmountFromPlanck(totalIssuance).toLocaleString()} + -
-
- Last finalised block -
-
- -
-
-
-
- Last best block -
-
- -
-
-
+ + + + + + + ); } @@ -195,18 +155,13 @@ function TotalStaked() { ); return ( -
-
- - Total staked {chainId !== stakingChainId && `@ relay-chain`} - -
-
- {useNativeTokenAmountFromPlanck( - queryResult === idle ? 0n : queryResult[0] + queryResult[1], - { chainId: stakingChainId }, - ).toLocaleString()} -
-
+ + {useNativeTokenAmountFromPlanck( + queryResult === idle ? 0n : queryResult[0] + queryResult[1], + { chainId: stakingChainId }, + ).toLocaleString()} + ); } diff --git a/src/features/governance/components/active-referenda.tsx b/src/features/governance/components/active-referenda.tsx new file mode 100644 index 0000000..3afd5bd --- /dev/null +++ b/src/features/governance/components/active-referenda.tsx @@ -0,0 +1,393 @@ +import { useReferendumOffChainDiscussion } from "../utils"; +import type { PreimagesBounded } from ".papi/descriptors/dist"; +import { idle } from "@reactive-dot/core"; +import { + useLazyLoadQuery, + useNativeTokenAmountFromPlanck, + useTypedApi, +} from "@reactive-dot/react"; +import CloseIcon from "@w3f/polkadot-icons/solid/Close"; +import { Suspense, use, useMemo } from "react"; +import { css } from "styled-system/css"; +import { CircularProgressIndicator } from "~/components/circular-progress-indicator"; +import { CodecView } from "~/components/codec-view"; +import { InfoHeader } from "~/components/info-header"; +import { Badge } from "~/components/ui/badge"; +import { Code } from "~/components/ui/code"; +import { Dialog } from "~/components/ui/dialog"; +import { Heading } from "~/components/ui/heading"; +import { HoverCard } from "~/components/ui/hover-card"; +import { IconButton } from "~/components/ui/icon-button"; +import { Link } from "~/components/ui/link"; +import * as Progress from "~/components/ui/styled/progress"; +import { Table } from "~/components/ui/table"; +import { Text } from "~/components/ui/text"; +import { AccountListItem } from "~/features/accounts/components/account-list-item"; +import { useGovernanceChainId } from "~/hooks/chain"; +import { range } from "~/utils"; + +export function ActiveReferenda() { + const [referendumCount, decidingCount] = useLazyLoadQuery( + (builder) => + builder + .readStorage("Referenda", "ReferendumCount", []) + .readStorageEntries("Referenda", "DecidingCount", []), + { + chainId: useGovernanceChainId(), + }, + ); + + const activeReferendumCount = decidingCount.reduce( + (prev, curr) => prev + curr[1], + 0, + ); + + const activeRefRange = range( + referendumCount - activeReferendumCount, + referendumCount, + ); + + const activeReferenda = useLazyLoadQuery( + (builder) => + builder.readStorages( + "Referenda", + "ReferendumInfoFor", + activeRefRange.map((number) => [number] as const), + ), + { chainId: useGovernanceChainId() }, + ); + + const activeRefByTrack = Object.groupBy( + activeReferenda + .map((ref, index) => ({ ref, number: activeRefRange.at(index)! })) + .filter((ref) => ref.ref?.type === "Ongoing") + .map((ref) => ({ + ...(ref.ref?.type === "Ongoing" ? ref.ref.value : ({} as never)), + number: ref.number, + })), + (ref) => ref.track, + ); + + return ( +
+ + + {activeReferendumCount.toLocaleString()} + + + {referendumCount.toLocaleString()} + + + + + + Number + Submitted at + Proposer + Proposed + Discussion + Status + Tally + + + + {Object.entries(activeRefByTrack).map(([trackNumber, refs]) => ( + + ref.number) ?? []} + /> + + ))} + + +
+ ); +} + +type ReferendaTrackProps = ReferendumProps & { + refNumbers: number[]; +}; + +function ReferendaTrack({ number, refNumbers }: ReferendaTrackProps) { + return ( + <> + + + + Track {number.toLocaleString()} + + + + {refNumbers + .toSorted((a, b) => b - a) + .map((number) => ( + + ))} + + ); +} + +type ReferendumProps = { + number: number; +}; + +function ReferendumRow({ number }: ReferendumProps) { + const info = useLazyLoadQuery( + (builder) => + builder.readStorage("Referenda", "ReferendumInfoFor", [number]), + { chainId: useGovernanceChainId() }, + ); + + if (info === undefined) { + return null; + } + + switch (info.type) { + case "Approved": + case "Cancelled": + case "Killed": + case "Rejected": + case "TimedOut": + return null; + case "Ongoing": + return ( + + {number.toLocaleString()} + {info.value.submitted.toLocaleString()} + + + + + }> + + + + + + + + + {info.value.deciding === undefined + ? "Preparing" + : info.value.deciding.confirming !== undefined + ? "Confirming" + : "Deciding"} + + + + + + + ); + } +} + +type TallyProps = { ayes: bigint; nays: bigint; support: bigint }; + +function Tally({ ayes: _ayes, nays: _nays, support: _support }: TallyProps) { + const ayes = useNativeTokenAmountFromPlanck(_ayes); + const nays = useNativeTokenAmountFromPlanck(_nays); + const ayesPercent = ayes.valueOf() / (ayes.valueOf() + nays.valueOf()); + + const support = useNativeTokenAmountFromPlanck(_support); + + return ( + + +
*": { gridArea: "this" }, + })} + > + + + + + +
+
+
+
+ + + + + + + +
+
Ayes
+
+ {ayes.toLocaleString(undefined, { notation: "compact" })} ≈{" "} + {ayesPercent.toLocaleString(undefined, { style: "percent" })} +
+
Nays
+
+ {nays.toLocaleString(undefined, { notation: "compact" })} ≈{" "} + {(1 - ayesPercent).toLocaleString(undefined, { + style: "percent", + })} +
+
Support
+
+ {support.toLocaleString(undefined, { notation: "compact" })} +
+
+
+
+ + ); +} + +type ReferendaCallProps = { + proposal: PreimagesBounded; +}; + +function ReferendaCall({ proposal }: ReferendaCallProps) { + if (proposal.type === "Legacy") { + throw new Error("Legacy proposals can't be resolved"); + } + + const storagePreimage = useLazyLoadQuery( + (builder) => + proposal.type !== "Lookup" + ? undefined + : builder.readStorage("Preimage", "PreimageFor", [ + [proposal.value.hash, proposal.value.len], + ]), + { chainId: useGovernanceChainId() }, + ); + + const api = useTypedApi({ chainId: useGovernanceChainId() }); + + const preimage = + proposal.type === "Inline" + ? proposal.value + : storagePreimage === idle + ? undefined + : storagePreimage; + + const callPromise = useMemo( + () => + preimage === undefined + ? undefined + : api.txFromCallData(preimage).then((tx) => tx.decodedCall), + [api, preimage], + ); + + if (callPromise === undefined) { + return "N/A"; + } + + return ; +} + +type SuspendableReferendumCallProps = { + callPromise: Promise<{ + type: string; + value: { + type: string; + value: unknown; + }; + }>; +}; + +function SuspendableReferendumCall({ + callPromise, +}: SuspendableReferendumCallProps) { + const call = use(callPromise); + + return ( + + + + {call.type}.{call.value.type} + + + + + + + Decoded call + + + + + + + + + + + ); +} + +function ReferendumDiscussion({ number }: ReferendumProps) { + return ( + }> + + + ); +} + +type SuspendableReferndumDiscussionProps = { + dataPromise: ReturnType; +}; + +function SuspendableReferndumDiscussion({ + dataPromise, +}: SuspendableReferndumDiscussionProps) { + const data = use(dataPromise); + return ( + + {data.title || NO TITLE} + + ); +} diff --git a/src/features/governance/components/concluded-referenda.tsx b/src/features/governance/components/concluded-referenda.tsx new file mode 100644 index 0000000..5e5aa47 --- /dev/null +++ b/src/features/governance/components/concluded-referenda.tsx @@ -0,0 +1,108 @@ +import { useLazyLoadQuery } from "@reactive-dot/react"; +import { Suspense } from "react"; +import { Badge } from "~/components/ui/badge"; +import { Table } from "~/components/ui/table"; +import { AccountListItem } from "~/features/accounts/components/account-list-item"; +import { useGovernanceChainId } from "~/hooks/chain"; +import { range } from "~/utils"; + +export function ConcludedReferenda() { + const [referendumCount, decidingCount] = useLazyLoadQuery( + (builder) => + builder + .readStorage("Referenda", "ReferendumCount", []) + .readStorageEntries("Referenda", "DecidingCount", []), + { + chainId: useGovernanceChainId(), + }, + ); + + const activeReferendumCount = decidingCount.reduce( + (prev, curr) => prev + curr[1], + 0, + ); + + const concludedRange = range( + 0, + referendumCount - activeReferendumCount + 1, + ).toReversed(); + + return ( + + + + Referendum + Concluded at + Proposer + Outcome + + + + {concludedRange.map((number) => ( + + + + ))} + + + ); +} + +type ConcludedReferendaItemProps = { + number: number; +}; + +function ConcludedReferendaItem({ number }: ConcludedReferendaItemProps) { + const info = useLazyLoadQuery( + (builder) => + builder.readStorage("Referenda", "ReferendumInfoFor", [number]), + { chainId: useGovernanceChainId() }, + ); + + switch (info?.type) { + case "Ongoing": + case undefined: + return null; + case "Approved": + case "Cancelled": + case "Killed": + case "Rejected": + case "TimedOut": { + const [at, proposer] = + typeof info.value === "number" + ? ([info.value, undefined] as const) + : info.value; + + return ( + + {number.toLocaleString()} + {at.toLocaleString()} + + {proposer === undefined ? undefined : ( + + )} + + + { + switch (info.type) { + case "Approved": + return "success"; + case "TimedOut": + return "warning"; + case "Cancelled": + case "Killed": + case "Rejected": + return "error"; + } + })()} + > + {info.type} + + + + ); + } + } +} diff --git a/src/features/governance/utils.ts b/src/features/governance/utils.ts new file mode 100644 index 0000000..3563b46 --- /dev/null +++ b/src/features/governance/utils.ts @@ -0,0 +1,33 @@ +import { useMemo } from "react"; +import { useGovernanceChainId } from "~/hooks/chain"; + +export function useReferendumOffChainDiscussion(referendaNumber: number) { + const chainId = useGovernanceChainId(); + + const baseUrl = useMemo(() => { + switch (chainId) { + case "polkadot": + return new URL(`https://polkadot.subsquare.io`); + case "kusama": + return new URL("https://kusama.subsquare.io"); + case "paseo": + return new URL("https://paseo.subsquare.io"); + case "westend": + return new URL("https://westend.subsquare.io"); + } + }, [chainId]); + + return useMemo( + () => + fetch(new URL(`/api/gov2/referendums/${referendaNumber}`, baseUrl)) + .then( + (response) => + response.json() as Promise<{ title: string; content: string }>, + ) + .then((data) => ({ + ...data, + url: new URL(`/referenda/${referendaNumber}`, baseUrl), + })), + [baseUrl, referendaNumber], + ); +} diff --git a/src/hooks/chain.ts b/src/hooks/chain.ts index 5da9972..613dc71 100644 --- a/src/hooks/chain.ts +++ b/src/hooks/chain.ts @@ -23,6 +23,31 @@ export function useAuraChainId() { : undefined; } +export function useRelayChainId() { + switch (useChainId()) { + case "polkadot": + case "polkadot_asset_hub": + case "polkadot_collectives": + case "polkadot_coretime": + case "polkadot_people": + case "hydration": + case "invarch": + return "polkadot" satisfies ChainId; + case "kusama": + case "kusama_asset_hub": + case "kusama_people": + return "kusama" satisfies ChainId; + case "westend": + case "westend_asset_hub": + case "westend_collectives": + case "westend_people": + return "westend" satisfies ChainId; + case "paseo": + case "paseo_asset_hub": + return "paseo" satisfies ChainId; + } +} + export function usePeopleChainId() { switch (useChainId()) { case "polkadot": @@ -49,6 +74,32 @@ export function usePeopleChainId() { } } +export function useGovernanceChainId() { + switch (useChainId()) { + case "polkadot": + case "polkadot_asset_hub": + case "polkadot_collectives": + case "polkadot_coretime": + case "polkadot_people": + case "hydration": + case "invarch": + return "polkadot" satisfies ChainId; + case "kusama": + case "kusama_asset_hub": + case "kusama_people": + return "kusama" satisfies ChainId; + case "paseo": + case "paseo_asset_hub": + case "paseo_people": + return "paseo" satisfies ChainId; + case "westend": + case "westend_asset_hub": + case "westend_collectives": + case "westend_people": + return "westend" satisfies ChainId; + } +} + export function useStakingChainId() { switch (useChainId()) { case "polkadot": diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 15a9e53..ed0a074 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -16,12 +16,15 @@ import { Route as rootRoute } from './routes/__root' import { Route as LayoutImport } from './routes/_layout' import { Route as IndexImport } from './routes/index' import { Route as LayoutQueriesImport } from './routes/_layout/queries' +import { Route as LayoutParachainsImport } from './routes/_layout/parachains' import { Route as LayoutExtrinsicsImport } from './routes/_layout/extrinsics' import { Route as LayoutExplorerImport } from './routes/_layout/explorer' import { Route as LayoutAssetsImport } from './routes/_layout/assets' import { Route as LayoutUtilitiesIndexImport } from './routes/_layout/utilities/index' +import { Route as LayoutReferendaIndexImport } from './routes/_layout/referenda/index' import { Route as LayoutCollectivesIndexImport } from './routes/_layout/collectives/index' import { Route as LayoutUtilitiesLayoutImport } from './routes/_layout/utilities/_layout' +import { Route as LayoutReferendaConcludedsImport } from './routes/_layout/referenda/concludeds' import { Route as LayoutCollectivesLayoutImport } from './routes/_layout/collectives/_layout' import { Route as LayoutAccountsLayoutImport } from './routes/_layout/accounts/_layout' import { Route as LayoutAccountsLayoutIndexImport } from './routes/_layout/accounts/_layout/index' @@ -73,6 +76,12 @@ const LayoutQueriesRoute = LayoutQueriesImport.update({ getParentRoute: () => LayoutRoute, } as any) +const LayoutParachainsRoute = LayoutParachainsImport.update({ + id: '/parachains', + path: '/parachains', + getParentRoute: () => LayoutRoute, +} as any) + const LayoutExtrinsicsRoute = LayoutExtrinsicsImport.update({ id: '/extrinsics', path: '/extrinsics', @@ -97,6 +106,12 @@ const LayoutUtilitiesIndexRoute = LayoutUtilitiesIndexImport.update({ getParentRoute: () => LayoutUtilitiesRoute, } as any) +const LayoutReferendaIndexRoute = LayoutReferendaIndexImport.update({ + id: '/referenda/', + path: '/referenda/', + getParentRoute: () => LayoutRoute, +} as any) + const LayoutCollectivesIndexRoute = LayoutCollectivesIndexImport.update({ id: '/', path: '/', @@ -108,6 +123,12 @@ const LayoutUtilitiesLayoutRoute = LayoutUtilitiesLayoutImport.update({ getParentRoute: () => LayoutUtilitiesRoute, } as any) +const LayoutReferendaConcludedsRoute = LayoutReferendaConcludedsImport.update({ + id: '/referenda/concludeds', + path: '/referenda/concludeds', + getParentRoute: () => LayoutRoute, +} as any) + const LayoutCollectivesLayoutRoute = LayoutCollectivesLayoutImport.update({ id: '/_layout', getParentRoute: () => LayoutCollectivesRoute, @@ -191,6 +212,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LayoutExtrinsicsImport parentRoute: typeof LayoutImport } + '/_layout/parachains': { + id: '/_layout/parachains' + path: '/parachains' + fullPath: '/parachains' + preLoaderRoute: typeof LayoutParachainsImport + parentRoute: typeof LayoutImport + } '/_layout/queries': { id: '/_layout/queries' path: '/queries' @@ -226,6 +254,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LayoutCollectivesLayoutImport parentRoute: typeof LayoutCollectivesRoute } + '/_layout/referenda/concludeds': { + id: '/_layout/referenda/concludeds' + path: '/referenda/concludeds' + fullPath: '/referenda/concludeds' + preLoaderRoute: typeof LayoutReferendaConcludedsImport + parentRoute: typeof LayoutImport + } '/_layout/utilities': { id: '/_layout/utilities' path: '/utilities' @@ -247,6 +282,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LayoutCollectivesIndexImport parentRoute: typeof LayoutCollectivesImport } + '/_layout/referenda/': { + id: '/_layout/referenda/' + path: '/referenda' + fullPath: '/referenda' + preLoaderRoute: typeof LayoutReferendaIndexImport + parentRoute: typeof LayoutImport + } '/_layout/utilities/': { id: '/_layout/utilities/' path: '/' @@ -382,20 +424,26 @@ interface LayoutRouteChildren { LayoutAssetsRoute: typeof LayoutAssetsRoute LayoutExplorerRoute: typeof LayoutExplorerRoute LayoutExtrinsicsRoute: typeof LayoutExtrinsicsRoute + LayoutParachainsRoute: typeof LayoutParachainsRoute LayoutQueriesRoute: typeof LayoutQueriesRoute LayoutAccountsRoute: typeof LayoutAccountsRouteWithChildren LayoutCollectivesRoute: typeof LayoutCollectivesRouteWithChildren + LayoutReferendaConcludedsRoute: typeof LayoutReferendaConcludedsRoute LayoutUtilitiesRoute: typeof LayoutUtilitiesRouteWithChildren + LayoutReferendaIndexRoute: typeof LayoutReferendaIndexRoute } const LayoutRouteChildren: LayoutRouteChildren = { LayoutAssetsRoute: LayoutAssetsRoute, LayoutExplorerRoute: LayoutExplorerRoute, LayoutExtrinsicsRoute: LayoutExtrinsicsRoute, + LayoutParachainsRoute: LayoutParachainsRoute, LayoutQueriesRoute: LayoutQueriesRoute, LayoutAccountsRoute: LayoutAccountsRouteWithChildren, LayoutCollectivesRoute: LayoutCollectivesRouteWithChildren, + LayoutReferendaConcludedsRoute: LayoutReferendaConcludedsRoute, LayoutUtilitiesRoute: LayoutUtilitiesRouteWithChildren, + LayoutReferendaIndexRoute: LayoutReferendaIndexRoute, } const LayoutRouteWithChildren = @@ -407,11 +455,14 @@ export interface FileRoutesByFullPath { '/assets': typeof LayoutAssetsRoute '/explorer': typeof LayoutExplorerRoute '/extrinsics': typeof LayoutExtrinsicsRoute + '/parachains': typeof LayoutParachainsRoute '/queries': typeof LayoutQueriesRoute '/accounts': typeof LayoutAccountsLayoutRouteWithChildren '/collectives': typeof LayoutCollectivesLayoutRouteWithChildren + '/referenda/concludeds': typeof LayoutReferendaConcludedsRoute '/utilities': typeof LayoutUtilitiesLayoutRouteWithChildren '/collectives/': typeof LayoutCollectivesIndexRoute + '/referenda': typeof LayoutReferendaIndexRoute '/utilities/': typeof LayoutUtilitiesIndexRoute '/accounts/validators': typeof LayoutAccountsLayoutValidatorsRoute '/collectives/ambassador': typeof LayoutCollectivesLayoutAmbassadorRoute @@ -426,10 +477,13 @@ export interface FileRoutesByTo { '/assets': typeof LayoutAssetsRoute '/explorer': typeof LayoutExplorerRoute '/extrinsics': typeof LayoutExtrinsicsRoute + '/parachains': typeof LayoutParachainsRoute '/queries': typeof LayoutQueriesRoute '/accounts': typeof LayoutAccountsLayoutIndexRoute '/collectives': typeof LayoutCollectivesIndexRoute + '/referenda/concludeds': typeof LayoutReferendaConcludedsRoute '/utilities': typeof LayoutUtilitiesIndexRoute + '/referenda': typeof LayoutReferendaIndexRoute '/accounts/validators': typeof LayoutAccountsLayoutValidatorsRoute '/collectives/ambassador': typeof LayoutCollectivesLayoutAmbassadorRoute '/collectives/fellowship': typeof LayoutCollectivesLayoutFellowshipRoute @@ -443,14 +497,17 @@ export interface FileRoutesById { '/_layout/assets': typeof LayoutAssetsRoute '/_layout/explorer': typeof LayoutExplorerRoute '/_layout/extrinsics': typeof LayoutExtrinsicsRoute + '/_layout/parachains': typeof LayoutParachainsRoute '/_layout/queries': typeof LayoutQueriesRoute '/_layout/accounts': typeof LayoutAccountsRouteWithChildren '/_layout/accounts/_layout': typeof LayoutAccountsLayoutRouteWithChildren '/_layout/collectives': typeof LayoutCollectivesRouteWithChildren '/_layout/collectives/_layout': typeof LayoutCollectivesLayoutRouteWithChildren + '/_layout/referenda/concludeds': typeof LayoutReferendaConcludedsRoute '/_layout/utilities': typeof LayoutUtilitiesRouteWithChildren '/_layout/utilities/_layout': typeof LayoutUtilitiesLayoutRouteWithChildren '/_layout/collectives/': typeof LayoutCollectivesIndexRoute + '/_layout/referenda/': typeof LayoutReferendaIndexRoute '/_layout/utilities/': typeof LayoutUtilitiesIndexRoute '/_layout/accounts/_layout/validators': typeof LayoutAccountsLayoutValidatorsRoute '/_layout/collectives/_layout/ambassador': typeof LayoutCollectivesLayoutAmbassadorRoute @@ -467,11 +524,14 @@ export interface FileRouteTypes { | '/assets' | '/explorer' | '/extrinsics' + | '/parachains' | '/queries' | '/accounts' | '/collectives' + | '/referenda/concludeds' | '/utilities' | '/collectives/' + | '/referenda' | '/utilities/' | '/accounts/validators' | '/collectives/ambassador' @@ -485,10 +545,13 @@ export interface FileRouteTypes { | '/assets' | '/explorer' | '/extrinsics' + | '/parachains' | '/queries' | '/accounts' | '/collectives' + | '/referenda/concludeds' | '/utilities' + | '/referenda' | '/accounts/validators' | '/collectives/ambassador' | '/collectives/fellowship' @@ -500,14 +563,17 @@ export interface FileRouteTypes { | '/_layout/assets' | '/_layout/explorer' | '/_layout/extrinsics' + | '/_layout/parachains' | '/_layout/queries' | '/_layout/accounts' | '/_layout/accounts/_layout' | '/_layout/collectives' | '/_layout/collectives/_layout' + | '/_layout/referenda/concludeds' | '/_layout/utilities' | '/_layout/utilities/_layout' | '/_layout/collectives/' + | '/_layout/referenda/' | '/_layout/utilities/' | '/_layout/accounts/_layout/validators' | '/_layout/collectives/_layout/ambassador' @@ -550,10 +616,13 @@ export const routeTree = rootRoute "/_layout/assets", "/_layout/explorer", "/_layout/extrinsics", + "/_layout/parachains", "/_layout/queries", "/_layout/accounts", "/_layout/collectives", - "/_layout/utilities" + "/_layout/referenda/concludeds", + "/_layout/utilities", + "/_layout/referenda/" ] }, "/_layout/assets": { @@ -568,6 +637,10 @@ export const routeTree = rootRoute "filePath": "_layout/extrinsics.tsx", "parent": "/_layout" }, + "/_layout/parachains": { + "filePath": "_layout/parachains.tsx", + "parent": "/_layout" + }, "/_layout/queries": { "filePath": "_layout/queries.tsx", "parent": "/_layout" @@ -603,6 +676,10 @@ export const routeTree = rootRoute "/_layout/collectives/_layout/fellowship" ] }, + "/_layout/referenda/concludeds": { + "filePath": "_layout/referenda/concludeds.tsx", + "parent": "/_layout" + }, "/_layout/utilities": { "filePath": "_layout/utilities", "parent": "/_layout", @@ -622,6 +699,10 @@ export const routeTree = rootRoute "filePath": "_layout/collectives/index.tsx", "parent": "/_layout/collectives" }, + "/_layout/referenda/": { + "filePath": "_layout/referenda/index.tsx", + "parent": "/_layout" + }, "/_layout/utilities/": { "filePath": "_layout/utilities/index.tsx", "parent": "/_layout/utilities" diff --git a/src/routes/_layout.tsx b/src/routes/_layout.tsx index 9a4f66e..5c6ba67 100644 --- a/src/routes/_layout.tsx +++ b/src/routes/_layout.tsx @@ -214,6 +214,16 @@ function Layout() { Assets + + + Referenda + + ; +} diff --git a/src/routes/_layout/referenda/concludeds.tsx b/src/routes/_layout/referenda/concludeds.tsx new file mode 100644 index 0000000..e6fcffb --- /dev/null +++ b/src/routes/_layout/referenda/concludeds.tsx @@ -0,0 +1,10 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { ConcludedReferenda } from "~/features/governance/components/concluded-referenda"; + +export const Route = createFileRoute("/_layout/referenda/concludeds")({ + component: ConcludedReferendaPage, +}); + +function ConcludedReferendaPage() { + return ; +} diff --git a/src/routes/_layout/referenda/index.tsx b/src/routes/_layout/referenda/index.tsx new file mode 100644 index 0000000..8135666 --- /dev/null +++ b/src/routes/_layout/referenda/index.tsx @@ -0,0 +1,10 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { ActiveReferenda } from "~/features/governance/components/active-referenda"; + +export const Route = createFileRoute("/_layout/referenda/")({ + component: ReferendaPage, +}); + +function ReferendaPage() { + return ; +} diff --git a/src/utils.ts b/src/utils.ts index a94b29a..fd9d527 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -97,3 +97,7 @@ export function getIdentityDisplayValue( export function ellipsize(value: string, length: number) { return value.slice(0, length) + "..." + value.slice(-length); } + +export function range(start: number, end: number) { + return Array.from({ length: end - start }, (_, k) => k + start); +}