This guide explains how to integrate the match social features (friend requests and team chat) into your tournament system.
The match social features allow players to:
- Send friend requests to opponents after completing a match
- Chat with teammates during and after matches
- Build connections within the gaming community
The following tables have been created:
- Stores friend requests between players after matches
- Fields: match_id, sender_id, receiver_id, status, message, created_at, updated_at
- Statuses: pending, accepted, rejected
- Stores team chat messages for a specific match
- Fields: match_id, team_id, sender_id, content, created_at
- Tracks which users have read which messages
- Fields: message_id, user_id, read_at
Main component that appears after a match is completed.
Props:
{
matchId: string;
tournamentId: string;
tournamentName: string;
roundNumber: number;
opponentId: string | null;
userTeamId: string | null;
onClose: () => void;
}Usage:
import PostMatchSocialPanel from './components/tournaments/PostMatchSocialPanel';
<PostMatchSocialPanel
matchId="match-uuid"
tournamentId="tournament-uuid"
tournamentName="Summer Championship"
roundNumber={2}
opponentId="opponent-user-id"
userTeamId="team-uuid"
onClose={() => setShowPanel(false)}
/>Modal for sending friend requests to opponents.
Props:
{
isOpen: boolean;
onClose: () => void;
matchId: string;
opponentId: string;
opponentUsername: string;
opponentAvatar: string | null;
tournamentName: string;
roundNumber: number;
onSuccess: () => void;
}Modal for team communication during matches.
Props:
{
isOpen: boolean;
onClose: () => void;
matchId: string;
tournamentId: string;
teamId: string;
tournamentName: string;
roundNumber: number;
}Add a real-time subscription to detect when matches are completed:
import { supabase } from '../lib/supabase';
import { useAuth } from '../contexts/AuthContext';
const { user } = useAuth();
useEffect(() => {
if (!user?.id || !tournamentId) return;
const channel = supabase
.channel(`tournament-matches-${tournamentId}`)
.on('postgres_changes', {
event: 'UPDATE',
schema: 'public',
table: 'tournament_matches',
filter: `tournament_id=eq.${tournamentId}`
}, async (payload) => {
const updatedMatch = payload.new;
// Check if match is completed (has a winner)
if (updatedMatch.winner_id) {
// Check if current user is involved in this match
const isUserInvolved = await checkIfUserInvolvedInMatch(
updatedMatch.id,
updatedMatch.player1_id,
updatedMatch.player2_id
);
if (isUserInvolved) {
// Show social panel
showPostMatchSocialPanel(updatedMatch);
}
}
})
.subscribe();
return () => {
supabase.removeChannel(channel);
};
}, [user?.id, tournamentId]);For solo tournaments:
const checkIfUserInvolvedInMatch = async (
matchId: string,
player1Id: string | null,
player2Id: string | null
): Promise<boolean> => {
if (!user?.id) return false;
return user.id === player1Id || user.id === player2Id;
};For team tournaments:
const checkIfUserInvolvedInMatch = async (
matchId: string,
team1Id: string | null,
team2Id: string | null
): Promise<boolean> => {
if (!user?.id) return false;
const { data: teamMembers } = await supabase
.from('team_members')
.select('team_id')
.eq('user_id', user.id)
.eq('status', 'active');
if (!teamMembers || teamMembers.length === 0) return false;
const userTeamIds = teamMembers.map(tm => tm.team_id);
return userTeamIds.includes(team1Id || '') || userTeamIds.includes(team2Id || '');
};For solo tournaments:
const getOpponentForMatch = (
player1Id: string | null,
player2Id: string | null
): string | null => {
if (!user?.id || !player1Id || !player2Id) return null;
if (user.id === player1Id) {
return player2Id;
} else if (user.id === player2Id) {
return player1Id;
}
return null;
};For team tournaments (get opposing team's representative player):
const getOpponentForMatch = async (
team1Id: string | null,
team2Id: string | null
): Promise<string | null> => {
if (!user?.id || !team1Id || !team2Id) return null;
// Get user's team
const { data: userTeam } = await supabase
.from('team_members')
.select('team_id')
.eq('user_id', user.id)
.eq('status', 'active')
.maybeSingle();
if (!userTeam) return null;
// Determine opponent team
const opponentTeamId = userTeam.team_id === team1Id ? team2Id : team1Id;
// Get captain or first member of opponent team
const { data: opponentMember } = await supabase
.from('team_members')
.select('user_id')
.eq('team_id', opponentTeamId)
.eq('status', 'active')
.order('role', { ascending: true })
.limit(1)
.maybeSingle();
return opponentMember?.user_id || null;
};const [showSocialPanel, setShowSocialPanel] = useState(false);
const [matchData, setMatchData] = useState({
matchId: '',
roundNumber: 0,
opponentId: null as string | null,
userTeamId: null as string | null
});
const showPostMatchSocialPanel = async (match: any) => {
const opponent = await getOpponentForMatch(
match.player1_id,
match.player2_id
);
let teamId = null;
if (isTeamTournament) {
teamId = await getUserTeamId();
}
setMatchData({
matchId: match.id,
roundNumber: match.round,
opponentId: opponent,
userTeamId: teamId
});
setShowSocialPanel(true);
};
// In your render:
{showSocialPanel && (
<PostMatchSocialPanel
matchId={matchData.matchId}
tournamentId={tournamentId}
tournamentName={tournamentName}
roundNumber={matchData.roundNumber}
opponentId={matchData.opponentId}
userTeamId={matchData.userTeamId}
onClose={() => setShowSocialPanel(false)}
/>
)}See PostMatchSocialPanelExample.tsx for a complete working example that can be integrated into the TournamentPage.
- Users can send friend requests to opponents after matches
- Optional personal message with the request
- Request status tracking (pending, accepted, rejected)
- Prevents duplicate requests
- Real-time messaging between team members
- Automatic welcome message on first use
- Message read status tracking
- Persistent chat history for the match
All tables use Row Level Security (RLS):
- Friend requests: Users can only view/modify their own requests
- Team chat: Only team members can view/send messages
- Read status: Users can only manage their own read status
- The social panel automatically checks for existing friend requests to prevent duplicates
- Team chat is only available for team tournaments
- Friend requests are only available when there's a valid opponent
- The panel can be dismissed and won't show again for the same match
- All operations are protected by RLS policies for security