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 (
+
+
+ }
+ onClick={upvote}
+ />
+ {review.overallVote}
+ }
+ onClick={downvote}
+ />
+
+
+
+
+ {review.reviewedUser}
+
+ {user.username === review.reviewedUser && (
+
+ {!inputMode ? (
+ }
+ onClick={() => setInputMode(true)}
+ />
+ ) : (
+ }
+ onClick={() =>
+ editReview({
+ id: review.id,
+ updatedReview: {
+ rating: rating,
+ reviewDescription: reviewContent,
+ },
+ })
+ }
+ />
+ )}
+ }
+ onClick={() => removeReview(review.id)}
+ />
+
+ )}
+
+
{formatDate(review.createdAt)}
+
+ {inputMode ? (
+
+ ) : (
+ <>
+ {[1, 2, 3, 4, 5].map((starValue) =>
+ starValue <= review.rating ? (
+
+ ) : null
+ )}
+ {Math.round(review.rating) !== review.rating && ½}
+ >
+ )}
+
+ {inputMode ? (
+
+
+ );
+}
+
+export default Review;
diff --git a/app/frontend/src/Components/GameDetails/Review/ReviewInput.tsx b/app/frontend/src/Components/GameDetails/Review/ReviewInput.tsx
new file mode 100644
index 00000000..234cd818
--- /dev/null
+++ b/app/frontend/src/Components/GameDetails/Review/ReviewInput.tsx
@@ -0,0 +1,72 @@
+import { useState } from "react";
+import { Button, Input, Rate } from "antd";
+import styles from "./Review.module.scss";
+import { CheckOutlined } from "@ant-design/icons";
+import { useMutation, useQueryClient } from "react-query";
+import { postReview } from "../../../Services/review";
+const { TextArea } = Input;
+
+function ReviewInput({ gameId }: { gameId: string }) {
+ const [content, setContent] = useState("");
+ const [rating, setRating] = useState(0);
+
+ const queryClient = useQueryClient();
+
+ const { mutate: addReview, isLoading } = useMutation(
+ (review: any) => postReview(review),
+ {
+ onSuccess() {
+ queryClient.invalidateQueries(["reviews", gameId, ""]);
+ },
+ onMutate(review: any) {
+ queryClient.setQueryData(["reviews", gameId, ""], (prev: any) => [
+ review,
+ ...prev,
+ ]);
+ },
+ }
+ );
+
+ const handleClick = () => {
+ const review = {
+ reviewDescription: content,
+ rating: rating,
+ gameId: gameId,
+ };
+ addReview(review);
+ };
+
+ return (
+
+
+
+
} onClick={() => handleClick()}>
+
+
+
+ );
+}
+
+export default ReviewInput;
diff --git a/app/frontend/src/Components/GameDetails/Review/Reviews.tsx b/app/frontend/src/Components/GameDetails/Review/Reviews.tsx
new file mode 100644
index 00000000..4ccf51ed
--- /dev/null
+++ b/app/frontend/src/Components/GameDetails/Review/Reviews.tsx
@@ -0,0 +1,47 @@
+import { useQuery } from "react-query";
+import Review from "./Review";
+import styles from "./Review.module.scss";
+import ReviewInput from "./ReviewInput";
+import { getAllReviews } from "../../../Services/review";
+import { useState } from "react";
+import { Input } from "antd";
+
+function Reviews({ gameId }: { gameId: string }) {
+ const [reviewedBy, setReviewedBy] = useState("");
+ const [searchText, setSearchText] = useState("");
+
+ const { Search } = Input;
+
+ const { data: reviews, isLoading } = useQuery(
+ ["reviews", gameId, reviewedBy],
+ () =>
+ reviewedBy === ""
+ ? getAllReviews(gameId).then((res) => res.data)
+ : getAllReviews(gameId, reviewedBy).then((res) => res.data)
+ );
+
+ return (
+ <>
+
+
+
+
+
+ {reviews &&
+ reviews
+ ?.filter((review: any) => {
+ return `${review.reviewDescription}${review.reviewedUser}`.includes(
+ searchText
+ );
+ })
+ .map((review: any) =>
)}
+
+ >
+ );
+}
+export default Reviews;
diff --git a/app/frontend/src/Components/Hooks/useVote.tsx b/app/frontend/src/Components/Hooks/useVote.tsx
new file mode 100644
index 00000000..c7f39e96
--- /dev/null
+++ b/app/frontend/src/Components/Hooks/useVote.tsx
@@ -0,0 +1,29 @@
+import { QueryKey, useMutation, useQueryClient } from "react-query";
+import { createVote } from "../../Services/vote";
+
+export function useVote({
+ voteType,
+ typeId,
+ invalidateKey,
+}: {
+ voteType: "POST" | "REVIEW";
+ typeId: string;
+ invalidateKey: QueryKey;
+}) {
+ const queryClient = useQueryClient();
+ const { mutate: vote, isLoading } = useMutation(
+ (choice: "UPVOTE" | "DOWNVOTE") => createVote({ voteType, typeId, choice }),
+ {
+ onSuccess() {
+ queryClient.invalidateQueries(invalidateKey);
+ },
+ }
+ );
+
+ return {
+ vote,
+ upvote: () => vote("UPVOTE"),
+ downvote: () => vote("DOWNVOTE"),
+ isLoading,
+ };
+}
diff --git a/app/frontend/src/Components/Providers/AntdConfigProvider.tsx b/app/frontend/src/Components/Providers/AntdConfigProvider.tsx
index 382b0e5e..74c9195b 100644
--- a/app/frontend/src/Components/Providers/AntdConfigProvider.tsx
+++ b/app/frontend/src/Components/Providers/AntdConfigProvider.tsx
@@ -13,7 +13,6 @@ function AntdConfigProvider({ children }: { children: ReactNode }) {
colorBgBase: getThemeColor("color-background"),
colorTextBase: getThemeColor("color-text"),
colorPrimary: getThemeColor("color-primary"),
- colorBgContainer: getThemeColor("color-container"),
},
};
return {children};
diff --git a/app/frontend/src/Library/utils/formatDate.ts b/app/frontend/src/Library/utils/formatDate.ts
new file mode 100644
index 00000000..07e22af2
--- /dev/null
+++ b/app/frontend/src/Library/utils/formatDate.ts
@@ -0,0 +1,8 @@
+
+export function formatDate(date: Date) {
+ return new Date(date).toLocaleDateString('en-GB', {
+ day: '2-digit',
+ month: '2-digit',
+ year: 'numeric'
+ });
+}
\ No newline at end of file
diff --git a/app/frontend/src/Pages/ForumPost/ForumPost.module.scss b/app/frontend/src/Pages/ForumPost/ForumPost.module.scss
new file mode 100644
index 00000000..ed819397
--- /dev/null
+++ b/app/frontend/src/Pages/ForumPost/ForumPost.module.scss
@@ -0,0 +1,20 @@
+@import "../../colors";
+
+.container {
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ padding: 2.5em;
+
+ .postContainer {
+ display: grid;
+ grid-template-rows: 50px 1fr;
+ border-radius: 0.5em;
+ padding: 1em;
+ background-color: $celadon;
+ .title {
+ font-size: 1.5em;
+ font-weight: bold;
+ }
+ }
+}
diff --git a/app/frontend/src/Pages/ForumPost/ForumPost.tsx b/app/frontend/src/Pages/ForumPost/ForumPost.tsx
new file mode 100644
index 00000000..9e69004a
--- /dev/null
+++ b/app/frontend/src/Pages/ForumPost/ForumPost.tsx
@@ -0,0 +1,23 @@
+import { useParams } from "react-router-dom";
+import styles from "./ForumPost.module.scss";
+import { useQuery } from "react-query";
+import { getPost } from "../../Services/forum";
+
+function ForumPost() {
+ const { postId } = useParams();
+ const { data: post, isLoading } = useQuery(["post", postId], () =>
+ getPost(postId!)
+ );
+ return (
+
+ {!isLoading && (
+
+ {post.title}
+ {post.postContent}
+
+ )}
+
+ );
+}
+
+export default ForumPost;
diff --git a/app/frontend/src/Pages/ForumPostForm/ForumPostForm.module.scss b/app/frontend/src/Pages/ForumPostForm/ForumPostForm.module.scss
new file mode 100644
index 00000000..ee42325a
--- /dev/null
+++ b/app/frontend/src/Pages/ForumPostForm/ForumPostForm.module.scss
@@ -0,0 +1,3 @@
+.container {
+ padding: 2.5em;
+}
diff --git a/app/frontend/src/Pages/ForumPostForm/ForumPostForm.tsx b/app/frontend/src/Pages/ForumPostForm/ForumPostForm.tsx
new file mode 100644
index 00000000..fc4a81aa
--- /dev/null
+++ b/app/frontend/src/Pages/ForumPostForm/ForumPostForm.tsx
@@ -0,0 +1,50 @@
+import { useNavigate, useParams, useSearchParams } from "react-router-dom";
+import styles from "./ForumPostForm.module.scss";
+import TextArea from "antd/es/input/TextArea";
+import { Button, Form, Input } from "antd";
+import { useMutation } from "react-query";
+import { createPost } from "../../Services/forum";
+
+function ForumPostForm() {
+ const [searchParams, setSearchParams] = useSearchParams();
+ const navigate = useNavigate();
+
+ const { mutate: addPost, isLoading } = useMutation(
+ ({ title, postContent }: { title: string; postContent: string }) =>
+ createPost({ forum: searchParams.get("forumId")!, title, postContent }),
+ {
+ onSuccess() {
+ navigate(searchParams.get("redirect") ?? "/");
+ },
+ }
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default ForumPostForm;
diff --git a/app/frontend/src/Pages/GameDetails/GameDetails.tsx b/app/frontend/src/Pages/GameDetails/GameDetails.tsx
index aa822134..f6b65ae8 100644
--- a/app/frontend/src/Pages/GameDetails/GameDetails.tsx
+++ b/app/frontend/src/Pages/GameDetails/GameDetails.tsx
@@ -2,35 +2,13 @@ import { StarFilled, StarOutlined } from "@ant-design/icons";
import styles from "./GameDetails.module.scss";
import { useState } from "react";
import Summary from "../../Components/GameDetails/Summary/Summary";
-import { useParams } from "react-router-dom";
+import { useParams, useSearchParams } from "react-router-dom";
import { getGame } from "../../Services/gamedetail";
import { useQuery } from "react-query";
import { PacmanLoader } from "react-spinners";
-
-function formatDate(date: Date) {
- const day = date.getDate();
- const monthIndex = date.getMonth();
- const year = date.getFullYear();
-
- const monthNames = [
- "January",
- "February",
- "March",
- "April",
- "May",
- "June",
- "July",
- "August",
- "September",
- "October",
- "November",
- "December",
- ];
-
- const monthName = monthNames[monthIndex];
-
- return `${day} ${monthName} ${year}`;
-}
+import Reviews from "../../Components/GameDetails/Review/Reviews";
+import Forum from "../../Components/Forum/Forum";
+import { formatDate } from "../../Library/utils/formatDate";
function GameDetails() {
const { gameId } = useParams();
@@ -40,11 +18,13 @@ function GameDetails() {
);
const score = 4;
+ const [searchParams] = useSearchParams();
+
const [subPage, setSubPage] = useState<"summary" | "reviews" | "forum">(
- "summary"
+ (searchParams.get("subPage") as any) ?? "summary"
);
- const date = new Date();
+ const date = data?.releaseDate;
return (
{isLoading ? (
@@ -83,15 +63,29 @@ function GameDetails() {
type="button"
className={subPage === name ? styles.active : ""}
onClick={() => setSubPage(name as any)}
- disabled
>
{name}
))}
-
-
-
+ {data && (
+
+ {subPage === "summary" ? (
+
+ ) : subPage === "forum" ? (
+ data?.forum ? (
+
+ ) : (
+ <>No forum on this game.>
+ )
+ ) : (
+
+ )}
+
+ )}
>
)}
diff --git a/app/frontend/src/Services/forum.ts b/app/frontend/src/Services/forum.ts
new file mode 100644
index 00000000..d1faa714
--- /dev/null
+++ b/app/frontend/src/Services/forum.ts
@@ -0,0 +1,69 @@
+import axios from "axios";
+export const getPostList = async ({
+ findDeleted = false,
+ tags = [],
+ search = "",
+ sortBy = "CREATION_DATE",
+ forum,
+ sortDirection = "DESCENDING",
+}: {
+ findDeleted?: boolean;
+ tags?: string[];
+ search?: string;
+ forum: string;
+ sortBy?: string;
+ sortDirection?: string;
+}) => {
+ const response = await axios.get(
+ import.meta.env.VITE_APP_API_URL + "/post/get-post-list",
+ {
+ params: {
+ findDeleted,
+ tags,
+ search,
+ sortBy,
+ forum,
+ sortDirection,
+ },
+ }
+ );
+ return response.data;
+};
+
+export const createPost = async ({
+ title,
+ postContent,
+ postImage,
+ forum,
+ tags = [],
+}: {
+ title: string;
+ postContent: string;
+ postImage?: string;
+ forum: string;
+ tags?: string[];
+}) => {
+ const response = await axios.post(
+ import.meta.env.VITE_APP_API_URL + "/post/create",
+ {
+ title,
+ postContent,
+ postImage,
+ forum,
+ tags,
+ }
+ );
+ return response.data;
+};
+
+export const getPost = async (id: string) => {
+ const response = await axios.get(
+ import.meta.env.VITE_APP_API_URL + "/post/get-post-detail",
+ {
+ params: {
+ id,
+ },
+ }
+ );
+ return response.data;
+};
diff --git a/app/frontend/src/Services/review.ts b/app/frontend/src/Services/review.ts
new file mode 100644
index 00000000..c58d9e8b
--- /dev/null
+++ b/app/frontend/src/Services/review.ts
@@ -0,0 +1,52 @@
+import axios from "axios";
+
+export async function getReview(id : string ) {
+
+ const reviews = await axios.get(`${import.meta.env.VITE_APP_API_URL}/review`, {
+ params: {
+ id
+ }
+ });
+
+ return reviews;
+}
+
+export async function getAllReviews(gameId : string, reviewedBy?: string) {
+
+ const reviews = await axios.get(`${import.meta.env.VITE_APP_API_URL}/review/get-all`, {
+ params: {
+ gameId,
+ reviewedBy
+ }
+ });
+
+ return reviews;
+}
+
+export async function postReview(review: any) {
+
+ await axios.post(`${import.meta.env.VITE_APP_API_URL}/review/create`, review);
+
+}
+
+export async function updateReview(id: string, review: any) {
+
+ await axios.put(`${import.meta.env.VITE_APP_API_URL}/review/update`, review, {
+ params: {
+ id
+ }
+ });
+
+}
+
+export async function deleteReview(id: string) {
+
+ await axios.delete(`${import.meta.env.VITE_APP_API_URL}/review/delete`, {
+ params: {
+ id
+ }
+ });
+
+}
+
+
diff --git a/app/frontend/src/Services/vote.tsx b/app/frontend/src/Services/vote.tsx
new file mode 100644
index 00000000..a70566ab
--- /dev/null
+++ b/app/frontend/src/Services/vote.tsx
@@ -0,0 +1,13 @@
+import axios from "axios";
+
+export const createVote = async (body: {
+ voteType: "POST" | "REVIEW";
+ typeId: string;
+ choice: "UPVOTE" | "DOWNVOTE";
+}) => {
+ const response = await axios.post(
+ import.meta.env.VITE_APP_API_URL + "/vote/create",
+ body
+ );
+ return response.data;
+};
diff --git a/app/frontend/src/router.tsx b/app/frontend/src/router.tsx
index 31162f40..58685de7 100644
--- a/app/frontend/src/router.tsx
+++ b/app/frontend/src/router.tsx
@@ -8,10 +8,11 @@ import Register from "./Pages/Register/Register";
import GameDetails from "./Pages/GameDetails/GameDetails";
import axios from "axios";
import Games from "./Pages/Games/Games";
+import ForumPostForm from "./Pages/ForumPostForm/ForumPostForm";
+import ForumPost from "./Pages/ForumPost/ForumPost";
axios.defaults.headers.common["Content-Type"] = "application/json";
-
const router = createBrowserRouter([
{
path: "/",
@@ -28,9 +29,22 @@ const router = createBrowserRouter([
element: ,
},
{
- path: "/game/:gameId",
+ path: "game/:gameId",
element: ,
},
+ {
+ path: "forum",
+ children: [
+ {
+ path: "form",
+ element: ,
+ },
+ {
+ path: "detail/:postId",
+ element: ,
+ },
+ ],
+ },
{
path: "games",
element: ,