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;
|
||||
|
||||
@@ -148,4 +148,92 @@ router.get('/:username', authenticate, async (req, res, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/users/:username/ratings - Get ratings for a user
|
||||
router.get('/:username/ratings', authenticate, async (req, res, next) => {
|
||||
try {
|
||||
const { username } = req.params;
|
||||
|
||||
// Find user
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { username },
|
||||
select: { id: true, username: true },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'User not found',
|
||||
});
|
||||
}
|
||||
|
||||
// Get ratings received by this user
|
||||
const ratings = await prisma.rating.findMany({
|
||||
where: { ratedId: user.id },
|
||||
include: {
|
||||
rater: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
avatar: true,
|
||||
},
|
||||
},
|
||||
match: {
|
||||
select: {
|
||||
id: true,
|
||||
slug: true,
|
||||
event: {
|
||||
select: {
|
||||
id: true,
|
||||
slug: true,
|
||||
name: true,
|
||||
startDate: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 50, // Last 50 ratings
|
||||
});
|
||||
|
||||
// Calculate average
|
||||
const averageRating = ratings.length > 0
|
||||
? ratings.reduce((sum, r) => sum + r.score, 0) / ratings.length
|
||||
: 0;
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
username: user.username,
|
||||
averageRating: averageRating.toFixed(1),
|
||||
ratingsCount: ratings.length,
|
||||
ratings: ratings.map(r => ({
|
||||
id: r.id,
|
||||
score: r.score,
|
||||
comment: r.comment,
|
||||
wouldCollaborateAgain: r.wouldCollaborateAgain,
|
||||
createdAt: r.createdAt,
|
||||
rater: {
|
||||
id: r.rater.id,
|
||||
username: r.rater.username,
|
||||
firstName: r.rater.firstName,
|
||||
lastName: r.rater.lastName,
|
||||
avatar: r.rater.avatar,
|
||||
},
|
||||
event: {
|
||||
id: r.match.event.id,
|
||||
slug: r.match.event.slug,
|
||||
name: r.match.event.name,
|
||||
startDate: r.match.event.startDate,
|
||||
},
|
||||
})),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
Reference in New Issue
Block a user