diff --git a/src/components/proposals/ProposalCard.tsx b/src/components/proposals/ProposalCard.tsx index bb8d2376..23f598f7 100644 --- a/src/components/proposals/ProposalCard.tsx +++ b/src/components/proposals/ProposalCard.tsx @@ -16,6 +16,8 @@ import { useProposalVote } from "@/hooks/useProposalVote"; import { Button } from "@/components/ui/button"; import { RefreshCw, AlertCircle } from "lucide-react"; import { motion } from "framer-motion"; +import { safeNumberFromBigInt } from "@/utils/proposal"; +import { cn } from "@/lib/utils"; interface ProposalCardProps { proposal: Proposal | ProposalWithDAO; @@ -31,14 +33,16 @@ export default function ProposalCard({ const router = useRouter(); // Use the unified status system - const { statusConfig, isActive, isPassed } = useProposalStatus(proposal); + const { status, statusConfig, isActive } = useProposalStatus(proposal); // Use centralized vote hook for consistent data fetching const { voteDisplayData, + calculations, error: hasVoteDataError, refreshVoteData, isLoading: isLoadingVotes, + vetoCheck, } = useProposalVote({ proposal, contractPrincipal: proposal.contract_principal, @@ -90,6 +94,43 @@ export default function ProposalCard({ // const liquidTokens = Number(proposal.liquid_tokens); const { totalVotes, hasVoteData } = voteSummary; + // Enhanced calculations for quorum and threshold display + const enhancedCalculations = useMemo(() => { + if (!calculations) return null; + + const quorumPercentage = safeNumberFromBigInt(proposal.voting_quorum); + const thresholdPercentage = safeNumberFromBigInt(proposal.voting_threshold); + + // Calculate if requirements are met + const metQuorum = calculations.participationRate >= quorumPercentage; + const metThreshold = + calculations.totalVotes > 0 + ? calculations.approvalRate >= thresholdPercentage + : false; + + return { + ...calculations, + quorumPercentage, + thresholdPercentage, + metQuorum, + metThreshold, + }; + }, [calculations, proposal]); + + // Helper function for status display + const getStatusText = (met: boolean, percentage?: number) => { + // If voting hasn't started (PENDING, DRAFT), show "Pending" + if (status === "PENDING" || status === "DRAFT") { + return "Pending"; + } + + if (isActive) { + return percentage !== undefined ? `${percentage.toFixed(1)}%` : "0%"; + } + + return met ? "Passed" : "Failed"; + }; + // Memoize DAO info const daoInfo = useMemo(() => { const proposalWithDAO = proposal as ProposalWithDAO; @@ -130,12 +171,93 @@ export default function ProposalCard({ ? `#${proposal.proposal_id}: ${proposal.title}` : proposal.title} -
+
+ + {/* Quorum and Threshold Badges */} + {enhancedCalculations && + statusConfig.label !== "Pending" && + statusConfig.label !== "Draft" && ( + <> + {/* Quorum Badge */} +
+ Quorum:{" "} + + {getStatusText( + enhancedCalculations.metQuorum, + enhancedCalculations.participationRate + )} + +
+ + {/* Threshold Badge */} +
+ + Threshold: + {" "} + + {getStatusText( + enhancedCalculations.metThreshold, + enhancedCalculations.approvalRate + )} + +
+ + )} + + {/* Veto Override Warning Badge */} + {vetoCheck?.vetoExceedsForVote && !isActive && ( +
+ ⚠️ Vetoed +
+ )} +
@@ -347,28 +469,11 @@ export default function ProposalCard({
)} - {/* Completed Status */} - {/* {isPassed && ( -
- Final result: - - - For - - ,{" "} - - {" "} - Against - - -
- )} */} - - {/* Enhanced Chart Section for detailed view - Hide for pending proposals */} + {/* Vote Status Chart - Show for active, veto period, execution window, passed, and failed proposals */} {(isActive || statusConfig.label === "Veto Period" || statusConfig.label === "Execution Window" || - isPassed || + statusConfig.label === "Passed" || statusConfig.label === "Failed") && statusConfig.label !== "Pending" && (
diff --git a/src/components/proposals/ProposalSubmission.tsx b/src/components/proposals/ProposalSubmission.tsx index f8fc91d8..f0791827 100644 --- a/src/components/proposals/ProposalSubmission.tsx +++ b/src/components/proposals/ProposalSubmission.tsx @@ -1319,6 +1319,28 @@ export function ProposalSubmission({
)} + + {/* Blocked User Lock Overlay */} + {hasAccessToken && + !needsXLink && + !isXLoading && + profile?.is_blocked === true && ( +
+
+
+ +
+
+

+ Account Restricted +

+

+ Your account is restricted from submitting contributions. +

+
+
+
+ )} {/* Airdrop notification - Commented out per user request */} {/* {hasAccessToken && (
@@ -1694,7 +1716,8 @@ export function ProposalSubmission({ isLoadingEmbed || !twitterEmbedData || !!xUsernameError || - !canSubmitContribution + !canSubmitContribution || + profile?.is_blocked === true } className="bg-primary hover:bg-primary/90 text-primary-foreground font-bold px-6 py-2 text-sm rounded-sm shadow-md hover:shadow-lg transition-all duration-200" > @@ -1746,6 +1769,11 @@ export function ProposalSubmission({ Loading Post Content...
+ ) : profile?.is_blocked === true ? ( +
+ + Account Restricted +
) : (
diff --git a/src/components/proposals/VoteStatusChart.tsx b/src/components/proposals/VoteStatusChart.tsx index 8c884ac8..94c15e6f 100644 --- a/src/components/proposals/VoteStatusChart.tsx +++ b/src/components/proposals/VoteStatusChart.tsx @@ -1,10 +1,11 @@ "use client"; -import { useState, useCallback } from "react"; +import { useState, useCallback, useMemo } from "react"; import { TokenBalance } from "@/components/reusables/BalanceDisplay"; import { Button } from "@/components/ui/button"; import { RefreshCw, AlertCircle } from "lucide-react"; import { useProposalVote } from "@/hooks/useProposalVote"; +import { safeNumberFromBigInt } from "@/utils/proposal"; import type { Proposal, ProposalWithDAO } from "@/types"; interface VoteStatusChartProps { @@ -22,7 +23,7 @@ const VoteStatusChart = ({ initialVotesAgainst, // refreshing = false, tokenSymbol = "", - liquidTokens, + // liquidTokens, proposal, }: VoteStatusChartProps) => { const [localRefreshing, setLocalRefreshing] = useState(false); @@ -62,6 +63,34 @@ const VoteStatusChart = ({ [refreshVoteData] ); + // Calculate progress bar size based on quorum (must be before early returns) + const progressBarCalculations = useMemo(() => { + if (!proposal || !calculations) return null; + + const quorumPercentage = safeNumberFromBigInt(proposal.voting_quorum); + const liquidTokensNum = calculations.liquidTokensNum; + + // Calculate quorum amount in tokens + const quorumAmount = (liquidTokensNum * quorumPercentage) / 100; + + // Bar width is quorum + 10%, or total votes if exceeded + const barWidth = Math.max(quorumAmount * 1.1, calculations.totalVotes); + + // Calculate percentages based on the bar width + const votesForPercentage = (calculations.votesForNum / barWidth) * 100; + const votesAgainstPercentage = + (calculations.votesAgainstNum / barWidth) * 100; + const quorumLinePercentage = (quorumAmount / barWidth) * 100; + + return { + votesForPercentage, + votesAgainstPercentage, + quorumLinePercentage, + quorumAmount, + barWidth, + }; + }, [proposal, calculations]); + // Show loading state if (isLoadingVotes && !error) { return ( @@ -100,7 +129,7 @@ const VoteStatusChart = ({ ); } - if (!voteDisplayData || !calculations) { + if (!voteDisplayData || !calculations || !progressBarCalculations) { return (
@@ -110,8 +139,6 @@ const VoteStatusChart = ({ ); } - // const isRefreshingAny = localRefreshing || refreshing; - // Main vote display return (
@@ -122,17 +149,29 @@ const VoteStatusChart = ({
{/* Votes against (red) */}
+ {/* Quorum line indicator */} +
+
+
@@ -171,26 +210,6 @@ const VoteStatusChart = ({ className="font-medium sm:hidden" />
- - {/* Liquid Tokens - Right */} - {liquidTokens && Number(liquidTokens) > 0 && ( -
- Liquid Token: - - -
- )}
); diff --git a/src/components/proposals/VotesTable.tsx b/src/components/proposals/VotesTable.tsx index 69af88d6..c3c35108 100644 --- a/src/components/proposals/VotesTable.tsx +++ b/src/components/proposals/VotesTable.tsx @@ -170,9 +170,9 @@ const VotesTable = ({ proposalId, limit }: VotesTableProps) => { - {displayedVotes.map((vote, index) => ( + {displayedVotes.map((vote) => ( {/* Voter */} diff --git a/src/components/proposals/VotingProgressChart.tsx b/src/components/proposals/VotingProgressChart.tsx index 9a42d253..3facf42b 100644 --- a/src/components/proposals/VotingProgressChart.tsx +++ b/src/components/proposals/VotingProgressChart.tsx @@ -57,6 +57,7 @@ const VotingProgressChart = ({ error: voteDataError, hasData, refreshVoteData, + vetoCheck, } = useProposalVote({ proposal, contractPrincipal, @@ -172,6 +173,17 @@ const VotingProgressChart = ({ const getResultStatus = () => { const StatusIcon = statusConfig.icon; + // Check if veto amount exceeds For votes - this overrides other status + if (vetoCheck.vetoExceedsForVote && !isActive) { + return { + status: "Failed", + color: "text-destructive", + icon: , + bgColor: "bg-destructive/10 border-destructive/20", + reason: "Veto amount exceeds For votes", + }; + } + switch (status) { case "DRAFT": return { @@ -742,6 +754,13 @@ const VotingProgressChart = ({ />
+ {/* Veto override message */} + {resultStatus.reason && ( +
+ ⚠️ {resultStatus.reason} +
+ )} + {/* Additional status-specific information */} {status === "EXECUTION_WINDOW" && (
@@ -760,7 +779,8 @@ const VotingProgressChart = ({ )} {status === "FAILED" && enhancedCalculations.metQuorum && - enhancedCalculations.metThreshold && ( + enhancedCalculations.metThreshold && + !resultStatus.reason && (
⚠️ Failed despite meeting requirements
diff --git a/src/components/proposals/sections/VetosSection.tsx b/src/components/proposals/sections/VetosSection.tsx index 29e53e85..0767037d 100644 --- a/src/components/proposals/sections/VetosSection.tsx +++ b/src/components/proposals/sections/VetosSection.tsx @@ -144,11 +144,6 @@ function VetosContent({
))} - {vetos && vetos.length > 3 && ( -

- +{vetos.length - 3} more vetos -

- )} ); diff --git a/src/hooks/useProposalVote.ts b/src/hooks/useProposalVote.ts index 3a5d92ee..79d5441e 100644 --- a/src/hooks/useProposalVote.ts +++ b/src/hooks/useProposalVote.ts @@ -3,6 +3,7 @@ import { useQuery, useQueryClient } from "@tanstack/react-query"; import { fetchProposalVotes } from "@/services/vote.service"; import { useProposalStatus } from "@/hooks/useProposalStatus"; import { useSmartCacheBusting } from "@/hooks/useSmartCacheBusting"; +import { useVetoCheck } from "@/hooks/useVetoCheck"; import type { Proposal, ProposalWithDAO } from "@/types"; interface UseProposalVoteProps { @@ -270,6 +271,12 @@ export function useProposalVote({ return !voteDisplayData && primaryQuery.error; }, [voteDisplayData, primaryQuery.error]); + // Check if veto amount exceeds For votes + const vetoCheck = useVetoCheck({ + proposal, + votesForNum: calculations?.votesForNum || 0, + }); + return { // Data voteDisplayData, @@ -291,6 +298,9 @@ export function useProposalVote({ // Raw data rawData: activeVoteData, + // Veto check + vetoCheck, + // Debug info cacheInfo: { shouldAlwaysBustCache, diff --git a/src/hooks/useVetoCheck.ts b/src/hooks/useVetoCheck.ts new file mode 100644 index 00000000..d85b90b3 --- /dev/null +++ b/src/hooks/useVetoCheck.ts @@ -0,0 +1,62 @@ +import type { Proposal, ProposalWithDAO } from "@/types"; +import { useMemo } from "react"; +import { useProposalVetos } from "@/hooks/useVetos"; + +interface UseVetoCheckProps { + proposal: Proposal | ProposalWithDAO; + votesForNum?: number; +} + +interface VetoCheckResult { + totalVetoAmount: number; + rawTotalVetoAmount: string; + vetoExceedsForVote: boolean; + isLoading: boolean; + error: boolean; +} + +/** + * Hook to check if veto amount exceeds the For votes + * Vetos are formatted the same way as votes (divided by 1e8) + */ +export function useVetoCheck({ + proposal, + votesForNum = 0, +}: UseVetoCheckProps): VetoCheckResult { + const { data: vetos, isLoading, error } = useProposalVetos(proposal.id || ""); + + // Calculate total veto amount + const vetoCalculations = useMemo(() => { + if (!vetos || vetos.length === 0) { + return { + totalVetoAmount: 0, + rawTotalVetoAmount: "0", + vetoExceedsForVote: false, + }; + } + + // Sum up all veto amounts + const totalRaw = vetos.reduce((sum, veto) => { + const amount = veto.amount ? parseFloat(veto.amount) : 0; + return sum + amount; + }, 0); + + // Format veto amount the same way as votes (divide by 1e8) + const totalFormatted = totalRaw; + + // Check if veto exceeds For votes + const vetoExceedsForVote = totalFormatted > votesForNum; + + return { + totalVetoAmount: totalFormatted, + rawTotalVetoAmount: totalRaw.toString(), + vetoExceedsForVote, + }; + }, [vetos, votesForNum]); + + return { + ...vetoCalculations, + isLoading, + error: !!error, + }; +} diff --git a/src/types/user.ts b/src/types/user.ts index c5374a00..321d430b 100644 --- a/src/types/user.ts +++ b/src/types/user.ts @@ -5,6 +5,7 @@ export interface Profile { username: string | null; // Twitter username provider_id: string | null; // Twitter/X provider user ID is_verified: boolean | null; // X verification status: true=verified, false=not verified, null=pending + is_blocked: boolean; // Whether the user account is blocked mainnet_address: string | null; testnet_address: string | null; role?: string;