import React, { useState, useEffect } from 'react'; import { Search, Globe, Users, Eye, ArrowLeftRight, Loader2, Clock, History, UserPlus, UserMinus, Check, X, Send, Settings, Save, ChevronLeft } from 'lucide-react'; import { useAuth } from '../contexts/AuthContext'; import { useToast } from '../contexts/ToastContext'; import { supabase } from '../lib/supabase'; import { getFriends, getPendingRequests, getSentRequests, searchUsers, sendFriendRequest, acceptFriendRequest, declineFriendRequest, removeFriend, Friend, } from '../services/friendsService'; import { getTrades, getTradeHistory, acceptTrade, declineTrade, cancelTrade, Trade, TradeItem, } from '../services/tradesService'; import { getUserCollection, getCardsByIds } from '../services/api'; import { Card } from '../types'; import TradeCreator from './TradeCreator'; import TradeDetail from './TradeDetail'; import ConfirmModal from './ConfirmModal'; interface UserProfile { id: string; username: string | null; collection_visibility: 'public' | 'friends' | 'private' | null; } interface CollectionItem { card: Card; quantity: number; } type Tab = 'browse' | 'friends' | 'trades' | 'profile'; type FriendsSubTab = 'list' | 'requests' | 'search'; type TradesSubTab = 'pending' | 'history'; const VISIBILITY_OPTIONS = [ { value: 'public', label: 'Public', description: 'Anyone can view' }, { value: 'friends', label: 'Friends', description: 'Friends only' }, { value: 'private', label: 'Private', description: 'Only you' }, ] as const; export default function Community() { const { user } = useAuth(); const toast = useToast(); const [activeTab, setActiveTab] = useState('browse'); const [loading, setLoading] = useState(true); // Browse state const [browseSearch, setBrowseSearch] = useState(''); const [publicUsers, setPublicUsers] = useState([]); const [selectedUser, setSelectedUser] = useState(null); const [selectedUserCollection, setSelectedUserCollection] = useState([]); const [loadingCollection, setLoadingCollection] = useState(false); const [showTradeCreator, setShowTradeCreator] = useState(false); const [userCollectionSearch, setUserCollectionSearch] = useState(''); // Friends state const [friendsSubTab, setFriendsSubTab] = useState('list'); const [friends, setFriends] = useState([]); const [pendingRequests, setPendingRequests] = useState([]); const [sentRequests, setSentRequests] = useState([]); const [friendSearch, setFriendSearch] = useState(''); const [friendSearchResults, setFriendSearchResults] = useState<{ id: string; username: string | null }[]>([]); const [searchingFriends, setSearchingFriends] = useState(false); // Trades state const [tradesSubTab, setTradesSubTab] = useState('pending'); const [pendingTrades, setPendingTrades] = useState([]); const [tradeHistory, setTradeHistory] = useState([]); const [tradeCardDetails, setTradeCardDetails] = useState>(new Map()); const [processingTradeId, setProcessingTradeId] = useState(null); const [selectedTrade, setSelectedTrade] = useState(null); // Profile state const [username, setUsername] = useState(''); const [collectionVisibility, setCollectionVisibility] = useState<'public' | 'friends' | 'private'>('private'); const [savingProfile, setSavingProfile] = useState(false); // Confirm modal state const [confirmModal, setConfirmModal] = useState<{ isOpen: boolean; title: string; message: string; onConfirm: () => void; variant: 'danger' | 'warning' | 'info' | 'success'; }>({ isOpen: false, title: '', message: '', onConfirm: () => {}, variant: 'danger' }); useEffect(() => { if (user) { loadAllData(); } }, [user]); // ============ REALTIME SUBSCRIPTIONS ============ // Subscribe to trade changes useEffect(() => { if (!user) return; const tradesChannel = supabase .channel('trades-changes') .on( 'postgres_changes', { event: '*', schema: 'public', table: 'trades', }, (payload: any) => { // Filter for trades involving this user const newData = payload.new || payload.old; if (newData && (newData.user1_id === user.id || newData.user2_id === user.id)) { console.log('Trade change:', payload); loadTradesData(); } } ) .subscribe(); return () => { supabase.removeChannel(tradesChannel); }; }, [user]); // Subscribe to friendship changes useEffect(() => { if (!user) return; const friendshipsChannel = supabase .channel('friendships-changes') .on( 'postgres_changes', { event: '*', schema: 'public', table: 'friendships', }, (payload: any) => { // Filter for friendships involving this user const newData = payload.new || payload.old; if (newData && (newData.requester_id === user.id || newData.addressee_id === user.id)) { console.log('Friendship change:', payload); loadFriendsData(); } } ) .subscribe(); return () => { supabase.removeChannel(friendshipsChannel); }; }, [user]); // Subscribe to profile changes (for visibility updates) useEffect(() => { if (!user) return; const profilesChannel = supabase .channel('profiles-changes') .on( 'postgres_changes', { event: 'UPDATE', schema: 'public', table: 'profiles', }, (payload: any) => { console.log('Profile change:', payload); // Reload public users if a profile's visibility changed if (payload.new && payload.old && payload.new.collection_visibility !== payload.old.collection_visibility) { loadPublicUsers(); } // Reload own profile if it's the current user if (payload.new && payload.new.id === user.id) { loadProfile(); } } ) .subscribe(); return () => { supabase.removeChannel(profilesChannel); }; }, [user]); // Subscribe to collection changes when viewing someone's collection useEffect(() => { if (!user || !selectedUser) return; const collectionsChannel = supabase .channel(`collections-${selectedUser.id}`) .on( 'postgres_changes', { event: '*', schema: 'public', table: 'collections', }, (payload: any) => { // Filter for the selected user's collections const data = payload.new || payload.old; if (data && data.user_id === selectedUser.id) { console.log('Collection change for viewed user:', payload); loadUserCollection(selectedUser.id); } } ) .subscribe(); return () => { supabase.removeChannel(collectionsChannel); }; }, [user, selectedUser]); const loadAllData = async () => { if (!user) return; setLoading(true); try { await Promise.all([ loadPublicUsers(), loadFriendsData(), loadTradesData(), loadProfile(), ]); } catch (error) { console.error('Error loading data:', error); } finally { setLoading(false); } }; // ============ BROWSE FUNCTIONS ============ const loadPublicUsers = async () => { const { data, error } = await supabase .from('profiles') .select('id, username, collection_visibility') .eq('collection_visibility', 'public') .neq('id', user?.id) .order('username'); if (!error && data) { setPublicUsers(data); } }; const loadUserCollection = async (userId: string) => { setLoadingCollection(true); try { const collectionMap = await getUserCollection(userId); if (collectionMap.size === 0) { setSelectedUserCollection([]); return; } const cardIds = Array.from(collectionMap.keys()); const cards = await getCardsByIds(cardIds); setSelectedUserCollection(cards.map((card) => ({ card, quantity: collectionMap.get(card.id) || 0, }))); } catch (error) { console.error('Error loading collection:', error); setSelectedUserCollection([]); } finally { setLoadingCollection(false); } }; // ============ FRIENDS FUNCTIONS ============ const loadFriendsData = async () => { if (!user) return; const [friendsData, pendingData, sentData] = await Promise.all([ getFriends(user.id), getPendingRequests(user.id), getSentRequests(user.id), ]); setFriends(friendsData); setPendingRequests(pendingData); setSentRequests(sentData); }; const handleSearchFriends = async () => { if (!user || friendSearch.trim().length < 2) return; setSearchingFriends(true); try { const results = await searchUsers(friendSearch, user.id); setFriendSearchResults(results || []); } catch (error) { console.error('Error searching users:', error); } finally { setSearchingFriends(false); } }; const handleSendRequest = async (addresseeId: string) => { if (!user) return; try { await sendFriendRequest(user.id, addresseeId); setFriendSearchResults((prev) => prev.filter((u) => u.id !== addresseeId)); await loadFriendsData(); toast.success('Friend request sent!'); } catch (error) { toast.error('Failed to send friend request'); } }; const handleAcceptRequest = async (friendshipId: string) => { try { await acceptFriendRequest(friendshipId); await loadFriendsData(); toast.success('Friend request accepted!'); } catch (error) { toast.error('Failed to accept request'); } }; const handleDeclineRequest = async (friendshipId: string) => { try { await declineFriendRequest(friendshipId); await loadFriendsData(); toast.info('Friend request declined'); } catch (error) { toast.error('Failed to decline request'); } }; const handleRemoveFriend = (friendshipId: string, friendName: string) => { setConfirmModal({ isOpen: true, title: 'Remove Friend', message: `Remove ${friendName} from your friends?`, variant: 'danger', onConfirm: async () => { try { await removeFriend(friendshipId); await loadFriendsData(); toast.success('Friend removed'); } catch (error) { toast.error('Failed to remove friend'); } }, }); }; const isAlreadyFriendOrPending = (userId: string) => { return friends.some((f) => f.id === userId) || pendingRequests.some((f) => f.id === userId) || sentRequests.some((f) => f.id === userId); }; // ============ TRADES FUNCTIONS ============ const loadTradesData = async () => { if (!user) return; const [pending, history] = await Promise.all([ getTrades(user.id).then((trades) => trades.filter((t) => t.status === 'pending')), getTradeHistory(user.id), ]); const allCardIds = new Set(); [...pending, ...history].forEach((trade) => { trade.items?.forEach((item) => allCardIds.add(item.card_id)); }); if (allCardIds.size > 0) { const cards = await getCardsByIds(Array.from(allCardIds)); const cardMap = new Map(); cards.forEach((card) => cardMap.set(card.id, card)); setTradeCardDetails(cardMap); } setPendingTrades(pending); setTradeHistory(history); }; const handleAcceptTrade = async (tradeId: string) => { setProcessingTradeId(tradeId); try { const success = await acceptTrade(tradeId); if (success) { await loadTradesData(); toast.success('Trade accepted! Cards exchanged.'); } else { toast.error('Failed. Check your collection.'); } } catch (error) { toast.error('Error accepting trade'); } finally { setProcessingTradeId(null); } }; const handleDeclineTrade = async (tradeId: string) => { setProcessingTradeId(tradeId); try { await declineTrade(tradeId); await loadTradesData(); toast.info('Trade declined'); } catch (error) { toast.error('Error declining trade'); } finally { setProcessingTradeId(null); } }; const handleCancelTrade = (tradeId: string) => { setConfirmModal({ isOpen: true, title: 'Cancel Trade', message: 'Cancel this trade offer?', variant: 'warning', onConfirm: async () => { setProcessingTradeId(tradeId); try { await cancelTrade(tradeId); await loadTradesData(); toast.info('Trade cancelled'); } catch (error) { toast.error('Error cancelling trade'); } finally { setProcessingTradeId(null); } }, }); }; // ============ PROFILE FUNCTIONS ============ const loadProfile = async () => { if (!user) return; const { data } = await supabase .from('profiles') .select('username, collection_visibility') .eq('id', user.id) .single(); if (data) { setUsername(data.username || ''); setCollectionVisibility(data.collection_visibility || 'private'); } }; const handleSaveProfile = async () => { if (!user) return; setSavingProfile(true); try { const { error } = await supabase .from('profiles') .upsert({ id: user.id, username, collection_visibility: collectionVisibility, updated_at: new Date(), }); if (error) throw error; toast.success('Profile updated!'); } catch (error) { toast.error('Failed to update profile'); } finally { setSavingProfile(false); } }; // ============ RENDER HELPERS ============ const calculateTradeItemsPrice = (items: TradeItem[] | undefined, ownerId: string): number => { const ownerItems = items?.filter((i) => i.owner_id === ownerId) || []; return ownerItems.reduce((total, item) => { const card = tradeCardDetails.get(item.card_id); const price = card?.prices?.usd ? parseFloat(card.prices.usd) : 0; return total + (price * item.quantity); }, 0); }; const renderTradeItems = (items: TradeItem[] | undefined, ownerId: string, label: string) => { const ownerItems = items?.filter((i) => i.owner_id === ownerId) || []; const totalPrice = calculateTradeItemsPrice(items, ownerId); if (ownerItems.length === 0) { return (

{label}:

Gift

); } return (

{label}:

${totalPrice.toFixed(2)}

{ownerItems.map((item) => { const card = tradeCardDetails.get(item.card_id); return (
{card?.name || 'Card'} {item.quantity > 1 && x{item.quantity}}
); })}
); }; // Loading state if (loading) { return (
); } // ============ USER COLLECTION VIEW ============ if (selectedUser) { const filteredUserCollection = selectedUserCollection.filter(({ card }) => card.name.toLowerCase().includes(userCollectionSearch.toLowerCase()) ); return (
{/* Header */}

{selectedUser.username}

{/* Search input */}
setUserCollectionSearch(e.target.value)} placeholder="Search cards..." className="w-full pl-9 pr-8 py-2 bg-gray-700 border border-gray-600 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent" /> {userCollectionSearch && ( )}
{/* Collection Grid */}
{loadingCollection ? (
) : selectedUserCollection.length === 0 ? (

Empty collection

) : filteredUserCollection.length === 0 ? (

No cards match "{userCollectionSearch}"

) : (
{filteredUserCollection.map(({ card, quantity }) => (
{card.name} x{quantity}
))}
)}
{showTradeCreator && ( setShowTradeCreator(false)} onTradeCreated={() => { setShowTradeCreator(false); loadTradesData(); toast.success('Trade proposal sent!'); }} /> )}
); } const filteredPublicUsers = publicUsers.filter( (u) => !browseSearch || u.username?.toLowerCase().includes(browseSearch.toLowerCase()) ); // ============ MAIN VIEW ============ return (
{/* Header */}

Community

{/* Tabs */}
{[ { id: 'browse' as Tab, label: 'Browse', icon: Globe }, { id: 'friends' as Tab, label: `Friends`, count: friends.length, icon: Users }, { id: 'trades' as Tab, label: `Trades`, count: pendingTrades.length, icon: ArrowLeftRight }, { id: 'profile' as Tab, label: 'Profile', icon: Settings }, ].map((tab) => ( ))}
{/* ============ BROWSE TAB ============ */} {activeTab === 'browse' && (
{/* Search */}
setBrowseSearch(e.target.value)} placeholder="Search users..." className="w-full pl-10 pr-4 py-2.5 bg-gray-800 border border-gray-700 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
{/* Users List */} {filteredPublicUsers.length === 0 ? (

No public collections

) : (
{filteredPublicUsers.map((userProfile) => ( ))}
)} {/* Friends shortcut */} {friends.length > 0 && (

Your friends

{friends.slice(0, 3).map((friend) => ( ))} {friends.length > 3 && ( )}
)}
)} {/* ============ FRIENDS TAB ============ */} {activeTab === 'friends' && (
{/* Sub tabs */}
{[ { id: 'list' as FriendsSubTab, label: 'List' }, { id: 'requests' as FriendsSubTab, label: 'Requests', count: pendingRequests.length }, { id: 'search' as FriendsSubTab, label: 'Add' }, ].map((tab) => ( ))}
{/* Friends List */} {friendsSubTab === 'list' && ( friends.length === 0 ? (

No friends yet

) : (
{friends.map((friend) => (
{friend.username || 'Unknown'}
))}
) )} {/* Requests */} {friendsSubTab === 'requests' && (
{pendingRequests.length > 0 && (

Received

{pendingRequests.map((req) => (
{req.username || 'Unknown'}
))}
)} {sentRequests.length > 0 && (

Sent

{sentRequests.map((req) => (
{req.username || 'Unknown'}
Pending
))}
)} {pendingRequests.length === 0 && sentRequests.length === 0 && (

No requests

)}
)} {/* Search/Add */} {friendsSubTab === 'search' && (
setFriendSearch(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleSearchFriends()} placeholder="Username..." className="flex-1 px-3 py-2.5 bg-gray-800 border border-gray-700 rounded-lg text-sm" />
{friendSearchResults.length > 0 && (
{friendSearchResults.map((result) => (
{result.username || 'Unknown'} {isAlreadyFriendOrPending(result.id) ? ( Connected ) : ( )}
))}
)}
)}
)} {/* ============ TRADES TAB ============ */} {activeTab === 'trades' && (
{/* Sub tabs */}
{/* Trades List */} {(tradesSubTab === 'pending' ? pendingTrades : tradeHistory).length === 0 ? (

{tradesSubTab === 'pending' ? 'No pending trades' : 'No history'}

) : (
{(tradesSubTab === 'pending' ? pendingTrades : tradeHistory).map((trade) => { const isUser1 = trade.user1_id === user?.id; const myUserId = user?.id || ''; const otherUserId = isUser1 ? trade.user2_id : trade.user1_id; const otherUser = isUser1 ? trade.user2 : trade.user1; const statusColors: Record = { accepted: 'text-green-400', declined: 'text-red-400', cancelled: 'text-gray-400', pending: 'text-yellow-400', }; // Both users can view details for pending trades const canViewDetails = trade.status === 'pending'; return (
canViewDetails && setSelectedTrade(trade)} > {/* Header */}
With: {otherUser?.username}
{trade.status}
{/* Items */}
{renderTradeItems(trade.items, myUserId, 'You Give')} {renderTradeItems(trade.items, otherUserId, 'You Get')}
{canViewDetails && (

Tap to view details

)} {/* Actions - Allow any user to cancel pending trade */} {tradesSubTab === 'pending' && (
e.stopPropagation()}>
)}
); })}
)}
)} {/* ============ PROFILE TAB ============ */} {activeTab === 'profile' && (
{/* Username */}
setUsername(e.target.value)} className="w-full px-3 py-2.5 bg-gray-800 border border-gray-700 rounded-lg text-sm" placeholder="Your username" />
{/* Visibility */}
{VISIBILITY_OPTIONS.map((option) => ( ))}
{/* Save */}
)} {/* Trade Detail Modal */} {selectedTrade && ( setSelectedTrade(null)} onAccept={handleAcceptTrade} onDecline={handleDeclineTrade} onTradeUpdated={() => { setSelectedTrade(null); loadTradesData(); }} /> )} {/* Confirm Modal */} setConfirmModal({ ...confirmModal, isOpen: false })} onConfirm={confirmModal.onConfirm} title={confirmModal.title} message={confirmModal.message} variant={confirmModal.variant} />
); }