SkateSpots/src/components/FollowListManager.tsx
SkateSpots 6def78fffe chore(lint): clear all no-unused-vars warnings (position-precise)
Removed/renamed the remaining 39 unused locals/dead code, file-by-file:
- unused destructured bindings/params (remove the binding, keep the call)
- unused single-line pure consts and dead functions/interfaces removed
- deleted the dead _FeedItem component and its now-unused imports
- amber _readChunks self-call fixed; SpotMap popup call site updated

Warnings: 182 -> 7 (only react-refresh/only-export-components remain).
no-explicit-any=0, exhaustive-deps=0, no-unused-vars=0.
Gate: 101 tests, 0 errors, boot smoke green. AGENTS.md baseline + guidance updated.
2026-09-26 22:20:21 +07:00

276 lines
11 KiB
TypeScript

import { useState } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Skeleton } from '@/components/ui/skeleton';
import { useFollowing, useFollowers, useFollowActions } from '@/hooks/useFollowing';
import { useAuthor } from '@/hooks/useAuthor';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { genUserName } from '@/lib/genUserName';
import { Users, UserPlus, UserMinus, Loader2 } from 'lucide-react';
function FollowingUserCard({ pubkey, onUnfollow }: { pubkey: string; onUnfollow: (pubkey: string) => void }) {
const author = useAuthor(pubkey);
const metadata = author.data?.metadata;
const displayName = metadata?.name ?? genUserName(pubkey);
const [isUnfollowing, setIsUnfollowing] = useState(false);
const handleUnfollow = async () => {
setIsUnfollowing(true);
await onUnfollow(pubkey);
setIsUnfollowing(false);
};
return (
<div className="flex items-center justify-between p-3 bg-black/50 rounded-lg border border-gray-700 hover:border-[#ff0040]/50 transition-all">
<div className="flex items-center gap-3 flex-1 min-w-0">
<Avatar className="h-10 w-10 border-2 border-[#ff0040]/30">
<AvatarImage src={metadata?.picture} alt={displayName} />
<AvatarFallback className="bg-black text-[#ff0040] text-xs">
{displayName.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<div className="font-bold text-white truncate">{displayName}</div>
{metadata?.nip05 && (
<div className="flex items-center gap-1 text-xs">
<div className="w-1.5 h-1.5 rounded-full bg-[#ccff00]" />
<span className="text-[#ccff00] truncate">{metadata.nip05}</span>
</div>
)}
{!metadata?.nip05 && (
<div className="text-xs text-gray-500 truncate font-mono">
{pubkey.slice(0, 8)}...{pubkey.slice(-8)}
</div>
)}
</div>
</div>
<Button
variant="outline"
size="sm"
onClick={handleUnfollow}
disabled={isUnfollowing}
className="gap-1 border-gray-600 hover:border-red-500 hover:text-red-500"
>
{isUnfollowing ? (
<Loader2 className="h-3 w-3 animate-spin" />
) : (
<UserMinus className="h-3 w-3" />
)}
Unfollow
</Button>
</div>
);
}
function FollowerUserCard({ pubkey }: { pubkey: string }) {
const author = useAuthor(pubkey);
const metadata = author.data?.metadata;
const displayName = metadata?.name ?? genUserName(pubkey);
return (
<div className="flex items-center gap-3 p-3 bg-black/50 rounded-lg border border-gray-700">
<Avatar className="h-10 w-10 border-2 border-[#ccff00]/30">
<AvatarImage src={metadata?.picture} alt={displayName} />
<AvatarFallback className="bg-black text-[#ccff00] text-xs">
{displayName.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<div className="font-bold text-white truncate">{displayName}</div>
{metadata?.nip05 && (
<div className="flex items-center gap-1 text-xs">
<div className="w-1.5 h-1.5 rounded-full bg-[#ccff00]" />
<span className="text-[#ccff00] truncate">{metadata.nip05}</span>
</div>
)}
{!metadata?.nip05 && (
<div className="text-xs text-gray-500 truncate font-mono">
{pubkey.slice(0, 8)}...{pubkey.slice(-8)}
</div>
)}
</div>
</div>
);
}
export function FollowListManager() {
const { user } = useCurrentUser();
const { data: following = [], isLoading: followingLoading } = useFollowing();
const { data: followers = [], isLoading: followersLoading } = useFollowers();
const { unfollow, followByIdentifier, isFollowing } = useFollowActions();
const [followInput, setFollowInput] = useState('');
const handleFollow = async (e: React.FormEvent) => {
e.preventDefault();
if (!followInput.trim()) return;
await followByIdentifier(followInput.trim());
setFollowInput('');
};
if (!user) {
return null;
}
return (
<Card className="bg-gradient-to-br from-purple-950/30 to-blue-950/30 border-2 border-purple-500/50">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-purple-400">
<Users className="h-5 w-5" />
Your Nostr Network
</CardTitle>
<CardDescription className="text-gray-300">
Manage who you follow. Following users helps build your Web of Trust and determines which spots show trust badges.
</CardDescription>
</CardHeader>
<CardContent>
<Tabs defaultValue="following" className="space-y-4">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="following" className="gap-2">
<UserPlus className="h-4 w-4" />
Following ({following.length})
</TabsTrigger>
<TabsTrigger value="followers" className="gap-2">
<Users className="h-4 w-4" />
Followers ({followers.length})
</TabsTrigger>
</TabsList>
<TabsContent value="following" className="space-y-4">
{/* Add Follow Input */}
<form onSubmit={handleFollow} className="space-y-2">
<div className="flex gap-2">
<Input
placeholder="npub1... or user@domain.com"
value={followInput}
onChange={(e) => setFollowInput(e.target.value)}
className="flex-1"
disabled={isFollowing}
/>
<Button
type="submit"
disabled={!followInput.trim() || isFollowing}
className="gap-2 bg-[#ff0040] hover:bg-[#ff0040]/90"
>
{isFollowing ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<UserPlus className="h-4 w-4" />
)}
Follow
</Button>
</div>
<p className="text-xs text-gray-500">
💡 Paste a Nostr pubkey (npub1...), NIP-05 address (user@domain.com), or hex pubkey
</p>
</form>
{/* Following List */}
{followingLoading ? (
<div className="space-y-2">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="p-3 bg-black/50 rounded-lg border border-gray-700">
<div className="flex items-center gap-3">
<Skeleton className="h-10 w-10 rounded-full" />
<div className="space-y-1 flex-1">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-3 w-24" />
</div>
</div>
</div>
))}
</div>
) : following.length > 0 ? (
<div className="space-y-2 max-h-96 overflow-y-auto">
{following.map((pubkey) => (
<FollowingUserCard
key={pubkey}
pubkey={pubkey}
onUnfollow={unfollow}
/>
))}
</div>
) : (
<div className="text-center py-8 border-2 border-dashed border-gray-700 rounded-lg">
<Users className="h-12 w-12 mx-auto mb-3 text-gray-600" />
<p className="text-gray-400 mb-2">You're not following anyone yet</p>
<p className="text-sm text-gray-500">
Follow skaters to see their spots with ✅ VERIFIED badges
</p>
</div>
)}
{/* Trust Info */}
{following.length > 0 && (
<div className="p-4 rounded-lg bg-green-950/30 border border-green-500/30 text-sm">
<p className="text-green-400">
<strong>✅ Trust Badges Active:</strong> Spots created by people you follow will now show VERIFIED badges.
Spots by their friends will show TRUSTED badges.
</p>
</div>
)}
</TabsContent>
<TabsContent value="followers" className="space-y-4">
{followersLoading ? (
<div className="space-y-2">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="p-3 bg-black/50 rounded-lg border border-gray-700">
<div className="flex items-center gap-3">
<Skeleton className="h-10 w-10 rounded-full" />
<div className="space-y-1 flex-1">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-3 w-24" />
</div>
</div>
</div>
))}
</div>
) : followers.length > 0 ? (
<div className="space-y-2 max-h-96 overflow-y-auto">
{followers.map((pubkey) => (
<FollowerUserCard key={pubkey} pubkey={pubkey} />
))}
</div>
) : (
<div className="text-center py-8 border-2 border-dashed border-gray-700 rounded-lg">
<Users className="h-12 w-12 mx-auto mb-3 text-gray-600" />
<p className="text-gray-400 mb-2">No followers yet</p>
<p className="text-sm text-gray-500">
Share your Nostr profile to gain followers!
</p>
</div>
)}
</TabsContent>
</Tabs>
{/* Info Card */}
<Card className="bg-blue-950/30 border-2 border-blue-500/50 mt-6">
<CardHeader>
<CardTitle className="text-sm text-blue-400">💡 How Following Affects Trust Badges</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm text-gray-300">
<p>
<strong className="text-white">✅ VERIFIED (Green):</strong> Spots created by people you directly follow
</p>
<p>
<strong className="text-white">🟡 TRUSTED (Yellow):</strong> Spots created by people your friends follow (2 hops)
</p>
<p>
<strong className="text-white">⚪ Unverified:</strong> Spots by users with no connection to your network
</p>
<p className="text-xs text-blue-400 mt-4">
💡 Tip: Follow active skateboarders to see more verified spots and build your trust network!
</p>
</CardContent>
</Card>
</CardContent>
</Card>
);
}