From a005df996572f9800f1c6b123060d65f910f01b9 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Wed, 26 Nov 2025 14:25:50 +0100 Subject: [PATCH] add trade detail view and counter offer functionality --- src/components/Community.tsx | 145 ++++++++++++---- src/components/TradeDetail.tsx | 308 +++++++++++++++++++++++++++++++++ 2 files changed, 420 insertions(+), 33 deletions(-) create mode 100644 src/components/TradeDetail.tsx diff --git a/src/components/Community.tsx b/src/components/Community.tsx index 005fc3c..7cd6671 100644 --- a/src/components/Community.tsx +++ b/src/components/Community.tsx @@ -26,6 +26,7 @@ import { import { getUserCollection, getCardsByIds } from '../services/api'; import { Card } from '../types'; import TradeCreator from './TradeCreator'; +import TradeDetail from './TradeDetail'; import ConfirmModal from './ConfirmModal'; interface UserProfile { @@ -79,6 +80,13 @@ export default function Community() { const [tradeHistory, setTradeHistory] = useState([]); const [tradeCardDetails, setTradeCardDetails] = useState>(new Map()); const [processingTradeId, setProcessingTradeId] = useState(null); + const [selectedTrade, setSelectedTrade] = useState(null); + const [counterOfferData, setCounterOfferData] = useState<{ + receiverId: string; + receiverUsername: string; + receiverCollection: CollectionItem[]; + initialOffer?: { senderCards: Card[]; receiverCards: Card[] }; + } | null>(null); // Profile state const [username, setUsername] = useState(''); @@ -310,6 +318,38 @@ export default function Community() { }); }; + const handleCounterOffer = async (trade: Trade, senderCards: Card[], receiverCards: Card[]) => { + try { + // Decline the original trade + await declineTrade(trade.id); + + // Load the sender's collection for the counter-offer + const collectionMap = await getUserCollection(trade.sender_id); + const cardIds = Array.from(collectionMap.keys()); + const cards = await getCardsByIds(cardIds); + const senderCollection = cards.map((card) => ({ + card, + quantity: collectionMap.get(card.id) || 0, + })); + + // Set up counter-offer data (swap sender and receiver) + setCounterOfferData({ + receiverId: trade.sender_id, + receiverUsername: trade.sender?.username || 'User', + receiverCollection: senderCollection, + initialOffer: { + senderCards: receiverCards, // What you want to give back + receiverCards: senderCards, // What you want to receive + }, + }); + + await loadTradesData(); + } catch (error) { + console.error('Error setting up counter-offer:', error); + toast.error('Failed to set up counter-offer'); + } + }; + // ============ PROFILE FUNCTIONS ============ const loadProfile = async () => { if (!user) return; @@ -348,15 +388,34 @@ export default function Community() { }; // ============ 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}:

+

Gift

+
+ ); } return (
-

{label}:

+
+

{label}:

+

${totalPrice.toFixed(2)}

+
{ownerItems.map((item) => { const card = tradeCardDetails.get(item.card_id); @@ -781,8 +840,16 @@ export default function Community() { pending: 'text-yellow-400', }; + const canViewDetails = !isSender && trade.status === 'pending'; + return ( -
+
canViewDetails && setSelectedTrade(trade)} + > {/* Header */}
@@ -802,36 +869,22 @@ export default function Community() { {renderTradeItems(trade.items, trade.receiver_id, isSender ? 'Get' : 'Send')}
- {/* Actions */} - {tradesSubTab === 'pending' && ( -
- {isSender ? ( - - ) : ( - <> - - - - )} + {canViewDetails && ( +

+ Tap to view details +

+ )} + + {/* Actions - Only show quick actions for sender (cancel) */} + {tradesSubTab === 'pending' && isSender && ( +
e.stopPropagation()}> +
)}
@@ -890,6 +943,32 @@ export default function Community() { )}
+ {/* Trade Detail Modal */} + {selectedTrade && ( + setSelectedTrade(null)} + onAccept={handleAcceptTrade} + onDecline={handleDeclineTrade} + onCounterOffer={handleCounterOffer} + /> + )} + + {/* Counter Offer Creator */} + {counterOfferData && ( + setCounterOfferData(null)} + onTradeCreated={() => { + setCounterOfferData(null); + loadTradesData(); + toast.success('Counter offer sent!'); + }} + /> + )} + {/* Confirm Modal */} void; + onAccept: (tradeId: string) => Promise; + onDecline: (tradeId: string) => Promise; + onCounterOffer: (trade: Trade, senderCards: Card[], receiverCards: Card[]) => void; +} + +interface TradeCardItem { + card: Card; + quantity: number; +} + +function calculateTotalPrice(items: TradeCardItem[]): number { + return items.reduce((total, { card, quantity }) => { + const price = card.prices?.usd ? parseFloat(card.prices.usd) : 0; + return total + (price * quantity); + }, 0); +} + +export default function TradeDetail({ + trade, + onClose, + onAccept, + onDecline, + onCounterOffer, +}: TradeDetailProps) { + const { user } = useAuth(); + const toast = useToast(); + const [loading, setLoading] = useState(true); + const [processing, setProcessing] = useState(false); + const [senderCards, setSenderCards] = useState([]); + const [receiverCards, setReceiverCards] = useState([]); + + const isSender = trade.sender_id === user?.id; + const isReceiver = trade.receiver_id === user?.id; + const otherUser = isSender ? trade.receiver : trade.sender; + + useEffect(() => { + loadTradeCards(); + }, [trade]); + + const loadTradeCards = async () => { + setLoading(true); + try { + const allCardIds = trade.items?.map(item => item.card_id) || []; + if (allCardIds.length === 0) { + setSenderCards([]); + setReceiverCards([]); + return; + } + + const cards = await getCardsByIds(allCardIds); + const cardMap = new Map(); + cards.forEach(card => cardMap.set(card.id, card)); + + const senderItems: TradeCardItem[] = []; + const receiverItems: TradeCardItem[] = []; + + trade.items?.forEach(item => { + const card = cardMap.get(item.card_id); + if (!card) return; + + if (item.owner_id === trade.sender_id) { + senderItems.push({ card, quantity: item.quantity }); + } else { + receiverItems.push({ card, quantity: item.quantity }); + } + }); + + setSenderCards(senderItems); + setReceiverCards(receiverItems); + } catch (error) { + console.error('Error loading trade cards:', error); + toast.error('Failed to load trade details'); + } finally { + setLoading(false); + } + }; + + const handleAccept = async () => { + setProcessing(true); + try { + await onAccept(trade.id); + onClose(); + } catch (error) { + console.error('Error accepting trade:', error); + } finally { + setProcessing(false); + } + }; + + const handleDecline = async () => { + setProcessing(true); + try { + await onDecline(trade.id); + onClose(); + } catch (error) { + console.error('Error declining trade:', error); + } finally { + setProcessing(false); + } + }; + + const handleCounterOffer = () => { + const senderCardsList = senderCards.map(item => item.card); + const receiverCardsList = receiverCards.map(item => item.card); + onCounterOffer(trade, senderCardsList, receiverCardsList); + onClose(); + }; + + const senderPrice = calculateTotalPrice(senderCards); + const receiverPrice = calculateTotalPrice(receiverCards); + + const yourCards = isSender ? senderCards : receiverCards; + const theirCards = isSender ? receiverCards : senderCards; + const yourPrice = isSender ? senderPrice : receiverPrice; + const theirPrice = isSender ? receiverPrice : senderPrice; + + return ( +
+
+ {/* Header */} +
+
+ +
+

Trade Details

+

+ {isSender ? 'To' : 'From'}: {otherUser?.username} +

+
+
+ +
+ + {/* Content */} +
+ {loading ? ( +
+ +
+ ) : ( +
+ {/* Your Side */} +
+
+

+ {isSender ? 'You Give' : 'You Receive'} +

+
+ + {yourPrice.toFixed(2)} +
+
+ + {yourCards.length === 0 ? ( +

Gift (no cards)

+ ) : ( +
+ {yourCards.map((item, idx) => ( +
+ {item.card.name} + {item.quantity > 1 && ( +
+ x{item.quantity} +
+ )} + {item.card.prices?.usd && ( +
+ ${item.card.prices.usd} +
+ )} +
+ ))} +
+ )} +
+ + {/* Their Side */} +
+
+

+ {isSender ? 'You Receive' : 'They Give'} +

+
+ + {theirPrice.toFixed(2)} +
+
+ + {theirCards.length === 0 ? ( +

Gift (no cards)

+ ) : ( +
+ {theirCards.map((item, idx) => ( +
+ {item.card.name} + {item.quantity > 1 && ( +
+ x{item.quantity} +
+ )} + {item.card.prices?.usd && ( +
+ ${item.card.prices.usd} +
+ )} +
+ ))} +
+ )} +
+
+ )} + + {/* Message */} + {trade.message && ( +
+

Message:

+

{trade.message}

+
+ )} + + {/* Price Difference */} + {!loading && (senderPrice > 0 || receiverPrice > 0) && ( +
+
+ Value Difference: + 5 ? 'text-yellow-400' : 'text-gray-300'}> + ${Math.abs(senderPrice - receiverPrice).toFixed(2)} + {senderPrice > receiverPrice ? ' in your favor' : senderPrice < receiverPrice ? ' in their favor' : ' (balanced)'} + +
+
+ )} +
+ + {/* Actions - Only for pending trades */} + {trade.status === 'pending' && !loading && ( +
+ {isReceiver ? ( + <> +
+ + +
+ + + ) : ( +

+ Waiting for {otherUser?.username} to respond... +

+ )} +
+ )} +
+
+ ); +}