Skip to content

Commit d5f9b7f

Browse files
committed
fix: agent detail page hydration error + leaderboard DB enrichment
- Replace locale-dependent Date formatting with deterministic YYYY-MM-DD to fix React error #418 (SSR/client hydration mismatch) - Add GET /wunderland/leaderboard backend endpoint returning per-agent post counts and engagement from DB - Frontend leaderboard API merges on-chain + DB data so agents show actual post counts and reputation instead of zeros Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9356e85 commit d5f9b7f

3 files changed

Lines changed: 336 additions & 9 deletions

File tree

‎app/src/app/agents/[address]/page.tsx‎

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import { use, useState } from 'react';
3+
import { use, useState, useEffect } from 'react';
44
import Link from 'next/link';
55
import { PublicKey, Transaction } from '@solana/web3.js';
66
import { useConnection, useWallet } from '@solana/wallet-adapter-react';
@@ -44,6 +44,16 @@ function explorerClusterParam(cluster: string): string {
4444
return `?cluster=${encodeURIComponent(cluster)}`;
4545
}
4646

47+
/** Locale-safe date that avoids SSR/client hydration mismatch */
48+
function formatDate(iso: string): string {
49+
try {
50+
const d = new Date(iso);
51+
return d.toISOString().split('T')[0]!; // YYYY-MM-DD, deterministic
52+
} catch {
53+
return '—';
54+
}
55+
}
56+
4757
function safeDonationNonce(): bigint {
4858
const rand = typeof crypto !== 'undefined' ? crypto.getRandomValues(new Uint16Array(1))[0] : Math.floor(Math.random() * 65536);
4959
return (BigInt(Date.now()) << 16n) | BigInt(rand);
@@ -64,8 +74,9 @@ export default function AgentProfilePage({ params }: { params: Promise<{ address
6474
const { connection } = useConnection();
6575
const { publicKey, connected, sendTransaction } = useWallet();
6676
const agentsState = useApi<{ agents: Agent[]; total: number }>('/api/agents');
77+
const [kind, setKind] = useState<'post' | 'comment'>('post');
6778
const postsState = useApi<{ posts: Post[]; total: number }>(
68-
`/api/posts?limit=1000&agent=${encodeURIComponent(address)}`,
79+
`/api/posts?limit=1000&agent=${encodeURIComponent(address)}&kind=${kind}`,
6980
);
7081

7182
const agent = agentsState.data?.agents.find((a) => a.address === address) ?? null;
@@ -290,7 +301,7 @@ export default function AgentProfilePage({ params }: { params: Promise<{ address
290301
</div>
291302

292303
<div className="flex justify-center md:justify-start gap-3 text-xs text-white/25">
293-
<span className="font-mono">since {new Date(agent.createdAt).toLocaleDateString()}</span>
304+
<span className="font-mono">since {formatDate(agent.createdAt)}</span>
294305
</div>
295306
</div>
296307
</div>
@@ -448,7 +459,7 @@ export default function AgentProfilePage({ params }: { params: Promise<{ address
448459
</div>
449460
<div className="flex items-center justify-between p-3 rounded-lg bg-black/20">
450461
<span className="text-xs text-white/40">Registered</span>
451-
<span className="text-xs font-mono text-white/50">{new Date(agent.createdAt).toISOString()}</span>
462+
<span className="text-xs font-mono text-white/50">{formatDate(agent.createdAt)}</span>
452463
</div>
453464
</div>
454465
</div>
@@ -460,6 +471,41 @@ export default function AgentProfilePage({ params }: { params: Promise<{ address
460471
ref={postsReveal.ref}
461472
className={`space-y-4 animate-in ${postsReveal.isVisible ? 'visible' : ''}`}
462473
>
474+
<div className="flex items-center justify-between gap-3 flex-wrap">
475+
<h2 className="font-display font-semibold text-lg">
476+
<span className="neon-glow-magenta">{kind === 'comment' ? 'Replies' : 'Posts'}</span>
477+
</h2>
478+
<div className="flex items-center gap-1">
479+
<button
480+
type="button"
481+
onClick={() => setKind('post')}
482+
className={`px-3 py-1.5 rounded-lg text-xs font-mono uppercase transition-all ${
483+
kind === 'post'
484+
? 'bg-[rgba(153,69,255,0.15)] text-[var(--sol-purple)] border border-[rgba(153,69,255,0.25)]'
485+
: 'bg-[var(--bg-glass)] text-[var(--text-tertiary)] border border-[var(--border-glass)] hover:text-[var(--text-secondary)]'
486+
}`}
487+
>
488+
Posts
489+
</button>
490+
<button
491+
type="button"
492+
onClick={() => setKind('comment')}
493+
className={`px-3 py-1.5 rounded-lg text-xs font-mono uppercase transition-all ${
494+
kind === 'comment'
495+
? 'bg-[rgba(0,255,200,0.10)] text-[var(--neon-cyan)] border border-[rgba(0,255,200,0.18)]'
496+
: 'bg-[var(--bg-glass)] text-[var(--text-tertiary)] border border-[var(--border-glass)] hover:text-[var(--text-secondary)]'
497+
}`}
498+
>
499+
Replies
500+
</button>
501+
{postsState.data && (
502+
<span className="ml-2 text-[10px] font-mono text-[var(--text-tertiary)]">
503+
{postsState.data.total} total
504+
</span>
505+
)}
506+
</div>
507+
</div>
508+
463509
{postsState.loading && (
464510
<div className="holo-card p-8 text-center">
465511
<div className="text-white/50 font-display font-semibold">Loading posts…</div>
@@ -482,7 +528,7 @@ export default function AgentProfilePage({ params }: { params: Promise<{ address
482528
<div className="holo-card p-8 text-center">
483529
<div className="text-white/60 font-display font-semibold">No posts yet</div>
484530
<div className="mt-2 text-xs text-white/25 font-mono">
485-
This agent hasn&apos;t anchored any posts.
531+
This agent hasn&apos;t anchored any {kind === 'comment' ? 'replies' : 'posts'}.
486532
</div>
487533
</div>
488534
)}
@@ -502,11 +548,26 @@ export default function AgentProfilePage({ params }: { params: Promise<{ address
502548
e/{post.enclaveName}
503549
</Link>
504550
)}
551+
{post.kind === 'comment' && (
552+
<span className="badge text-[10px] bg-[rgba(0,255,200,0.08)] text-[var(--neon-cyan)] border border-[rgba(0,255,200,0.15)]">
553+
REPLY
554+
</span>
555+
)}
505556
</div>
506557
<span className="text-white/20 text-[10px] font-mono">
507-
{new Date(post.timestamp).toLocaleDateString()}
558+
{formatDate(post.timestamp)}
508559
</span>
509560
</div>
561+
562+
{post.kind === 'comment' && post.replyTo && (
563+
<div className="mb-3 text-[10px] font-mono text-[var(--text-tertiary)]">
564+
↳ reply to{' '}
565+
<Link href={`/posts/${post.replyTo}`} className="text-[var(--neon-cyan)] hover:underline">
566+
{post.replyTo.slice(0, 12)}…
567+
</Link>
568+
</div>
569+
)}
570+
510571
{post.content ? (
511572
<p className="text-white/70 text-sm leading-relaxed mb-4 whitespace-pre-line">
512573
{post.content}
@@ -534,6 +595,14 @@ export default function AgentProfilePage({ params }: { params: Promise<{ address
534595
</span>
535596
<span className="text-white/20">{post.commentCount} replies</span>
536597
<TipButton contentHash={post.contentHash} enclavePda={post.enclavePda} />
598+
{post.kind === 'comment' && post.replyTo && (
599+
<Link
600+
href={`/posts/${post.replyTo}`}
601+
className="text-[10px] font-mono text-white/30 hover:text-[var(--neon-cyan)] transition-colors"
602+
>
603+
Context
604+
</Link>
605+
)}
537606
<Link
538607
href={`/posts/${post.id}`}
539608
className="text-[10px] font-mono text-white/30 hover:text-[var(--neon-cyan)] transition-colors"
Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,60 @@
11
import { NextResponse } from 'next/server';
22
import { getLeaderboardServer } from '@/lib/solana-server';
33

4+
const BACKEND_URL = process.env.WUNDERLAND_BACKEND_URL || 'http://localhost:3001';
5+
6+
type BackendLeaderboardEntry = {
7+
seedId: string;
8+
name: string;
9+
agentPda: string | null;
10+
posts: number;
11+
comments: number;
12+
entries: number;
13+
reputation: number;
14+
};
15+
416
export async function GET() {
517
const leaderboard = await getLeaderboardServer();
18+
19+
// Try to enrich with backend DB stats (post counts, engagement)
20+
let backendStats: BackendLeaderboardEntry[] = [];
21+
try {
22+
const res = await fetch(`${BACKEND_URL}/api/wunderland/leaderboard`, {
23+
signal: AbortSignal.timeout(5_000),
24+
});
25+
if (res.ok) {
26+
backendStats = (await res.json()) as BackendLeaderboardEntry[];
27+
}
28+
} catch {
29+
// Backend unavailable — use on-chain data only
30+
}
31+
32+
// Build lookup by agent PDA
33+
const backendByPda = new Map<string, BackendLeaderboardEntry>();
34+
for (const entry of backendStats) {
35+
if (entry.agentPda) {
36+
backendByPda.set(entry.agentPda, entry);
37+
}
38+
}
39+
40+
// Merge: prefer larger value from either source
41+
const enriched = leaderboard.map((agent) => {
42+
const db = backendByPda.get(agent.address);
43+
return {
44+
...agent,
45+
totalPosts: Math.max(agent.totalPosts, db?.entries ?? 0),
46+
reputation: Math.max(agent.reputation, db?.reputation ?? 0),
47+
};
48+
});
49+
50+
// Re-sort by reputation (merged), then by totalPosts
51+
enriched.sort((a, b) => b.reputation - a.reputation || b.totalPosts - a.totalPosts);
52+
53+
// Re-rank
54+
const ranked = enriched.map((agent, i) => ({ ...agent, rank: i + 1 }));
55+
656
return NextResponse.json({
7-
leaderboard,
8-
total: leaderboard.length,
57+
leaderboard: ranked,
58+
total: ranked.length,
959
});
1060
}

0 commit comments

Comments
 (0)