From f769e00f5263123186aab5641f78b7ea86947d72 Mon Sep 17 00:00:00 2001 From: SONIA Date: Fri, 26 Jun 2026 22:21:57 +0000 Subject: [PATCH] feat: add user dashboard and wallet summary to home page (#282) - Add UserDashboard component with wallet balance, active proposal count, voted count, and unvoted notification nudge - Show per-proposal vote history (icon + type) for connected wallet - Quick-access 'New Proposal' button and disconnect button - Wire WalletProvider in main.tsx so wallet state is shared - App.tsx now uses useWallet() hook instead of local state --- frontend/src/App.tsx | 75 ++++++---- frontend/src/components/UserDashboard.tsx | 158 ++++++++++++++++++++++ frontend/src/main.tsx | 5 +- 3 files changed, 212 insertions(+), 26 deletions(-) create mode 100644 frontend/src/components/UserDashboard.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 0e2f46c..819461f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,24 +1,26 @@ import { useState, useEffect, useMemo } from 'react'; -import type { Proposal, ProposalState } from './types'; -import { fetchAllProposals, fetchTokenBalance, fetchTokenDecimals } from './api'; +import type { Proposal, ProposalState, VoteRecord } from './types'; +import { fetchAllProposals, fetchTokenDecimals, fetchVoteRecord } from './api'; import { ProposalCard } from './components/ProposalCard'; import { ProposalSkeleton } from './components/ProposalSkeleton'; import { ProposalDetail } from './components/ProposalDetail'; +import { UserDashboard } from './components/UserDashboard'; +import { useWallet } from './WalletContext'; import { ACTIVE_NETWORK } from './config'; -import { formatTokenAmount } from './utils'; const ALL_STATES: ProposalState[] = ['Active', 'Passed', 'Rejected', 'Executed', 'Cancelled']; export default function App() { + const { walletAddress, tokenBalance, connect, disconnect } = useWallet(); + const [proposals, setProposals] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [search, setSearch] = useState(''); const [stateFilter, setStateFilter] = useState('All'); const [selected, setSelected] = useState(null); - const [walletAddress, setWalletAddress] = useState(null); - const [tokenBalance, setTokenBalance] = useState(null); const [decimals, setDecimals] = useState(0); + const [votedMap, setVotedMap] = useState>(new Map()); useEffect(() => { Promise.all([fetchAllProposals(), fetchTokenDecimals()]) @@ -30,6 +32,25 @@ export default function App() { .finally(() => setLoading(false)); }, []); + // Load vote records for the connected wallet + useEffect(() => { + if (!walletAddress || proposals.length === 0) { + setVotedMap(new Map()); + return; + } + Promise.all( + proposals.map(p => + fetchVoteRecord(Number(p.id), walletAddress).then(r => [p.id, r] as const) + ) + ).then(entries => { + const map = new Map(); + entries.forEach(([id, record]) => { + if (record) map.set(id, record); + }); + setVotedMap(map); + }); + }, [walletAddress, proposals]); + const filtered = useMemo(() => { return proposals.filter(p => { const matchState = stateFilter === 'All' || p.state === stateFilter; @@ -47,26 +68,30 @@ export default function App() {

๐ŸŒŒ CosmosVote

On-chain governance ยท {ACTIVE_NETWORK} -
- {walletAddress ? ( -
-
{walletAddress.slice(0, 6)}...{walletAddress.slice(-4)}
- {tokenBalance !== null && ( -
{formatTokenAmount(tokenBalance, decimals)}
- )} -
- ) : ( - - )} -
+ {!walletAddress && ( + + )}
+ {/* User dashboard โ€” shown only when wallet is connected */} + {walletAddress && ( + alert('Proposal creation coming soon.')} + onDisconnect={disconnect} + /> + )} + {/* Filters */}
Error: {error}

} - +
{loading && ( <> @@ -118,7 +143,7 @@ export default function App() {

No proposals found.

)} {!loading && filtered.map(p => ( - setSelected(p)} /> + setSelected(p)} /> ))}
@@ -133,4 +158,4 @@ export default function App() { )} ); -} \ No newline at end of file +} diff --git a/frontend/src/components/UserDashboard.tsx b/frontend/src/components/UserDashboard.tsx new file mode 100644 index 0000000..fe2f124 --- /dev/null +++ b/frontend/src/components/UserDashboard.tsx @@ -0,0 +1,158 @@ +import type { Proposal, VoteRecord } from '../types'; +import { formatTokenAmount } from '../utils'; + +interface Props { + walletAddress: string; + tokenBalance: bigint | null; + decimals: number; + proposals: Proposal[]; + votedMap: Map; + onCreateProposal: () => void; + onDisconnect: () => void; +} + +const VOTE_ICON: Record = { Yes: 'โœ…', No: 'โŒ', Abstain: 'โฌœ' }; + +export function UserDashboard({ + walletAddress, + tokenBalance, + decimals, + proposals, + votedMap, + onCreateProposal, + onDisconnect, +}: Props) { + const active = proposals.filter(p => p.state === 'Active'); + const voted = proposals.filter(p => votedMap.has(p.id)); + const unvoted = active.filter(p => !votedMap.has(p.id)); + + return ( + + ); +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 24dd442..92785e1 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,6 +1,7 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import App from './App'; +import { WalletProvider } from './WalletContext'; import { validateConfig } from './config'; const root = document.getElementById('root'); @@ -10,7 +11,9 @@ try { validateConfig(); createRoot(root).render( - + + + ); } catch (error) {