3 Commits

5 changed files with 223 additions and 13 deletions

View File

@@ -95,6 +95,34 @@ export default function Collection() {
calculateTotalValue();
}, [user]);
// Subscribe to realtime updates for collection total value
useEffect(() => {
if (!user) return;
const profileChannel = supabase
.channel('profile-total-value-changes')
.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'profiles',
filter: `id=eq.${user.id}`,
},
(payload: any) => {
if (payload.new?.collection_total_value !== undefined) {
console.log('Collection total value updated:', payload.new.collection_total_value);
setTotalCollectionValue(payload.new.collection_total_value);
}
}
)
.subscribe();
return () => {
supabase.removeChannel(profileChannel);
};
}, [user]);
// Load user's collection from Supabase on mount
useEffect(() => {
const loadCollection = async () => {
@@ -168,7 +196,13 @@ export default function Collection() {
quantity: result.items.get(card.id) || 0,
}));
setCollection(prev => [...prev, ...newCards]);
// Deduplicate: only add cards that aren't already in the collection
setCollection(prev => {
const existingIds = new Set(prev.map(item => item.card.id));
const uniqueNewCards = newCards.filter(item => !existingIds.has(item.card.id));
return [...prev, ...uniqueNewCards];
});
setOffset(prev => prev + PAGE_SIZE);
} catch (error) {
console.error('Error loading more cards:', error);

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { Search, Globe, Users, Eye, ArrowLeftRight, Loader2, Clock, History, UserPlus, UserMinus, Check, X, Send, Settings, Save, ChevronLeft, RefreshCw, Plus, Minus } from 'lucide-react';
import { Search, Globe, Users, Eye, ArrowLeftRight, Loader2, Clock, History, UserPlus, UserMinus, Check, X, Send, Settings, Save, ChevronLeft, RefreshCw, Plus, Minus, AlertTriangle } from 'lucide-react';
import { useAuth } from '../contexts/AuthContext';
import { useToast } from '../contexts/ToastContext';
import { supabase } from '../lib/supabase';
@@ -208,6 +208,7 @@ export default function Community() {
}, [user]);
// Subscribe to collection changes when viewing someone's collection
// Auto-price trigger is disabled, so no more infinite loops!
useEffect(() => {
if (!user || !selectedUser) return;
@@ -221,10 +222,11 @@ export default function Community() {
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);
console.log('Collection change for viewed user:', payload.eventType);
// Reload on any change (INSERT/UPDATE/DELETE)
// No more infinite loops since auto-price trigger is disabled
loadUserCollection(selectedUser.id);
}
}
@@ -371,7 +373,13 @@ export default function Community() {
quantity: result.items.get(card.id) || 0,
}));
setSelectedUserCollection(prev => [...prev, ...newCards]);
// Deduplicate: only add cards that aren't already in the collection
setSelectedUserCollection(prev => {
const existingIds = new Set(prev.map(item => item.card.id));
const uniqueNewCards = newCards.filter(item => !existingIds.has(item.card.id));
return [...prev, ...uniqueNewCards];
});
setUserCollectionOffset(prev => prev + PAGE_SIZE);
} catch (error) {
console.error('Error loading more cards:', error);
@@ -405,6 +413,34 @@ export default function Community() {
};
}, [selectedUser, hasMoreUserCards, isLoadingMoreUserCards, loadMoreUserCards]);
// Subscribe to realtime updates for selected user's collection total value
useEffect(() => {
if (!selectedUser) return;
const userProfileChannel = supabase
.channel(`user-profile-value-${selectedUser.id}`)
.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'profiles',
filter: `id=eq.${selectedUser.id}`,
},
(payload: any) => {
if (payload.new?.collection_total_value !== undefined) {
console.log(`User ${selectedUser.username}'s collection total value updated:`, payload.new.collection_total_value);
setUserCollectionTotalValue(payload.new.collection_total_value);
}
}
)
.subscribe();
return () => {
supabase.removeChannel(userProfileChannel);
};
}, [selectedUser]);
// ============ FRIENDS FUNCTIONS ============
const loadFriendsData = async () => {
if (!user) return;
@@ -1376,9 +1412,14 @@ export default function Community() {
With: <strong>{otherUser?.username}</strong>
</span>
</div>
<span className={`text-xs capitalize ${statusColors[trade.status]}`}>
{trade.status}
</span>
<div className="flex items-center gap-1.5">
{trade.status === 'pending' && !trade.is_valid && (
<AlertTriangle size={14} className="text-red-400" title="Trade no longer valid" />
)}
<span className={`text-xs capitalize ${statusColors[trade.status]}`}>
{trade.status}
</span>
</div>
</div>
{/* Items */}

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { X, Check, ArrowLeftRight, DollarSign, Loader2, Edit, RefreshCcw, History } from 'lucide-react';
import { X, Check, ArrowLeftRight, DollarSign, Loader2, Edit, RefreshCcw, History, AlertTriangle } from 'lucide-react';
import { useAuth } from '../contexts/AuthContext';
import { useToast } from '../contexts/ToastContext';
import { Trade, TradeHistoryEntry, getTradeVersionHistory } from '../services/tradesService';
@@ -222,6 +222,18 @@ export default function TradeDetail({
</div>
) : (
<div className="space-y-4">
{/* Invalid Trade Warning */}
{trade.status === 'pending' && !trade.is_valid && (
<div className="bg-red-900/30 border border-red-600 rounded-lg p-3 flex items-start gap-2">
<AlertTriangle size={20} className="text-red-400 flex-shrink-0 mt-0.5" />
<div>
<h4 className="font-semibold text-red-400 text-sm">Trade No Longer Valid</h4>
<p className="text-red-200 text-xs mt-1">
One or more cards in this trade are no longer available in the required quantities. This trade cannot be accepted until it is updated.
</p>
</div>
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Your Side */}
<div className="space-y-3">
@@ -367,8 +379,9 @@ export default function TradeDetail({
<div className="flex gap-2">
<button
onClick={handleAccept}
disabled={processing}
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 bg-green-600 hover:bg-green-700 disabled:bg-gray-600 rounded-lg font-medium transition"
disabled={processing || !trade.is_valid}
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 bg-green-600 hover:bg-green-700 disabled:bg-gray-600 disabled:cursor-not-allowed rounded-lg font-medium transition"
title={!trade.is_valid ? 'This trade is no longer valid' : ''}
>
{processing ? (
<Loader2 className="animate-spin" size={18} />
@@ -431,8 +444,9 @@ export default function TradeDetail({
<div className="flex gap-2">
<button
onClick={handleAccept}
disabled={processing}
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 bg-green-600 hover:bg-green-700 disabled:bg-gray-600 rounded-lg font-medium transition"
disabled={processing || !trade.is_valid}
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 bg-green-600 hover:bg-green-700 disabled:bg-gray-600 disabled:cursor-not-allowed rounded-lg font-medium transition"
title={!trade.is_valid ? 'This trade is no longer valid' : ''}
>
{processing ? (
<Loader2 className="animate-spin" size={18} />

View File

@@ -18,6 +18,7 @@ export interface Trade {
updated_at: string | null;
version: number;
editor_id: string | null;
is_valid: boolean;
user1?: { username: string | null };
user2?: { username: string | null };
items?: TradeItem[];
@@ -160,6 +161,25 @@ export async function createTrade(params: CreateTradeParams): Promise<Trade> {
// Accept a trade (executes the card transfer)
export async function acceptTrade(tradeId: string): Promise<boolean> {
// First check if the trade is valid
const { data: trade, error: tradeError } = await supabase
.from('trades')
.select('is_valid, status')
.eq('id', tradeId)
.single();
if (tradeError) throw tradeError;
// Prevent accepting invalid trades
if (!trade.is_valid) {
throw new Error('This trade is no longer valid. One or more cards are no longer available in the required quantities.');
}
// Prevent accepting non-pending trades
if (trade.status !== 'pending') {
throw new Error('This trade has already been processed.');
}
const { data, error } = await supabase.rpc('execute_trade', {
trade_id: tradeId,
});

View File

@@ -0,0 +1,101 @@
-- Add is_valid column to trades table
ALTER TABLE public.trades
ADD COLUMN IF NOT EXISTS is_valid BOOLEAN DEFAULT true;
-- Create index for filtering by validity
CREATE INDEX IF NOT EXISTS idx_trades_is_valid ON public.trades(is_valid);
-- Function to validate if a trade can still be executed based on current collections
CREATE OR REPLACE FUNCTION public.validate_trade(p_trade_id uuid)
RETURNS boolean
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
v_item RECORD;
v_collection_quantity integer;
BEGIN
-- Check each item in the trade
FOR v_item IN
SELECT owner_id, card_id, quantity
FROM public.trade_items
WHERE trade_id = p_trade_id
LOOP
-- Get the quantity of this card in the owner's collection
SELECT COALESCE(quantity, 0) INTO v_collection_quantity
FROM public.collections
WHERE user_id = v_item.owner_id
AND card_id = v_item.card_id;
-- If owner doesn't have enough of this card, trade is invalid
IF v_collection_quantity < v_item.quantity THEN
RETURN false;
END IF;
END LOOP;
-- All items are available, trade is valid
RETURN true;
END;
$$;
-- Function to check and update validity of affected trades when collections change
CREATE OR REPLACE FUNCTION public.update_affected_trades_validity()
RETURNS TRIGGER
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
v_user_id uuid;
v_card_id text;
v_trade RECORD;
v_is_valid boolean;
BEGIN
-- Get the user_id and card_id from the changed row
IF (TG_OP = 'DELETE') THEN
v_user_id := OLD.user_id;
v_card_id := OLD.card_id;
ELSE
v_user_id := NEW.user_id;
v_card_id := NEW.card_id;
END IF;
-- Find all pending trades that involve this card from this user
FOR v_trade IN
SELECT DISTINCT t.id
FROM public.trades t
JOIN public.trade_items ti ON ti.trade_id = t.id
WHERE t.status = 'pending'
AND ti.owner_id = v_user_id
AND ti.card_id = v_card_id
LOOP
-- Validate the trade
v_is_valid := public.validate_trade(v_trade.id);
-- Update the trade's validity
UPDATE public.trades
SET is_valid = v_is_valid,
updated_at = now()
WHERE id = v_trade.id;
END LOOP;
IF (TG_OP = 'DELETE') THEN
RETURN OLD;
ELSE
RETURN NEW;
END IF;
END;
$$;
-- Create trigger to auto-update trade validity when collections change
CREATE TRIGGER update_trades_on_collection_change
AFTER UPDATE OR DELETE ON public.collections
FOR EACH ROW
EXECUTE FUNCTION public.update_affected_trades_validity();
-- Add comment
COMMENT ON COLUMN public.trades.is_valid IS 'Indicates if the trade can still be executed based on current collections. Auto-updated when collections change.';
-- Initial validation: set is_valid for all existing pending trades
UPDATE public.trades
SET is_valid = public.validate_trade(id)
WHERE status = 'pending';