Skip to content

Commit 8784bb0

Browse files
committed
feat: implement Fan component for following creators and registering as a fan
1 parent 0f13444 commit 8784bb0

File tree

1 file changed

+237
-0
lines changed

1 file changed

+237
-0
lines changed

packages/nextjs/components/Fan.tsx

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
import { useEffect, useState } from "react";
2+
import { Button } from "./ui/button";
3+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card";
4+
import { Input } from "./ui/input";
5+
import { Heart, HeartHandshake } from "lucide-react";
6+
import { isAddress } from "viem";
7+
import { useAccount } from "wagmi";
8+
import { useRegisterFan } from "~~/hooks/useRegisterFan";
9+
import { checkFollowStatus } from "~~/lib/eik";
10+
import { followCreator } from "~~/lib/follow";
11+
import { notification } from "~~/utils/scaffold-eth";
12+
13+
export const Fan = () => {
14+
const { address } = useAccount();
15+
const [creatorAddress, setCreatorAddress] = useState("");
16+
const [fanLabel, setFanLabel] = useState("");
17+
const [registeredENS, setRegisteredENS] = useState<string | null>(null);
18+
const [isCheckingFollow] = useState(false);
19+
const [isFollowing, setIsFollowing] = useState(false);
20+
const [followStatus, setFollowStatus] = useState<boolean | null>(null);
21+
const { registerFan, isPending } = useRegisterFan();
22+
23+
// Check initial follow status when creator address changes
24+
useEffect(() => {
25+
const checkInitialFollowStatus = async () => {
26+
if (address && creatorAddress.trim()) {
27+
const follows = await checkFollowStatus(address, creatorAddress.trim());
28+
setFollowStatus(follows);
29+
} else {
30+
setFollowStatus(null);
31+
}
32+
};
33+
34+
checkInitialFollowStatus();
35+
}, [address, creatorAddress]);
36+
37+
const handleFollow = async () => {
38+
if (!address || !creatorAddress.trim()) {
39+
notification.error("Please connect wallet and enter creator address");
40+
return;
41+
}
42+
43+
try {
44+
setIsFollowing(true);
45+
const result = await followCreator(creatorAddress.trim(), address);
46+
47+
if (result.success) {
48+
notification.success("Successfully followed creator! You can now register as a fan.");
49+
setFollowStatus(true);
50+
} else {
51+
notification.error(result.error || "Failed to follow creator");
52+
}
53+
} catch (error) {
54+
console.error("Follow error:", error);
55+
notification.error("Failed to follow creator");
56+
} finally {
57+
setIsFollowing(false);
58+
}
59+
};
60+
61+
// const checkFollow = async (creator: string, fan: string): Promise<boolean> => {
62+
// try {
63+
// setIsCheckingFollow(true);
64+
// const follows = await checkFollowStatus(fan, creator);
65+
// return follows;
66+
// } catch (error) {
67+
// console.error("Error checking follow status:", error);
68+
// // For demo purposes, return false if API fails (strict mode)
69+
// notification.warning("Could not verify follow status from EIK API");
70+
// return false;
71+
// } finally {
72+
// setIsCheckingFollow(false);
73+
// }
74+
// };
75+
76+
const handleRegister = async () => {
77+
if (!address) {
78+
notification.error("Please connect your wallet first");
79+
return;
80+
}
81+
82+
if (!creatorAddress.trim()) {
83+
notification.error("Please enter creator's address");
84+
return;
85+
}
86+
87+
const trimmedAddress = creatorAddress.trim();
88+
const isValidAddress = isAddress(trimmedAddress);
89+
const isENSName = trimmedAddress.endsWith(".eth");
90+
91+
if (!isValidAddress && !isENSName) {
92+
notification.error("Please enter a valid Ethereum address or ENS name");
93+
return;
94+
}
95+
96+
if (!fanLabel.trim()) {
97+
notification.error("Please enter your fan label");
98+
return;
99+
}
100+
101+
try {
102+
// Check follow status using real EIK API
103+
// const follows = await checkFollow(trimmedAddress, address);
104+
105+
// if (!follows) {
106+
// notification.error("You must follow this creator before registering as a fan. Use the 'Follow Creator' button below!");
107+
// return;
108+
// }
109+
110+
// Generate a deterministic creator node hash from creator address
111+
// In a real implementation, you'd query the contract to find the actual creator node
112+
// const creatorNodeHash = keccak256(toBytes(trimmedAddress.toLowerCase()));
113+
114+
const result = await registerFan(trimmedAddress, fanLabel.trim());
115+
116+
if (result) {
117+
const ensName = `${fanLabel.trim()}.${trimmedAddress.toLowerCase()}`;
118+
setRegisteredENS(ensName);
119+
notification.success(`Fan registration successful! Your ENS: ${ensName}`);
120+
setCreatorAddress("");
121+
setFanLabel("");
122+
}
123+
} catch (error: any) {
124+
console.error("Fan registration failed:", error);
125+
notification.error(error?.message || "Fan registration failed");
126+
}
127+
};
128+
129+
return (
130+
<Card className="w-full max-w-md">
131+
<CardHeader className="text-center">
132+
<CardTitle>Register as Fan</CardTitle>
133+
<CardDescription>Get your fan.creator.fantoken.eth subdomain</CardDescription>
134+
</CardHeader>
135+
<CardContent className="space-y-4">
136+
{registeredENS ? (
137+
<div className="text-center space-y-4">
138+
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
139+
<p className="text-blue-800 dark:text-blue-200 font-medium">🎉 Fan Registration Successful!</p>
140+
<p className="text-blue-700 dark:text-blue-300 text-sm mt-1">
141+
Your ENS: <strong>{registeredENS}</strong>
142+
</p>
143+
</div>
144+
<Button variant="outline" onClick={() => setRegisteredENS(null)} className="w-full">
145+
Register for Another Creator
146+
</Button>
147+
</div>
148+
) : (
149+
<>
150+
<div className="space-y-2">
151+
<label htmlFor="creator-address" className="text-sm font-medium">
152+
Creator&apos;s Address or ENS
153+
</label>
154+
<Input
155+
id="creator-address"
156+
placeholder="0x... or creator.eth"
157+
value={creatorAddress}
158+
onChange={e => setCreatorAddress(e.target.value)}
159+
disabled={isPending || isCheckingFollow}
160+
/>
161+
</div>
162+
163+
<div className="space-y-2">
164+
<label htmlFor="fan-label" className="text-sm font-medium">
165+
Your Fan Label
166+
</label>
167+
<Input
168+
id="fan-label"
169+
placeholder="e.g. alice, bob123"
170+
value={fanLabel}
171+
onChange={e => setFanLabel(e.target.value)}
172+
disabled={isPending || isCheckingFollow}
173+
/>
174+
<p className="text-xs text-muted-foreground">
175+
This will create: {fanLabel || "your-label"}.{creatorAddress || "creator.fantoken.eth"}
176+
</p>
177+
</div>
178+
179+
<Button
180+
onClick={handleRegister}
181+
disabled={isPending || isCheckingFollow || !address || !creatorAddress.trim() || !fanLabel.trim()}
182+
className="w-full"
183+
>
184+
{isPending ? "Registering..." : isCheckingFollow ? "Checking Follow Status..." : "Register as Fan"}
185+
</Button>
186+
187+
{!address && (
188+
<p className="text-sm text-muted-foreground text-center">Please connect your wallet to register</p>
189+
)}
190+
191+
{/* Follow Status & Button */}
192+
{creatorAddress.trim() && (
193+
<div className="space-y-3">
194+
<div className="flex items-center justify-center space-x-2 p-3 rounded-lg bg-muted/50">
195+
{followStatus === true ? (
196+
<>
197+
<HeartHandshake className="w-5 h-5 text-green-600" />
198+
<span className="text-sm text-green-600 font-medium">Following this creator</span>
199+
</>
200+
) : followStatus === false ? (
201+
<>
202+
<Heart className="w-5 h-5 text-muted-foreground" />
203+
<span className="text-sm text-muted-foreground">Not following this creator</span>
204+
</>
205+
) : (
206+
<span className="text-sm text-muted-foreground">Checking follow status...</span>
207+
)}
208+
</div>
209+
210+
{followStatus === false && (
211+
<Button onClick={handleFollow} disabled={isFollowing} variant="outline" className="w-full">
212+
{isFollowing ? (
213+
<>
214+
<Heart className="w-4 h-4 mr-2 animate-pulse" />
215+
Following...
216+
</>
217+
) : (
218+
<>
219+
<HeartHandshake className="w-4 h-4 mr-2" />
220+
Follow Creator
221+
</>
222+
)}
223+
</Button>
224+
)}
225+
</div>
226+
)}
227+
228+
<div className="text-xs text-muted-foreground text-center space-y-1">
229+
<p>⚠️ You must be following this creator to register</p>
230+
<p>Follow status verified via EIK (Ethereum Identity Kit)</p>
231+
</div>
232+
</>
233+
)}
234+
</CardContent>
235+
</Card>
236+
);
237+
};

0 commit comments

Comments
 (0)