diff --git a/app/frontend/src/Components/Forum/Forum.module.scss b/app/frontend/src/Components/Forum/Forum.module.scss new file mode 100644 index 00000000..ca16e0e1 --- /dev/null +++ b/app/frontend/src/Components/Forum/Forum.module.scss @@ -0,0 +1,21 @@ +@import "../../colors"; + +.container { + height: 100%; + width: 100%; + overflow: auto; + padding: 1em; + + .top { + display: flex; + justify-content: end; + } + + .posts { + padding: 1em; + display: flex; + flex-direction: column; + align-items: stretch; + gap: 1em; + } +} diff --git a/app/frontend/src/Components/Forum/Forum.tsx b/app/frontend/src/Components/Forum/Forum.tsx new file mode 100644 index 00000000..ce1a5937 --- /dev/null +++ b/app/frontend/src/Components/Forum/Forum.tsx @@ -0,0 +1,44 @@ +import { useNavigate, useParams } from "react-router-dom"; +import styles from "./Forum.module.scss"; +import { useQuery } from "react-query"; +import { getPostList } from "../../Services/forum"; +import { Button } from "antd"; +import { PlusCircleOutlined } from "@ant-design/icons"; +import { useAuth } from "../Hooks/useAuth"; +import ForumPost from "./ForumPost/ForumPost"; + +function Forum({ + forumId, + redirect = "/", +}: { + forumId: string; + redirect?: string; +}) { + const { isLoggedIn } = useAuth(); + const { data: posts, isLoading } = useQuery(["forum", forumId], () => + getPostList({ forum: forumId }) + ); + const navigate = useNavigate(); + return ( +
+
+ {isLoggedIn && ( + + )} +
+
+ {posts?.map((post: any) => ( + + ))} +
+
+ ); +} + +export default Forum; diff --git a/app/frontend/src/Components/Forum/ForumPost/ForumPost.module.scss b/app/frontend/src/Components/Forum/ForumPost/ForumPost.module.scss new file mode 100644 index 00000000..d4296e63 --- /dev/null +++ b/app/frontend/src/Components/Forum/ForumPost/ForumPost.module.scss @@ -0,0 +1,50 @@ +@import "../../../colors"; + +.container { + position: relative; + padding: 1em; + background-color: $celadon; + border-radius: 0.5em; + display: grid; + gap: 0.5em; + grid-template-areas: + "v t" + "v m"; + grid-template-rows: 50px 25px; + grid-template-columns: 25px 1fr; + .vote { + grid-area: v; + grid-template-rows: repeat(3, 1fr); + justify-items: center; + align-items: center; + display: grid; + color: $orange; + + button { + all: unset; + cursor: pointer; + color: $orange-dark-40; + + .active { + color: $orange; + } + } + } + .title { + font-weight: bold; + font-size: 1.2em; + grid-area: t; + } + .meta { + display: flex; + gap: 1em; + font-size: 0.8em; + opacity: 80%; + grid-area: m; + } + .readMore { + position: absolute; + bottom: 0.5em; + right: 0.5em; + } +} diff --git a/app/frontend/src/Components/Forum/ForumPost/ForumPost.tsx b/app/frontend/src/Components/Forum/ForumPost/ForumPost.tsx new file mode 100644 index 00000000..ef96849d --- /dev/null +++ b/app/frontend/src/Components/Forum/ForumPost/ForumPost.tsx @@ -0,0 +1,36 @@ +import { Button } from "antd"; +import { formatDate } from "../../../Library/utils/formatDate"; +import styles from "./ForumPost.module.scss"; +import { ArrowDownOutlined, ArrowUpOutlined } from "@ant-design/icons"; +import { useVote } from "../../Hooks/useVote"; + +function ForumPost({ post, forumId }: { post: any; forumId: string }) { + const { upvote, downvote } = useVote({ + voteType: "POST", + typeId: post.id, + invalidateKey: ["forum", forumId], + }); + return ( +
+
+ +
{post.overallVote}
+ +
+
{post.title}
+
+ {post.poster.username} + {post.createdAt && formatDate(post.createdAt)} +
+
+ +
+
+ ); +} + +export default ForumPost; diff --git a/app/frontend/src/Components/GameDetails/Review/Review.module.scss b/app/frontend/src/Components/GameDetails/Review/Review.module.scss new file mode 100644 index 00000000..e7830612 --- /dev/null +++ b/app/frontend/src/Components/GameDetails/Review/Review.module.scss @@ -0,0 +1,114 @@ +@import "../../../colors"; + + +.review-input-container { + display: flex; + flex-direction: column; + width: 350px; + color: $color-text-light ; + background-color: $blue-green; + border-radius: 0.5em; + margin: 5px 5px 5px 5px; + overflow: hidden; + padding: 5px; + gap: 5px; + justify-content: center; + + .header { + display: flex; + flex-direction: column; + gap: 7px; + margin: 5px 0px 0px 10px; + font-size: 18px; + } + + .content-input { + background-color: $prussian-blue-dark-10 !important; + flex: 4; + width: 100%; + border-radius: 0.5em; + padding: 5px; + font-size: 13px; + } + + .button { + color: $celadon; + } +} + +.reviews-subpage-container { + display: flex; + flex-wrap: wrap; + padding: 10px; +} +.review-container { + display: flex; + flex-shrink: 0; + width: min-content; + min-width: 350px; + background-color: $prussian-blue-dark-40; + border-radius: 0.5em; + margin: 5px 5px 5px 5px; + overflow: hidden; + + .vote { + display: flex; + flex-direction: column; + background-color: $celadon-light-10; + padding: 10px; + align-items: center; + gap: 5px; + color: $color-text; + width: 60px; + justify-content: center; + } + + .review { + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 10px; + color: $color-text-light; + gap: 5px; + justify-content: space-between; + flex: 1; + .header { + display: flex; + justify-content: flex-start; + align-items: center; + gap: 100px; + .user { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .buttons { + display: flex; + justify-self: flex-end; + } + } + .date { + font-size: 10px; + align-self: flex-start; + } + .stars { + display: flex; + justify-content: center; + align-items: center; + gap: 2px; + color: $yellow; + align-self: flex-end; + } + + .content { + background-color: $prussian-blue-dark-10 !important; + flex: 4; + width: 100%; + border-radius: 0.5em; + padding: 5px; + font-size: 13px; + } + } + +} \ No newline at end of file diff --git a/app/frontend/src/Components/GameDetails/Review/Review.tsx b/app/frontend/src/Components/GameDetails/Review/Review.tsx new file mode 100644 index 00000000..4f451076 --- /dev/null +++ b/app/frontend/src/Components/GameDetails/Review/Review.tsx @@ -0,0 +1,161 @@ +import { Button, Rate } from "antd"; +import styles from "./Review.module.scss"; +import { + CheckOutlined, + DeleteOutlined, + DownOutlined, + EditOutlined, + StarFilled, + UpOutlined, +} from "@ant-design/icons"; +import TextArea from "antd/es/input/TextArea"; +import { useState } from "react"; +import { formatDate } from "../../../Library/utils/formatDate"; +import { useAuth } from "../../Hooks/useAuth"; +import { useMutation, useQueryClient } from "react-query"; +import { deleteReview, updateReview } from "../../../Services/review"; +import { useVote } from "../../Hooks/useVote"; + +function Review({ review }: { review: any }) { + const [inputMode, setInputMode] = useState(false); + const [reviewContent, setReviewContent] = useState(review.reviewDescription); + const [rating, setRating] = useState(review.rating); + + const { user } = useAuth(); + const queryClient = useQueryClient(); + + const { upvote, downvote } = useVote({ + voteType: "REVIEW", + typeId: review.id, + invalidateKey: ["reviews", review.gameId, ""], + }); + + const { mutate: removeReview } = useMutation( + (id: string) => deleteReview(id), + { + onSuccess() { + queryClient.invalidateQueries(["reviews", review.gameId]); + }, + onMutate(id: string) { + queryClient.setQueriesData(["reviews", review.gameId], (prev: any) => { + return prev?.filter((review: any) => id !== review.id); + }); + }, + } + ); + + const { mutate: editReview } = useMutation( + ({ id, updatedReview }: { id: string; updatedReview: any }) => + updateReview(id, updatedReview), + { + onSuccess() { + queryClient.invalidateQueries(["reviews", review.gameId]); + setInputMode(false); + }, + onMutate({ id, updatedReview }) { + queryClient.setQueriesData(["reviews", review.gameId], (prev: any) => + prev?.map((review: any) => + review.id === id ? { ...review, ...updatedReview } : review + ) + ); + }, + } + ); + + return ( +
+
+
+
+
+
+ {review.reviewedUser} +
+ {user.username === review.reviewedUser && ( +
+ {!inputMode ? ( +
+ )} +
+
{formatDate(review.createdAt)}
+
+ {inputMode ? ( + + ) : ( + <> + {[1, 2, 3, 4, 5].map((starValue) => + starValue <= review.rating ? ( + + ) : null + )} + {Math.round(review.rating) !== review.rating && ½} + + )} +
+ {inputMode ? ( +