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} -
+ Your account is restricted from submitting contributions. +
+- +{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;