Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
28efc38
fix(cache): clear min stake entries during invalidate-all
Mosas2000 Apr 10, 2026
3cfa5ae
test(vitest): align @ alias with src root
Mosas2000 Apr 11, 2026
d87b9d9
test(jsdom): add matchMedia and scrollIntoView shims
Mosas2000 Apr 11, 2026
57fe3df
test(app): fix hydration test mocks and module paths
Mosas2000 Apr 11, 2026
9fc7fdb
test(app): stabilize command palette integration tests
Mosas2000 Apr 11, 2026
e207f3a
chore(frontend): trim ui imports
Mosas2000 Apr 15, 2026
5c94520
chore(frontend): clear lint tail
Mosas2000 Apr 15, 2026
04b72aa
chore(frontend): clean notification center
Mosas2000 Apr 15, 2026
8cdf5d6
chore(frontend): tidy remaining dashboards
Mosas2000 Apr 15, 2026
4da9fa2
chore(frontend): tidy stateful widgets
Mosas2000 Apr 15, 2026
5209ca2
chore(frontend): trim proposal flow ui
Mosas2000 Apr 15, 2026
018bbba
chore(frontend): trim voting widgets
Mosas2000 Apr 15, 2026
b1847d9
chore(frontend): finish analytics cleanup
Mosas2000 Apr 16, 2026
c8c01f4
chore(frontend): clean dashboard components
Mosas2000 Apr 16, 2026
6b4c525
chore(frontend): simplify shell components
Mosas2000 Apr 16, 2026
6f268cb
chore(frontend): clean onboarding and layout
Mosas2000 Apr 16, 2026
f5dae18
chore(frontend): clean activity widgets
Mosas2000 Apr 16, 2026
08750ee
chore(frontend): fix budget dashboard
Mosas2000 Apr 16, 2026
e20725e
Refine app shell lint
Mosas2000 Apr 16, 2026
0278ba5
Refine analytics charts
Mosas2000 Apr 16, 2026
9ca20f1
Trim remaining lint tail
Mosas2000 Apr 16, 2026
e048edd
Merge origin/main into feature branch
Mosas2000 Apr 16, 2026
36514b1
Add wallet activity timeline
Mosas2000 Apr 17, 2026
2ac9c4b
Add live contract event stream
Mosas2000 Apr 21, 2026
a5d533a
Merge remote-tracking branch 'origin/main' into fix/live-contract-eve…
Mosas2000 Apr 21, 2026
7141849
Merge remote-tracking branch 'origin/main' into fix/live-contract-eve…
Mosas2000 Apr 21, 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
21 changes: 5 additions & 16 deletions frontend/components/dashboard/ContractEventStream.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,7 @@ export const ContractEventStream: React.FC<ContractEventStreamProps> = ({
setError(null);

try {
const fetchedEvents = await fetchContractEventStream(
contractPrincipal,
apiUrl
);
const fetchedEvents = await fetchContractEventStream(contractPrincipal, apiUrl);
setEvents(fetchedEvents);
setLastUpdated(Date.now());
} catch (err) {
Expand Down Expand Up @@ -85,7 +82,7 @@ export const ContractEventStream: React.FC<ContractEventStreamProps> = ({

const formatTime = (timestamp: number) => {
const date = new Date(timestamp);
return date.toLocaleTimeString([], {
return date.toLocaleTimeString([], {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
Expand Down Expand Up @@ -162,17 +159,11 @@ export const ContractEventStream: React.FC<ContractEventStreamProps> = ({
</div>

<div className="divide-y divide-gray-200 max-h-96 overflow-y-auto">
{error && (
<div className="p-4 bg-red-50 text-red-700 text-sm">
{error.message}
</div>
)}
{error && <div className="p-4 bg-red-50 text-red-700 text-sm">{error.message}</div>}

{filteredEvents.length === 0 && !isLoading && (
<div className="p-8 text-center text-gray-500">
{events.length === 0
? 'No events found'
: 'No events match your filters'}
{events.length === 0 ? 'No events found' : 'No events match your filters'}
</div>
)}

Expand All @@ -186,9 +177,7 @@ export const ContractEventStream: React.FC<ContractEventStreamProps> = ({
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="font-medium text-gray-900">
{event.description}
</span>
<span className="font-medium text-gray-900">{event.description}</span>
{event.status === 'failed' && (
<span className="flex items-center gap-1 text-red-600 text-xs">
<X size={14} />
Expand Down
1 change: 1 addition & 0 deletions frontend/components/dashboard/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export { default as UserProfile } from './UserProfile';
export { default as UserInsights } from './UserInsights';
export { default as UserNetwork } from './UserNetwork';
export { default as ActivityFeed } from './ActivityFeed';
export { default as ContractEventStream } from './ContractEventStream';
export { default as AuditTrail } from './AuditTrail';
export { PerformanceMetricsPanel } from './PerformanceMetricsPanel';
export { AnalyticsKPIPanel } from './AnalyticsKPIPanel';
Expand Down
260 changes: 212 additions & 48 deletions frontend/src/app/profile/page.tsx
Original file line number Diff line number Diff line change
@@ -1,61 +1,225 @@
'use client';

import React from 'react';
import { useCallback, useEffect, useState } from 'react';
import Header from '@/components/Header';
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 { fetchUserProfile } from '@/lib/profile-data';
import { toErrorMessage } from '@/lib/errors';
import {
useWalletAddress,
useWalletConnect,
useWalletConnected,
useWalletDisconnect,
useWalletLoading,
} from '@/store/wallet-selectors';
import { LoadingSpinner } from '@/components/LoadingSpinner';
import type { UserProfile } from '@/types/profile';
import { Settings, LogOut, Wallet } from 'lucide-react';

function shortenAddress(address: string): string {
return `${address.slice(0, 8)}...${address.slice(-6)}`;
}

function TimelinePlaceholder({ onConnect }: { onConnect: () => void }) {
return (
<div className="rounded-3xl border border-white/10 bg-white/5 p-8 text-center">
<h3 className="text-lg font-semibold text-white mb-2">Wallet activity timeline</h3>
<p className="text-sm text-slate-400 mb-6">
Connect your wallet to see recent proposals, votes, and execution activity.
</p>
<button
type="button"
onClick={onConnect}
className="rounded-xl bg-orange-600 px-5 py-2.5 text-sm font-semibold text-white transition-all hover:bg-orange-500"
>
Connect Wallet
</button>
</div>
);
}

function TimelineLoading() {
return (
<div className="rounded-3xl border border-white/10 bg-white/5 p-8">
<div className="animate-pulse space-y-4">
<div className="h-5 w-48 rounded bg-white/10" />
<div className="h-4 w-72 rounded bg-white/10" />
<div className="space-y-3 pt-4">
<div className="h-20 rounded-2xl bg-white/5" />
<div className="h-20 rounded-2xl bg-white/5" />
<div className="h-20 rounded-2xl bg-white/5" />
</div>
</div>
</div>
);
}

function TimelineError({
error,
onRetry,
}: {
error: string;
onRetry: () => void;
}) {
return (
<div className="rounded-3xl border border-red-500/20 bg-red-500/10 p-8">
<h3 className="text-lg font-semibold text-white mb-2">Unable to load activity</h3>
<p className="text-sm text-red-200 mb-6">{error}</p>
<button
type="button"
onClick={onRetry}
className="rounded-xl border border-red-400/30 px-5 py-2.5 text-sm font-semibold text-white transition-all hover:bg-red-500/20"
>
Retry
</button>
</div>
);
}

export default function ProfilePage() {
const walletLoading = useWalletLoading();
const connected = useWalletConnected();
const address = useWalletAddress();
const connect = useWalletConnect();
const disconnect = useWalletDisconnect();

const [profile, setProfile] = useState<UserProfile | null>(null);
const [activityLoading, setActivityLoading] = useState(false);
const [activityError, setActivityError] = useState<string | null>(null);

const loadProfile = useCallback(async () => {
if (!address || !connected) return;

setActivityLoading(true);
setActivityError(null);

try {
const data = await fetchUserProfile(address);
setProfile(data);
} catch (error) {
setActivityError(toErrorMessage(error));
} finally {
setActivityLoading(false);
}
}, [address, connected]);

useEffect(() => {
if (!connected || !address) return;

const timeout = window.setTimeout(() => {
void loadProfile();
}, 0);

return () => window.clearTimeout(timeout);
}, [address, connected, loadProfile]);

const isCurrentProfile = profile?.address === address;
const visibleActivity = isCurrentProfile ? profile.activity : [];
const showTimelineLoading = Boolean(connected && address && (activityLoading || !isCurrentProfile));
const avatarInitials = address ? address.slice(0, 2).toUpperCase() : 'SP';

if (walletLoading) {
return (
<div className="min-h-screen bg-transparent">
<Header />

<main className="pt-32 pb-20 px-8 max-w-7xl mx-auto">
<div className="flex flex-col md:flex-row justify-between items-start gap-12 mb-20">
<div className="flex items-center gap-8">
<div className="w-32 h-32 rounded-[40px] bg-gradient-to-br from-orange-600 to-orange-500 flex items-center justify-center text-4xl font-black text-white shadow-2xl relative group">
SP
<div className="absolute inset-0 bg-white/10 opacity-0 group-hover:opacity-100 transition-opacity rounded-[40px] border border-white/20" />
</div>
<div>
<h2 className="text-5xl font-black text-white uppercase tracking-tighter mb-2">Sprint Citizen</h2>
<div className="flex items-center gap-3">
<Wallet className="w-4 h-4 text-slate-500" />
<p className="text-sm font-bold text-slate-500 uppercase tracking-widest leading-relaxed">
SP12...ABCD
</p>
</div>
</div>
</div>

<div className="flex gap-4">
<button className="p-4 bg-white/5 border border-white/10 rounded-2xl hover:bg-white/10 transition-all text-slate-400 hover:text-white">
<Settings className="w-5 h-5" />
</button>
<button className="flex items-center gap-3 px-8 py-4 bg-red-600/10 border border-red-500/20 rounded-2xl text-red-500 text-[10px] font-black uppercase tracking-widest hover:bg-red-600 hover:text-white transition-all">
<LogOut className="w-4 h-4" />
Disconnect
</button>
</div>
</div>

<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-8">
<div className="lg:col-span-1">
<DelegationStats />
</div>
<div className="lg:col-span-2">
<BadgeGallery />
</div>
</div>

<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
<InterestProfiler />
<UserDashboard />
</div>
</main>
</div>
<div className="min-h-screen flex items-center justify-center">
<LoadingSpinner />
</div>
);
}

return (
<div className="min-h-screen bg-transparent">
<Header />

<main className="pt-32 pb-20 px-8 max-w-7xl mx-auto">
<div className="flex flex-col md:flex-row justify-between items-start gap-12 mb-20">
<div className="flex items-center gap-8">
<div className="w-32 h-32 rounded-[40px] bg-gradient-to-br from-orange-600 to-orange-500 flex items-center justify-center text-4xl font-black text-white shadow-2xl relative group">
{avatarInitials}
<div className="absolute inset-0 bg-white/10 opacity-0 group-hover:opacity-100 transition-opacity rounded-[40px] border border-white/20" />
</div>
<div>
<h2 className="text-5xl font-black text-white uppercase tracking-tighter mb-2">
{address ? 'Wallet Profile' : 'Sprint Citizen'}
</h2>
<div className="flex items-center gap-3">
<Wallet className="w-4 h-4 text-slate-500" />
<p className="text-sm font-bold text-slate-500 uppercase tracking-widest leading-relaxed">
{address ? shortenAddress(address) : 'Connect a wallet to view your profile'}
</p>
</div>
</div>
</div>

<div className="flex gap-4">
<button className="p-4 bg-white/5 border border-white/10 rounded-2xl hover:bg-white/10 transition-all text-slate-400 hover:text-white">
<Settings className="w-5 h-5" />
</button>
{connected && address ? (
<button
type="button"
onClick={disconnect}
className="flex items-center gap-3 px-8 py-4 bg-red-600/10 border border-red-500/20 rounded-2xl text-red-500 text-[10px] font-black uppercase tracking-widest hover:bg-red-600 hover:text-white transition-all"
>
<LogOut className="w-4 h-4" />
Disconnect
</button>
) : (
<button
type="button"
onClick={connect}
className="flex items-center gap-3 px-8 py-4 bg-orange-600/10 border border-orange-500/20 rounded-2xl text-orange-400 text-[10px] font-black uppercase tracking-widest hover:bg-orange-600 hover:text-white transition-all"
>
<Wallet className="w-4 h-4" />
Connect Wallet
</button>
)}
</div>
</div>

<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-8">
<div className="lg:col-span-1">
<DelegationStats />
</div>
<div className="lg:col-span-2">
<BadgeGallery />
</div>
</div>

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

<section className="rounded-[32px] border border-white/10 bg-white/5 p-8">
<div className="flex flex-col gap-2 md:flex-row md:items-end md:justify-between mb-6">
<div>
<h3 className="text-2xl font-black text-white uppercase tracking-tight">Wallet activity timeline</h3>
<p className="text-sm text-slate-400">
Recent proposal, voting, and execution activity for the connected wallet.
</p>
</div>
{address && (
<p className="text-[10px] font-black uppercase tracking-widest text-slate-500">
{shortenAddress(address)}
</p>
)}
</div>

{!connected || !address ? (
<TimelinePlaceholder onConnect={connect} />
) : showTimelineLoading ? (
<TimelineLoading />
) : activityError ? (
<TimelineError error={activityError} onRetry={loadProfile} />
) : (
<ActivityTimeline activity={visibleActivity} />
)}
</section>
</main>
</div>
);
}
Loading
Loading