diff --git a/backend/API/getReplies.js b/backend/API/getReplies.js new file mode 100644 index 0000000..46fd170 --- /dev/null +++ b/backend/API/getReplies.js @@ -0,0 +1,45 @@ +const mongoose = require("mongoose"); +const TrendingComment = require('../schema/trendingComment'); +const Vote = require('../schema/votes'); + +const getRepliesForComment = async (req, res) => { + const commentId = req.params.commentId; // This is the ID of the parent comment + const userId = req.params.userId; // Assuming you are using some form of user auth to get user info + + try { + const replies = await TrendingComment.find({ parentId: commentId }) + .lean() + .sort({ createdAt: -1 }); + + // Aggregate votes for replies similarly as you did for comments + const replyIds = replies.map(reply => reply._id); + const votes = await Vote.aggregate([ + { $match: { commentId: { $in: replyIds } } }, + { $group: { _id: "$commentId", totalVotes: { $sum: "$voteType" }, + userVote: { + $max: { + $cond: { if: { $eq: ["$userId", userId] }, then: "$voteType", else: 0 } + } + }}} + ]); + + const votesMap = votes.reduce((acc, vote) => { + acc[vote._id.toString()] = { totalVotes: vote.totalVotes, userVote: vote.userVote }; + return acc; + }, {}); + + // Attach vote data to replies + const repliesWithVotes = replies.map(reply => ({ + ...reply, + votes: votesMap[reply._id.toString()] ? votesMap[reply._id.toString()].totalVotes : 0, + userVote: votesMap[reply._id.toString()] ? votesMap[reply._id.toString()].userVote : 0, + })); + + res.json(repliesWithVotes); + } catch (error) { + console.error('Error fetching replies:', error); + res.status(500).json({ message: error.message }); + } +}; + +module.exports = getRepliesForComment; \ No newline at end of file diff --git a/backend/API/getTrendingComments.js b/backend/API/getTrendingComments.js index a4486e9..6ccbcfb 100644 --- a/backend/API/getTrendingComments.js +++ b/backend/API/getTrendingComments.js @@ -1,18 +1,62 @@ +const mongoose = require("mongoose"); const TrendingComment = require('../schema/trendingComment'); +const Vote = require('../schema/votes'); const getTrendingComments = async (req, res) => { + const userId = req.params.userId; // Assuming you have user information from session or token + const resumeId = req.params.resumeId; + try { - let comments = await TrendingComment.find({ resumeId: req.params.resumeId, parentId: null }) - .sort({ createdAt: -1 }); - comments = await Promise.all(comments.map(async (comment) => { - const replies = await TrendingComment.find({ parentId: comment._id }).sort({ createdAt: 1 }); - return { ...comment.toObject(), replies }; + // const comments = await TrendingComment.find({ resumeId, parentId: null}) + // .lean() + // .sort({ createdAt: -1 }); + + const comments = await TrendingComment.aggregate([ + // Match top-level comments + { $match: { resumeId: mongoose.Types.ObjectId(resumeId), parentId: null } }, + // Lookup to find replies and count them + { $lookup: { + from: "trendingcomments", // assuming collection name is trendingcomments + localField: "_id", + foreignField: "parentId", + as: "replies" + }}, + { $addFields: { + repliesCount: { $size: "$replies" } + }}, + { $project: { replies: 0 } }, // Exclude the replies field from output + // Sort comments by creation date + { $sort: { createdAt: -1 } } + ]); + // Get vote counts and user-specific votes in one go, if possible + const commentIds = comments.map(comment => comment._id); + const votes = await Vote.aggregate([ + { $match: { commentId: { $in: commentIds } } }, + { $group: { _id: "$commentId", totalVotes: { $sum: "$voteType" }, + userVote: { + $max: { + $cond: { if: { $eq: ["$userId", userId] }, then: "$voteType", else: 0 } + } + }}} + ]); + + const votesMap = votes.reduce((acc, vote) => { + acc[vote._id.toString()] = { totalVotes: vote.totalVotes, userVote: vote.userVote }; + return acc; + }, {}); + + // Attach vote data to comments + const commentsWithVotes = comments.map(comment => ({ + ...comment, + votes: votesMap[comment._id.toString()] ? votesMap[comment._id.toString()].totalVotes : 0, + userVote: votesMap[comment._id.toString()] ? votesMap[comment._id.toString()].userVote : 0, })); - res.json(comments); + + res.json(commentsWithVotes); } catch (error) { console.error('Error fetching trending comments:', error); res.status(500).json({ message: error.message }); } }; -module.exports = getTrendingComments; +module.exports = getTrendingComments; \ No newline at end of file diff --git a/backend/API/votes.js b/backend/API/votes.js index 994eb73..7ae069d 100644 --- a/backend/API/votes.js +++ b/backend/API/votes.js @@ -1,23 +1,61 @@ +const mongoose = require("mongoose"); +const Vote = require('../schema/votes'); const TrendingComment = require('../schema/trendingComment'); const voteOnComment = async (req, res) => { - const { commentId, delta } = req.body; + const { userId, commentId, voteType } = req.body; + try { - const comment = await TrendingComment.findByIdAndUpdate( - commentId, - { $inc: { votes: delta } }, // Increment or decrement the votes field - { new: true } // Return the updated document - ); - - if (!comment) { - return res.status(404).send({ message: "Comment not found" }); + const existingVote = await Vote.findOne({ commentId, userId }); + + if (existingVote) { + if (voteType === 0) { + await existingVote.remove(); + } else if (existingVote.voteType !== voteType) { + existingVote.voteType = voteType; + await existingVote.save(); + } + } else { + if (voteType !== 0) { // Only save if voteType is not 0 + const newVote = new Vote({ commentId, userId, voteType }); + await newVote.save(); + } } - res.json(comment); + await updateCommentVotes(commentId); + res.status(200).json({ message: 'Vote registered successfully.' }); + } catch (error) { + res.status(500).json({ message: 'Error processing vote', error: error.message }); + } +}; + +// Helper function to update the vote count on a comment +async function updateCommentVotes(commentId) { + const votes = await Vote.aggregate([ + { $match: { commentId: mongoose.Types.ObjectId(commentId) } }, + { $group: { _id: null, totalVotes: { $sum: '$voteType' } } } + ]); + + const totalVotes = votes.length > 0 ? votes[0].totalVotes : 0; + await TrendingComment.findByIdAndUpdate(commentId, { votes: totalVotes }); +} + + +const getVoteStatus = async (req, res) => { + const { userId, commentId } = req.query; // Assuming userId and commentId are passed as query parameters + + try { + const existingVote = await Vote.findOne({ commentId, userId }); + + if (existingVote) { + res.status(200).json({ voteType: existingVote.voteType }); + } else { + res.status(200).json({ voteType: 0 }); // No vote + } } catch (error) { - console.error('Failed to update comment votes:', error); - res.status(500).send({ message: "Failed to update comment votes due to an internal error." }); + res.status(500).json({ message: 'Error fetching vote status', error: error.message }); } }; -module.exports = voteOnComment; \ No newline at end of file +module.exports = { voteOnComment, getVoteStatus }; +// module.exports = voteOnComment; \ No newline at end of file diff --git a/backend/Utils/CommentFilter.js b/backend/Utils/CommentFilter.js index e4e3539..deff796 100644 --- a/backend/Utils/CommentFilter.js +++ b/backend/Utils/CommentFilter.js @@ -1,6 +1,6 @@ const Filter = require('bad-words'); const filter = new Filter(); -const customWords = ['stupid', 'idiot', 'dumb', 'crazy','hate','bad','horrible','terrible']; // Add more as needed +const customWords = ['stupid', 'idiot', 'dumb', 'crazy','hate','bad','horrible','terrible', 'awful', 'disgusting']; // Add more as needed filter.addWords(...customWords) const isBad = (text) => filter.isProfane(text); diff --git a/backend/schema/votes.js b/backend/schema/votes.js new file mode 100644 index 0000000..09b891b --- /dev/null +++ b/backend/schema/votes.js @@ -0,0 +1,15 @@ +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + +const voteSchema = new Schema({ + commentId: { type: Schema.Types.ObjectId, ref: 'TrendingComment', required: true }, + userId: { type: Schema.Types.ObjectId, ref: 'User', required: true }, + voteType: { type: Number, required: true, validate: { + validator: function(v) { + return v === 1 || v === -1 || v === 0; // Ensures vote is either an upvote (+1) or a downvote (-1) + }, + message: props => `${props.value} is not a valid vote type!` + }} +}); + +module.exports = mongoose.model('Vote', voteSchema); \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index 16092e6..86ba528 100644 --- a/backend/server.js +++ b/backend/server.js @@ -23,9 +23,10 @@ const getResumebyId = require('./API/getResumebyId'); const getUserbyId = require('./API/getUserbyId'); const getTrendingComments = require('./API/getTrendingComments'); const postTrendingComment = require('./API/postTrendingComment'); -const voteOnComment = require('./API/votes'); +const { voteOnComment, getVoteStatus } = require('./API/votes'); const { blockUser, checkIfBlocked, getBlockedUsers } = require('./API/BlockingUser'); const BlockedUser = require('./schema/blockedUsers'); +const getRepliesForComment = require('./API/getReplies'); // Direct messaging API's const sendMessage = require('./API/sendMessage'); @@ -184,6 +185,8 @@ app.post('/block-user', blockUser); app.get('/api/trending/get-comments/:resumeId', getTrendingComments); app.post('/trending/post-comments', postTrendingComment); app.post('/api/comments/vote', voteOnComment); +app.get('/comments/vote-status', getVoteStatus); +app.get('/api/comments/replies/:commentId', getRepliesForComment); //app.post('/get-number-of-unread-dms', getNumberOfUnreadDms); app.post('/api/dm-status/update', updateDmStatus); app.get('/api/dm-status', getDmStatus); diff --git a/frontend/dump.rdb b/frontend/dump.rdb new file mode 100644 index 0000000..1e35748 Binary files /dev/null and b/frontend/dump.rdb differ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a1e690d..c5cb7fc 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -23730,16 +23730,16 @@ } }, "node_modules/typescript": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", - "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=14.17" + "node": ">=4.2.0" } }, "node_modules/unbox-primitive": { diff --git a/frontend/src/pages/ProfilePage.css b/frontend/src/pages/ProfilePage.css index 25851fc..15dd41e 100644 --- a/frontend/src/pages/ProfilePage.css +++ b/frontend/src/pages/ProfilePage.css @@ -84,7 +84,7 @@ align-items: center; width: 125px; position: absolute; - left: 90px; + left: 65px; top: 50px; } @@ -139,7 +139,7 @@ background-color: #26ACD6; position: relative; margin-right: 10px; - top: 60px; + top: 70px; } /* Container for field labels and values */ @@ -149,6 +149,7 @@ flex-direction: column; align-items: flex-start; position: relative; + text-align: left; margin-left: 10px; margin-top: 40px; } diff --git a/frontend/src/pages/ResumeComment.js b/frontend/src/pages/ResumeComment.js index 73833c4..0e8b5d6 100644 --- a/frontend/src/pages/ResumeComment.js +++ b/frontend/src/pages/ResumeComment.js @@ -3,7 +3,6 @@ import axios from 'axios'; import { Worker, Viewer, SpecialZoomLevel } from '@react-pdf-viewer/core'; import { useParams, useNavigate } from 'react-router-dom'; import useIsAuthenticated from 'react-auth-kit/hooks/useIsAuthenticated'; - import useAuthUser from 'react-auth-kit/hooks/useAuthUser'; import '@react-pdf-viewer/core/lib/styles/index.css'; import './ResumeComment.css'; @@ -27,7 +26,6 @@ const ResumeComment = () => { const [highlights, setHighlights] = useState([]); const [selectedCommentId, setSelectedCommentId] = useState(null); const [isModalOpen, setModalOpen] = useState(false); - const viewerRef = useRef(null); const commentBoxRef = useRef(null); diff --git a/frontend/src/pages/TrendingResumes.css b/frontend/src/pages/TrendingResumes.css index 087b9a7..0b71992 100644 --- a/frontend/src/pages/TrendingResumes.css +++ b/frontend/src/pages/TrendingResumes.css @@ -72,65 +72,22 @@ } .comments-container { - width: 400px; /* Adjust width as needed */ + width: 450px; /* Adjust width as needed */ height: 780px; /* Set to match the PDF viewer height */ overflow-y: auto; /* Enables vertical scrolling when content overflows */ padding: 0px; /* Adjust padding as needed */ margin-top: 30px; box-sizing: border-box; /* Includes padding and border in the element's total width and height */ -} -/* Styles for the comment input and send button */ -.comment-input-container { display: flex; - align-items: center; - margin-top: 10px; /* Space above the input form */ - background: #FFFFFF; /* Match the modal background for consistency */ - border-radius: 5px; /* Rounded corners for the input box */ - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); /* Soft shadow for depth */ -} - -.comment-input { - flex-grow: 1; /* Allow input to fill the space */ - border: none; - border-radius: 5px 0 0 5px; /* Rounded corners on the left side */ - padding: 10px 15px; /* Padding inside the input */ - font-size: 16px; /* Adequate font size for readability */ - outline: none; /* Remove focus outline */ -} - -.comment-submit-button { - padding: 10px 15px; /* Padding matching the input */ - border: none; - background-color: #26ACD6; /* Vibrant color for the button */ - color: white; /* Text color for the button */ - cursor: pointer; /* Pointer cursor on hover */ - border-radius: 0 5px 5px 0; /* Rounded corners on the right side */ - font-size: 16px; /* Match the input text size */ - display: flex; - align-items: center; - justify-content: center; -} - -.comment-submit-button:hover { - background-color: #238fb5; /* Slightly darker on hover for feedback */ -} - -.comment-submit-button svg { - margin-left: 5px; /* Space between text and icon */ -} - -/* Ensuring that the input and button are visually connected */ -.comment-input:focus + .comment-submit-button { - border-color: #26ACD6; /* Highlight color when input is focused */ + flex-direction: column; } .comments-list { - height: 780px; /* Set to match the PDF viewer height */ + height: 100px; /* Set to match the PDF viewer height */ padding: 20px; /* Adjust padding as needed */ } .comment-item { - display: flex; align-items: center; background: #f7f7f7; border-radius: 8px; @@ -150,7 +107,6 @@ height: 35px; background-color: #ccc; border-radius: 50%; - display: flex; justify-content: center; align-items: center; margin-right: 10px; @@ -158,26 +114,145 @@ .comment-username { font-weight: bold; - margin-right: -20px; margin-top: -27px; + margin-right: 10px; } .comment-text { margin-top: 20px; text-align: left; color: #333; + margin-left: 5px; } .comment-votes { display: flex; align-items: center; font-size: 16px; + margin-top: 5px; } .comment-votes svg { margin: 0 5px; /* Spacing around icons */ } +.reply-text { + color: #48abe4; /* Green color to suggest interaction */ + cursor: pointer; /* Pointer cursor to indicate clickability */ + margin-left: 5px; /* Space from the down arrow */ + font-size: 16px; /* Font size to match other text elements */ + font-weight: 500; + display: flex; + align-items: center; +} + +.reply-input { + flex-grow: 1; /* Allow input to fill the space */ + border: none; + border-radius: 5px 0 0 5px; /* Rounded corners on the left side */ + padding: 10px 15px; /* Padding inside the input */ + font-size: 16px; /* Adequate font size for readability */ + display: flex; + margin-left: -8px; + margin-top: 10px; + outline: none; /* Remove focus outline */ +} + +.reply-submit-button { + padding: 10px 15px; /* Padding matching the input */ + border: none; + background-color: #26ACD6; /* Vibrant color for the button */ + color: white; /* Text color for the button */ + cursor: pointer; /* Pointer cursor on hover */ + border-radius: 0 5px 5px 0; /* Rounded corners on the right side */ + font-size: 16px; /* Match the input text size */ + display: flex; + margin-top: 10px; + align-items: center; + justify-content: center; +} + +.reply-submit-button:hover { + background-color: #238fb5; +} + +.reply-submit-button svg { + margin-left: 5px; /* Space between text and icon */ +} + +.view-replies-text { + margin-top: 10px; + margin-right: 90px; + cursor: pointer; + color: #26ACD6; /* Bootstrap primary color for consistency */ +} + +.view-replies-text:hover { + color: #238fb5; /* Darker blue on hover */ +} + +.replies-container { + width: 300px; /* Adjust width as needed */ + height: 150px; /* Set to match the PDF viewer height */ + overflow-y: auto; /* Enables vertical scrolling when content overflows */ + margin-left: -20px; + box-sizing: border-box; /* Includes padding and border in the element's total width and height */ + display: flex; + flex-direction: column; +} + +.reply-username { + font-weight: bold; + margin-top: -27px; + margin-right: 10px; +} + +/* Styles for the comment input and send button */ +.comment-input-container { + display: flex; + align-items: center; + margin-top: 20px; /* Space above the input form */ + margin-bottom: 10px; /* Space above the input form */ + background: #FFFFFF; /* Match the modal background for consistency */ + border-radius: 5px; /* Rounded corners for the input box */ + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); /* Soft shadow for depth */ +} + +.comment-input { + flex-grow: 1; /* Allow input to fill the space */ + border: none; + border-radius: 5px 0 0 5px; /* Rounded corners on the left side */ + padding: 10px 15px; /* Padding inside the input */ + font-size: 16px; /* Adequate font size for readability */ + outline: none; /* Remove focus outline */ +} + +.comment-submit-button { + padding: 10px 15px; /* Padding matching the input */ + border: none; + background-color: #26ACD6; /* Vibrant color for the button */ + color: white; /* Text color for the button */ + cursor: pointer; /* Pointer cursor on hover */ + border-radius: 0 5px 5px 0; /* Rounded corners on the right side */ + font-size: 16px; /* Match the input text size */ + display: flex; + align-items: center; + justify-content: center; +} + +.comment-submit-button:hover { + background-color: #238fb5; /* Slightly darker on hover for feedback */ +} + +.comment-submit-button svg { + margin-left: 5px; /* Space between text and icon */ +} + +/* Ensuring that the input and button are visually connected */ +.comment-input:focus + .comment-submit-button { + border-color: #26ACD6; /* Highlight color when input is focused */ +} + .comment-button { display: inline-flex; /* Aligns icon and text within the button */ align-items: center; /* Centers items vertically */ diff --git a/frontend/src/pages/TrendingResumes.js b/frontend/src/pages/TrendingResumes.js index 2d6340b..33dbb51 100644 --- a/frontend/src/pages/TrendingResumes.js +++ b/frontend/src/pages/TrendingResumes.js @@ -13,6 +13,17 @@ import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'; import HatefulCommentModal from '../components/MsgFilter'; +// Profile Pics +import bearTwemoji from '../images/profilePics/bearTwemoji.png'; +import bunnyTwemoji from '../images/profilePics/bunnyTwemoji.png'; +import catTwemoji from '../images/profilePics/catTwemoji.png'; +import cowTwemoji from '../images/profilePics/cowTwemoji.png'; +import dogTwemoji from '../images/profilePics/dogTwemoji.png'; +import horseTwemoji from '../images/profilePics/horseTwemoji.png'; +import pigTwemoji from '../images/profilePics/pigTwemoji.png'; +import tigerTwemoji from '../images/profilePics/tigerTwemoji.png'; +import { extractColors } from 'extract-colors' + function TrendingResumes() { const [resumes, setResumes] = useState([]); const pdfContainerRef = useRef(null); @@ -23,6 +34,174 @@ function TrendingResumes() { const [votes, setVotes] = useState({}); const [voteStatus, setVoteStatus] = useState({}); const [isModalOpen, setModalOpen] = useState(false); + const [activeReplyInput, setActiveReplyInput] = useState(null); + const [replyInputs, setReplyInputs] = useState({}); + const [isSubmitting, setIsSubmitting] = useState(false); + + + // A dictionary that maps profile picture STRINGS to IMAGES + const profilePicDictionary = { + "bearTwemoji.png": bearTwemoji, + "bunnyTwemoji.png": bunnyTwemoji, + "catTwemoji.png": catTwemoji, + "cowTwemoji.png": cowTwemoji, + "dogTwemoji.png": dogTwemoji, + "horseTwemoji.png": horseTwemoji, + "pigTwemoji.png": pigTwemoji, + "tigerTwemoji.png": tigerTwemoji + }; + + // Profile pic background colour + const [allUsernames, setAllUsernames] = useState([]); + const [matchedListProfileColourDict, setMatchedListProfileColourDict] = useState({}); + const [userToProfilePic, setUserToProfilePic] = useState({}); + + // Getting all user's who currently have comments + useEffect(() => { + const uniqueUsernames = getUniqueUsernames(comments); + setAllUsernames(uniqueUsernames); + }, [comments]); + + const getUniqueUsernames = (comments) => { + const usernames = []; + for (let resume of resumes) { + const resumeId = resume._id; + if (comments[resumeId]) { + for (let comment of comments[resumeId]) { + usernames.push(comment.username); + } + } + } + return [...new Set(usernames)]; + }; + + useEffect(() => { + if(allUsernames.length > 0) { + fetchProfilePics(); + } + }, [allUsernames]); + + useEffect(() => { + fetchColourOthers(); + }, [userToProfilePic]); + + const hslToHex = (h, s, l) => { + l /= 100; + const a = s * Math.min(l, 1 - l) / 100; + const f = n => { + const k = (n + h / 30) % 12; + const colour = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1); + return Math.round(255 * colour).toString(16).padStart(2, '0'); + }; + return `#${f(0)}${f(8)}${f(4)}`; + } + + const loadImageAndExtractColour = async (profilePic) => { + try { + const imgSrc = profilePicDictionary[profilePic]; + if (imgSrc) { + const img = new Image(); + img.src = imgSrc; + img.crossOrigin = 'anonymous'; + + return new Promise((resolve, reject) => { + img.onload = async () => { + try { + const returnedColours = await extractColors(imgSrc); + const colours = returnedColours.sort((a, b) => b.area - a.area); // Sorting by most prominent colours + if (colours.length > 0) { + const { hue, saturation, lightness } = colours[0]; + const adjustedSaturation = Math.min(1, saturation + 0.7); + const adjustedLightness = Math.min(0.85, lightness + 0.5); // Increase the brightness + const adjustedColour = hslToHex( + hue * 360, // Convert hue to degrees + adjustedSaturation * 100, // Convert to percentage + adjustedLightness * 100 // Convert to percentage + ); + resolve(adjustedColour); + } else { + reject('No colours found'); + } + } catch (err) { + reject('Error extracting colour:', err); + } + } + }); + } + } + catch (error) { + + } + }; + + const fetchColourOthers = async () => { + try { + // Create an array of promises + const colourPromises = allUsernames.map(async (username) => { + const colour = await loadImageAndExtractColour(userToProfilePic[username]); + return { username: username, colour }; + }); + + // Wait for all promises to resolve + const userToColour = await Promise.all(colourPromises); + + // Update the state with the colour dictionary + const newMatchedListProfileColourDict = {}; + userToColour.forEach(({ username, colour }) => { + newMatchedListProfileColourDict[username] = colour; + }); + + console.log(newMatchedListProfileColourDict); + setMatchedListProfileColourDict(newMatchedListProfileColourDict); + } catch (err) { + console.error('Error fetching colour:', err); + } + }; + + const fetchProfilePics = async () => { + console.log(allUsernames); + const profilePicPromises = allUsernames.map(username => getProfilePic(username)); + const profilePicResults = await Promise.all(profilePicPromises); + const profilePicsMap = {}; + profilePicResults.forEach((result) => { + profilePicsMap[result.username] = result.profilePic; + }); + setUserToProfilePic(profilePicsMap); + }; + + // Getting Profile Pic + const getProfilePic = async (username) => { + + const info = { + username: username + }; + + try { + // Return a new Promise to handle async operations + return new Promise((resolve, reject) => { + fetch('http://localhost:3001/get-profile-pic', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(info) + }) + .then(async (response) => { + if (response.ok) { + const data = await response.json(); + resolve({ username, profilePic: data.profilePic }); + } else { + reject('Failed to fetch profile pic'); + } + }) + .catch((error) => { + reject('Error fetching profile pic:', error); + }); + }); + } catch (error) { + return Promise.reject('Unexpected error:', error); + } + } useEffect(() => { const fetchResumesTrending = async () => { @@ -36,19 +215,50 @@ function TrendingResumes() { console.error('Failed to fetch public trending resumes', error); } }; - + fetchResumesTrending(); }, []); - + + const fetchVoteStatus = async (commentId, userId) => { + try { + const response = await axios.get('http://localhost:3001/comments/vote-status', { + params: { userId, commentId } + }); + setVoteStatus(prev => ({ + ...prev, + [commentId]: response.data.voteType === 1 ? 'up' : response.data.voteType === -1 ? 'down' : null + })); + } catch (error) { + console.error('Error fetching vote status:', error); + } + }; + const fetchComments = async (resumeId) => { try { const response = await axios.get(`http://localhost:3001/api/trending/get-comments/${resumeId}`); - setComments(prev => ({ ...prev, [resumeId]: response.data })); + const commentsData = response.data; + const newVotes = {}; + const userId = auth.id; // Ensure you are correctly retrieving the user's ID + commentsData.forEach(comment => { + newVotes[comment._id] = comment.votes; + if (userId) { + fetchVoteStatus(comment._id, userId); + } + }); + + commentsData.sort((a, b) => { + const votesA = newVotes[a._id] || 0; + const votesB = newVotes[b._id] || 0; + return votesB - votesA; + }); + + setComments(prev => ({ ...prev, [resumeId]: commentsData })); + setVotes(prev => ({ ...prev, ...newVotes })); } catch (error) { console.error('Error fetching comments:', error); } - }; - + }; + const handleAddCommentClick = (resumeId) => { setActiveCommentInput(resumeId === activeCommentInput ? null : resumeId); }; @@ -67,7 +277,8 @@ function TrendingResumes() { const response = await axios.post('http://localhost:3001/trending/post-comments', { resumeId, text, - username + username, + repliesCount: 0 }); const updatedComments = comments[resumeId] ? [...comments[resumeId], response.data] : [response.data]; setComments({ ...comments, [resumeId]: updatedComments }); @@ -87,49 +298,177 @@ function TrendingResumes() { const closeModal = () => { setModalOpen(false); }; - + const handleVote = async (commentId, type) => { - const existingVote = voteStatus[commentId]; - let delta = 0; - - if (type === 'up') { - delta = existingVote === 'up' ? -1 : 1; // Toggle the upvote or set an upvote - } else if (type === 'down') { - delta = existingVote === 'down' ? 1 : -1; // Toggle the downvote or set a downvote + const userId = auth.id; // Ensure user ID is correctly retrieved + if (!userId) { + console.error("User ID is missing"); + alert("Please log in to vote."); + return; } - - // Update the votes count optimistically - setVotes(prev => ({ - ...prev, - [commentId]: (prev[commentId] || 0) + delta - })); + + const currentVote = voteStatus[commentId]; // Get the current vote status for this comment + let newVoteType = 0; // Default to no vote + let delta = 0; // Change in vote count + + switch (type) { + case 'up': + if (currentVote === 'up') { + delta = -1; + newVoteType = 0; + } else { + delta = currentVote === 'down' ? 2 : 1; + newVoteType = 1; + } + break; + case 'down': + if (currentVote === 'down') { + delta = 1; + newVoteType = 0; + } else { + delta = currentVote === 'up' ? -2 : -1; + newVoteType = -1; + } + break; + } + + console.log(`Current vote: ${currentVote}, New vote type: ${newVoteType}, Delta: ${delta}`); - // Update the vote status - setVoteStatus(prev => ({ - ...prev, - [commentId]: existingVote === type ? null : type // Toggle or set the vote type + // Optimistically update UI + setVotes(prevVotes => ({ + ...prevVotes, + [commentId]: (prevVotes[commentId] || 0) + delta })); - // Send the vote change to the backend + setVoteStatus(prevStatus => ({ + ...prevStatus, + [commentId]: newVoteType === 0 ? null : type + })); + try { await axios.post('http://localhost:3001/api/comments/vote', { + userId, commentId, - delta: existingVote === type ? -delta : delta + voteType: newVoteType }); + console.log('Vote updated successfully'); } catch (error) { console.error('Failed to update vote:', error); - // Rollback optimistic updates in case of failure - setVotes(prev => ({ - ...prev, - [commentId]: (prev[commentId] || 0) - delta + // Roll back optimistic updates in case of a server error + setVotes(prevVotes => ({ + ...prevVotes, + [commentId]: (prevVotes[commentId] || 0) - delta })); - setVoteStatus(prev => ({ - ...prev, - [commentId]: existingVote // Reset to the original vote status + setVoteStatus(prevStatus => ({ + ...prevStatus, + [commentId]: currentVote })); } + }; + + const handleAddReplyClick = (commentId) => { + setActiveReplyInput(commentId === activeReplyInput ? null : commentId); + setActiveCommentInput(null); // Ensure comment input is closed when opening reply input }; + const handleReplyChange = (text, commentId) => { + setReplyInputs(prev => ({ ...prev, [commentId]: text })); + }; + + const handleReplySubmit = async (event, commentId, resumeId) => { + event.preventDefault(); + const text = replyInputs[commentId]; + if (!text) return; + + try { + const username = auth.name; + const response = await axios.post('http://localhost:3001/trending/post-comments', { + resumeId, + text, + username, + parentId: commentId // Ensuring this is treated as a reply + }); + + // Assuming the response includes the newly created reply + const newReply = response.data; + + // Update the comments state to include the new reply and increment the replies count + setComments(prevComments => ({ + ...prevComments, + [resumeId]: prevComments[resumeId].map(comment => { + if (comment._id === commentId) { + // Append the new reply to the existing replies array and increment repliesCount + return { + ...comment, + replies: [...(comment.replies || []), newReply], + repliesCount: (comment.repliesCount || 0) + 1 // Increment the count + }; + } + return comment; + }) + })); + + setReplyInputs(prev => ({ ...prev, [commentId]: '' })); // Clear the input after submission + setActiveReplyInput(null); // Hide the reply input + } catch (error) { + console.error('Failed to post reply:', error); + } + }; + + const fetchAndCheckReplies = async (commentId, resumeId) => { + const commentIndex = comments[resumeId].findIndex(c => c._id === commentId); + if (commentIndex === -1) return; // Exit if no comment found + + const comment = comments[resumeId][commentIndex]; + + if (!comment.replies || comment.replies.length === 0 || !comment.showReplies) { + try { + const response = await axios.get(`http://localhost:3001/api/comments/replies/${commentId}`); + const replies = response.data; + + // Initialize or update the vote counts for each reply + const newVotes = replies.reduce((acc, reply) => { + acc[reply._id] = reply.votes; // Assumes 'votes' is the total vote count returned by the backend + return acc; + }, {}); + + // Fetch vote statuses for each reply + replies.forEach(reply => { + fetchVoteStatus(reply._id, auth.id); // Assuming 'auth.id' is available for current user ID + }); + + // Update comments state with the new replies and set them to be visible + setComments(prevComments => ({ + ...prevComments, + [resumeId]: prevComments[resumeId].map((c, idx) => + idx === commentIndex ? {...c, replies, showReplies: true} : c) + })); + setVotes(prev => ({ ...prev, ...newVotes })); + } catch (error) { + console.error("Failed to fetch replies:", error); + } + } else { + // Simply toggle the visibility if already fetched + setComments(prevComments => ({ + ...prevComments, + [resumeId]: prevComments[resumeId].map((c, idx) => + idx === commentIndex ? {...c, showReplies: !c.showReplies} : c) + })); + } + }; + + const profilePicDisplay = (username) => { + return ( +