From abbe68888d733bfdc42d7a319369469cfb50c196 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Wed, 26 Nov 2025 15:34:41 +0100 Subject: [PATCH] add trade editing functionality and version history tracking --- src/components/Community.tsx | 58 +---- src/components/TradeCreator.tsx | 118 +++++++--- src/components/TradeDetail.tsx | 377 +++++++++++++++++++++++--------- src/services/tradesService.ts | 137 ++++++++++++ 4 files changed, 505 insertions(+), 185 deletions(-) diff --git a/src/components/Community.tsx b/src/components/Community.tsx index 7cd6671..9155986 100644 --- a/src/components/Community.tsx +++ b/src/components/Community.tsx @@ -81,12 +81,6 @@ export default function Community() { 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(''); @@ -318,37 +312,6 @@ 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 () => { @@ -840,7 +803,8 @@ export default function Community() { pending: 'text-yellow-400', }; - const canViewDetails = !isSender && trade.status === 'pending'; + // Both users can view details for pending trades + const canViewDetails = trade.status === 'pending'; return (
- Tap to view details + {isSender ? 'Tap to view/edit' : 'Tap to view details'}

)} @@ -950,21 +914,9 @@ export default function Community() { onClose={() => setSelectedTrade(null)} onAccept={handleAcceptTrade} onDecline={handleDeclineTrade} - onCounterOffer={handleCounterOffer} - /> - )} - - {/* Counter Offer Creator */} - {counterOfferData && ( - setCounterOfferData(null)} - onTradeCreated={() => { - setCounterOfferData(null); + onTradeUpdated={() => { + setSelectedTrade(null); loadTradesData(); - toast.success('Counter offer sent!'); }} /> )} diff --git a/src/components/TradeCreator.tsx b/src/components/TradeCreator.tsx index c91c4ca..a5effc9 100644 --- a/src/components/TradeCreator.tsx +++ b/src/components/TradeCreator.tsx @@ -3,7 +3,7 @@ import { X, ArrowLeftRight, ArrowRight, ArrowLeft, Minus, Send, Gift, Loader2, S import { useAuth } from '../contexts/AuthContext'; import { useToast } from '../contexts/ToastContext'; import { getUserCollection, getCardsByIds } from '../services/api'; -import { createTrade } from '../services/tradesService'; +import { createTrade, updateTrade } from '../services/tradesService'; import { Card } from '../types'; interface CollectionItem { @@ -182,6 +182,11 @@ interface TradeCreatorProps { receiverCollection: CollectionItem[]; onClose: () => void; onTradeCreated: () => void; + editMode?: boolean; + existingTradeId?: string; + initialSenderCards?: Card[]; + initialReceiverCards?: Card[]; + initialMessage?: string; } type MobileStep = 'want' | 'give' | 'review'; @@ -192,13 +197,18 @@ export default function TradeCreator({ receiverCollection, onClose, onTradeCreated, + editMode = false, + existingTradeId, + initialSenderCards = [], + initialReceiverCards = [], + initialMessage = '', }: TradeCreatorProps) { const { user } = useAuth(); const toast = useToast(); const [myCollection, setMyCollection] = useState([]); const [loading, setLoading] = useState(true); const [submitting, setSubmitting] = useState(false); - const [message, setMessage] = useState(''); + const [message, setMessage] = useState(initialMessage); const [isGiftMode, setIsGiftMode] = useState(false); const [mobileStep, setMobileStep] = useState('want'); @@ -222,6 +232,57 @@ export default function TradeCreator({ } }, [isGiftMode]); + // Pre-populate cards in edit mode + useEffect(() => { + if (!editMode || !myCollection.length || !receiverCollection.length) return; + if (initialSenderCards.length === 0 && initialReceiverCards.length === 0) return; + + console.log('Pre-populating cards', { + initialSenderCards: initialSenderCards.length, + initialReceiverCards: initialReceiverCards.length, + myCollection: myCollection.length, + receiverCollection: receiverCollection.length + }); + + // Pre-populate sender cards with their quantities + const senderMap = new Map(); + initialSenderCards.forEach(card => { + const collectionItem = myCollection.find(c => c.card.id === card.id); + if (collectionItem) { + // Find the quantity from trade items if card has quantity property + const quantity = (card as any).quantity || 1; + console.log('Adding sender card:', card.name, 'qty:', quantity); + senderMap.set(card.id, { + card: card, + quantity: quantity, + maxQuantity: collectionItem.quantity, + }); + } else { + console.log('Card not found in my collection:', card.name, card.id); + } + }); + setMyOfferedCards(senderMap); + + // Pre-populate receiver cards with their quantities + const receiverMap = new Map(); + initialReceiverCards.forEach(card => { + const collectionItem = receiverCollection.find(c => c.card.id === card.id); + if (collectionItem) { + // Find the quantity from trade items if card has quantity property + const quantity = (card as any).quantity || 1; + console.log('Adding receiver card:', card.name, 'qty:', quantity); + receiverMap.set(card.id, { + card: card, + quantity: quantity, + maxQuantity: collectionItem.quantity, + }); + } else { + console.log('Card not found in their collection:', card.name, card.id); + } + }); + setWantedCards(receiverMap); + }, [editMode, myCollection, receiverCollection, initialSenderCards, initialReceiverCards]); + const loadMyCollection = async () => { if (!user) return; setLoading(true); @@ -324,18 +385,32 @@ export default function TradeCreator({ quantity: item.quantity, })); - await createTrade({ - senderId: user.id, - receiverId, - message: message || undefined, - senderCards, - receiverCards, - }); + if (editMode && existingTradeId) { + // Update existing trade + await updateTrade({ + tradeId: existingTradeId, + editorId: user.id, + message: message || undefined, + senderCards, + receiverCards, + }); + toast.success('Trade updated!'); + } else { + // Create new trade + await createTrade({ + senderId: user.id, + receiverId, + message: message || undefined, + senderCards, + receiverCards, + }); + toast.success('Trade offer sent!'); + } onTradeCreated(); } catch (error) { - console.error('Error creating trade:', error); - toast.error('Failed to create trade'); + console.error('Error with trade:', error); + toast.error(editMode ? 'Failed to update trade' : 'Failed to create trade'); } finally { setSubmitting(false); } @@ -487,12 +562,6 @@ export default function TradeCreator({ placeholder="Add a message..." className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent" /> - {message && ( -
-

Preview:

-

{message}

-
- )}
)} @@ -559,7 +628,7 @@ export default function TradeCreator({
-

Trade with {receiverUsername}

+

{editMode ? 'Edit Trade' : `Trade with ${receiverUsername}`}

+ )}
)} @@ -260,7 +379,9 @@ export default function TradeDetail({ {/* Actions - Only for pending trades */} {trade.status === 'pending' && !loading && (
- {isReceiver ? ( + {/* Only the user who DIDN'T make the last edit can respond */} + {trade.editor_id && trade.editor_id !== user?.id ? ( + /* User receives the last edit - can accept/decline/counter */ <>
- ) : ( + ) : trade.editor_id === user?.id ? ( + /* User made the last edit - waiting for response */

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

+ ) : ( + /* No editor yet (version 1) - original flow */ + <> + {isSender ? ( + + ) : ( + <> +
+ + +
+ + + )} + )}
)} diff --git a/src/services/tradesService.ts b/src/services/tradesService.ts index 41104f5..375ed18 100644 --- a/src/services/tradesService.ts +++ b/src/services/tradesService.ts @@ -16,11 +16,32 @@ export interface Trade { message: string | null; created_at: string | null; updated_at: string | null; + version: number; + editor_id: string | null; sender?: { username: string | null }; receiver?: { username: string | null }; items?: TradeItem[]; } +export interface TradeHistoryEntry { + id: string; + trade_id: string; + version: number; + editor_id: string; + message: string | null; + created_at: string; + editor?: { username: string | null }; + items?: TradeHistoryItem[]; +} + +export interface TradeHistoryItem { + id: string; + history_id: string; + owner_id: string; + card_id: string; + quantity: number; +} + export interface CreateTradeParams { senderId: string; receiverId: string; @@ -29,6 +50,14 @@ export interface CreateTradeParams { receiverCards: { cardId: string; quantity: number }[]; } +export interface UpdateTradeParams { + tradeId: string; + editorId: string; + message?: string; + senderCards: { cardId: string; quantity: number }[]; + receiverCards: { cardId: string; quantity: number }[]; +} + // Get all trades for a user export async function getTrades(userId: string): Promise { const { data, error } = await supabase @@ -182,3 +211,111 @@ export async function getTradeHistory(userId: string): Promise { if (error) throw error; return data as Trade[]; } + +// Update an existing trade (for edits and counter-offers) +export async function updateTrade(params: UpdateTradeParams): Promise { + const { tradeId, editorId, message, senderCards, receiverCards } = params; + + // Get current trade info + const { data: currentTrade, error: tradeError } = await supabase + .from('trades') + .select('version, sender_id, receiver_id') + .eq('id', tradeId) + .single(); + + if (tradeError) throw tradeError; + + const newVersion = (currentTrade.version || 1) + 1; + + // Save current state to history before updating + const { data: historyEntry, error: historyError } = await supabase + .from('trade_history') + .insert({ + trade_id: tradeId, + version: currentTrade.version || 1, + editor_id: editorId, + message: message || null, + }) + .select() + .single(); + + if (historyError) throw historyError; + + // Save current items to history + const { data: currentItems } = await supabase + .from('trade_items') + .select('*') + .eq('trade_id', tradeId); + + if (currentItems && currentItems.length > 0) { + const historyItems = currentItems.map(item => ({ + history_id: historyEntry.id, + owner_id: item.owner_id, + card_id: item.card_id, + quantity: item.quantity, + })); + + await supabase.from('trade_history_items').insert(historyItems); + } + + // Update the trade + const { data: updatedTrade, error: updateError } = await supabase + .from('trades') + .update({ + message, + version: newVersion, + editor_id: editorId, + updated_at: new Date().toISOString(), + }) + .eq('id', tradeId) + .select() + .single(); + + if (updateError) throw updateError; + + // Delete existing items + await supabase.from('trade_items').delete().eq('trade_id', tradeId); + + // Add new items + const senderItems = senderCards.map((card) => ({ + trade_id: tradeId, + owner_id: currentTrade.sender_id, + card_id: card.cardId, + quantity: card.quantity, + })); + + const receiverItems = receiverCards.map((card) => ({ + trade_id: tradeId, + owner_id: currentTrade.receiver_id, + card_id: card.cardId, + quantity: card.quantity, + })); + + const allItems = [...senderItems, ...receiverItems]; + + if (allItems.length > 0) { + const { error: itemsError } = await supabase + .from('trade_items') + .insert(allItems); + + if (itemsError) throw itemsError; + } + + return updatedTrade; +} + +// Get version history for a trade +export async function getTradeVersionHistory(tradeId: string): Promise { + const { data, error } = await supabase + .from('trade_history') + .select(` + *, + editor:profiles!trade_history_editor_id_fkey(username), + items:trade_history_items(*) + `) + .eq('trade_id', tradeId) + .order('version', { ascending: true }); + + if (error) throw error; + return data as TradeHistoryEntry[]; +}