diff --git a/login-server.js b/login-server.js index f32af355cb82bec6f6e24119eb51daf5aacb398b..b6089b56d6974f0f6d5059e4f8a95545a3f2e56f 100644 --- a/login-server.js +++ b/login-server.js @@ -449,40 +449,24 @@ app.get("/video-views/:fileName", (req, res) => { }); }); -// app.get("/fetch-reply-liked", authenticateTokenGet, (req, res) => { -// // console.log("Request headers:", req.headers); -// const user_id = req.user.userId; -// const { reply_id } = req.query; - -// const db = dbRequest(dbHost); - - -// console.log("User ID:", user_id); - -// const query = "SELECT * FROM reply_likes WHERE user_id = ? AND reply_id = ?"; -// db.query(query, [user_id, reply_id], (err, results) => { -// db.destroy(); - -// if (err) { -// console.error("Database error:", err); -// return res.status(500).json({ message: "Database error" }); -// } - -// if (results.length > 0) { -// // If a matching record is found, return a success response with the data -// return res.status(200).json({ liked: true }); -// } else { -// // If no matching record is found, return a response indicating the reply is not liked -// return res.status(200).json({ liked: false }); -// } -// }).catch((error) => { -// console.error("Error:", error.message); -// db.destroy(); -// return res.status(400).json({ message: error.message }); -// }); -// }); +app.get("/fetch-reply-liked", authenticateTokenGet, (req, res) => { + const user_id = req.user.userId; + const { reply_id } = req.query; + const db = dbRequest(dbHost); + const query = "SELECT * FROM reply_likes WHERE user_id = ? AND reply_id = ?"; + db.query(query, [user_id, reply_id], (err, results) => { + if (err) { + console.error("Database error:", err); + db.destroy(); + return res.status(500).json({ message: "Database error" }); + } + + db.destroy(); + return res.status(200).json({ liked: results.length > 0 }); + }); +}); // Updated like-video endpoint app.post("/like-reply", authenticateTokenGet, (req, res) => { @@ -492,60 +476,56 @@ app.post("/like-reply", authenticateTokenGet, (req, res) => { console.log("User ID:", userId); + // Check if user already liked the reply + const checkLikeQuery = + "SELECT * FROM reply_likes WHERE user_id = ? AND reply_id = ?"; + db.query(checkLikeQuery, [userId, reply_id], (err, results) => { + if (err) { + console.error("Database error:", err); + db.destroy(); + return res.status(500).json({ message: "Database error" }); + } - // Check if user already liked the reply - const checkLikeQuery = - "SELECT * FROM reply_likes WHERE user_id = ? AND reply_id = ?"; - db.query(checkLikeQuery, [userId, reply_id], (err, results) => { + if (results.length > 0) { + // User already liked the video -> Unlike it + const unlikeQuery = + "DELETE FROM reply_likes WHERE user_id = ? AND reply_id = ?"; + db.query(unlikeQuery, [userId, reply_id], (err) => { if (err) { console.error("Database error:", err); db.destroy(); return res.status(500).json({ message: "Database error" }); } - - if (results.length > 0) { - // User already liked the video -> Unlike it - const unlikeQuery = - "DELETE FROM reply_likes WHERE user_id = ? AND reply_id = ?"; - db.query(unlikeQuery, [userId, reply_id], (err) => { - if (err) { - console.error("Database error:", err); - db.destroy(); - return res.status(500).json({ message: "Database error" }); - } - db.destroy(); - return res - .status(200) - .json({ message: "Reply unliked successfully" }); - }); - } else { - // User hasn't liked the comment -> Like it - const likeQuery = - "INSERT INTO reply_likes (user_id, reply_id) VALUES (?, ?)"; - db.query(likeQuery, [userId, reply_id], (err) => { - if (err) { - console.error("Database error:", err); - db.destroy(); - return res.status(500).json({ message: "Database error" }); - } - db.destroy(); - return res - .status(200) - .json({ message: "Reply liked successfully" }); - }); + db.destroy(); + return res.status(200).json({ message: "Reply unliked successfully" }); + }); + } else { + // User hasn't liked the comment -> Like it + const likeQuery = + "INSERT INTO reply_likes (user_id, reply_id) VALUES (?, ?)"; + db.query(likeQuery, [userId, reply_id], (err) => { + if (err) { + console.error("Database error:", err); + db.destroy(); + return res.status(500).json({ message: "Database error" }); } + db.destroy(); + return res.status(200).json({ message: "Reply liked successfully" }); }); + } + }); }); app.get("/reply-like-count", authenticateTokenGet, (req, res) => { - const {reply_id} = req.query; + const { reply_id } = req.query; const db = dbRequest(dbHost); - const query = "SELECT COUNT(*) AS like_count FROM reply_likes WHERE reply_id = ?"; + const query = + "SELECT COUNT(*) AS like_count FROM reply_likes WHERE reply_id = ?"; db.query(query, [reply_id], (err, results) => { db.destroy(); if (err) { console.error("Database error:", err); - + return res.status(500).json({ message: "Database error" }); } res.json({ like_count: results[0].like_count }); // Send response @@ -589,23 +569,38 @@ app.post("/record-anonymous-view", (req, res) => { // addReply const export const addReply = async (req, res) => { const db = dbRequest(dbHost); - const { filename, content } = req.body; + const { commentId, content } = req.body; const userId = req.user.userId; - // const commentId = - getVideoIdFromFileName(db, fileName) - .then((videoId) => { - const addReply = "INSERT INTO REPLY (creatorId, content, commentId) VALUES (userId, content, commentId)"; - db.query(() => { - - }) - .catch((error) => { - console.error("Error:", error.message); + if (!commentId || !content) { + db.destroy(); + return res + .status(400) + .json({ message: "Comment ID and content are required" }); + } + + try { + const addReplyQuery = + "INSERT INTO REPLY (creator_id, content, comment_id) VALUES (?, ?, ?)"; + db.query(addReplyQuery, [userId, content, commentId], (err, result) => { + if (err) { + console.error("Database error:", err); db.destroy(); - return res.status(400).json({ message: error.message }); + return res.status(500).json({ message: "Database error" }); + } + + db.destroy(); + return res.status(201).json({ + message: "Reply added successfully", + replyId: result.insertId, }); }); -} + } catch (error) { + console.error("Error:", error.message); + db.destroy(); + return res.status(400).json({ message: error.message }); + } +}; // Register routes app.post("/signup", signup); @@ -615,4 +610,4 @@ app.post("/addReply", addReply); // Start the Server app.listen(port, () => { console.log(`Login Server is running at http://localhost:${port}`); -}); \ No newline at end of file +}); diff --git a/src/App.tsx b/src/App.tsx index de9d7f23cabf190e3fe2e3466394d0dc512a142b..9950b923a28c9185a7af5dbf1c6b55d84ee5aedf 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -109,13 +109,16 @@ function Home() { // - replyVisible toggles showing the reply input field. // - repliesVisible toggles showing/hiding the entire replies list. const [replyInputs, setReplyInputs] = useState<{ [key: number]: string }>({}); - const [replyVisible, setReplyVisible] = useState<{ [key: number]: boolean }>({}); - const [repliesVisible, setRepliesVisible] = useState<{ [key: number]: boolean }>({}); + const [replyVisible, setReplyVisible] = useState<{ [key: number]: boolean }>( + {} + ); + const [repliesVisible, setRepliesVisible] = useState<{ + [key: number]: boolean; + }>({}); // The comment section is toggled by the COMMENT button. const [showComments, setShowComments] = useState(false); - const [loggedIn, setLoggedIn] = useState(false); const [username, setUsername] = useState(""); const [userID, setUserID] = useState(0); @@ -124,26 +127,28 @@ function Home() { const [viewCount, setViewCount] = useState(0); const [viewRecorded, setViewRecorded] = useState(false); - const [replyLikeCount, setReplyLikeCount] = useState<{ [key: number]: number }>({}); // Like counts are stored with replyId as keys + const [replyLikeCount, setReplyLikeCount] = useState<{ + [key: number]: number; + }>({}); // Like counts are stored with replyId as keys const [replyLiked, setReplyLiked] = useState<{ [key: number]: boolean }>({}); - + // const getReplyLikeCount = async (replyId: number): Promise<number | string> => { // try { // const response = await axios.get( // `${loginServer}/reply-like-count/${replyId}` // ); - + // // Update the state with the fetched like count // setReplyLikeCount(prev => ({ // ...prev, // [replyId]: response.data.likeCount // })); - + // // Return the like count immediately // return response.data.likeCount; // } catch (error) { // console.error("Error fetching like count:", error); - + // // Return a fallback value in case of error // return "Error"; // } @@ -158,44 +163,42 @@ function Home() { // }; // Function to set the reply liked status to true -const likeReply = (replyId: number) => { - setReplyLiked((prev) => ({ - ...prev, - [replyId]: true, // Set the liked status to true - })); -}; - -// Function to set the reply liked status to false -const unlikeReply = (replyId: number) => { - setReplyLiked((prev) => ({ - ...prev, - [replyId]: false, // Set the liked status to false - })); -}; - -// Function to decrement the like count for a specific reply -const decrementLikeCount = (replyId: number) => { - setReplyLikeCount((prev) => { - const currentLikeCount = prev[replyId] || 0; // Get the current like count for the replyId, default to 0 - return { - ...prev, // Spread the previous state - [replyId]: Math.max(0, currentLikeCount - 1), // Update the like count for this specific replyId, ensuring it doesn't go below 0 - }; - }); -}; + const likeReply = (replyId: number) => { + setReplyLiked((prev) => ({ + ...prev, + [replyId]: true, // Set the liked status to true + })); + }; + // Function to set the reply liked status to false + const unlikeReply = (replyId: number) => { + setReplyLiked((prev) => ({ + ...prev, + [replyId]: false, // Set the liked status to false + })); + }; -// Function to increment the like count for a specific reply -const incrementLikeCount = (replyId: number) => { - setReplyLikeCount((prev) => { - const currentLikeCount = prev[replyId] || 0; // Get the current like count for the replyId, default to 0 - return { - ...prev, // Spread the previous state - [replyId]: currentLikeCount + 1, // Increment the like count for this specific replyId - }; - }); -}; + // Function to decrement the like count for a specific reply + const decrementLikeCount = (replyId: number) => { + setReplyLikeCount((prev) => { + const currentLikeCount = prev[replyId] || 0; // Get the current like count for the replyId, default to 0 + return { + ...prev, // Spread the previous state + [replyId]: Math.max(0, currentLikeCount - 1), // Update the like count for this specific replyId, ensuring it doesn't go below 0 + }; + }); + }; + // Function to increment the like count for a specific reply + const incrementLikeCount = (replyId: number) => { + setReplyLikeCount((prev) => { + const currentLikeCount = prev[replyId] || 0; // Get the current like count for the replyId, default to 0 + return { + ...prev, // Spread the previous state + [replyId]: currentLikeCount + 1, // Increment the like count for this specific replyId + }; + }); + }; const navigate = useNavigate(); @@ -220,53 +223,60 @@ const incrementLikeCount = (replyId: number) => { useEffect(() => { const fetchReplyLikes = async () => { - const initialLikedState: { [key: number]: boolean } = {}; - const token = localStorage.getItem("authToken"); - - if (!token) { - console.error("Authorization token is missing!"); - return; - } - + if (!loggedIn || !comments.length) return; + + const token = localStorage.getItem("authToken"); + if (!token) return; + + const initialLikedState = { ...replyLiked }; // Preserve existing state + const initialLikeCountState = { ...replyLikeCount }; // Preserve existing state + for (const comment of comments) { if (Array.isArray(comment.replies)) { for (const reply of comment.replies) { - try { - console.log(`Fetching like status for reply_id: ${reply.id}`); - console.log("Sending token:", token); - const response = await axios.get(`${loginServer}/fetch-reply-liked`, { - params: { auth: token, reply_id: reply.id }, - }); - - initialLikedState[reply.id] = response.data.liked; - } catch (err) { - console.error("Error fetching reply like status:", err); - initialLikedState[reply.id] = false; - } - - try { - console.log(`Fetching like count for reply_id: ${reply.id}`); - console.log("Sending token:", token); - const response = await axios.get(`${loginServer}/reply-like-count`, { - params: { reply_id: reply.id }, - }); - replyLikeCount[reply.id] = response.data.like_count; - } catch (err) { - console.error("Error fetching reply like count:", err); + // Only fetch for replies we don't already have data for + if (initialLikedState[reply.id] === undefined) { + try { + const likeStatusResponse = await axios.get( + `${loginServer}/fetch-reply-liked`, + { + params: { auth: token, reply_id: reply.id }, + } + ); + initialLikedState[reply.id] = likeStatusResponse.data.liked; + + const likeCountResponse = await axios.get( + `${loginServer}/reply-like-count`, + { + params: { reply_id: reply.id, auth: token }, + } + ); + initialLikeCountState[reply.id] = + likeCountResponse.data.like_count; + } catch (err) { + console.error( + `Error fetching data for reply ${reply.id}:`, + err + ); + initialLikedState[reply.id] = false; + initialLikeCountState[reply.id] = 0; + } } } } } + setReplyLiked(initialLikedState); + setReplyLikeCount(initialLikeCountState); }; fetchReplyLikes(); - }, [comments]); - - + }, [comments, loggedIn]); const handleNext = () => { - setVideoIndex((prevIndex) => (prevIndex + initState) % filteredArray.length); + setVideoIndex( + (prevIndex) => (prevIndex + initState) % filteredArray.length + ); }; const handleBackToLogin = () => { @@ -428,7 +438,9 @@ const incrementLikeCount = (replyId: number) => { console.error("Error: fileName is missing."); return; } - const response = await axios.get(`${loginServer}/video-views/${fileName}`); + const response = await axios.get( + `${loginServer}/video-views/${fileName}` + ); setViewCount(response.data.viewCount); } catch (error) { console.error("Error fetching view count:", error); @@ -462,43 +474,55 @@ const incrementLikeCount = (replyId: number) => { } } - async function handleReplyLike(reply_id: number) { + async function handleReplyLike(reply_id) { if (!userID || !loggedIn) { alert("You must be logged in to like replies."); return; } - const fileName = currentVideo.split("/").pop(); - if (!fileName) { - console.error("Error: fileName is missing."); - return; - } + const token = localStorage.getItem("authToken"); if (!token) { alert("Authentication error. Please log in again."); setLoggedIn(false); return; } + + const fileName = currentVideo.split("/").pop(); + if (!fileName) { + console.error("Error: fileName is missing."); + return; + } + try { const response = await axios.post( `${loginServer}/like-reply`, - { fileName: fileName, reply_id }, + { fileName, reply_id }, { params: { auth: token } } ); - if (response.data.message.includes("unliked")) { - unlikeReply(reply_id); - decrementLikeCount(reply_id); - } else { - likeReply(reply_id); - incrementLikeCount(reply_id); - } + + // Update both states atomically + setReplyLiked((prev) => { + const newState = { ...prev, [reply_id]: !prev[reply_id] }; + + // Update like count based on the new liked state + setReplyLikeCount((prevCounts) => { + const currentCount = prevCounts[reply_id] || 0; + return { + ...prevCounts, + [reply_id]: newState[reply_id] + ? currentCount + 1 + : Math.max(0, currentCount - 1), + }; + }); + + return newState; + }); } catch (error) { - console.error("Error liking/unliking video:", error); + console.error("Error liking/unliking reply:", error); alert("Failed to process like. Please try again."); } } - - // Toggle the comment section using the COMMENT button. const toggleComments = () => { setShowComments((prev) => !prev); @@ -556,14 +580,20 @@ const incrementLikeCount = (replyId: number) => { }); let replies: any[] = []; try { - const repliesResponse = await axios.get(`${uploadServer}/get-replies`, { - params: { comment_id: comment.id }, - }); + const repliesResponse = await axios.get( + `${uploadServer}/get-replies`, + { + params: { comment_id: comment.id }, + } + ); replies = await Promise.all( repliesResponse.data.map(async (reply: any) => { - const replyUserResponse = await axios.get(`${uploadServer}/user`, { - params: { userID: reply.creator_id }, - }); + const replyUserResponse = await axios.get( + `${uploadServer}/user`, + { + params: { userID: reply.creator_id }, + } + ); return { id: reply.id, username: replyUserResponse.data.username, @@ -672,7 +702,9 @@ const incrementLikeCount = (replyId: number) => { ENGAGE <i className="fa-solid fa-upload"></i> </button> </div> - <div className="back-button-section">{/* Removed VIDEO INFO button */}</div> + <div className="back-button-section"> + {/* Removed VIDEO INFO button */} + </div> <div className="login-button-section"> <button className="control-button" @@ -702,7 +734,7 @@ const incrementLikeCount = (replyId: number) => { maxHeight: "40vh", overflowY: "auto", }} - > + > <div className="comments-list"> {comments.map((c) => ( <div key={c.id} className="comment-box"> @@ -710,94 +742,124 @@ const incrementLikeCount = (replyId: number) => { <strong>{c.username}</strong> ({c.created_at}): {c.comment} </p> - <div style={{display:"flex", gap:"5x"}}> - - {/* Toggle button for showing/hiding replies using icons */} - {c.replies && c.replies.length > 0 && ( - <div style={{ width: "24px", textAlign: "center" }}> - <button - onClick={() => toggleRepliesVisible(c.id)} - style={{ - border: "none", - background: "transparent", - cursor: "pointer", - }} - > - {repliesVisible[c.id] ? ( - <i - className="fa-solid fa-chevron-up" - style={{ fontSize: "1.2em", color: "#333" }} - ></i> - ) : ( - <i - className="fa-solid fa-chevron-down" - style={{ fontSize: "1.2em", color: "#333" }} - ></i> - )} - </button> - </div> - )} - - { loggedIn && ( - <div > - <button onClick={() => toggleReplyInput(c.id)}><i className="fa-regular fa-comments"></i></button> - {replyVisible[c.id] && ( - <div style={{ marginTop: "5px", display: "flex", alignItems: "center", gap: "8px", minHeight: "40px" }}> - <input - type="text" - value={replyInputs[c.id] || ""} - onChange={(e) => - setReplyInputs((prev) => ({ - ...prev, - [c.id]: e.target.value, - })) - } - placeholder="Write a reply..." - /> - <button onClick={() => postReply(c.id)}><i className="fa-regular fa-paper-plane"></i></button> + <div style={{ display: "flex", gap: "5x" }}> + {/* Toggle button for showing/hiding replies using icons */} + {c.replies && c.replies.length > 0 && ( + <div style={{ width: "24px", textAlign: "center" }}> + <button + onClick={() => toggleRepliesVisible(c.id)} + style={{ + border: "none", + background: "transparent", + cursor: "pointer", + }} + > + {repliesVisible[c.id] ? ( + <i + className="fa-solid fa-chevron-up" + style={{ fontSize: "1.2em", color: "#333" }} + ></i> + ) : ( + <i + className="fa-solid fa-chevron-down" + style={{ fontSize: "1.2em", color: "#333" }} + ></i> + )} + </button> </div> )} - </div> - )} + {loggedIn && ( + <div> + <button onClick={() => toggleReplyInput(c.id)}> + <i className="fa-regular fa-comments"></i> + </button> + {replyVisible[c.id] && ( + <div + style={{ + marginTop: "5px", + display: "flex", + alignItems: "center", + gap: "8px", + minHeight: "40px", + }} + > + <input + type="text" + value={replyInputs[c.id] || ""} + onChange={(e) => + setReplyInputs((prev) => ({ + ...prev, + [c.id]: e.target.value, + })) + } + placeholder="Write a reply..." + /> + <button onClick={() => postReply(c.id)}> + <i className="fa-regular fa-paper-plane"></i> + </button> + </div> + )} + </div> + )} </div> - {repliesVisible[c.id] && c.replies && c.replies.length > 0 && ( - <div style={{ marginLeft: "20px" }}> - + {repliesVisible[c.id] && + c.replies && + c.replies.length > 0 && ( + <div style={{ marginLeft: "20px" }}> {c.replies.map((r) => ( <div> <div> - <p key={r.id}> - <strong>{r.username}</strong> ({r.created_at}): {r.reply} - </p> + <p key={r.id}> + <strong>{r.username}</strong> ({r.created_at}):{" "} + {r.reply} + </p> </div> - <div style={{display:"flex", gap:"3px", position:"relative", top:"-10px", marginBottom:"-10px"}}> - <button onClick={() => handleReplyLike(r.id)} style={{ color: replyLiked[r.id] ? "red" : "black" }}> + <div + style={{ + display: "flex", + gap: "3px", + position: "relative", + top: "-10px", + marginBottom: "-10px", + }} + > + <button + onClick={() => handleReplyLike(r.id)} + style={{ + color: replyLiked[r.id] ? "red" : "black", + }} + > <i className="fa-regular fa-thumbs-up"></i> </button> - <div id={`like-count-${r.id}`}>{replyLikeCount[r.id] !== undefined ? replyLikeCount[r.id] : ""}</div> {/* Unique ID for like count */} + <div id={`like-count-${r.id}`}> + {replyLikeCount[r.id] !== undefined + ? replyLikeCount[r.id] + : ""} + </div>{" "} + {/* Unique ID for like count */} </div> </div> - ))} - </div> - )} + ))} + </div> + )} </div> ))} </div> {loggedIn && ( - <div className="comment-input-div"> - <textarea - id="comment-input" - value={comment} - onChange={(e) => setComment(e.target.value)} - placeholder="Write a comment..." - ></textarea> - <button onClick={postComment}> - <i className="fa-solid fa-paper-plane"></i> - </button> - </div> - )} + <div className="comment-input-div"> + <textarea + id="comment-input" + value={comment} + onChange={(e) => setComment(e.target.value)} + placeholder="Write a comment..." + ></textarea> + <button onClick={postComment}> + <i className="fa-solid fa-paper-plane"></i> + </button> + </div> + )} </div> )} @@ -842,4 +904,4 @@ function App() { ); } -export default App; \ No newline at end of file +export default App;