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.
276 lines
11 KiB
TypeScript
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>
|
|
);
|
|
}
|