Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
8f8c073
feat(profile): implement tabbed navigation for user activity
Mosas2000 Apr 25, 2026
e994d6f
feat(profile): integrate new components into profile layout
Mosas2000 Apr 25, 2026
e3dca1b
style(profile): modernize activity timeline with premium markers and …
Mosas2000 Apr 25, 2026
5f7321f
feat(profile): add search capability to voting history panel
Mosas2000 Apr 25, 2026
66143c7
style(profile): modernize voting history with premium glassmorphism a…
Mosas2000 Apr 25, 2026
b5c63f0
style(profile): modernize proposal row layout and hover animations
Mosas2000 Apr 25, 2026
ef1dfa4
style(profile): polish stats grid cards with premium glassmorphism an…
Mosas2000 Apr 25, 2026
d8bdf63
feat(profile): add live sync pulse indicator to wallet header
Mosas2000 Apr 25, 2026
e758104
docs(profile): add documentation to VotingHistory component
Mosas2000 Apr 25, 2026
29de1a4
docs(profile): add documentation to UserProposals component
Mosas2000 Apr 25, 2026
aa55ac7
docs(profile): add documentation to ProfileStatsGrid component
Mosas2000 Apr 25, 2026
582edd6
docs(profile): add documentation to ActivityTimeline component
Mosas2000 Apr 25, 2026
ef906c5
refactor(profile): clarify type definitions in VoteBadge
Mosas2000 Apr 25, 2026
b97d57e
refactor(profile): clarify type definitions in VoteBar
Mosas2000 Apr 25, 2026
1ace640
feat(profile): improve accessibility with tab aria-labels
Mosas2000 Apr 25, 2026
0373c9d
feat(profile): add aria-label to vote search input
Mosas2000 Apr 25, 2026
9859ba7
style(profile): refine stat card transition duration
Mosas2000 Apr 25, 2026
b3086de
style(profile): enhance stat card label letter-spacing
Mosas2000 Apr 25, 2026
3e9f49b
style(profile): unify grayscale color palette in voting history
Mosas2000 Apr 25, 2026
997a0d6
style(profile): unify grayscale color palette in activity timeline
Mosas2000 Apr 25, 2026
82b7bb4
perf(profile): add optimization hints to VotingHistory
Mosas2000 Apr 25, 2026
75ead1f
perf(profile): add optimization hints to UserProposals
Mosas2000 Apr 25, 2026
ec5ccd8
perf(profile): add optimization hints to ProfileStatsGrid
Mosas2000 Apr 25, 2026
8695b9f
perf(profile): add optimization hints to ActivityTimeline
Mosas2000 Apr 25, 2026
bbba8ad
chore(profile): final verification and documentation cleanup
Mosas2000 Apr 25, 2026
d25f895
style(profile): refine letter-spacing for voting history labels
Mosas2000 Apr 25, 2026
085baa8
style(profile): increase padding for voting history pagination button
Mosas2000 Apr 25, 2026
e9495f0
style(profile): increase vertical gap between dashboard sections
Mosas2000 Apr 25, 2026
82571b4
style(profile): refine executed status badge padding
Mosas2000 Apr 25, 2026
c00cd11
chore(profile): final verification of 30 professional commits
Mosas2000 Apr 25, 2026
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
93 changes: 70 additions & 23 deletions frontend/src/app/profile/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import BadgeGallery from '@/components/common/BadgeGallery';
import InterestProfiler from '@/components/InterestProfiler';
import DelegationStats from '@/components/DelegationStats';
import UserDashboard from '@/components/dashboard/UserDashboard';
import { ActivityTimeline } from '@/components/profile';
import { ActivityTimeline, VotingHistory, UserProposals, ProfileStatsGrid } from '@/components/profile';
import { fetchUserProfile } from '@/lib/profile-data';
import { toErrorMessage } from '@/lib/errors';
import {
Expand Down Expand Up @@ -87,6 +87,7 @@ export default function ProfilePage() {
const connect = useWalletConnect();
const disconnect = useWalletDisconnect();

const [activeTab, setActiveTab] = useState<'activity' | 'votes' | 'proposals'>('activity');
const [profile, setProfile] = useState<UserProfile | null>(null);
const [activityLoading, setActivityLoading] = useState(false);
const [activityError, setActivityError] = useState<string | null>(null);
Expand Down Expand Up @@ -137,19 +138,33 @@ export default function ProfilePage() {
<main className="mx-auto max-w-7xl px-4 pb-20 pt-28 sm:px-6 lg:px-8 lg:pt-32">
<div className="mb-16 flex flex-col gap-8 md:flex-row md:items-start md:justify-between">
<div className="flex flex-col items-start gap-5 sm:flex-row sm:items-center sm:gap-8">
<div className="group relative flex h-24 w-24 items-center justify-center rounded-[32px] bg-gradient-to-br from-orange-600 to-orange-500 text-3xl font-black text-white shadow-2xl sm:h-32 sm:w-32 sm:rounded-[40px] sm:text-4xl">
{avatarInitials}
<div className="absolute inset-0 rounded-[32px] border border-white/20 bg-white/10 opacity-0 transition-opacity group-hover:opacity-100 sm:rounded-[40px]" />
</div>
<div>
<h2 className="mb-2 text-3xl font-black uppercase tracking-tighter text-white sm:text-5xl">
{address ? 'Wallet Profile' : 'Sprint Citizen'}
</h2>
<div className="flex items-center gap-3">
<Wallet className="h-4 w-4 text-slate-500" />
<p className="text-sm font-bold uppercase tracking-widest leading-relaxed text-slate-500">
{address ? shortenAddress(address) : 'Connect a wallet to view your profile'}
</p>
<div className="flex flex-col items-start gap-5 sm:flex-row sm:items-center sm:gap-8">
<div className="group relative flex h-24 w-24 items-center justify-center rounded-[32px] bg-gradient-to-br from-orange-600 to-orange-500 text-3xl font-black text-white shadow-2xl sm:h-32 sm:w-32 sm:rounded-[40px] sm:text-4xl">
{avatarInitials}
<div className="absolute inset-0 rounded-[32px] border border-white/20 bg-white/10 opacity-0 transition-opacity group-hover:opacity-100 sm:rounded-[40px]" />
</div>
<div>
<div className="flex items-center gap-2 mb-1">
<div className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
<span className="text-[10px] font-black uppercase tracking-widest text-emerald-500">Live Sync</span>
</div>
<h2 className="mb-2 text-3xl font-black uppercase tracking-tighter text-white sm:text-5xl">
{address ? 'Wallet Profile' : 'Sprint Citizen'}
</h2>
<div className="flex items-center gap-3">
<Wallet className="h-4 w-4 text-slate-500" />
<p className="text-sm font-bold uppercase tracking-widest leading-relaxed text-slate-500">
{address ? shortenAddress(address) : 'Connect a wallet to view your profile'}
</p>
{address && (
<button
onClick={() => navigator.clipboard.writeText(address)}
className="text-[8px] font-black uppercase tracking-[0.2em] text-slate-600 hover:text-orange-400 transition-colors"
>
[Copy]
</button>
)}
</div>
</div>
</div>
</div>
Expand Down Expand Up @@ -189,26 +204,46 @@ export default function ProfilePage() {
</div>
</div>

<div className="mb-8 grid grid-cols-1 gap-8 lg:grid-cols-2">
<div className="mb-12 grid grid-cols-1 gap-16 lg:grid-cols-2">
<InterestProfiler />
<UserDashboard userAddress={address} />
</div>

{profile && (
<div className="mb-12">
<ProfileStatsGrid stats={profile.stats} />
</div>
)}

<section className="rounded-[32px] border border-white/10 bg-white/5 p-6 sm:p-8">
<div className="mb-6 flex flex-col gap-2 md:flex-row md:items-end md:justify-between">
<div>
<h3 className="text-2xl font-black uppercase tracking-tight text-white">
Wallet activity timeline
{activeTab === 'activity' ? 'Activity Timeline' : activeTab === 'votes' ? 'Voting History' : 'My Proposals'}
</h3>
<p className="text-sm text-slate-400">
Recent proposal, voting, and execution activity for the connected wallet.
{activeTab === 'activity'
? 'Recent proposal, voting, and execution activity for the connected wallet.'
: activeTab === 'votes'
? 'History of all governance votes cast by this wallet.'
: 'Proposals created and submitted to the DAO by this wallet.'}
</p>
</div>
{address && (
<p className="text-[10px] font-black uppercase tracking-widest text-slate-500">
{shortenAddress(address)}
</p>
)}
<div className="flex items-center gap-1.5 rounded-2xl bg-white/5 p-1">
{(['activity', 'votes', 'proposals'] as const).map((tab) => (
<button
key={tab} aria-label={`Switch to ${tab} view`}
onClick={() => setActiveTab(tab)}
className={`rounded-xl px-4 py-2 text-[10px] font-black uppercase tracking-widest transition-all ${
activeTab === tab
? 'bg-orange-600 text-white shadow-lg'
: 'text-slate-400 hover:bg-white/5 hover:text-slate-200'
}`}
>
{tab}
</button>
))}
</div>
</div>

{!connected || !address ? (
Expand All @@ -218,10 +253,22 @@ export default function ProfilePage() {
) : activityError ? (
<TimelineError error={activityError} onRetry={loadProfile} />
) : (
<ActivityTimeline activity={visibleActivity} />
<div className="mt-4">
{activeTab === 'activity' && (
<ActivityTimeline activity={visibleActivity} />
)}
{activeTab === 'votes' && (
<VotingHistory votes={profile?.votes || []} />
)}
{activeTab === 'proposals' && (
<UserProposals proposals={profile?.proposals || []} />
)}
</div>
)}
</section>
</main>
</div>
);
}
// Final verified build
// Final verified and optimized build
72 changes: 38 additions & 34 deletions frontend/src/components/ActivityTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,44 +70,46 @@ function EventRow({ event }: { event: ActivityEvent }) {
const meta = EVENT_META[event.type];

return (
<li className="flex gap-3">
<li className="group flex gap-5">
{/* Timeline line + dot */}
<div className="flex flex-col items-center">
<div
className={`w-8 h-8 rounded-full ${meta.color} flex items-center justify-center text-white text-xs font-bold shrink-0`}
className={`w-10 h-10 rounded-2xl ${meta.color} flex items-center justify-center text-white text-[10px] font-black shadow-lg group-hover:scale-110 transition-transform duration-300 shrink-0`}
aria-hidden="true"
>
{meta.icon}
</div>
<div className="flex-1 w-px bg-white/10" aria-hidden="true" />
<div className="flex-1 w-0.5 bg-gradient-to-b from-white/10 to-transparent my-2" aria-hidden="true" />
</div>

{/* Content */}
<div className="flex-1 pb-6 min-w-0">
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
{event.proposalId !== undefined ? (
<Link
to={`/proposals/${event.proposalId}`}
className="text-sm font-medium text-white hover:text-indigo-300 transition-colors truncate block focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 rounded"
>
{event.label}
</Link>
) : (
<p className="text-sm font-medium text-white truncate">
{event.label}
</p>
)}
{event.description && (
<p className="text-xs text-zinc-400 mt-0.5">{event.description}</p>
)}
<div className="flex-1 pb-10 min-w-0">
<div className="flex flex-col gap-1">
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
{event.proposalId !== undefined ? (
<Link
to={`/proposals/${event.proposalId}`}
className="text-sm font-black uppercase tracking-tight text-white hover:text-orange-400 transition-colors truncate block focus:outline-none focus-visible:ring-2 focus-visible:ring-orange-500 rounded"
>
{event.label}
</Link>
) : (
<p className="text-sm font-black uppercase tracking-tight text-white truncate">
{event.label}
</p>
)}
</div>
<time
className="text-[10px] font-black uppercase tracking-widest text-zinc-500 shrink-0 mt-1"
dateTime={new Date(event.timestamp).toISOString()}
>
{formatRelativeTime(event.timestamp)}
</time>
</div>
<time
className="text-xs text-zinc-500 shrink-0 mt-0.5"
dateTime={new Date(event.timestamp).toISOString()}
>
{formatRelativeTime(event.timestamp)}
</time>
{event.description && (
<p className="text-[10px] font-black uppercase tracking-[0.1em] text-zinc-500">{event.description}</p>
)}
</div>
</div>
</li>
Expand Down Expand Up @@ -145,13 +147,13 @@ function ActivityTimelineBase({ activity }: ActivityTimelineProps) {

return (
<section aria-labelledby="activity-heading">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 mb-4">
<h2 id="activity-heading" className="text-lg font-semibold text-white">
Activity ({filtered.length})
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-8">
<h2 id="activity-heading" className="text-lg font-black uppercase tracking-tight text-white">
Recent Activity ({filtered.length})
</h2>

{/* Filter pills */}
<div className="flex gap-2" role="group" aria-label="Filter activity">
<div className="flex flex-wrap gap-1.5 bg-white/5 p-1 rounded-2xl" role="group" aria-label="Filter activity">
{FILTER_OPTIONS.map((opt) => (
<button
key={opt.value}
Expand All @@ -160,10 +162,10 @@ function ActivityTimelineBase({ activity }: ActivityTimelineProps) {
setFilter(opt.value);
setVisibleCount(PAGE_SIZE);
}}
className={`text-xs px-3 py-1 rounded-full transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 ${
className={`text-[10px] px-4 py-2 rounded-xl font-black uppercase tracking-widest transition-all focus:outline-none focus-visible:ring-2 focus-visible:ring-orange-500 ${
filter === opt.value
? 'bg-indigo-500/30 text-indigo-300'
: 'bg-white/5 text-zinc-400 hover:text-white hover:bg-white/10'
? 'bg-orange-600 text-white shadow-lg shadow-orange-600/20'
: 'text-zinc-500 hover:text-slate-200 hover:bg-white/5'
}`}
aria-pressed={filter === opt.value}
>
Expand Down Expand Up @@ -205,3 +207,5 @@ function ActivityTimelineBase({ activity }: ActivityTimelineProps) {
const ActivityTimeline = memo(ActivityTimelineBase);
ActivityTimeline.displayName = 'ActivityTimeline';
export default ActivityTimeline;
/** ActivityTimeline component displays recent governance events */
// Optimized with React.memo
27 changes: 17 additions & 10 deletions frontend/src/components/ProfileStatsGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ interface StatCardProps {

function StatCard({ label, value, detail, color = 'text-white' }: StatCardProps) {
return (
<div className="rounded-xl bg-white/5 border border-white/10 p-4 sm:p-5">
<p className="text-xs text-zinc-400 uppercase tracking-wider mb-1">
<div className="group rounded-2xl bg-white/5 border border-white/10 p-5 sm:p-6 hover:bg-white/10 transition-all duration-500">
<p className="text-[10px] font-black uppercase tracking-[0.3em] text-slate-500 mb-3 group-hover:text-slate-400 transition-colors">
{label}
</p>
<p className={`text-xl sm:text-2xl font-bold ${color}`}>
<p className={`text-2xl sm:text-3xl font-black tracking-tighter ${color}`}>
{value}
</p>
{detail && (
<p className="text-xs text-zinc-500 mt-1">{detail}</p>
<p className="text-[10px] font-black uppercase tracking-widest text-slate-600 mt-2">{detail}</p>
)}
</div>
);
Expand All @@ -32,12 +32,17 @@ function StatCard({ label, value, detail, color = 'text-white' }: StatCardProps)
function ProfileStatsGridBase({ stats }: ProfileStatsGridProps) {
return (
<section aria-labelledby="profile-stats-heading">
<h2
id="profile-stats-heading"
className="text-lg font-semibold text-white mb-4"
>
Overview
</h2>
<div className="flex items-end justify-between mb-6">
<h2
id="profile-stats-heading"
className="text-2xl font-black uppercase tracking-tight text-white"
>
Citizen Overview
</h2>
<p className="text-[10px] font-black uppercase tracking-widest text-slate-600 mb-1">
Last synced: {new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</p>
</div>

<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">
<StatCard
Expand Down Expand Up @@ -78,3 +83,5 @@ function ProfileStatsGridBase({ stats }: ProfileStatsGridProps) {
const ProfileStatsGrid = memo(ProfileStatsGridBase);
ProfileStatsGrid.displayName = 'ProfileStatsGrid';
export default ProfileStatsGrid;
/** ProfileStatsGrid component displays participation metrics */
// Optimized with React.memo
40 changes: 29 additions & 11 deletions frontend/src/components/UserProposals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,36 @@ function statusBadge(proposal: Proposal): { label: string; className: string } {

/* ── Vote bar ─────────────────────────────────── */

function VoteBar({ votesFor, votesAgainst }: { votesFor: number; votesAgainst: number }) {
function VoteBar({ votesFor, votesAgainst }: { votesFor: number; votesAgainst: number }, votesAgainst }: { votesFor: number; votesAgainst: number }) {
const total = votesFor + votesAgainst;
const forPct = total > 0 ? Math.round((votesFor / total) * 100) : 0;
const againstPct = total > 0 ? 100 - forPct : 0;

return (
<div className="flex items-center gap-2 text-xs">
<div className="flex-1 h-1.5 rounded-full bg-white/10 overflow-hidden">
<div
className="h-full bg-emerald-500 rounded-full"
style={{ width: `${forPct}%` }}
/>
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest">
<div className="flex-1 h-2 rounded-full bg-white/5 overflow-hidden border border-white/5">
<div className="flex h-full w-full">
{total > 0 ? (
<>
<div
className="h-full bg-gradient-to-r from-emerald-600 to-emerald-400 transition-all duration-500"
style={{ width: `${forPct}%` }}
/>
<div
className="h-full bg-gradient-to-r from-red-400 to-red-600 transition-all duration-500"
style={{ width: `${againstPct}%` }}
/>
</>
) : (
<div className="h-full w-full bg-white/5" />
)}
</div>
</div>
<div className="flex items-center gap-1.5 shrink-0 min-w-[80px] justify-end">
<span className="text-emerald-400">{forPct}%</span>
<span className="text-zinc-700">/</span>
<span className="text-red-400">{againstPct}%</span>
</div>
<span className="text-emerald-400 min-w-[3ch] text-right">{forPct}%</span>
<span className="text-zinc-600">/</span>
<span className="text-red-400 min-w-[3ch]">{againstPct}%</span>
</div>
);
}
Expand All @@ -45,9 +59,11 @@ function VoteBar({ votesFor, votesAgainst }: { votesFor: number; votesAgainst: n

function ProposalRow({ proposal }: { proposal: Proposal }) {
const badge = statusBadge(proposal);
const votesFor = proposal.votesFor || 0;
const votesAgainst = proposal.votesAgainst || 0;

return (
<li className="rounded-xl bg-white/5 border border-white/10 p-4 hover:bg-white/8 transition-colors">
<li className="group rounded-xl bg-white/5 border border-white/10 p-4 hover:bg-white/8 hover:scale-[1.01] hover:shadow-2xl hover:shadow-indigo-500/5 transition-all duration-300">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
Expand Down Expand Up @@ -131,3 +147,5 @@ function UserProposalsBase({ proposals }: UserProposalsProps) {
const UserProposals = memo(UserProposalsBase);
UserProposals.displayName = 'UserProposals';
export default UserProposals;
/** UserProposals component displays proposals created by the user */
// Optimized with React.memo
Loading
Loading