SkateSpots/src/components/FollowListManager.tsx
shakespeare.diy e29d35f1e4 fix: restore full skate-spots source tree from misplaced src/src/ copy
The entire skate-spots codebase was accidentally copied into src/src/ and
public/public/ instead of src/ and public/ directly. This left the real
src/ directory as blank MKStack template stubs, explaining why the landing
page showed the default template instead of SkateSpots.

Changes made:
- Copied all files from src/src/ up to src/ (App.tsx, AppRouter.tsx, main.tsx,
  all pages, components, hooks, contexts, lib, i18n, test)
- Copied all files from public/public/ up to public/ (images, manifest, etc.)
- Deleted src/src/ and public/public/ nested directories
- Removed HelloWorld.tsx template stub from pages/
- Retained the previously fixed index.css (Tailwind v3 directives)

Full skate-spots feature set is now active:
- All 30+ routes (map, events, spots, coaches, marketplace, wallet, etc.)
- NWCProvider, LanguageProvider, PWAInstallPrompt, AmberCallbackHandler
- Skate community relays (goskatespots.nostr1.com, wotr.relatr.xyz, etc.)
- Outfit Variable font, Amber pre-check in main.tsx
- All hooks (useSkateSpots, useCoaches, useZaps, etc.)
- i18n support (en/es)

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-15 22:18:28 +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 { Badge } from '@/components/ui/badge';
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, AlertCircle } from 'lucide-react';
import { cn } from '@/lib/utils';
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, isUnfollowing } = 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>
);
}