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 ( + + {/* Background circle */} + + {/* Progress circle */} + + + ); +} 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;