feat: implement Ratings API (Phase 2.5)
Complete the match lifecycle with partner rating functionality. Backend changes: - Add POST /api/matches/:slug/ratings endpoint to create ratings * Validate score range (1-5) * Prevent duplicate ratings (unique constraint per match+rater+rated) * Auto-complete match when both users have rated * Return detailed rating data with user and event info - Add GET /api/users/:username/ratings endpoint to fetch user ratings * Calculate and return average rating * Include rater details and event context for each rating * Limit to last 50 ratings - Add hasRated field to GET /api/matches/:slug response * Check if current user has already rated the match * Enable frontend to prevent duplicate rating attempts Frontend changes: - Update RatePartnerPage to use real API instead of mocks * Load match data and partner info * Submit ratings with score, comment, and wouldCollaborateAgain * Check hasRated flag and redirect if already rated * Validate match status before allowing rating * Show loading state and proper error handling - Update MatchChatPage to show rating status * Replace "Rate Partner" button with "✓ Rated" badge when user has rated * Improve button text from "End & rate" to "Rate Partner" - Add ratings API functions * matchesAPI.createRating(slug, ratingData) * ratingsAPI.getUserRatings(username) User flow: 1. After match is accepted, users can rate each other 2. Click "Rate Partner" in chat to navigate to rating page 3. Submit 1-5 star rating with optional comment 4. Rating saved and user redirected to matches list 5. Chat shows "✓ Rated" badge instead of rating button 6. Match marked as 'completed' when both users have rated 7. Users cannot rate the same match twice
This commit is contained in:
@@ -427,6 +427,17 @@ router.get('/:slug', authenticate, async (req, res, next) => {
|
||||
const partner = isUser1 ? match.user2 : match.user1;
|
||||
const isInitiator = match.user1Id === userId;
|
||||
|
||||
// Check if current user has already rated this match
|
||||
const userRating = await prisma.rating.findUnique({
|
||||
where: {
|
||||
matchId_raterId_ratedId: {
|
||||
matchId: match.id,
|
||||
raterId: userId,
|
||||
ratedId: partner.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
@@ -443,6 +454,7 @@ router.get('/:slug', authenticate, async (req, res, next) => {
|
||||
status: match.status,
|
||||
roomId: match.roomId,
|
||||
isInitiator,
|
||||
hasRated: !!userRating,
|
||||
createdAt: match.createdAt,
|
||||
},
|
||||
});
|
||||
@@ -701,4 +713,139 @@ router.delete('/:slug', authenticate, async (req, res, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/matches/:slug/ratings - Rate a partner after match
|
||||
router.post('/:slug/ratings', authenticate, async (req, res, next) => {
|
||||
try {
|
||||
const { slug } = req.params;
|
||||
const userId = req.user.id;
|
||||
const { score, comment, wouldCollaborateAgain } = req.body;
|
||||
|
||||
// Validation
|
||||
if (!score || score < 1 || score > 5) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Score must be between 1 and 5',
|
||||
});
|
||||
}
|
||||
|
||||
// Find match
|
||||
const match = await prisma.match.findUnique({
|
||||
where: { slug },
|
||||
select: {
|
||||
id: true,
|
||||
user1Id: true,
|
||||
user2Id: true,
|
||||
status: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!match) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Match not found',
|
||||
});
|
||||
}
|
||||
|
||||
// Check authorization - user must be part of this match
|
||||
if (match.user1Id !== userId && match.user2Id !== userId) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'You are not authorized to rate this match',
|
||||
});
|
||||
}
|
||||
|
||||
// Check if match is accepted
|
||||
if (match.status !== 'accepted' && match.status !== 'completed') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Match must be accepted before rating',
|
||||
});
|
||||
}
|
||||
|
||||
// Determine who is being rated (the other user in the match)
|
||||
const ratedUserId = match.user1Id === userId ? match.user2Id : match.user1Id;
|
||||
|
||||
// Check if user already rated this match
|
||||
const existingRating = await prisma.rating.findUnique({
|
||||
where: {
|
||||
matchId_raterId_ratedId: {
|
||||
matchId: match.id,
|
||||
raterId: userId,
|
||||
ratedId: ratedUserId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (existingRating) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'You have already rated this match',
|
||||
});
|
||||
}
|
||||
|
||||
// Create rating
|
||||
const rating = await prisma.rating.create({
|
||||
data: {
|
||||
matchId: match.id,
|
||||
raterId: userId,
|
||||
ratedId: ratedUserId,
|
||||
score,
|
||||
comment: comment || null,
|
||||
wouldCollaborateAgain: wouldCollaborateAgain || false,
|
||||
},
|
||||
include: {
|
||||
rater: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
},
|
||||
},
|
||||
rated: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Check if both users have rated - if so, mark match as completed
|
||||
const otherUserRating = await prisma.rating.findUnique({
|
||||
where: {
|
||||
matchId_raterId_ratedId: {
|
||||
matchId: match.id,
|
||||
raterId: ratedUserId,
|
||||
ratedId: userId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (otherUserRating) {
|
||||
// Both users have rated - mark match as completed
|
||||
await prisma.match.update({
|
||||
where: { id: match.id },
|
||||
data: { status: 'completed' },
|
||||
});
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: rating,
|
||||
});
|
||||
} catch (error) {
|
||||
// Handle unique constraint violation
|
||||
if (error.code === 'P2002') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'You have already rated this match',
|
||||
});
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
Reference in New Issue
Block a user