feat: add match slugs for security and fix message history loading
Security improvements: - Add random CUID slugs to Match model to prevent ID enumeration attacks - Update all match URLs from /matches/:id to /matches/:slug - Keep numeric IDs for internal Socket.IO operations only Backend changes: - Add slug field to matches table with unique index - Update all match endpoints to use slug-based lookups (GET, PUT, DELETE) - Add GET /api/matches/:slug/messages endpoint to fetch message history - Include matchSlug in all Socket.IO notifications Frontend changes: - Update all match routes to use slug parameter - Update MatchesPage to use slug for accept/reject/navigate operations - Update MatchChatPage to fetch match data by slug and load message history - Update RatePartnerPage to use slug parameter - Add matchesAPI.getMatchMessages() function Bug fixes: - Fix MatchChatPage not loading message history from database on mount - Messages now persist and display correctly when users reconnect
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "matches" ADD COLUMN "slug" VARCHAR(50) NOT NULL DEFAULT gen_random_uuid()::text;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "matches_slug_key" ON "matches"("slug");
|
||||||
@@ -131,6 +131,7 @@ model Message {
|
|||||||
// Matches (pairs of users for collaboration)
|
// Matches (pairs of users for collaboration)
|
||||||
model Match {
|
model Match {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
|
slug String @unique @default(cuid()) @db.VarChar(50)
|
||||||
user1Id Int @map("user1_id")
|
user1Id Int @map("user1_id")
|
||||||
user2Id Int @map("user2_id")
|
user2Id Int @map("user2_id")
|
||||||
eventId Int @map("event_id")
|
eventId Int @map("event_id")
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ router.post('/', authenticate, async (req, res, next) => {
|
|||||||
const targetSocketRoom = `user_${targetUserId}`;
|
const targetSocketRoom = `user_${targetUserId}`;
|
||||||
io.to(targetSocketRoom).emit('match_request_received', {
|
io.to(targetSocketRoom).emit('match_request_received', {
|
||||||
matchId: match.id,
|
matchId: match.id,
|
||||||
|
matchSlug: match.slug,
|
||||||
from: {
|
from: {
|
||||||
id: match.user1.id,
|
id: match.user1.id,
|
||||||
username: match.user1.username,
|
username: match.user1.username,
|
||||||
@@ -255,6 +256,7 @@ router.get('/', authenticate, async (req, res, next) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: match.id,
|
id: match.id,
|
||||||
|
slug: match.slug,
|
||||||
partner: {
|
partner: {
|
||||||
id: partner.id,
|
id: partner.id,
|
||||||
username: partner.username,
|
username: partner.username,
|
||||||
@@ -280,14 +282,94 @@ router.get('/', authenticate, async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /api/matches/:id - Get specific match
|
// GET /api/matches/:slug/messages - Get messages for a match
|
||||||
router.get('/:id', authenticate, async (req, res, next) => {
|
router.get('/:slug/messages', authenticate, async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { slug } = req.params;
|
||||||
|
const userId = req.user.id;
|
||||||
|
|
||||||
|
// Find match
|
||||||
|
const match = await prisma.match.findUnique({
|
||||||
|
where: { slug },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
user1Id: true,
|
||||||
|
user2Id: true,
|
||||||
|
roomId: true,
|
||||||
|
status: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Match not found',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check authorization
|
||||||
|
if (match.user1Id !== userId && match.user2Id !== userId) {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
error: 'You are not authorized to view messages for this match',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if match is accepted
|
||||||
|
if (match.status !== 'accepted' || !match.roomId) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Match must be accepted before viewing messages',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get messages
|
||||||
|
const messages = await prisma.message.findMany({
|
||||||
|
where: { roomId: match.roomId },
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
avatar: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: 'asc' },
|
||||||
|
take: 100, // Last 100 messages
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
count: messages.length,
|
||||||
|
data: messages.map(msg => ({
|
||||||
|
id: msg.id,
|
||||||
|
roomId: msg.roomId,
|
||||||
|
userId: msg.user.id,
|
||||||
|
username: msg.user.username,
|
||||||
|
avatar: msg.user.avatar,
|
||||||
|
firstName: msg.user.firstName,
|
||||||
|
lastName: msg.user.lastName,
|
||||||
|
content: msg.content,
|
||||||
|
type: msg.type,
|
||||||
|
createdAt: msg.createdAt,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/matches/:slug - Get specific match
|
||||||
|
router.get('/:slug', authenticate, async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { slug } = req.params;
|
||||||
const userId = req.user.id;
|
const userId = req.user.id;
|
||||||
|
|
||||||
const match = await prisma.match.findUnique({
|
const match = await prisma.match.findUnique({
|
||||||
where: { id: parseInt(id) },
|
where: { slug },
|
||||||
include: {
|
include: {
|
||||||
user1: {
|
user1: {
|
||||||
select: {
|
select: {
|
||||||
@@ -349,6 +431,7 @@ router.get('/:id', authenticate, async (req, res, next) => {
|
|||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
id: match.id,
|
id: match.id,
|
||||||
|
slug: match.slug,
|
||||||
partner: {
|
partner: {
|
||||||
id: partner.id,
|
id: partner.id,
|
||||||
username: partner.username,
|
username: partner.username,
|
||||||
@@ -368,15 +451,15 @@ router.get('/:id', authenticate, async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// PUT /api/matches/:id/accept - Accept a pending match
|
// PUT /api/matches/:slug/accept - Accept a pending match
|
||||||
router.put('/:id/accept', authenticate, async (req, res, next) => {
|
router.put('/:slug/accept', authenticate, async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { slug } = req.params;
|
||||||
const userId = req.user.id;
|
const userId = req.user.id;
|
||||||
|
|
||||||
// Find match
|
// Find match
|
||||||
const match = await prisma.match.findUnique({
|
const match = await prisma.match.findUnique({
|
||||||
where: { id: parseInt(id) },
|
where: { slug },
|
||||||
include: {
|
include: {
|
||||||
user1: {
|
user1: {
|
||||||
select: {
|
select: {
|
||||||
@@ -437,7 +520,7 @@ router.put('/:id/accept', authenticate, async (req, res, next) => {
|
|||||||
|
|
||||||
// Update match status and link to chat room
|
// Update match status and link to chat room
|
||||||
const updated = await tx.match.update({
|
const updated = await tx.match.update({
|
||||||
where: { id: parseInt(id) },
|
where: { slug },
|
||||||
data: {
|
data: {
|
||||||
status: 'accepted',
|
status: 'accepted',
|
||||||
roomId: chatRoom.id,
|
roomId: chatRoom.id,
|
||||||
@@ -487,6 +570,7 @@ router.put('/:id/accept', authenticate, async (req, res, next) => {
|
|||||||
|
|
||||||
const notification = {
|
const notification = {
|
||||||
matchId: updatedMatch.id,
|
matchId: updatedMatch.id,
|
||||||
|
matchSlug: updatedMatch.slug,
|
||||||
roomId: updatedMatch.roomId,
|
roomId: updatedMatch.roomId,
|
||||||
event: {
|
event: {
|
||||||
slug: updatedMatch.event.slug,
|
slug: updatedMatch.event.slug,
|
||||||
@@ -523,6 +607,7 @@ router.put('/:id/accept', authenticate, async (req, res, next) => {
|
|||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
id: updatedMatch.id,
|
id: updatedMatch.id,
|
||||||
|
slug: updatedMatch.slug,
|
||||||
partner: {
|
partner: {
|
||||||
id: partner.id,
|
id: partner.id,
|
||||||
username: partner.username,
|
username: partner.username,
|
||||||
@@ -542,15 +627,15 @@ router.put('/:id/accept', authenticate, async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// DELETE /api/matches/:id - Reject or cancel a match
|
// DELETE /api/matches/:slug - Reject or cancel a match
|
||||||
router.delete('/:id', authenticate, async (req, res, next) => {
|
router.delete('/:slug', authenticate, async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { slug } = req.params;
|
||||||
const userId = req.user.id;
|
const userId = req.user.id;
|
||||||
|
|
||||||
// Find match
|
// Find match
|
||||||
const match = await prisma.match.findUnique({
|
const match = await prisma.match.findUnique({
|
||||||
where: { id: parseInt(id) },
|
where: { slug },
|
||||||
include: {
|
include: {
|
||||||
event: {
|
event: {
|
||||||
select: {
|
select: {
|
||||||
@@ -586,7 +671,7 @@ router.delete('/:id', authenticate, async (req, res, next) => {
|
|||||||
|
|
||||||
// Delete match (will cascade delete chat room if exists)
|
// Delete match (will cascade delete chat room if exists)
|
||||||
await prisma.match.delete({
|
await prisma.match.delete({
|
||||||
where: { id: parseInt(id) },
|
where: { slug },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Emit socket event to the other user
|
// Emit socket event to the other user
|
||||||
@@ -597,6 +682,7 @@ router.delete('/:id', authenticate, async (req, res, next) => {
|
|||||||
|
|
||||||
io.to(otherUserSocketRoom).emit('match_cancelled', {
|
io.to(otherUserSocketRoom).emit('match_cancelled', {
|
||||||
matchId: match.id,
|
matchId: match.id,
|
||||||
|
matchSlug: match.slug,
|
||||||
event: {
|
event: {
|
||||||
slug: match.event.slug,
|
slug: match.event.slug,
|
||||||
name: match.event.name,
|
name: match.event.name,
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ function App() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/matches/:matchId/chat"
|
path="/matches/:slug/chat"
|
||||||
element={
|
element={
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
<MatchChatPage />
|
<MatchChatPage />
|
||||||
@@ -136,7 +136,7 @@ function App() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/matches/:matchId/rate"
|
path="/matches/:slug/rate"
|
||||||
element={
|
element={
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
<RatePartnerPage />
|
<RatePartnerPage />
|
||||||
|
|||||||
@@ -2,14 +2,16 @@ import { useState, useRef, useEffect } from 'react';
|
|||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import Layout from '../components/layout/Layout';
|
import Layout from '../components/layout/Layout';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import { mockUsers } from '../mocks/users';
|
import { matchesAPI } from '../services/api';
|
||||||
import { Send, Video, Upload, X, Check, Link as LinkIcon } from 'lucide-react';
|
import { Send, Video, Upload, X, Check, Link as LinkIcon, Loader2 } from 'lucide-react';
|
||||||
import { connectSocket, getSocket } from '../services/socket';
|
import { connectSocket, getSocket } from '../services/socket';
|
||||||
|
|
||||||
const MatchChatPage = () => {
|
const MatchChatPage = () => {
|
||||||
const { matchId } = useParams();
|
const { slug } = useParams();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [match, setMatch] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
const [messages, setMessages] = useState([]);
|
const [messages, setMessages] = useState([]);
|
||||||
const [newMessage, setNewMessage] = useState('');
|
const [newMessage, setNewMessage] = useState('');
|
||||||
const [selectedFile, setSelectedFile] = useState(null);
|
const [selectedFile, setSelectedFile] = useState(null);
|
||||||
@@ -22,8 +24,40 @@ const MatchChatPage = () => {
|
|||||||
const messagesEndRef = useRef(null);
|
const messagesEndRef = useRef(null);
|
||||||
const fileInputRef = useRef(null);
|
const fileInputRef = useRef(null);
|
||||||
|
|
||||||
// Partner user (mockup - TODO: fetch from backend in Phase 2)
|
// Fetch match data
|
||||||
const partner = mockUsers[1]; // sarah_swing
|
useEffect(() => {
|
||||||
|
const loadMatch = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const result = await matchesAPI.getMatch(slug);
|
||||||
|
setMatch(result.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load match:', error);
|
||||||
|
alert('Failed to load match. Redirecting to matches page.');
|
||||||
|
navigate('/matches');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadMatch();
|
||||||
|
}, [slug, navigate]);
|
||||||
|
|
||||||
|
// Load message history
|
||||||
|
useEffect(() => {
|
||||||
|
const loadMessages = async () => {
|
||||||
|
if (!match) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await matchesAPI.getMatchMessages(slug);
|
||||||
|
setMessages(result.data || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load messages:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadMessages();
|
||||||
|
}, [match, slug]);
|
||||||
|
|
||||||
|
const partner = match?.partner;
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
@@ -34,6 +68,9 @@ const MatchChatPage = () => {
|
|||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Wait for match to be loaded
|
||||||
|
if (!match) return;
|
||||||
|
|
||||||
// Connect to Socket.IO
|
// Connect to Socket.IO
|
||||||
const socket = connectSocket();
|
const socket = connectSocket();
|
||||||
|
|
||||||
@@ -45,9 +82,9 @@ const MatchChatPage = () => {
|
|||||||
// Socket event listeners
|
// Socket event listeners
|
||||||
socket.on('connect', () => {
|
socket.on('connect', () => {
|
||||||
setIsConnected(true);
|
setIsConnected(true);
|
||||||
// Join match room
|
// Join match room using numeric match ID for socket
|
||||||
socket.emit('join_match_room', { matchId: parseInt(matchId) });
|
socket.emit('join_match_room', { matchId: match.id });
|
||||||
console.log(`Joined match room ${matchId}`);
|
console.log(`Joined match room ${match.id}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('disconnect', () => {
|
socket.on('disconnect', () => {
|
||||||
@@ -65,11 +102,11 @@ const MatchChatPage = () => {
|
|||||||
socket.off('disconnect');
|
socket.off('disconnect');
|
||||||
socket.off('match_message');
|
socket.off('match_message');
|
||||||
};
|
};
|
||||||
}, [matchId, user.id]);
|
}, [match, user.id]);
|
||||||
|
|
||||||
const handleSendMessage = (e) => {
|
const handleSendMessage = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!newMessage.trim()) return;
|
if (!newMessage.trim() || !match) return;
|
||||||
|
|
||||||
const socket = getSocket();
|
const socket = getSocket();
|
||||||
if (!socket || !socket.connected) {
|
if (!socket || !socket.connected) {
|
||||||
@@ -77,9 +114,9 @@ const MatchChatPage = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send message via Socket.IO
|
// Send message via Socket.IO using numeric match ID
|
||||||
socket.emit('send_match_message', {
|
socket.emit('send_match_message', {
|
||||||
matchId: parseInt(matchId),
|
matchId: match.id,
|
||||||
content: newMessage,
|
content: newMessage,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -168,7 +205,7 @@ const MatchChatPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleEndMatch = () => {
|
const handleEndMatch = () => {
|
||||||
navigate(`/matches/${matchId}/rate`);
|
navigate(`/matches/${slug}/rate`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getWebRTCStatusColor = () => {
|
const getWebRTCStatusColor = () => {
|
||||||
@@ -197,6 +234,16 @@ const MatchChatPage = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (loading || !match || !partner) {
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<div className="flex justify-center items-center py-12">
|
||||||
|
<Loader2 className="w-8 h-8 animate-spin text-primary-600" />
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<div className="max-w-4xl mx-auto">
|
<div className="max-w-4xl mx-auto">
|
||||||
@@ -206,13 +253,17 @@ const MatchChatPage = () => {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<img
|
<img
|
||||||
src={partner.avatar}
|
src={partner.avatar || `https://api.dicebear.com/7.x/avataaars/svg?seed=${partner.username}`}
|
||||||
alt={partner.username}
|
alt={partner.username}
|
||||||
className="w-12 h-12 rounded-full border-2 border-white"
|
className="w-12 h-12 rounded-full border-2 border-white"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold">{partner.username}</h2>
|
<h2 className="text-xl font-bold">
|
||||||
<p className="text-sm text-primary-100">⭐ {partner.rating} • {partner.matches_count} collaborations</p>
|
{partner.firstName && partner.lastName
|
||||||
|
? `${partner.firstName} ${partner.lastName}`
|
||||||
|
: partner.username}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-primary-100">@{partner.username}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -66,13 +66,13 @@ const MatchesPage = () => {
|
|||||||
|
|
||||||
const handleMatchCancelled = (data) => {
|
const handleMatchCancelled = (data) => {
|
||||||
// Remove cancelled match from list
|
// Remove cancelled match from list
|
||||||
setMatches(prev => prev.filter(m => m.id !== data.matchId));
|
setMatches(prev => prev.filter(m => m.slug !== data.matchSlug));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAccept = async (matchId) => {
|
const handleAccept = async (matchSlug) => {
|
||||||
try {
|
try {
|
||||||
setProcessingMatchId(matchId);
|
setProcessingMatchId(matchSlug);
|
||||||
await matchesAPI.acceptMatch(matchId);
|
await matchesAPI.acceptMatch(matchSlug);
|
||||||
|
|
||||||
// Reload matches
|
// Reload matches
|
||||||
await loadMatches();
|
await loadMatches();
|
||||||
@@ -87,17 +87,17 @@ const MatchesPage = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReject = async (matchId) => {
|
const handleReject = async (matchSlug) => {
|
||||||
if (!confirm('Are you sure you want to reject this match request?')) {
|
if (!confirm('Are you sure you want to reject this match request?')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setProcessingMatchId(matchId);
|
setProcessingMatchId(matchSlug);
|
||||||
await matchesAPI.deleteMatch(matchId);
|
await matchesAPI.deleteMatch(matchSlug);
|
||||||
|
|
||||||
// Remove from list
|
// Remove from list
|
||||||
setMatches(prev => prev.filter(m => m.id !== matchId));
|
setMatches(prev => prev.filter(m => m.slug !== matchSlug));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to reject match:', error);
|
console.error('Failed to reject match:', error);
|
||||||
alert('Failed to reject match. Please try again.');
|
alert('Failed to reject match. Please try again.');
|
||||||
@@ -108,7 +108,7 @@ const MatchesPage = () => {
|
|||||||
|
|
||||||
const handleOpenChat = (match) => {
|
const handleOpenChat = (match) => {
|
||||||
if (match.status === 'accepted' && match.roomId) {
|
if (match.status === 'accepted' && match.roomId) {
|
||||||
navigate(`/matches/${match.id}/chat`);
|
navigate(`/matches/${match.slug}/chat`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -187,7 +187,7 @@ const MatchesPage = () => {
|
|||||||
onAccept={handleAccept}
|
onAccept={handleAccept}
|
||||||
onReject={handleReject}
|
onReject={handleReject}
|
||||||
onOpenChat={handleOpenChat}
|
onOpenChat={handleOpenChat}
|
||||||
processing={processingMatchId === match.id}
|
processing={processingMatchId === match.slug}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -208,7 +208,7 @@ const MatchesPage = () => {
|
|||||||
onAccept={handleAccept}
|
onAccept={handleAccept}
|
||||||
onReject={handleReject}
|
onReject={handleReject}
|
||||||
onOpenChat={handleOpenChat}
|
onOpenChat={handleOpenChat}
|
||||||
processing={processingMatchId === match.id}
|
processing={processingMatchId === match.slug}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -295,7 +295,7 @@ const MatchCard = ({ match, onAccept, onReject, onOpenChat, processing }) => {
|
|||||||
{isIncoming && (
|
{isIncoming && (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={() => onAccept(match.id)}
|
onClick={() => onAccept(match.slug)}
|
||||||
disabled={processing}
|
disabled={processing}
|
||||||
className="p-2 bg-green-600 text-white rounded-full hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
className="p-2 bg-green-600 text-white rounded-full hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
title="Accept"
|
title="Accept"
|
||||||
@@ -307,7 +307,7 @@ const MatchCard = ({ match, onAccept, onReject, onOpenChat, processing }) => {
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => onReject(match.id)}
|
onClick={() => onReject(match.slug)}
|
||||||
disabled={processing}
|
disabled={processing}
|
||||||
className="p-2 bg-red-600 text-white rounded-full hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
className="p-2 bg-red-600 text-white rounded-full hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
title="Reject"
|
title="Reject"
|
||||||
@@ -319,7 +319,7 @@ const MatchCard = ({ match, onAccept, onReject, onOpenChat, processing }) => {
|
|||||||
|
|
||||||
{isOutgoing && (
|
{isOutgoing && (
|
||||||
<button
|
<button
|
||||||
onClick={() => onReject(match.id)}
|
onClick={() => onReject(match.slug)}
|
||||||
disabled={processing}
|
disabled={processing}
|
||||||
className="px-3 py-1.5 text-sm border border-gray-300 text-gray-700 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
className="px-3 py-1.5 text-sm border border-gray-300 text-gray-700 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { mockUsers } from '../mocks/users';
|
|||||||
import { Star } from 'lucide-react';
|
import { Star } from 'lucide-react';
|
||||||
|
|
||||||
const RatePartnerPage = () => {
|
const RatePartnerPage = () => {
|
||||||
const { matchId } = useParams();
|
const { slug } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [rating, setRating] = useState(0);
|
const [rating, setRating] = useState(0);
|
||||||
const [hoveredRating, setHoveredRating] = useState(0);
|
const [hoveredRating, setHoveredRating] = useState(0);
|
||||||
|
|||||||
@@ -286,24 +286,29 @@ export const matchesAPI = {
|
|||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
async getMatch(matchId) {
|
async getMatch(matchSlug) {
|
||||||
const data = await fetchAPI(`/matches/${matchId}`);
|
const data = await fetchAPI(`/matches/${matchSlug}`);
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
async acceptMatch(matchId) {
|
async acceptMatch(matchSlug) {
|
||||||
const data = await fetchAPI(`/matches/${matchId}/accept`, {
|
const data = await fetchAPI(`/matches/${matchSlug}/accept`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
async deleteMatch(matchId) {
|
async deleteMatch(matchSlug) {
|
||||||
const data = await fetchAPI(`/matches/${matchId}`, {
|
const data = await fetchAPI(`/matches/${matchSlug}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async getMatchMessages(matchSlug) {
|
||||||
|
const data = await fetchAPI(`/matches/${matchSlug}/messages`);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export { ApiError };
|
export { ApiError };
|
||||||
|
|||||||
Reference in New Issue
Block a user