SkateSpots/src/components/FollowListManager.tsx
SkateSpots 0b796440f1 feat(i18n): Spanish batch 2 — nav/chrome, events/shops/tours, crews/DMs, settings/auth, wallet/stash/newsletter/stats
~3,470 new keys (5,770 total, EN=ES), ~115 more files wired.
- nav + chromeUi: header, More menu, burger menu, footer, map toolbar widgets
  (sort/trust/weather), spot-type label helper.
- placesUi: events, shops, tours, food. socialUi: crews, DMs, notifications,
  collectives, crew chat/coordinator. settingsUi: settings, auth/signup,
  onboarding, profile edit, legal. walletUi: wallet/Cashu, stash, mints,
  app shop, newsletter, community stats, leaderboards, profile.
- Fixed 738 usages where an agent dropped the socialUi. prefix (caught by check:i18n).
2026-10-02 10:11:06 +07:00

278 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';
import { useTranslation } from '@/contexts/language';
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 { t } = useTranslation();
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" />
)}
{t('settingsUi.followList.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 { t } = useTranslation();
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" />
{t('settingsUi.followList.title')}
</CardTitle>
<CardDescription className="text-gray-300">
{t('settingsUi.followList.desc')}
</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" />
{t('settingsUi.followList.following', { count: following.length })}
</TabsTrigger>
<TabsTrigger value="followers" className="gap-2">
<Users className="h-4 w-4" />
{t('settingsUi.followList.followers', { count: 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" />
)}
{t('settingsUi.followList.follow')}
</Button>
</div>
<p className="text-xs text-gray-500">
{t('settingsUi.followList.followHint')}
</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">{t('settingsUi.followList.notFollowing')}</p>
<p className="text-sm text-gray-500">
{t('settingsUi.followList.notFollowingHint')}
</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>{t('settingsUi.followList.trustActiveStrong')}</strong> {t('settingsUi.followList.trustActive')}
</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">{t('settingsUi.followList.noFollowers')}</p>
<p className="text-sm text-gray-500">
{t('settingsUi.followList.noFollowersHint')}
</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">{t('settingsUi.followList.howTitle')}</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm text-gray-300">
<p>
<strong className="text-white">{t('settingsUi.followList.verifiedStrong')}</strong> {t('settingsUi.followList.verifiedDesc')}
</p>
<p>
<strong className="text-white">{t('settingsUi.followList.trustedStrong')}</strong> {t('settingsUi.followList.trustedDesc')}
</p>
<p>
<strong className="text-white">{t('settingsUi.followList.unverifiedStrong')}</strong> {t('settingsUi.followList.unverifiedDesc')}
</p>
<p className="text-xs text-blue-400 mt-4">
{t('settingsUi.followList.tip')}
</p>
</CardContent>
</Card>
</CardContent>
</Card>
);
}