diff --git a/backend/api/router.go b/backend/api/router.go index 1520701..5e0bde5 100644 --- a/backend/api/router.go +++ b/backend/api/router.go @@ -71,6 +71,8 @@ func Router(mux *mux.Router, db *sql.DB) { // Likes & dislikes for comments and posts ... the getPosts and getComments methods return the number of likes and dislikes with each post/comment mux.HandleFunc("/vote", voteHandler.VotePostOrCommentHandler).Methods("POST") + mux.HandleFunc("/vote", voteHandler.GetItemVotesHandler).Methods("GET") + //mux.HandleFunc("/user/vote", voteHandler.GetUserVoteActionHandler).Methods("GET") // Groups groupHandler := handler.NewGroupHandler(groupRepository, sessionRepository, groupMemberRepository, notificationHandler, userRepository, friendsRepository) diff --git a/backend/pkg/db/database.db b/backend/pkg/db/database.db index 744f349..882eabf 100644 Binary files a/backend/pkg/db/database.db and b/backend/pkg/db/database.db differ diff --git a/backend/pkg/handler/voteHandler.go b/backend/pkg/handler/voteHandler.go index 12640fc..3d94b5b 100644 --- a/backend/pkg/handler/voteHandler.go +++ b/backend/pkg/handler/voteHandler.go @@ -6,11 +6,13 @@ import ( "backend/util" "encoding/json" "fmt" + "log" "net/http" + "strconv" ) type VoteHandler struct { - voteRepo *repository.VoteRepository + voteRepo *repository.VoteRepository sessionRepo *repository.SessionRepository } @@ -45,22 +47,24 @@ func (h *VoteHandler) VotePostOrCommentHandler(w http.ResponseWriter, r *http.Re return } - err = h.voteRepo.VoteItem(voteData, userID); if err != nil { + err = h.voteRepo.VoteItem(voteData, userID) + if err != nil { errmsg := fmt.Sprintf("Failed to vote %s: %s", voteData.Item, err.Error()) http.Error(w, errmsg, http.StatusInternalServerError) } - - var likes, dislikes, getVoteError = h.voteRepo.GetItemVotes(voteData.Item, voteData.ItemID); if getVoteError != nil { - http.Error(w, "Failed to fetch updated votes: " + err.Error(), http.StatusInternalServerError) + + var likes, dislikes, getVoteError = h.voteRepo.GetItemVotes(voteData.Item, voteData.ItemID) + if getVoteError != nil { + http.Error(w, "Failed to fetch updated votes: "+err.Error(), http.StatusInternalServerError) return } // Create a response struct for the votes response := struct { - Likes int `json:"likes"` + Likes int `json:"likes"` Dislikes int `json:"dislikes"` }{ - Likes: likes, + Likes: likes, Dislikes: dislikes, } @@ -78,25 +82,90 @@ func (h *VoteHandler) VotePostOrCommentHandler(w http.ResponseWriter, r *http.Re } } +func (h *VoteHandler) GetItemVotesHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Invalid request method", http.StatusMethodNotAllowed) + return + } + + // Extract item type and item ID from request parameters + itemType := r.URL.Query().Get("itemType") + itemIDStr := r.URL.Query().Get("itemID") + itemID, err := strconv.Atoi(itemIDStr) + if err != nil { + http.Error(w, "Invalid item ID", http.StatusBadRequest) + return + } + + // Retrieve the user ID from the session token + userID, err := h.sessionRepo.GetUserIDFromSessionToken(util.GetSessionToken(r)) + if err != nil { + http.Error(w, "User not authenticated", http.StatusUnauthorized) + return + } + + // Call the GetItemVotes function from the repository to get the total likes and dislikes + likes, dislikes, err := h.voteRepo.GetItemVotes(itemType, itemID) + if err != nil { + http.Error(w, "Failed to fetch item votes: "+err.Error(), http.StatusInternalServerError) + return + } + + // Get the user's vote action + userAction, err := h.voteRepo.GetUserVoteAction(userID, itemType, itemID) + if err != nil { + http.Error(w, "Failed to fetch user vote action", http.StatusInternalServerError) + return + } + + // Construct a response containing the retrieved vote information and user action + response := struct { + Likes int `json:"likes"` + Dislikes int `json:"dislikes"` + UserAction string `json:"userAction"` + }{ + Likes: likes, + Dislikes: dislikes, + UserAction: userAction, + } + + // Serialize the response to JSON + responseData, err := json.Marshal(response) + if err != nil { + http.Error(w, "Failed to marshal response data", http.StatusInternalServerError) + return + } + + // Set the content type header and write the response + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, err = w.Write(responseData) + if err != nil { + // Handle error writing response here + http.Error(w, "Failed to write response data", http.StatusInternalServerError) + return + } +} + func (h *VoteHandler) AppendVotesToPostsResponse(posts []model.Post) ([]model.PostsResponse, error) { postsResponse := make([]model.PostsResponse, len(posts)) for i, post := range posts { likes, dislikes, err := h.voteRepo.GetItemVotes("post", post.Id) if err != nil { - return nil, err + log.Fatal(err) } postsResponse[i] = model.PostsResponse{ - Id: post.Id, - UserID: post.UserID, - GroupID: post.GroupID, - Title: post.Title, - Content: post.Content, - ImageURL: post.ImageURL, - PrivacySetting: post.PrivacySetting, - CreatedAt: post.CreatedAt, - Likes: likes, - Dislikes: dislikes, - } + Id: post.Id, + UserID: post.UserID, + GroupID: post.GroupID, + Title: post.Title, + Content: post.Content, + ImageURL: post.ImageURL, + PrivacySetting: post.PrivacySetting, + CreatedAt: post.CreatedAt, + Likes: likes, + Dislikes: dislikes, + } } return postsResponse, nil } @@ -106,7 +175,7 @@ func (h *VoteHandler) AppendVotesToCommentsResponse(comments []model.Comment) ([ for i, comment := range comments { likes, dislikes, err := h.voteRepo.GetItemVotes("comment", comment.Id) if err != nil { - return nil, err + log.Fatal(err) } commentsResponse[i] = model.CommentsResponse{ Id: comment.Id, @@ -120,4 +189,4 @@ func (h *VoteHandler) AppendVotesToCommentsResponse(comments []model.Comment) ([ } } return commentsResponse, nil -} \ No newline at end of file +} diff --git a/backend/pkg/model/structs.go b/backend/pkg/model/structs.go index 98b2282..6acbc8e 100644 --- a/backend/pkg/model/structs.go +++ b/backend/pkg/model/structs.go @@ -261,4 +261,5 @@ type VoteData struct { Item string `json:"item"` // 'comment' or 'post' ItemID int `json:"item_id"` // comment or post id Action string `json:"action"` // 'like' or 'dislike' + UserId int `json:"user_id"` } diff --git a/backend/pkg/repository/voteRepository.go b/backend/pkg/repository/voteRepository.go index df3a01a..686240b 100644 --- a/backend/pkg/repository/voteRepository.go +++ b/backend/pkg/repository/voteRepository.go @@ -4,6 +4,7 @@ import ( "backend/pkg/model" "database/sql" "fmt" + "log" // Import the log package for logging ) type VoteRepository struct { @@ -38,16 +39,45 @@ func (r *VoteRepository) VoteItem(vote model.VoteData, userID int) error { return err } -func (r *VoteRepository) GetItemVotes(itemType string, itemID int) (int, int, error){ +func (r *VoteRepository) GetItemVotes(itemType string, itemID int) (int, int, error) { var likes, dislikes int query := fmt.Sprintf("SELECT COUNT(*) FROM votes WHERE %sID = ? AND type = ?", itemType) - err := r.db.QueryRow(query, itemID, "upvote").Scan(&likes) + err := r.db.QueryRow(query, itemID, "like").Scan(&likes) if err != nil { + log.Printf("Error retrieving upvotes for %s ID %d: %v\n", itemType, itemID, err) return 0, 0, err } - err = r.db.QueryRow(query, itemID, "downvote").Scan(&dislikes) + err = r.db.QueryRow(query, itemID, "dislike").Scan(&dislikes) if err != nil { + log.Printf("Error retrieving downvotes for %s ID %d: %v\n", itemType, itemID, err) return 0, 0, err } return likes, dislikes, nil -} \ No newline at end of file +} + +func (r *VoteRepository) GetUserVoteAction(userID int, itemType string, itemID int) (string, error) { + var action string + query := fmt.Sprintf("SELECT type FROM votes WHERE %sID = ? AND userID = ?", itemType) + err := r.db.QueryRow(query, itemID, userID).Scan(&action) + if err != nil { + if err == sql.ErrNoRows { + // If no vote action found for the user and item, return an empty string + return "", nil + } + log.Printf("Error retrieving user vote action for %s ID %d: %v\n", itemType, itemID, err) + return "", err + } + return action, nil +} + +/* func (r *VoteRepository) HasUserVoted(itemType string, itemID, userID int) bool { + // Check if the user has voted for the given item + var voteCount int + query := fmt.Sprintf("SELECT COUNT(*) FROM votes WHERE %sID = ? AND userID = ?", itemType) + err := r.db.QueryRow(query, itemID, userID).Scan(&voteCount) + if err != nil { + log.Printf("Error checking user vote for %s ID %d: %v\n", itemType, itemID, err) + return false // Assume no vote has been cast in case of an error + } + return voteCount > 0 // Return true if the user has voted (voteCount > 0), false otherwise +} */ diff --git a/frontend/src/components/comments/Comment.tsx b/frontend/src/components/comments/Comment.tsx index f27b7eb..b6b13e4 100644 --- a/frontend/src/components/comments/Comment.tsx +++ b/frontend/src/components/comments/Comment.tsx @@ -1,5 +1,5 @@ -import { formatDate } from '@/hooks/utils' -import { useState } from 'react'; +import { useEffect, useState } from 'react'; +import { BiSolidLike, BiSolidDislike } from 'react-icons/bi'; export interface CommentProps { id: number; @@ -14,27 +14,132 @@ export interface CommentProps { profile_image?: string; } -const Comment: React.FC = ({ - id, - postId, - userId, - content, - image, - created_at, - likes, - dislikes, - username, - profile_image -}) => { - const formattedCreatedAt = formatDate(created_at); - const [isModalOpen, setIsModalOpen] = useState(false); + +const Comment: React.FC = ({ id, postId, userId, content, image, created_at, likes, dislikes, username, profile_image }) => { + const BE_PORT = process.env.NEXT_PUBLIC_BACKEND_PORT; + const FE_URL = process.env.NEXT_PUBLIC_FRONTEND_URL; + const formattedCreatedAt = created_at; + const [likeCount, setLikeCount] = useState(likes); + const [dislikeCount, setDislikeCount] = useState(dislikes); + const [liked, setLiked] = useState(false); + const [disliked, setDisliked] = useState(false); + + useEffect(() => { + const fetchData = async () => { + try { + // Fetch initial counts + const response = await fetch(`${FE_URL}:${BE_PORT}/vote`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include' + }); + if (!response.ok) { + throw new Error('Failed to fetch initial counts'); + } + const { likes: initialLikes, dislikes: initialDislikes } = await response.json(); + setLikeCount(initialLikes); + setDislikeCount(initialDislikes); + + // Fetch user's vote action + const userVoteResponse = await fetch(`${FE_URL}:${BE_PORT}/vote?id=${id}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include' + }); + if (!userVoteResponse.ok) { + throw new Error('Failed to fetch user vote'); + } + const { action } = await userVoteResponse.json(); + if (action === 'like') { + setLiked(true); + } else if (action === 'dislike') { + setDisliked(true); + } + } catch (error) { + console.error('Error fetching data:', error); + } + }; + + fetchData(); + }, [id]); + + + const handleLike = async () => { + if (!liked) { + try { + const response = await fetch(`${FE_URL}:${BE_PORT}/vote`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + item: 'comment', + item_id: id, + action: 'like', + }), + credentials: 'include' + }); + if (!response.ok) { + throw new Error('Failed to vote'); + } + setLikeCount(likeCount + 1); // Update like count after successful vote + setLiked(true); + if (disliked) { + setDisliked(false); + setDislikeCount(dislikeCount - 1); + } + } catch (error) { + console.error('Error voting:', error); + } + } else { + setLikeCount(likeCount - 1); + setLiked(false); + } + }; + + const handleDislike = async () => { + if (!disliked) { + try { + const response = await fetch(`${FE_URL}:${BE_PORT}/vote`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + item: 'comment', + item_id: id, + action: 'dislike', + }), + credentials: 'include' + }); + if (!response.ok) { + throw new Error('Failed to vote'); + } + setDislikeCount(dislikeCount + 1); // Update dislike count after successful vote + setDisliked(true); + if (liked) { + setLiked(false); + setLikeCount(likeCount - 1); + } + } catch (error) { + console.error('Error voting:', error); + } + } else { + setDislikeCount(dislikeCount - 1); + setDisliked(false); + } + }; + + return (
- {/* Comments inside the CommentsBox.tsx collapsing box*/}
- {/* TODO: Link Profile picture to comment */} - Tailwind CSS chat bubble component + Profile
@@ -43,18 +148,20 @@ const Comment: React.FC = ({
{content} -
- {image && setIsModalOpen(true)} />} + {image && } +
+ +
- {/* TODO: comment uploaded images */} - {isModalOpen && ( -
setIsModalOpen(false)}> - e.stopPropagation()} /> -
- )}
); } -export default Comment; \ No newline at end of file +export default Comment; diff --git a/frontend/src/components/comments/CreateComment.tsx b/frontend/src/components/comments/CreateComment.tsx index dabcd29..5edb6c7 100644 --- a/frontend/src/components/comments/CreateComment.tsx +++ b/frontend/src/components/comments/CreateComment.tsx @@ -1,5 +1,6 @@ import { useState } from "react"; import { CommentProps } from "./Comment"; +import { FaHeart, FaHeartBroken } from "react-icons/fa"; interface CreateCommentProps { postId: number; @@ -12,6 +13,10 @@ const CreateComment: React.FC = ({ postId, setComments }) => const [selectedFile, setSelectedFile] = useState(null); const [commentContent, setCommentContent] = useState(''); + const [likeCount, setLikeCount] = useState(0); + const [dislikeCount, setDislikeCount] = useState(0); + const [liked, setLiked] = useState(false); + const [disliked, setDisliked] = useState(false); const handleFileChange = (event: React.ChangeEvent) => { if (event.target.files && event.target.files.length > 0) { @@ -23,6 +28,34 @@ const CreateComment: React.FC = ({ postId, setComments }) => setCommentContent(event.target.value); } + const handleLike = () => { + if (!liked) { + setLikeCount(likeCount + 1); + setLiked(true); + if (disliked) { + setDislikeCount(dislikeCount - 1); + setDisliked(false); + } + } else { + setLikeCount(likeCount - 1); + setLiked(false); + } + } + + const handleDislike = () => { + if (!disliked) { + setDislikeCount(dislikeCount + 1); + setDisliked(true); + if (liked) { + setLikeCount(likeCount - 1); + setLiked(false); + } + } else { + setDislikeCount(dislikeCount - 1); + setDisliked(false); + } + } + const handleSubmit = async (event: React.FormEvent) => { // Create a new comment event.preventDefault(); @@ -125,12 +158,19 @@ const CreateComment: React.FC = ({ postId, setComments }) => Send message
- {/* Like Button */} - + {/* Like and Dislike Buttons */} +
+ + +
); } -export default CreateComment; \ No newline at end of file +export default CreateComment; diff --git a/frontend/src/components/postcreation/Post.tsx b/frontend/src/components/postcreation/Post.tsx index fba83ae..273a12c 100644 --- a/frontend/src/components/postcreation/Post.tsx +++ b/frontend/src/components/postcreation/Post.tsx @@ -23,6 +23,7 @@ export interface PostProps { } const Post: React.FC = ({ id, userId, groupId, title, content, image_url, privacySetting, created_at, likes, dislikes, creator, creator_avatar, comments, setComments }) => { + return (
{/* Post Content */}