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
4 changes: 2 additions & 2 deletions src/components/CommentSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
import { useState, useCallback } from "react";
import { useAccount, useSignMessage } from "wagmi";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { truncateAddress } from "../../lib/utils";
import { ConnectWallet } from "./ConnectWallet";
import { FarcasterAvatar } from "./FarcasterAvatar";

interface Comment {
id: number;
Expand Down Expand Up @@ -182,7 +182,7 @@ export function CommentSection({
<div key={c.id} className="text-sm">
<div className="flex items-baseline gap-2">
<span className="text-foreground text-xs font-medium">
{truncateAddress(c.commenter_address)}
<FarcasterAvatar address={c.commenter_address} size={12} />
</span>
<span className="text-muted text-[10px]">
{relativeTime(c.created_at)}
Expand Down
16 changes: 14 additions & 2 deletions src/components/ConnectWallet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import { useEffect, useRef, useState } from "react";
import { useAccount, useConnect, useDisconnect } from "wagmi";
import { isFarcasterMiniApp } from "../../lib/farcaster-connector";
import { truncateAddress } from "../../lib/utils";
import { useConnectedIdentity } from "../hooks/useConnectedIdentity";

export function ConnectWallet() {
const { address, isConnected } = useAccount();
const { connect, connectors, isPending } = useConnect();
const { disconnect } = useDisconnect();
const autoConnectAttempted = useRef(false);
const [inMiniApp, setInMiniApp] = useState(false);
const { profile } = useConnectedIdentity();

// Detect Farcaster mini app context once on mount
useEffect(() => {
Expand All @@ -36,8 +38,18 @@ export function ConnectWallet() {
if (isConnected && address) {
return (
<div className="border-border flex items-center gap-3 rounded border px-3 py-2 text-sm">
<span className="text-accent font-medium">
{truncateAddress(address)}
<span className="text-accent inline-flex items-center gap-1.5 font-medium">
{profile?.pfpUrl && (
// eslint-disable-next-line @next/next/no-img-element
<img
src={profile.pfpUrl}
alt=""
width={16}
height={16}
className="rounded-full"
/>
)}
{profile ? `@${profile.username}` : truncateAddress(address)}
</span>
<button
onClick={() => disconnect()}
Expand Down
69 changes: 69 additions & 0 deletions src/components/FarcasterAvatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"use client";

import { useEffect, useState } from "react";
import { getFarcasterProfile } from "../../lib/actions";
import { truncateAddress } from "../../lib/utils";
import type { FarcasterProfile } from "../../lib/farcaster";

/**
* Resolves an Ethereum address to a Farcaster identity via server action.
* Shows avatar + @username with link, or falls back to truncated address.
*/
export function FarcasterAvatar({
address,
size = 14,
className,
linkProfile = true,
}: {
address: string;
size?: number;
className?: string;
linkProfile?: boolean;
}) {
const [profile, setProfile] = useState<FarcasterProfile | null>(null);
const [loaded, setLoaded] = useState(false);

useEffect(() => {
let cancelled = false;
getFarcasterProfile(address).then((p) => {
if (!cancelled) {
setProfile(p);
setLoaded(true);
}
});
return () => {
cancelled = true;
};
}, [address]);

if (!loaded || !profile) {
return <span className={className}>{truncateAddress(address)}</span>;
}

return (
<span className={`inline-flex items-center gap-1 ${className ?? ""}`}>
{profile.pfpUrl && (
// eslint-disable-next-line @next/next/no-img-element
<img
src={profile.pfpUrl}
alt=""
width={size}
height={size}
className="rounded-full"
/>
)}
{linkProfile ? (
<a
href={`https://farcaster.com/${profile.username}`}
target="_blank"
rel="noopener noreferrer"
className="text-foreground hover:text-accent transition-colors"
>
@{profile.username}
</a>
) : (
<span>@{profile.username}</span>
)}
</span>
);
}
4 changes: 2 additions & 2 deletions src/components/StoryCard.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Link from "next/link";
import type { Storyline } from "../../lib/supabase";
import { truncateAddress } from "../../lib/utils";
import { AgentBadge } from "./AgentBadge";
import { WriterIdentityClient } from "./WriterIdentityClient";
import { RatingSummary } from "./RatingSummary";
import { StoryCardStats } from "./StoryCardStats";
import { ViewCount } from "./ViewCount";
Expand Down Expand Up @@ -41,7 +41,7 @@ export function StoryCard({

{/* Author + meta */}
<div className="text-muted mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs">
<span>By: {truncateAddress(storyline.writer_address)}</span>
<span className="inline-flex items-center gap-0.5">By: <WriterIdentityClient address={storyline.writer_address} linkProfile={false} /></span>
<span>
{storyline.plot_count} {storyline.plot_count === 1 ? "plot" : "plots"}
</span>
Expand Down
22 changes: 13 additions & 9 deletions src/components/WriterIdentityClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type { FarcasterProfile } from "../../lib/farcaster";
* Client component that resolves a Farcaster identity via server action.
* Shows a truncated address while loading, then replaces with avatar + username.
*/
export function WriterIdentityClient({ address }: { address: string }) {
export function WriterIdentityClient({ address, linkProfile = true }: { address: string; linkProfile?: boolean }) {
const [profile, setProfile] = useState<FarcasterProfile | null>(null);
const [loaded, setLoaded] = useState(false);

Expand Down Expand Up @@ -42,14 +42,18 @@ export function WriterIdentityClient({ address }: { address: string }) {
className="rounded-full"
/>
)}
<a
href={`https://farcaster.com/${profile.username}`}
target="_blank"
rel="noopener noreferrer"
className="text-foreground hover:text-accent transition-colors"
>
@{profile.username}
</a>
{linkProfile ? (
<a
href={`https://farcaster.com/${profile.username}`}
target="_blank"
rel="noopener noreferrer"
className="text-foreground hover:text-accent transition-colors"
>
@{profile.username}
</a>
) : (
<span>@{profile.username}</span>
)}
</span>
);
}
40 changes: 40 additions & 0 deletions src/hooks/useConnectedIdentity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"use client";

import { useEffect, useRef, useState } from "react";
import { useAccount } from "wagmi";
import { getFarcasterProfile } from "../../lib/actions";
import type { FarcasterProfile } from "../../lib/farcaster";

/**
* Resolves the connected wallet's Farcaster identity.
* Caches result for the session (re-fetches only on address change).
*/
export function useConnectedIdentity() {
const { address } = useAccount();
const [result, setResult] = useState<{
profile: FarcasterProfile | null;
resolvedFor: string | undefined;
}>({ profile: null, resolvedFor: undefined });
const fetchingRef = useRef(false);

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

let cancelled = false;
fetchingRef.current = true;
getFarcasterProfile(address).then((p) => {
if (!cancelled) {
setResult({ profile: p, resolvedFor: address });
fetchingRef.current = false;
}
});
return () => {
cancelled = true;
};
}, [address]);

if (!address) return { profile: null, loading: false };

const loading = result.resolvedFor !== address;
return { profile: loading ? null : result.profile, loading };
}
Loading