Spaces:
Sleeping
Sleeping
| // src/controllers/review.controller.js | |
| const Review = require('../models/Review'); | |
| const Media = require('../models/Media'); | |
| const User = require('../models/User'); | |
| // --- Helper: Recalculate Average Rating --- | |
| // We run this every time a review is added. | |
| // It uses MongoDB's math engine to average the scores instantly. | |
| const updateMediaStats = async (mediaId) => { | |
| const stats = await Review.aggregate([ | |
| { $match: { mediaId: mediaId } }, | |
| { | |
| $group: { | |
| _id: '$mediaId', | |
| avgRating: { $avg: '$rating' }, | |
| reviewCount: { $sum: 1 } | |
| } | |
| } | |
| ]); | |
| if (stats.length > 0) { | |
| await Media.findByIdAndUpdate(mediaId, { | |
| 'brutaleStats.avgRating': stats[0].avgRating.toFixed(1), | |
| 'brutaleStats.reviewCount': stats[0].reviewCount | |
| }); | |
| } | |
| }; | |
| // --- 1. POST: Create Review --- | |
| exports.createReview = async (req, res) => { | |
| try { | |
| const { mediaId, rating, content, guestName } = req.body; | |
| // COERCE isSpoiler safely | |
| const isSpoiler = (() => { | |
| const val = req.body.isSpoiler; | |
| if (typeof val === 'boolean') return val; | |
| if (typeof val === 'string') return val === 'true'; | |
| return false; | |
| })(); | |
| const userId = req.user ? req.user.id : null; | |
| // A. Validation | |
| if (!mediaId || !rating || !content) { | |
| return res.status(400).json({ error: 'Missing required fields' }); | |
| } | |
| // --- NEW: load username for logged-in user --- | |
| let finalGuestName = guestName; // default for guests | |
| if (userId) { | |
| const user = await User.findById(userId).select("username"); | |
| finalGuestName = user.username; // overwrite guestName | |
| } else { | |
| finalGuestName = guestName || "Anonymous Brutalist"; | |
| } | |
| // B. Anti-Spam | |
| const tenMinutesAgo = new Date(Date.now() - 10 * 60 * 1000); | |
| const duplicateCheck = await Review.findOne({ | |
| mediaId, | |
| $or: [ | |
| { userId: userId }, | |
| { guestName: finalGuestName } | |
| ], | |
| createdAt: { $gte: tenMinutesAgo } | |
| }); | |
| if (duplicateCheck) { | |
| return res.status(429).json({ error: 'Chill! You just reviewed this. Wait 10 mins.' }); | |
| } | |
| // C. Build Review | |
| const reviewData = { | |
| mediaId, | |
| userId, | |
| rating, | |
| content, | |
| isSpoiler, | |
| guestName: finalGuestName, // <-- ALWAYS included | |
| slangTags: [] | |
| }; | |
| const newReview = await Review.create(reviewData); | |
| // D. Update user stats | |
| if (userId) { | |
| await User.findByIdAndUpdate(userId, { $inc: { 'stats.reviewsCount': 1 } }); | |
| } | |
| // E. Update media stats | |
| updateMediaStats(mediaId); | |
| res.status(201).json(newReview); | |
| } catch (error) { | |
| console.error('Review Error:', error); | |
| res.status(500).json({ error: 'Server crashed processing your review.' }); | |
| } | |
| }; | |
| // --- 2. GET: Fetch Reviews --- | |
| exports.getReviews = async (req, res) => { | |
| try { | |
| const { mediaId } = req.params; | |
| const page = parseInt(req.query.page) || 1; | |
| const limit = 10; | |
| const reviews = await Review.find({ mediaId }) | |
| .populate('userId', 'username honestyScore badges') // Get author details | |
| .sort({ createdAt: -1 }) // Newest first | |
| .skip((page - 1) * limit) | |
| .limit(limit); | |
| res.json(reviews); | |
| } catch (error) { | |
| res.status(500).json({ error: 'Failed to fetch reviews' }); | |
| } | |
| }; | |
| // --- 3. GET: Reviews by User --- | |
| exports.getReviewsByUser = async (req, res) => { | |
| try { | |
| const { userId } = req.params; | |
| const reviews = await Review.find({ userId }) | |
| .populate('mediaId', 'title posterPath type') // bring media info | |
| .sort({ createdAt: -1 }); | |
| res.json(reviews); | |
| } catch (error) { | |
| console.error("User Review Fetch Error:", error); | |
| res.status(500).json({ error: 'Failed to fetch user review history' }); | |
| } | |
| }; | |
| // ------------------- | |
| exports.addReply = async (req, res) => { | |
| try { | |
| const { id } = req.params; // Review ID | |
| const { content, guestName } = req.body; | |
| const userId = req.user ? req.user.id : null; | |
| if (!content) return res.status(400).json({ error: 'Content required' }); | |
| // 1. Find Review | |
| const review = await Review.findById(id); | |
| if (!review) return res.status(404).json({ error: 'Review not found' }); | |
| // 2. Determine reply name | |
| let finalGuestName; | |
| if (userId) { | |
| const user = await User.findById(userId).select("username"); | |
| finalGuestName = user.username; // logged in user = username | |
| } else { | |
| finalGuestName = guestName || "Anonymous Brutalist"; // guest user | |
| } | |
| // 3. Build reply object | |
| const newReply = { | |
| content, | |
| userId: userId || null, | |
| guestName: finalGuestName | |
| }; | |
| // 4. Add reply and save | |
| review.replies.push(newReply); | |
| await review.save(); | |
| // 5. Populate username immediately | |
| await review.populate("replies.userId", "username"); | |
| // 6. Get the reply we just pushed | |
| let added = review.replies[review.replies.length - 1]; | |
| // Normalize shape for frontend (safe, clean) | |
| const normalizedReply = { | |
| _id: added._id, | |
| content: added.content, | |
| createdAt: added.createdAt, | |
| username: added.userId ? added.userId.username : added.guestName, | |
| userId: added.userId?._id || null | |
| }; | |
| res.status(201).json(normalizedReply); | |
| } catch (error) { | |
| console.error("Reply Error:", error); | |
| res.status(500).json({ error: 'Failed to add reply' }); | |
| } | |
| }; | |