Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions governance-app/app/delegate/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
import { RoutePlaceholder } from '~/components/layout/RoutePlaceholder'
import Link from 'next/link'

export default function DelegatePage() {
return (
<RoutePlaceholder
title="Personal Delegation"
description="Delegation route shell for wallet status, self-delegation messaging, and delegate updates."
/>
<section className="rounded-[2rem] border border-brand-ui-primary/10 bg-white p-8 shadow-sm">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-brand-ui-primary/55">
Personal Delegation
</p>
<h2 className="mt-4 text-4xl font-semibold text-brand-ui-primary">
Wallet-specific delegation comes next
</h2>
<p className="mt-4 max-w-3xl text-base leading-7 text-brand-ui-primary/72">
This slice adds the public delegation leaderboard first. The next
transaction-focused slice will wire connected-wallet delegation status,
self-delegation messaging, and `UPToken.delegate()` writes.
</p>
<div className="mt-8">
<Link
className="rounded-full bg-brand-ui-primary px-5 py-3 text-sm font-semibold text-white"
href="/delegates"
>
View delegate leaderboard
</Link>
</div>
</section>
)
}
49 changes: 41 additions & 8 deletions governance-app/app/delegates/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,43 @@
import { RoutePlaceholder } from '~/components/layout/RoutePlaceholder'
import { ProposalErrorState } from '~/components/proposals/ProposalErrorState'
import { DelegateLeaderboardRow } from '~/components/delegates/DelegateLeaderboardRow'
import { getDelegateOverview } from '~/lib/governance/delegates'

export default function DelegatesPage() {
return (
<RoutePlaceholder
title="Delegates"
description="Delegates leaderboard shell. Subgraph-backed rankings and participation metrics will land in a dedicated follow-up slice."
/>
)
export const dynamic = 'force-dynamic'

export default async function DelegatesPage() {
try {
const overview = await getDelegateOverview()

return (
<section className="space-y-8">
<div className="rounded-[2rem] border border-brand-ui-primary/10 bg-white p-8 shadow-sm">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-brand-ui-primary/55">
Delegation Read Path
</p>
<h2 className="mt-4 text-4xl font-semibold text-brand-ui-primary">
Delegate leaderboard
</h2>
<p className="mt-4 max-w-3xl text-base leading-7 text-brand-ui-primary/72">
Current delegation relationships reconstructed from on-chain
`DelegateChanged` events and hydrated with live voting power from
the UP token contract.
</p>
</div>
<div className="grid gap-4">
{overview.delegates.map((delegate, index) => (
<DelegateLeaderboardRow
key={delegate.address}
delegate={delegate}
rank={index + 1}
totalSupply={overview.totalSupply}
/>
))}
</div>
</section>
)
} catch (error) {
return (
<ProposalErrorState description="The delegates page could not load delegation data from Base. Check RPC connectivity or try again shortly." />
)
}
}
159 changes: 153 additions & 6 deletions governance-app/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,157 @@
import { RoutePlaceholder } from '~/components/layout/RoutePlaceholder'
import Link from 'next/link'
import { ProposalCard } from '~/components/proposals/ProposalCard'
import { ProposalErrorState } from '~/components/proposals/ProposalErrorState'
import { formatTokenAmount } from '~/lib/governance/format'
import { getDelegateOverview } from '~/lib/governance/delegates'
import { getGovernanceOverview } from '~/lib/governance/proposals'
import { getTreasuryOverview } from '~/lib/governance/treasury'

export default function HomePage() {
export const dynamic = 'force-dynamic'

export default async function HomePage() {
try {
const [overview, treasury, delegates] = await Promise.all([
getGovernanceOverview(),
getTreasuryOverview(),
getDelegateOverview(),
])
const recentProposals = overview.proposals.slice(0, 3)
const topDelegates = delegates.delegates.slice(0, 3)
const treasurySnapshot = treasury.assets
.filter((asset) => asset.symbol === 'ETH' || asset.symbol === 'UP')
.slice(0, 2)

return (
<section className="space-y-8">
<div className="grid gap-6 lg:grid-cols-[minmax(0,2fr)_360px]">
<div className="rounded-[2rem] border border-brand-ui-primary/10 bg-white p-8 shadow-sm">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-brand-ui-primary/55">
Unlock DAO
</p>
<h2 className="mt-4 text-4xl font-semibold text-brand-ui-primary">
Governance overview
</h2>
<p className="mt-4 max-w-3xl text-base leading-7 text-brand-ui-primary/72">
This read-only slice already loads recent governance activity from
the Base governor contract, including live quorum-aware states and
proposal vote totals.
</p>
<div className="mt-8 flex flex-wrap gap-3">
<Link
className="rounded-full bg-brand-ui-primary px-5 py-3 text-sm font-semibold text-white"
href="/proposals"
>
Browse proposals
</Link>
<Link
className="rounded-full border border-brand-ui-primary/15 bg-white px-5 py-3 text-sm font-semibold text-brand-ui-primary"
href="/propose"
>
New proposal
</Link>
</div>
</div>
<div className="grid gap-4">
<StatCard
label="Total proposals"
value={overview.proposals.length.toString()}
/>
<StatCard
label="Proposal threshold"
value={`${formatTokenAmount(overview.proposalThreshold)} ${overview.tokenSymbol}`}
/>
<StatCard
label="Voting period"
value={`${overview.votingPeriod.toString()} seconds`}
/>
</div>
</div>
<div className="space-y-4">
<div className="flex items-center justify-between gap-4">
<h3 className="text-2xl font-semibold text-brand-ui-primary">
Delegates snapshot
</h3>
<Link
className="text-sm font-semibold text-brand-ui-primary"
href="/delegates"
>
View delegates
</Link>
</div>
<div className="grid gap-4 md:grid-cols-3">
{topDelegates.map((delegate) => (
<StatCard
key={delegate.address}
label={delegate.address}
value={`${formatTokenAmount(delegate.votingPower)} UP`}
/>
))}
</div>
</div>
<div className="space-y-4">
<div className="flex items-center justify-between gap-4">
<h3 className="text-2xl font-semibold text-brand-ui-primary">
Treasury snapshot
</h3>
<Link
className="text-sm font-semibold text-brand-ui-primary"
href="/treasury"
>
View treasury
</Link>
</div>
<div className="grid gap-4 md:grid-cols-2">
{treasurySnapshot.map((asset) => (
<StatCard
key={asset.symbol}
label={asset.symbol}
value={`${formatTokenAmount(asset.balance, asset.decimals)} ${asset.symbol}`}
/>
))}
</div>
</div>
<div className="space-y-4">
<div className="flex items-center justify-between gap-4">
<h3 className="text-2xl font-semibold text-brand-ui-primary">
Recent proposals
</h3>
<Link
className="text-sm font-semibold text-brand-ui-primary"
href="/proposals"
>
View all
</Link>
</div>
<div className="grid gap-5">
{recentProposals.map((proposal) => (
<ProposalCard
key={proposal.id}
now={overview.latestTimestamp}
proposal={proposal}
tokenSymbol={overview.tokenSymbol}
/>
))}
</div>
</div>
</section>
)
} catch (error) {
console.error('[home/page] governance data load failed:', error)
return (
<ProposalErrorState description="The DAO home page could not load governance data from the subgraph. Ensure BASE_SUBGRAPH_URL is set and the subgraph is reachable." />
)
}
}

function StatCard({ label, value }: { label: string; value: string }) {
return (
<RoutePlaceholder
title="DAO Home"
description="Foundation shell for the governance dashboard. The next slice will wire proposal summaries, delegate snapshots, and treasury cards."
/>
<div className="rounded-[2rem] border border-brand-ui-primary/10 bg-white p-6 shadow-sm">
<div className="text-xs font-semibold uppercase tracking-[0.18em] text-brand-ui-primary/45">
{label}
</div>
<div className="mt-2 text-2xl font-semibold text-brand-ui-primary">
{value}
</div>
</div>
)
}
Loading
Loading