diff --git a/app/components/common/CircularProgress.tsx b/app/components/common/CircularProgress.tsx
new file mode 100644
index 0000000..71ea257
--- /dev/null
+++ b/app/components/common/CircularProgress.tsx
@@ -0,0 +1,57 @@
+import { Intent } from "@blueprintjs/core";
+
+interface CircularProgressProps {
+ value: number; // 0 to 1
+ size?: number;
+ strokeWidth?: number;
+ intent?: Intent;
+}
+
+export function CircularProgress({ value, size = 16, strokeWidth = 2, intent = Intent.PRIMARY }: CircularProgressProps) {
+ const radius = (size - strokeWidth) / 2;
+ const circumference = radius * 2 * Math.PI;
+ const offset = circumference - value * circumference;
+
+ const getIntentColor = (intent: Intent) => {
+ switch (intent) {
+ case Intent.PRIMARY:
+ return "var(--blue-500, #106ba3)";
+ case Intent.SUCCESS:
+ return "var(--green-500, #0d8050)";
+ case Intent.WARNING:
+ return "var(--orange-500, #bf7326)";
+ case Intent.DANGER:
+ return "var(--red-500, #c23030)";
+ default:
+ return "var(--blue-500, #106ba3)";
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/app/components/governance/MotionDetails.tsx b/app/components/governance/MotionDetails.tsx
index a4740d8..6f43a0a 100644
--- a/app/components/governance/MotionDetails.tsx
+++ b/app/components/governance/MotionDetails.tsx
@@ -1,4 +1,4 @@
-import { Card, Classes, Tag, Intent, Button, Icon, ProgressBar } from "@blueprintjs/core";
+import { Card, Tag, Intent, Button, Icon, ProgressBar } from "@blueprintjs/core";
import { useTranslation } from "react-i18next";
import { DeriveCollectiveProposal } from "@polkadot/api-derive/types";
import { AccountName } from "app/components/common/AccountName";
@@ -6,12 +6,11 @@ import { formatBalance } from "@polkadot/util";
import { BountyDetails } from "./BountyDetails";
import { useApi } from "app/components/Api";
import { useEffect, useState } from "react";
-import { formatDuration } from "app/utils/time";
+import { formatTimeLeft } from "app/utils/time";
import { useLocation } from "@remix-run/react";
import useToaster from "app/hooks/useToaster";
// Constants
-const BLOCK_TIME_SECONDS = 60;
const DEFAULT_VOTING_PERIOD = 2880; // About 2 days with 60-second blocks
function TimeRemaining({ motion }: { motion: DeriveCollectiveProposal }) {
@@ -96,23 +95,7 @@ function TimeRemaining({ motion }: { motion: DeriveCollectiveProposal }) {
return null;
}
- const formatTimeLeft = (blocks: number) => {
- const seconds = blocks * BLOCK_TIME_SECONDS;
- const hours = Math.floor(seconds / 3600);
- const days = Math.floor(hours / 24);
- const remainingHours = hours % 24;
-
- if (days > 0) {
- return `${days}d ${remainingHours}h left`;
- }
- if (hours > 0) {
- return `${hours}h left`;
- }
- const minutes = Math.ceil(seconds / 60);
- return `${minutes}m left`;
- };
-
- const display = timeRemaining > 0 ? formatTimeLeft(timeRemaining) : "(ended)";
+ const display = timeRemaining > 0 ? formatTimeLeft(timeRemaining) + " left" : "(ended)";
return (
diff --git a/app/routes/governance.members.tsx b/app/routes/governance.members.tsx
index 81485a8..e9f47bc 100644
--- a/app/routes/governance.members.tsx
+++ b/app/routes/governance.members.tsx
@@ -1,11 +1,13 @@
import { useApi } from "../components/Api";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
-import { Card, Elevation, Spinner, Tag, Intent, HTMLTable, Classes } from "@blueprintjs/core";
+import { Spinner, Tag, Intent, HTMLTable, Classes } from "@blueprintjs/core";
import type { AccountId } from "@polkadot/types/interfaces";
import type { DeriveElectionsInfo, DeriveCouncilVotes } from "@polkadot/api-derive/types";
import { formatBalance } from "@polkadot/util";
import { AccountName, extractIdentity } from "app/components/common/AccountName";
+import { formatTimeLeft } from "app/utils/time";
+import { CircularProgress } from "app/components/common/CircularProgress";
interface CouncilMember {
address: string;
@@ -41,6 +43,12 @@ export default function GovernanceMembers() {
const [loading, setLoading] = useState(true);
const [electionsInfo, setElectionsInfo] = useState
(null);
const [allVotes, setAllVotes] = useState>({});
+ const [termProgress, setTermProgress] = useState<{
+ current: number;
+ total: number;
+ blockNumber: number;
+ mod: number;
+ } | null>(null);
useEffect(() => {
if (!api) return;
@@ -64,6 +72,38 @@ export default function GovernanceMembers() {
.catch(console.error);
}, [api]);
+ useEffect(() => {
+ async function fetchTermProgress() {
+ if (!api) return;
+
+ try {
+ // Try all possible module paths for term duration
+ const termDuration =
+ api.consts.elections?.termDuration || api.consts.phragmenElection?.termDuration || api.consts.electionsPhragmen?.termDuration;
+
+ const blockNumber = await api.derive.chain.bestNumber();
+
+ if (termDuration) {
+ const total = parseInt(termDuration.toString());
+ const current = parseInt(blockNumber.toString());
+ const mod = current % total;
+ const remaining = total - mod;
+
+ setTermProgress({
+ current: remaining,
+ total,
+ blockNumber: current,
+ mod,
+ });
+ }
+ } catch (error) {
+ console.error("Error calculating term progress:", error);
+ }
+ }
+
+ fetchTermProgress();
+ }, [api]);
+
useEffect(() => {
async function fetchCouncilData() {
if (!api || !electionsInfo) return;
@@ -110,9 +150,31 @@ export default function GovernanceMembers() {
return ;
}
+ const termProgressPercentage = termProgress ? ((termProgress.total - termProgress.current) / termProgress.total) * 100 : 0;
+ const remainingBlocks = termProgress ? termProgress.total - termProgress.mod : 0;
+ const timeLeft = formatTimeLeft(remainingBlocks);
+ const totalTime = termProgress ? formatTimeLeft(termProgress.total) : "";
+
return (
-
{t("governance.council_members")}
+
+
{t("governance.council_members")}
+ {termProgress && (
+
+
+
+ {t("governance.term_progress")} ({termProgressPercentage.toFixed(1)}%)
+
+
+
+
+ {timeLeft} {t("governance.remaining")} / {totalTime}
+
+
+
+
+ )}
+
diff --git a/app/translations/en.json b/app/translations/en.json
index 57e88d5..4ebe0ff 100644
--- a/app/translations/en.json
+++ b/app/translations/en.json
@@ -261,7 +261,10 @@
"bounty_next_action_update_overdue": "Update from curator is overdue",
"bounty_next_action_pending_payout": "Waiting for payout",
"bounty_next_action_claim_payout": "Claim payout",
- "no_motions": "No motions available."
+ "no_motions": "No motions available.",
+ "term_progress": "Term Progress",
+ "blocks_remaining": "blocks remaining",
+ "remaining": "remaining"
}
}
}
diff --git a/app/utils/time.ts b/app/utils/time.ts
index e878e37..a1cbeeb 100644
--- a/app/utils/time.ts
+++ b/app/utils/time.ts
@@ -1,7 +1,22 @@
-export function formatDuration(secondsDuration) {
- /**
- * Return a string representing the duration in seconds in format like "54 seconds" or "4 minutes 3 seconds"
- */
+export const BLOCK_TIME_SECONDS = 60;
+
+export function formatTimeLeft(blocks: number): string {
+ const totalSeconds = blocks * BLOCK_TIME_SECONDS;
+ const totalHours = totalSeconds / 3600;
+ const days = Math.floor(totalHours / 24);
+ const remainingHours = Math.floor(totalHours % 24);
+
+ if (days > 0) {
+ return remainingHours > 0 ? `${days}d ${remainingHours}h` : `${days}d`;
+ }
+ if (totalHours >= 1) {
+ return `${Math.floor(totalHours)}h`;
+ }
+ const minutes = Math.ceil(totalSeconds / 60);
+ return `${minutes}m`;
+}
+
+export function formatDuration(secondsDuration: number): string {
const hours = Math.floor(secondsDuration / 3600);
const minutes = Math.floor((secondsDuration - hours * 3600) / 60);
const seconds = secondsDuration - hours * 3600 - minutes * 60;