Skip to content

Commit

Permalink
Added pagination, replies, and styling improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
el-agua committed Nov 28, 2024
1 parent eea0a63 commit d21825b
Show file tree
Hide file tree
Showing 6 changed files with 613 additions and 232 deletions.
68 changes: 59 additions & 9 deletions backend/review/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
make_subdict,
)

import math


"""
You might be wondering why these API routes are using the @api_view function decorator
Expand Down Expand Up @@ -880,16 +882,34 @@ def get(self, request, semester, course_code):
if (instructor == "null"):
instructor = "all"
sort_by = request.query_params.get("sort_by") or "newest"

page = request.query_params.get("page") or 0
page_size = request.query_params.get("page_size") or 20
page = int(page)

own_comments = bool(request.query_params.get("self")) or False
page_size = request.query_params.get("page_size") or 5

queryset = self.get_queryset()
queryset = self.get_queryset().filter(parent__isnull=True) # Only consider base comments

if instructor != "all":
queryset = queryset.all().filter(instructor=instructor)

semesters = list(
map(
lambda x: x["section__course__semester"],
list(queryset.values("section__course__semester").distinct()),
)
)

# add filters
if semester_arg != "all":
queryset = queryset.all().filter(section__course__semester=semester_arg)
if instructor != "all":
queryset = queryset.all().filter(instructor=instructor)

num_pages = math.ceil(queryset.count() / page_size)

if own_comments:
queryset = queryset.all().filter(author=request.user)

# apply ordering
if sort_by == "top":
"""
Expand All @@ -907,17 +927,42 @@ def get(self, request, semester, course_code):
elif sort_by == "newest":
queryset = queryset.all().order_by("-base_id", "path")

print(queryset)
# apply pagination (not sure how django handles OOB errors)
if queryset:
if page * page_size >= queryset.count():
return Response(
{"message": "Page out of bounds."}, status=status.HTTP_400_BAD_REQUEST)
queryset = queryset.all()[page * page_size: max((page + 1) * page_size, queryset.count())]

replies = Comment.objects.none()
for comment in queryset:
replies |= Comment.objects.filter(base=comment)

queryset = queryset | replies

if queryset:
user_upvotes = queryset.filter(upvotes=request.user, id=OuterRef("id"))
user_downvotes = queryset.filter(downvotes=request.user, id=OuterRef("id"))
queryset = queryset.annotate(
user_upvoted=Exists(user_upvotes), user_downvoted=Exists(user_downvotes)
)
queryset = queryset.all()[page * page_size: (page + 1) * page_size]

response_body = {"comments": CommentListSerializer(queryset, many=True).data}
if sort_by == "top":
"""
probably not right as is at the moment –
likes are marked on a per comment basis not group basis
"""
queryset = queryset.annotate(
base_votes=Count("base__upvotes") - Count("base__downvotes")
).order_by("-base_votes", "base_id", "path")
queryset = queryset.annotate(
semester=F("section__course__semester"),
)
elif sort_by == "oldest":
queryset = queryset.all().order_by("path")
elif sort_by == "newest":
queryset = queryset.all().order_by("-base_id", "path")
response_body = {"comments": CommentListSerializer(queryset, many=True).data, "semesters": semesters, "num_pages": num_pages}

return Response(response_body, status=status.HTTP_200_OK)

Expand Down Expand Up @@ -1032,7 +1077,6 @@ def create(self, request):
print(section)
except Exception as e:
print(e)
print("hi")
return Response({"message": "Section not found."}, status=status.HTTP_404_NOT_FOUND)
if instructor:
instructor = get_object_or_404(Instructor, id=int(instructor[0]))
Expand All @@ -1043,9 +1087,15 @@ def create(self, request):
parent_id = request.data.get("parent")
print("new section", section)
print("new comment section", section.course.topic)
print("parent_id", parent_id)
parent = get_object_or_404(Comment, pk=parent_id) if parent_id is not None else None
comment = Comment.objects.create(
text=request.data.get("text"), author=request.user, section=section, parent=parent, instructor=instructor)
text=request.data.get("text"),
author=request.user,
section=section,
parent=parent,
instructor=instructor
)
base = parent.base if parent else comment
prefix = parent.path + "." if parent else ""
path = prefix + "{:0{}d}".format(comment.id, 10)
Expand Down
198 changes: 146 additions & 52 deletions frontend/review/src/components/Comments/Comment.js
Original file line number Diff line number Diff line change
@@ -1,65 +1,159 @@
import React, { forwardRef, useState, useEffect } from "react";
import { formatDate, truncateText } from "../../utils/helpers";
import { apiReplies } from "../../utils/api";
import { faThumbsUp, faThumbsDown } from "@fortawesome/free-solid-svg-icons"
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faThumbsUp, faThumbsDown, faComment } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";

export const Comment = forwardRef(({ comment, isReply, isUserComment }, ref) => {
const [showReplies, setShowReplies] = useState(false);
const [replies, setReplies] = useState([]);
const [seeMore, setSeeMore] = useState(true);
export const Comment = forwardRef(
({ comment, isUserComment, setReply }, ref) => {
const [showReplies, setShowReplies] = useState(false);
const [replies, setReplies] = useState([]);
const [seeMore, setSeeMore] = useState(true);

const [liked, setLiked] = useState(false);
const [disliked, setDisliked] = useState(false);
const [liked, setLiked] = useState(false);
const [disliked, setDisliked] = useState(false);

useEffect(() => {
if (showReplies && replies.length === 0 && comment.replies > 0) {
apiReplies(comment.id).then(res => {
setReplies(res.replies);
});
}
}, [comment.id, comment.replies, replies.length, showReplies]);
const indentation = comment.path.split(".").length; // Number of replies deep

return (
<div className={`comment ${isReply ? "reply" : ""} ${isUserComment ? "user" : ""}`} ref={ref}>
<div className="top">
<b>{isUserComment ? "You" : comment.author_name}</b>
<sub>{formatDate(comment.modified_at)}</sub>
</div>
{seeMore ? (
<p>{comment.text}</p>
) : (
<>
<p>{comment.text ? truncateText(comment.text, 150) : ""}</p>
useEffect(() => {
if (showReplies && replies.length === 0 && comment.replies > 0) {
apiReplies(comment.id).then((res) => {
setReplies(res.replies);
});
}
}, [comment.id, comment.replies, replies.length, showReplies]);

return (
<div
style={{
paddingLeft: indentation === 1 ? "0" : `${2 * (indentation - 1)}vw`,
position: "relative",
}}
>
{indentation > 1 && (
<div
style={{
position: "absolute",
left: `${2 * (indentation - 1)}vw`,
top: 0,
bottom: 0,
width: "2px",
backgroundColor: "#ccc",
borderRadius: "1px",
}}
></div>
)}
<div
className={`comment ${indentation !== 1 ? "reply" : ""} ${
isUserComment ? "user" : ""
}`}
ref={ref}
>
<div className="top">
<b>{isUserComment ? "You" : comment.author_name}</b>
<sub>{formatDate(comment.modified_at)}</sub>
</div>
{seeMore ? (
<p style={{ marginLeft: "10px" }}>{comment.text}</p>
) : (
<>
<p style={{ marginLeft: "10px" }}>
{comment.text ? truncateText(comment.text, 150) : ""}
</p>
<button
className="btn-borderless btn"
onClick={() => setSeeMore(true)}
>
See More
</button>
</>
)}
<div
className="icon-wrapper"
style={{
borderRadius: "50px",
padding: "2px",
display: "inline-block",
transition: "background-color 0.3s",
marginTop: "10px",
}}
>
<button
className=" btn-borderless btn"
onClick={() => setSeeMore(true)}
className={`btn icon ${liked ? "active" : ""}`}
onClick={() => {
setLiked(!liked);
disliked && setDisliked(false);
}}
style={{ fontSize: "0.9em" }}
>
See More
<FontAwesomeIcon icon={faThumbsUp} />
</button>
</>
)}
<div className="icon-wrapper">
<button className={`btn icon ${liked ? "active" : ""}`} onClick={() => {setLiked(!liked); disliked && setDisliked(false)}}><FontAwesomeIcon icon={faThumbsUp} /></button>
<span>{comment.votes + liked - disliked}</span>
<button className={`btn icon ${disliked ? "active" : ""}`} onClick={() => {setDisliked(!disliked); liked && setLiked(false)}}><FontAwesomeIcon icon={faThumbsDown} /></button>
</div>
{comment.replies > 0 && (
<>
<span style={{ fontSize: "0.9em" }}>{comment.votes + liked - disliked}</span>
<button
className="btn-borderless btn"
onClick={() => setShowReplies(!showReplies)}
className={`btn icon ${disliked ? "active" : ""}`}
onClick={() => {
setDisliked(!disliked);
liked && setLiked(false);
}}
style={{ fontSize: "0.9em" }}
>
{showReplies ? "Hide" : "Show"} Replies
<FontAwesomeIcon icon={faThumbsDown} />
</button>
<div style={{ paddingLeft: isReply ? "0" : "2vw" }}>
{showReplies &&
replies.map(reply => (
<Comment key={reply.id} comment={reply} isReply />
))}
</div>
</>
)}
</div>
);
});
</div>
<div
className="icon-wrapper"
style={{
borderRadius: "50px",
padding: "2px",
display: "inline-block",
transition: "background-color 0.3s",
marginTop: "10px",
}}
>
{!isUserComment && (
<button
className="btn icon"
onClick={() => setReply(comment)}
style={{ fontSize: "0.9em" }}
>
<FontAwesomeIcon icon={faComment} /> Reply
</button>
)}
</div>
{comment.replies > 0 && (
<>
<button
className="btn-borderless btn"
onClick={() => setShowReplies(!showReplies)}
>
{showReplies ? "Hide" : "Show"} Replies
</button>
<div style={{ paddingLeft: indentation !== 1 ? "0" : "2vw" }}>
{showReplies &&
replies.map((reply) => (
<Comment key={reply.id} comment={reply} />
))}
</div>
</>
)}
</div>
</div>
);
}
);

// Add CSS for hover effect
const styles = `
.icon-wrapper {
background-color: transparent;
}
.icon-wrapper:hover {
background-color: #f0f0f0;
}
`;

// Inject styles into the document
const styleSheet = document.createElement("style");
styleSheet.type = "text/css";
styleSheet.innerText = styles;
document.head.appendChild(styleSheet);
Loading

0 comments on commit d21825b

Please sign in to comment.