From 7b29dc772f71636c61b1eae49f10a23abc6ffb14 Mon Sep 17 00:00:00 2001 From: Jacob Date: Wed, 15 Apr 2026 01:06:41 -0400 Subject: [PATCH 1/4] alot of buttons added and moveed things around for better organization --- backend/Dockerfile | 3 +- .../0002_alter_reviews_unique_together.py | 17 ++ backend/src/restapi/gametime/models.py | 9 + backend/src/restapi/gametime/urls.py | 5 +- backend/src/restapi/gametime/views.py | 82 +++--- docker-compose.yml | 2 +- frontend/src/api/endpoints.tsx | 233 ++++++++++++++++++ .../{authentication.tsx => endpoints.tsx} | 0 frontend/src/pages/Account/Account.module.css | 10 +- frontend/src/pages/Account/Account.tsx | 15 +- .../src/pages/gamePage/gamePage.module.css | 11 + frontend/src/pages/gamePage/gamePage.tsx | 189 +++++++++----- 12 files changed, 468 insertions(+), 108 deletions(-) create mode 100644 backend/src/restapi/gametime/migrations/0002_alter_reviews_unique_together.py create mode 100644 frontend/src/api/endpoints.tsx rename frontend/src/auth/{authentication.tsx => endpoints.tsx} (100%) diff --git a/backend/Dockerfile b/backend/Dockerfile index 4d31d9c..e06bb31 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -8,9 +8,10 @@ RUN pip install --no-cache-dir -r requirements.txt COPY . /src + EXPOSE 8000 -CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"] +CMD ["sh", "-c", "python manage.py flush && python manage.py makemigrations && python manage.py migrate && python manage.py runserver 0.0.0.0:8000"] diff --git a/backend/src/restapi/gametime/migrations/0002_alter_reviews_unique_together.py b/backend/src/restapi/gametime/migrations/0002_alter_reviews_unique_together.py new file mode 100644 index 0000000..adb1f23 --- /dev/null +++ b/backend/src/restapi/gametime/migrations/0002_alter_reviews_unique_together.py @@ -0,0 +1,17 @@ +# Generated by Django 6.0.2 on 2026-04-14 20:52 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('gametime', '0001_initial'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='reviews', + unique_together={('userID', 'gameID')}, + ), + ] diff --git a/backend/src/restapi/gametime/models.py b/backend/src/restapi/gametime/models.py index 6396c48..d0ecbf8 100644 --- a/backend/src/restapi/gametime/models.py +++ b/backend/src/restapi/gametime/models.py @@ -13,16 +13,25 @@ class BACKLOG(models.Model): userID = models.ForeignKey(USER, on_delete=models.CASCADE) gameID = models.IntegerField() + class Meta: + unique_together = ('userID', 'gameID') + class COMPLETED(models.Model): userID = models.ForeignKey(USER, on_delete=models.CASCADE) gameID = models.IntegerField() + + class Meta: + unique_together = ('userID', 'gameID') # Favorites table class FAVORITES(models.Model): userID = models.ForeignKey(USER, on_delete=models.CASCADE) gameID = models.IntegerField() + + class Meta: + unique_together = ('userID', 'gameID') # User Reviews diff --git a/backend/src/restapi/gametime/urls.py b/backend/src/restapi/gametime/urls.py index f3bccfc..3bbaee0 100644 --- a/backend/src/restapi/gametime/urls.py +++ b/backend/src/restapi/gametime/urls.py @@ -10,8 +10,9 @@ path('create-account/', views.createAccount, name='create-account'), path('reviews//', views.getReviews, name='reviews'), path('user/create-review/', views.createReview, name='create-review'), - path('user/account/favorites/', views.addFavorite), - path('user/account/followed-games/', views.followGame), + path('user/account/favorites/', views.handleFavorites), + path('user/account/followed-games/', views.handleFollowGames), path('user/account/followed-users/', views.followUser), + path('user/account/backlog/', views.handleBacklog) ] diff --git a/backend/src/restapi/gametime/views.py b/backend/src/restapi/gametime/views.py index 2a20647..aa8d13d 100644 --- a/backend/src/restapi/gametime/views.py +++ b/backend/src/restapi/gametime/views.py @@ -6,7 +6,7 @@ from django.contrib.auth import authenticate from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response -from .models import USER, REVIEWS, FAVORITES, FOLLOWGAME, FOLLOWUSER +from .models import USER, REVIEWS, FAVORITES, FOLLOWGAME, FOLLOWUSER, BACKLOG from .serializers import reviewSerializer # Create your views here. @@ -208,69 +208,62 @@ def deleteReview(request): return Response({"error: Could not delete review."}, status=400) -@api_view(['POST']) +@api_view(['POST', 'DELETE']) @permission_classes([IsAuthenticated]) -def addFavorite(request): +def handleFavorites(request): + user = request.user + gameID = request.data.get("gameID") if request.method == 'POST': - data = request.data favorites = FAVORITES.objects.create( - userID=data['userID'], - gameID=data['gameID'], + userID=user, + gameID=gameID, ) favorites.save() content = { "Favorite has been added!" } return Response(content) - return Response({"error: Could not add to favorites."}, status=400) - - -@api_view(['DELETE']) -@permission_classes([IsAuthenticated]) -def removeFavorite(request): if request.method == 'DELETE': - data = request.data favorites = FAVORITES.objects.get( - userID=data['userID'], - gameID=data['gameID'], + userID=user, + gameID=gameID, ) favorites.delete() - favorites.save() content = { "Favorite has been removed." } return Response(content) - return Response({"error: Could not remove from favorites."}, status=400) + return Response({"error: Could not fufil request."}, status=400) -@api_view(['POST']) + + +@api_view(['POST', 'DELETE']) @permission_classes([IsAuthenticated]) -def followGame(request): +def handleFollowGames(request): + user = request.user + gameId = request.data.get("gameID") + if request.method == 'POST': - data = request.data + gamefollows = FOLLOWGAME.objects.create( - followed=data['followed'], - follower=data['follower'], + gameID=gameId, + followerID=user, ) gamefollows.save() content = { "Game has been followed." } return Response(content) - return Response({"error: Could not follow user."}, status=400) - -@api_view(['DELETE']) -@permission_classes([IsAuthenticated]) -def unfollowGame(request): if request.method == 'DELETE': - data = request.data + gamefollows = FOLLOWGAME.objects.get( - followerID=data['followerID'], - gameID=data['gameID'], + followerID=user, + gameID=gameId, ) gamefollows.delete() - gamefollows.save() + content = { "Game has been unfollowed." } @@ -278,13 +271,35 @@ def unfollowGame(request): return Response({"error: Could not unfollow game."}) +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +def handleBacklog(request): + if request.method == 'POST': + user = request.user + backlogId = request.data.get("backlogID") + logged = BACKLOG.objects.filter( + gameID=backlogId, + userID=user + ) + logged.delete() + content = {"message": "Backlog"} + return Response(content, status=200) + return Response({"Couldn't backlog item"}) + + + + + @api_view(['POST']) @permission_classes([IsAuthenticated]) def followUser(request): + + if request.method == 'POST': data = request.data + user = USER.objects.get(username=data['username']) userfollows = FOLLOWUSER.objects.create( - followerID=data['followerID'], + followerID=user, gameID=data['gameID'], ) userfollows.save() @@ -300,8 +315,9 @@ def followUser(request): def unfollowUser(request): if request.method == 'DELETE': data = request.data + user = USER.objects.get(username=data['username']) userfollows = FOLLOWUSER.objects.get( - followerID=data['followerID'], + followerID=user, gameID=data['gameID'], ) userfollows.delete() diff --git a/docker-compose.yml b/docker-compose.yml index a361b83..d0b3926 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: - "8000:8000" volumes: - ./backend:/src - command: python manage.py runserver 0.0.0.0:8000 + command: sh -c "python manage.py makemigrations && python manage.py migrate && python manage.py runserver 0.0.0.0:8000" frontend: build: ./frontend diff --git a/frontend/src/api/endpoints.tsx b/frontend/src/api/endpoints.tsx new file mode 100644 index 0000000..bd2b3e7 --- /dev/null +++ b/frontend/src/api/endpoints.tsx @@ -0,0 +1,233 @@ + +export default async function getGame(id: string) { + try { + const res = await fetch(`http://127.0.0.1:8000/gametime/game/${id}/`); + if (!res.ok) { + throw new Error("Failed to fetch game"); + } + const data = await res.json(); + return data; + } + catch (err) { + console.error("Game page error:", err); + throw err; + } +} + + + + + + export const handleSubmitReview = (token: string, id: string, reviewText: string, selectedRating: number) => { + + + + + + + if (reviewText.trim() === "" && selectedRating === 0) { + + return; + } + else { + + + + fetch('http://127.0.0.1:8000/gametime/user/create-review/', { + method: 'POST', + headers: { + 'Authorization': `Token ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify( + { + gameID: id, + rating: selectedRating, + review: reviewText, + username: localStorage.getItem("username") || "Anonymous", + date: new Date().toISOString() + } + ), + }) + .then((res) => { + if (!res.ok) { + throw new Error("Network response was not ok: " + res.statusText); + } + return res.json(); + }) + .then((data) => { + console.log("Review submitted successfully:", data); + // Optionally, you can update the reviews state here to show the new review immediately + }) + .catch((err) => { + console.error("Error submitting review:", err); + alert("There was an error submitting your review. Please try again."); + }); + + + } + }; + + + + + + + + +export const handleFollowGame = (id: string, token: string) => { + + + + try{ + fetch(`http://127.0.0.1:8000/gametime/user/account/followed-games/`, { + method: 'POST', + headers: { + 'Authorization': `Token ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + gameID: id, + username: localStorage.getItem("username") || "Anonymous" + }), + }) + .then((res) => { + if (!res.ok) { + throw new Error("Network response was not ok: " + res.statusText); + } + return res.json(); + }) + .then((data) => { + console.log("Game followed successfully:", data); + + }) + .catch((err) => { + console.error("Error following game:", err); + alert("There was an error following the game. Please try again."); + }); + } + catch (err) { + console.error("Error following game:", err); + } + } + + + +export const handleUnfollowGame = (id: string, token: string) => { + try{ + fetch(`http://127.0.0.1:8000/gametime/user/account/followed-games/`, { + method: 'DELETE', + headers: { + 'Authorization': `Token ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ gameID: id }), + }) + .then((res) => { + if (!res.ok) { + throw new Error("Network response was not ok: " + res.statusText); + } + return res.json(); + }) + .then((data) => { + console.log("Game unfollowed successfully:", data); + }) + .catch((err) => { + console.error("Error unfollowing game:", err); + alert("There was an error unfollowing the game. Please try again."); + }); + } + catch (err) { + console.error("Error unfollowing game:", err); + + + } +} + +export const handleAddFavoriteGame = (id: string, token: string) => { + try{ + fetch(`http://127.0.0.1:8000/gametime/user/account/favorites/`, { + method: 'POST', + headers: { + 'Authorization': `Token ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ gameID: id }), + }) + .then((res) => { + if (!res.ok) { + throw new Error("Network response was not ok: " + res.statusText); + } + return res.json(); + }) + .then((data) => { + console.log("Game favorited successfully:", data); + }) + .catch((err) => { + console.error("Error favoriting game:", err); + alert("There was an error favoriting the game. Please try again."); + }); + } + catch (err) { + console.error("Error favoriting game:", err); + } + } + +export const handleUnfavoriteGame = (id: string, token: string) => { + try{ + fetch(`http://127.0.0.1:8000/gametime/user/account/favorites/`, { + method: 'DELETE', + headers: { + 'Authorization': `Token ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ gameID: id }), + }) + .then((res) => { + if (!res.ok) { + throw new Error("Network response was not ok: " + res.statusText); + } + return res.json(); + }) + .then((data) => { + console.log("Game unfavorited successfully:", data); + }) + .catch((err) => { + console.error("Error unfavoriting game:", err); + alert("There was an error unfavoriting the game. Please try again."); + }); + } + catch (err) { + console.error("Error unfavoriting game:", err); + } + } + + + export const AddToBacklog = (id: string, token: string) => { + try{ + fetch(`http://127.0.0.1:8000/gametime/user/account/backlog/`, { + method: 'POST', + headers: { + 'Authorization': `Token ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ gameID: id }), + }) + .then((res) => { + if (!res.ok) { + throw new Error("Network response was not ok: " + res.statusText); + } + return res.json(); + }) + .then((data) => { + console.log("Game added to backlog successfully:", data); + }) + .catch((err) => { + console.error("Error adding game to backlog:", err); + alert("There was an error adding the game to your backlog. Please try again."); + }); + } + catch (err) { + console.error("Error adding game to backlog:", err); + } + } diff --git a/frontend/src/auth/authentication.tsx b/frontend/src/auth/endpoints.tsx similarity index 100% rename from frontend/src/auth/authentication.tsx rename to frontend/src/auth/endpoints.tsx diff --git a/frontend/src/pages/Account/Account.module.css b/frontend/src/pages/Account/Account.module.css index d985bfd..fbe9123 100644 --- a/frontend/src/pages/Account/Account.module.css +++ b/frontend/src/pages/Account/Account.module.css @@ -19,7 +19,7 @@ .accountLayout { display: grid; - grid-template-columns: 360px 360px 360px; + grid-template-columns: 1fr; gap: 1.5rem; align-items: start; } @@ -29,9 +29,8 @@ .card { background: #0f1115; color: white; - padding: 28px; + padding: 22px; border-radius: 16px; - width: 360px; border: 1px solid rgba(255, 255, 255, 0.08); } @@ -65,13 +64,18 @@ display: flex; flex-direction: column; gap: 0.8rem; + width: fit-content; + margin: 0 auto; margin-bottom: 1.5rem; + text-align: left + } .infoGroup p { margin: 0; line-height: 1.5; color: #e6edf3; + } diff --git a/frontend/src/pages/Account/Account.tsx b/frontend/src/pages/Account/Account.tsx index 674d522..1f27043 100644 --- a/frontend/src/pages/Account/Account.tsx +++ b/frontend/src/pages/Account/Account.tsx @@ -1,7 +1,7 @@ import { useNavigate } from "react-router-dom"; import styles from "./Account.module.css"; import { useEffect, useState } from "react"; -import Authentication from "../../auth/authentication"; +import Authentication from "../../auth/endpoints"; import type { userInfo } from "../../types/types"; type Review = { @@ -128,9 +128,10 @@ export default function Account() {
{/* Main account information box */}
-

Account

+
+

Account

Username: {data?.username ?? "Loading..."}

@@ -155,6 +156,16 @@ export default function Account() { > Sign Out + +
{/* Favorite games box */} diff --git a/frontend/src/pages/gamePage/gamePage.module.css b/frontend/src/pages/gamePage/gamePage.module.css index 86ca6a0..2d95ac0 100644 --- a/frontend/src/pages/gamePage/gamePage.module.css +++ b/frontend/src/pages/gamePage/gamePage.module.css @@ -134,6 +134,17 @@ cursor: pointer; } +.followButton, .favoriteButton, .backlogButton { + background: #3e4b5f; + color: rgba(242, 237, 237, 0.87); + border: none; + border-radius: 6px; + padding: .5rem .5rem; + font-weight: 600; + font-size: 1rem; + cursor: pointer; +} + .submitButton:hover { background: #388bfd; } diff --git a/frontend/src/pages/gamePage/gamePage.tsx b/frontend/src/pages/gamePage/gamePage.tsx index 22b7e12..40e9496 100644 --- a/frontend/src/pages/gamePage/gamePage.tsx +++ b/frontend/src/pages/gamePage/gamePage.tsx @@ -5,7 +5,11 @@ import styles from "./gamePage.module.css"; import { fromUnixTime, format } from "date-fns"; import type { getReview } from "../../types/types"; import { useNavigate } from "react-router-dom"; -import Authentication from "../../auth/authentication"; +import Authentication from "../../auth/endpoints"; + + +import getGame from "../../api/endpoints"; +import { handleFollowGame, handleUnfollowGame, handleSubmitReview, handleAddFavoriteGame, handleUnfavoriteGame, AddToBacklog } from "../../api/endpoints"; @@ -18,7 +22,15 @@ function GamePage() { const [game, setGame] = useState(null); const [reviewText, setReviewText] = useState(""); const [selectedRating, setSelectedRating] = useState(0); - const [buttonName, setButtonName] = useState("sign in"); + const [submitButtonName, setSubmitButtonName] = useState("sign in"); + + const [followButtonName, setFollowButtonName] = useState("follow"); + const [favoriteButtonName, setFavoriteButtonName] = useState("favorite"); + const [backlogButtonName, setBacklogButtonName] = useState("add to backlog"); + + + const [authenticated, setAuthenticated] = useState(false); + const navigate = useNavigate(); // placeholder till proper implementation @@ -45,6 +57,91 @@ function GamePage() { }; + + + const handleSubmitReviewButton = (e: React.FormEvent) => { + e.preventDefault(); + + if (submitButtonName === "sign in") { + navigate("/sign-in"); + return; + } + + // later connect backend here + handleSubmitReview(token || "", id || "", reviewText, selectedRating); + + + setReviewText(""); + setSelectedRating(0); + }; + + + + const handleFollowGameButton = () => { + if (followButtonName === "follow") { + try{ + handleFollowGame(id || "", token || ""); + setFollowButtonName("unfollow"); + } + catch (err) { + console.error("Error following game:", err); + } + } + else { + try{ + handleUnfollowGame(id || "", token || ""); + setFollowButtonName("follow"); + } + catch (err) { + console.error("Error unfollowing game:", err); + } + } + }; + + const handleFavoriteGameButton = () => { + if (favoriteButtonName === "favorite") { + try{ + handleAddFavoriteGame(id || "", token || ""); + setFavoriteButtonName("unfavorite"); + } + catch (err) { + console.error("Error favoriting game:", err); + } + } + else { + try{ + handleUnfavoriteGame(id || "", token || ""); + setFavoriteButtonName("favorite"); + } + catch (err) { + console.error("Error unfavoriting game:", err); + } + } + }; + + const handleBacklogButton = () => { + if (backlogButtonName === "add to backlog") { + try{ + AddToBacklog(id || "", token || ""); + setBacklogButtonName("remove from backlog"); + } + catch (err) { + console.error("Error adding game to backlog:", err); + } + } + }; + + const handleFollowUserButton = () => { + // placeholder for follow user functionality + try{ + // handleFollowUser(userId || "", token || ""); + + } + catch (err) { + console.error("Error following user:", err); + } + }; + useEffect(() => { async function fetchingData() { try { @@ -52,26 +149,26 @@ function GamePage() { const res = await Authentication(url, token || ""); if (res?.status === 401) { - setButtonName("sign in"); + setSubmitButtonName("sign in"); } else { - setButtonName("submit review"); + setSubmitButtonName("submit review"); + setAuthenticated(true); } if (!id) return; - fetch(`http://127.0.0.1:8000/gametime/game/${id}/`) - .then((res) => res.json()) - .then((data) => { - setGame(data[0] ?? null); - }) - .catch((err) => console.error("Game page error:", err)); + const gameinfo = await getGame(id); + + setGame(gameinfo[0]); + + } catch (err) { console.error("Game page authentication error:", err); - setButtonName("sign in"); + setSubmitButtonName("sign in"); } } fetchingData(); @@ -99,59 +196,7 @@ function GamePage() { - const handleSubmitReview = (e: React.FormEvent) => { - e.preventDefault(); - - if (buttonName === "sign in") { - navigate("/sign-in"); - return; - } - - // later connect backend here - - if (reviewText.trim() === "" && selectedRating === 0) { - console.log("yogi didn't like you") - return; - } - else { - - - - fetch('http://127.0.0.1:8000/gametime/user/create-review/', { - method: 'POST', - headers: { - 'Authorization': `Token ${token}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify( - { - gameID: id, - rating: selectedRating, - review: reviewText, - username: localStorage.getItem("username") || "Anonymous", - date: new Date().toISOString() - } - ), - }) - .then((res) => { - if (!res.ok) { - throw new Error("Network response was not ok: " + res.statusText); - } - return res.json(); - }) - .then((data) => { - console.log("Review submitted successfully:", data); - // Optionally, you can update the reviews state here to show the new review immediately - }) - .catch((err) => { - console.error("Error submitting review:", err); - alert("There was an error submitting your review. Please try again."); - }); - - setReviewText(""); - setSelectedRating(0); - } - }; + @@ -241,6 +286,17 @@ function GamePage() { alt={game.name} /> )} + +
+ {authenticated && ( + + )} + {authenticated && } + {authenticated && } +
+

Release Date: {time}

@@ -282,6 +338,7 @@ function GamePage() {
{review.username} + {authenticated && } {review.formatedDate} @@ -308,7 +365,7 @@ function GamePage() {

Write a Review

-
+
@@ -332,7 +389,7 @@ function GamePage() { />
From b19b2d32c6b0445a6b9a5397b966a1b0bde0e14d Mon Sep 17 00:00:00 2001 From: Jacob Date: Wed, 15 Apr 2026 19:43:25 -0400 Subject: [PATCH 2/4] added alot of stuff cool --- backend/src/restapi/.env | 2 - ..._alter_backlog_unique_together_and_more.py | 25 + backend/src/restapi/gametime/serializers.py | 8 +- backend/src/restapi/gametime/urls.py | 2 + backend/src/restapi/gametime/views.py | 51 +- frontend/src/api/endpoints.tsx | 459 +++++++++++------- frontend/src/pages/Account/Account.module.css | 39 +- frontend/src/pages/Account/Account.tsx | 47 +- frontend/src/pages/gamePage/gamePage.tsx | 24 +- frontend/src/types/types.tsx | 16 + 10 files changed, 434 insertions(+), 239 deletions(-) delete mode 100644 backend/src/restapi/.env create mode 100644 backend/src/restapi/gametime/migrations/0003_alter_backlog_unique_together_and_more.py diff --git a/backend/src/restapi/.env b/backend/src/restapi/.env deleted file mode 100644 index 5919ac0..0000000 --- a/backend/src/restapi/.env +++ /dev/null @@ -1,2 +0,0 @@ -CLIENT_ID = ooafi5f92k40ipxkplc9kdvydtgbyi -CLIENT_SECRET = 4860yvr178bnaugoc50ukvlzqdo5ll \ No newline at end of file diff --git a/backend/src/restapi/gametime/migrations/0003_alter_backlog_unique_together_and_more.py b/backend/src/restapi/gametime/migrations/0003_alter_backlog_unique_together_and_more.py new file mode 100644 index 0000000..015f0bc --- /dev/null +++ b/backend/src/restapi/gametime/migrations/0003_alter_backlog_unique_together_and_more.py @@ -0,0 +1,25 @@ +# Generated by Django 6.0.2 on 2026-04-15 23:13 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('gametime', '0002_alter_reviews_unique_together'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='backlog', + unique_together={('userID', 'gameID')}, + ), + migrations.AlterUniqueTogether( + name='completed', + unique_together={('userID', 'gameID')}, + ), + migrations.AlterUniqueTogether( + name='favorites', + unique_together={('userID', 'gameID')}, + ), + ] diff --git a/backend/src/restapi/gametime/serializers.py b/backend/src/restapi/gametime/serializers.py index f7219a9..818887c 100644 --- a/backend/src/restapi/gametime/serializers.py +++ b/backend/src/restapi/gametime/serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from .models import REVIEWS +from .models import REVIEWS, FAVORITES class reviewSerializer(serializers.ModelSerializer): @@ -12,3 +12,9 @@ class reviewSerializer(serializers.ModelSerializer): class Meta: model = REVIEWS fields = ['username', 'review', 'rating', 'formatedDate'] + + +class gameSerializer(serializers.ModelSerializer): + class Meta: + model = FAVORITES + fields = ['gameID'] diff --git a/backend/src/restapi/gametime/urls.py b/backend/src/restapi/gametime/urls.py index 3bbaee0..52ff6ba 100644 --- a/backend/src/restapi/gametime/urls.py +++ b/backend/src/restapi/gametime/urls.py @@ -10,6 +10,8 @@ path('create-account/', views.createAccount, name='create-account'), path('reviews//', views.getReviews, name='reviews'), path('user/create-review/', views.createReview, name='create-review'), + path('followers//', views.getFollowers), + path('favorites//', views.getFavorites), path('user/account/favorites/', views.handleFavorites), path('user/account/followed-games/', views.handleFollowGames), path('user/account/followed-users/', views.followUser), diff --git a/backend/src/restapi/gametime/views.py b/backend/src/restapi/gametime/views.py index aa8d13d..3a2136b 100644 --- a/backend/src/restapi/gametime/views.py +++ b/backend/src/restapi/gametime/views.py @@ -7,7 +7,7 @@ from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response from .models import USER, REVIEWS, FAVORITES, FOLLOWGAME, FOLLOWUSER, BACKLOG -from .serializers import reviewSerializer +from .serializers import reviewSerializer, gameSerializer # Create your views here. # get the client id from the .env file @@ -287,43 +287,50 @@ def handleBacklog(request): return Response({"Couldn't backlog item"}) - - - -@api_view(['POST']) +@api_view(['POST', 'DELETE', 'GET']) @permission_classes([IsAuthenticated]) def followUser(request): - - + followerID = request.user + followed = request.data.get("username") + followedUser = USER.objects.get(username=followed) if request.method == 'POST': - data = request.data - user = USER.objects.get(username=data['username']) userfollows = FOLLOWUSER.objects.create( - followerID=user, - gameID=data['gameID'], + followed=followedUser, + follower=followerID, ) userfollows.save() content = { "User has been followed." } return Response(content) - return Response({"error: Could not follow user."}, status=400) - -@api_view(['DELETE']) -@permission_classes([IsAuthenticated]) -def unfollowUser(request): if request.method == 'DELETE': - data = request.data - user = USER.objects.get(username=data['username']) userfollows = FOLLOWUSER.objects.get( - followerID=user, - gameID=data['gameID'], + followed=followedUser, + follower=followerID, ) userfollows.delete() - userfollows.save() content = { "User has been unfollowed." } return Response(content) - return Response({"error: Could not unfollow user."}, status=400) + if request.method == 'GET': + count = FOLLOWUSER.objects.filter(following=request.user).count() + return Response(count) + + return Response({"error: Could not follow user."}, status=400) + +@api_view(['GET']) +def getFollowers(request, user): + if request.method == 'GET': + user_obj = USER.objects.filter(username=user).first() + count = user_obj.followers.count() + return Response({'followers': count}) + +@api_view(['GET']) +def getFavorites(request, user): + if request.method == 'GET': + user_obj = USER.objects.filter(username=user).first() + games = FAVORITES.objects.filter(userID=user_obj) + content = gameSerializer(games, many=True).data + return Response(content) diff --git a/frontend/src/api/endpoints.tsx b/frontend/src/api/endpoints.tsx index bd2b3e7..bdc0c4e 100644 --- a/frontend/src/api/endpoints.tsx +++ b/frontend/src/api/endpoints.tsx @@ -15,219 +15,314 @@ export default async function getGame(id: string) { } +export const getFollowers = async (username: string) => { + try { + const res = await fetch(`http://127.0.0.1:8000/gametime/followers/${username}/`, { method: 'GET' }); + if (!res.ok) { + throw new Error("Failed to fetch followers"); + } + const data = await res.json(); + return data // Assuming the response is an array of followed users + + } + catch (err) { + console.error("Error fetching followers:", err); + throw err; + } +} +export const getFavorites = async (username: string) => { + try { + const res = await fetch(`http://127.0.0.1:8000/gametime/favorites/${username}/`, { + method: 'GET', + }); + if (!res.ok) { + throw new Error("Failed to fetch favorites"); + } + const data = await res.json(); - export const handleSubmitReview = (token: string, id: string, reviewText: string, selectedRating: number) => { - + const favorites = data.map((item: { gameID: number }) => item.gameID).join(", "); + if (favorites.length === 0) { + return []; + } + const gameData = await getGame(favorites); + return gameData; // Assuming the response is an array of followed users + } + catch (err) { + console.error("Error fetching favorites:", err); + throw err; + } +} - - - if (reviewText.trim() === "" && selectedRating === 0) { - - return; - } - else { - - - - fetch('http://127.0.0.1:8000/gametime/user/create-review/', { - method: 'POST', - headers: { - 'Authorization': `Token ${token}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify( - { - gameID: id, - rating: selectedRating, - review: reviewText, - username: localStorage.getItem("username") || "Anonymous", - date: new Date().toISOString() - } - ), - }) - .then((res) => { - if (!res.ok) { - throw new Error("Network response was not ok: " + res.statusText); - } - return res.json(); - }) - .then((data) => { - console.log("Review submitted successfully:", data); - // Optionally, you can update the reviews state here to show the new review immediately - }) - .catch((err) => { - console.error("Error submitting review:", err); - alert("There was an error submitting your review. Please try again."); - }); - - - } - }; +export const handleSubmitReview = (token: string, id: string, reviewText: string, selectedRating: number) => { + if (reviewText.trim() === "" && selectedRating === 0) { -export const handleFollowGame = (id: string, token: string) => { + return; + } + else { - - try{ - fetch(`http://127.0.0.1:8000/gametime/user/account/followed-games/`, { - method: 'POST', - headers: { - 'Authorization': `Token ${token}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ + + fetch('http://127.0.0.1:8000/gametime/user/create-review/', { + method: 'POST', + headers: { + 'Authorization': `Token ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify( + { gameID: id, - username: localStorage.getItem("username") || "Anonymous" - }), - }) - .then((res) => { - if (!res.ok) { - throw new Error("Network response was not ok: " + res.statusText); - } - return res.json(); - }) - .then((data) => { - console.log("Game followed successfully:", data); - - }) - .catch((err) => { - console.error("Error following game:", err); - alert("There was an error following the game. Please try again."); - }); - } - catch (err) { - console.error("Error following game:", err); - } + rating: selectedRating, + review: reviewText, + username: localStorage.getItem("username") || "Anonymous", + date: new Date().toISOString() + } + ), + }) + .then((res) => { + if (!res.ok) { + throw new Error("Network response was not ok: " + res.statusText); + } + return res.json(); + }) + .then((data) => { + console.log("Review submitted successfully:", data); + // Optionally, you can update the reviews state here to show the new review immediately + }) + .catch((err) => { + console.error("Error submitting review:", err); + alert("There was an error submitting your review. Please try again."); + }); + + + } +}; + + + + + + + + +export const handleFollowGame = (id: string, token: string) => { + + + + try { + fetch(`http://127.0.0.1:8000/gametime/user/account/followed-games/`, { + method: 'POST', + headers: { + 'Authorization': `Token ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ gameID: id }), + }) + .then((res) => { + if (!res.ok) { + throw new Error("Network response was not ok: " + res.statusText); + } + return res.json(); + }) + .then((data) => { + console.log("Game followed successfully:", data); + + }) + .catch((err) => { + console.error("Error following game:", err); + alert("There was an error following the game. Please try again."); + }); } - + catch (err) { + console.error("Error following game:", err); + } +} + export const handleUnfollowGame = (id: string, token: string) => { - try{ - fetch(`http://127.0.0.1:8000/gametime/user/account/followed-games/`, { - method: 'DELETE', - headers: { - 'Authorization': `Token ${token}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ gameID: id }), - }) - .then((res) => { - if (!res.ok) { - throw new Error("Network response was not ok: " + res.statusText); - } - return res.json(); - }) - .then((data) => { - console.log("Game unfollowed successfully:", data); - }) - .catch((err) => { - console.error("Error unfollowing game:", err); - alert("There was an error unfollowing the game. Please try again."); - }); - } - catch (err) { - console.error("Error unfollowing game:", err); - + try { + fetch(`http://127.0.0.1:8000/gametime/user/account/followed-games/`, { + method: 'DELETE', + headers: { + 'Authorization': `Token ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ gameID: id }), + }) + .then((res) => { + if (!res.ok) { + throw new Error("Network response was not ok: " + res.statusText); + } + return res.json(); + }) + .then((data) => { + console.log("Game unfollowed successfully:", data); + }) + .catch((err) => { + console.error("Error unfollowing game:", err); + alert("There was an error unfollowing the game. Please try again."); + }); + } + catch (err) { + console.error("Error unfollowing game:", err); + } } export const handleAddFavoriteGame = (id: string, token: string) => { - try{ - fetch(`http://127.0.0.1:8000/gametime/user/account/favorites/`, { - method: 'POST', - headers: { - 'Authorization': `Token ${token}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ gameID: id }), - }) - .then((res) => { - if (!res.ok) { - throw new Error("Network response was not ok: " + res.statusText); - } - return res.json(); - }) - .then((data) => { - console.log("Game favorited successfully:", data); - }) - .catch((err) => { - console.error("Error favoriting game:", err); - alert("There was an error favoriting the game. Please try again."); - }); - } - catch (err) { - console.error("Error favoriting game:", err); + try { + fetch(`http://127.0.0.1:8000/gametime/user/account/favorites/`, { + method: 'POST', + headers: { + 'Authorization': `Token ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ gameID: id }), + }) + .then((res) => { + if (!res.ok) { + throw new Error("Network response was not ok: " + res.statusText); + } + return res.json(); + }) + .then((data) => { + console.log("Game favorited successfully:", data); + }) + .catch((err) => { + console.error("Error favoriting game:", err); + alert("There was an error favoriting the game. Please try again."); + }); } + catch (err) { + console.error("Error favoriting game:", err); } +} export const handleUnfavoriteGame = (id: string, token: string) => { - try{ - fetch(`http://127.0.0.1:8000/gametime/user/account/favorites/`, { - method: 'DELETE', - headers: { - 'Authorization': `Token ${token}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ gameID: id }), - }) - .then((res) => { - if (!res.ok) { - throw new Error("Network response was not ok: " + res.statusText); - } - return res.json(); - }) - .then((data) => { - console.log("Game unfavorited successfully:", data); - }) - .catch((err) => { - console.error("Error unfavoriting game:", err); - alert("There was an error unfavoriting the game. Please try again."); - }); - } - catch (err) { - console.error("Error unfavoriting game:", err); - } + try { + fetch(`http://127.0.0.1:8000/gametime/user/account/favorites/`, { + method: 'DELETE', + headers: { + 'Authorization': `Token ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ gameID: id }), + }) + .then((res) => { + if (!res.ok) { + throw new Error("Network response was not ok: " + res.statusText); + } + return res.json(); + }) + .then((data) => { + console.log("Game unfavorited successfully:", data); + }) + .catch((err) => { + console.error("Error unfavoriting game:", err); + alert("There was an error unfavoriting the game. Please try again."); + }); + } + catch (err) { + console.error("Error unfavoriting game:", err); } +} +export const handleFollowUser = (username: string, token: string) => { + try { + fetch(`http://127.0.0.1:8000/gametime/user/account/followed-users/`, { + method: 'POST', + headers: { + 'Authorization': `Token ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ username: username }), + }) + .then((res) => { + if (!res.ok) { + throw new Error("Network response was not ok: " + res.statusText); + } + return res.json(); + }) + .then((data) => { + console.log("User followed successfully:", data); + }) + .catch((err) => { + console.error("Error following user:", err); + alert("There was an error following the user. Please try again."); + }); + } + catch (err) { + console.error("Error following user:", err); + } +} - export const AddToBacklog = (id: string, token: string) => { - try{ - fetch(`http://127.0.0.1:8000/gametime/user/account/backlog/`, { - method: 'POST', - headers: { - 'Authorization': `Token ${token}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ gameID: id }), - }) - .then((res) => { - if (!res.ok) { - throw new Error("Network response was not ok: " + res.statusText); - } - return res.json(); - }) - .then((data) => { - console.log("Game added to backlog successfully:", data); - }) - .catch((err) => { - console.error("Error adding game to backlog:", err); - alert("There was an error adding the game to your backlog. Please try again."); - }); - } - catch (err) { - console.error("Error adding game to backlog:", err); - } +export const handleUnfollowUser = (username: string, token: string) => { + try { + fetch(`http://127.0.0.1:8000/gametime/user/account/followed-users/`, { + method: 'DELETE', + headers: { + 'Authorization': `Token ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ username: username }), + }) + .then((res) => { + if (!res.ok) { + throw new Error("Network response was not ok: " + res.statusText); + } + return res.json(); + }) + .then((data) => { + console.log("User unfollowed successfully:", data); + }) + .catch((err) => { + console.error("Error unfollowing user:", err); + alert("There was an error unfollowing the user. Please try again."); + }); } + catch (err) { + console.error("Error unfollowing user:", err); + } +} + + +export const AddToBacklog = (id: string, token: string) => { + try { + fetch(`http://127.0.0.1:8000/gametime/user/account/backlog/`, { + method: 'POST', + headers: { + 'Authorization': `Token ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ gameID: id }), + }) + .then((res) => { + if (!res.ok) { + throw new Error("Network response was not ok: " + res.statusText); + } + return res.json(); + }) + .then((data) => { + console.log("Game added to backlog successfully:", data); + }) + .catch((err) => { + console.error("Error adding game to backlog:", err); + alert("There was an error adding the game to your backlog. Please try again."); + }); + } + catch (err) { + console.error("Error adding game to backlog:", err); + } +} diff --git a/frontend/src/pages/Account/Account.module.css b/frontend/src/pages/Account/Account.module.css index fbe9123..0ea9688 100644 --- a/frontend/src/pages/Account/Account.module.css +++ b/frontend/src/pages/Account/Account.module.css @@ -65,17 +65,16 @@ flex-direction: column; gap: 0.8rem; width: fit-content; - margin: 0 auto; + margin: 0 auto; margin-bottom: 1.5rem; text-align: left - } .infoGroup p { margin: 0; line-height: 1.5; color: #e6edf3; - + } @@ -96,11 +95,12 @@ color: white; } -/* 4 favorite games grid */ + .favoriteGrid { display: grid; - grid-template-columns: repeat(2, minmax(140px, 1fr)); - gap: 0.75rem; + grid-template-columns: repeat(3, 1fr); + gap: 12px; + padding: auto; } @@ -190,4 +190,31 @@ .signOutButton:hover { background: #388bfd; +} + + +.resultItem { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +.title { + white-space: normal; + overflow-wrap: break-word; + word-break: break-word; + text-align: center; + max-width: 100%; + max-width: 250px; +} + +.image { + width: 100%; + max-width: 180px; + height: auto; + border-radius: 10px; + object-fit: cover; + + transition: transform 0.25s ease, filter 0.25s ease; } \ No newline at end of file diff --git a/frontend/src/pages/Account/Account.tsx b/frontend/src/pages/Account/Account.tsx index 1f27043..121a61c 100644 --- a/frontend/src/pages/Account/Account.tsx +++ b/frontend/src/pages/Account/Account.tsx @@ -3,19 +3,10 @@ import styles from "./Account.module.css"; import { useEffect, useState } from "react"; import Authentication from "../../auth/endpoints"; import type { userInfo } from "../../types/types"; +import { getFavorites, getFollowers } from "../../api/endpoints"; +import type {FavoriteGame, Review} from "../../types/types"; -type Review = { - id: number; - gameTitle: string; - rating: number; - reviewText: string; - createdAt: string; -}; -type FavoriteGame = { - id: number; - title: string; -}; export default function Account() { const navigate = useNavigate(); @@ -27,6 +18,10 @@ export default function Account() { // Pull auth token from localStorage for authenticated account requests const token = localStorage.getItem("token"); + + const [followers, setFollowers] = useState(0); + const [favorites, setFavorites] = useState([]); + // CHANGE THIS!!!! Dummy review data for layout/testing until backend const [recentReviews] = useState([ { @@ -66,19 +61,18 @@ export default function Account() { }, ]); - // CHANGE THIS AS WELL!!!!! Dummy favorite games until backend favorites are implemented - const [favoriteGames] = useState([ - { id: 1, title: "Resident Evil 4" }, - { id: 2, title: "Dead Space" }, - { id: 3, title: "Silent Hill 2" }, - { id: 4, title: "Resident Evil Remake" }, - ]); useEffect(() => { async function loadAccount() { try { // Authentication helper makes the authenticated request const res = await Authentication(url, token || ""); + const followers = await getFollowers( localStorage.getItem("username") || ""); + setFollowers(followers.followers); + console.log("Followers count:", followers); + const favorites = await getFavorites(localStorage.getItem("username") || ""); + setFavorites(favorites); + // If token is invalid or expired, redirect to sign-in if (res?.status === 401) { @@ -96,6 +90,8 @@ export default function Account() { } } + + loadAccount(); }, [navigate, token]); @@ -142,7 +138,7 @@ export default function Account() { Date Joined: {formattedJoinDate}

- Followers: {data?.followers ?? 0} + Followers: {followers}

@@ -173,9 +169,16 @@ export default function Account() {

Favorite Games

- {favoriteGames.map((game) => ( -
- {game.title} + {favorites && favorites.map((game) => ( +
+

{game.name}

+ {game.cover && ( + {`${game.name} + )} +
))}
diff --git a/frontend/src/pages/gamePage/gamePage.tsx b/frontend/src/pages/gamePage/gamePage.tsx index 40e9496..38f55a3 100644 --- a/frontend/src/pages/gamePage/gamePage.tsx +++ b/frontend/src/pages/gamePage/gamePage.tsx @@ -9,7 +9,7 @@ import Authentication from "../../auth/endpoints"; import getGame from "../../api/endpoints"; -import { handleFollowGame, handleUnfollowGame, handleSubmitReview, handleAddFavoriteGame, handleUnfavoriteGame, AddToBacklog } from "../../api/endpoints"; +import { handleFollowGame, handleUnfollowGame, handleSubmitReview, handleAddFavoriteGame, handleUnfavoriteGame, handleUnfollowUser, handleFollowUser, AddToBacklog } from "../../api/endpoints"; @@ -27,6 +27,7 @@ function GamePage() { const [followButtonName, setFollowButtonName] = useState("follow"); const [favoriteButtonName, setFavoriteButtonName] = useState("favorite"); const [backlogButtonName, setBacklogButtonName] = useState("add to backlog"); + const [followUserButtonName, setFollowUserButtonName] = useState<{[key:string]: boolean}>({}); const [authenticated, setAuthenticated] = useState(false); @@ -131,16 +132,31 @@ function GamePage() { } }; - const handleFollowUserButton = () => { + const handleFollowUserButton = (username: string) => { // placeholder for follow user functionality + const isFollowing = followUserButtonName[username]; + if (!isFollowing) { + setFollowUserButtonName((prev) => ({ ...prev, [username]: true })); + try{ + console.log("we are ", localStorage.getItem("username"), "and we want to follow ", username); + handleFollowUser(username || "", token || ""); + + } + catch (err) { + console.error("Error following user:", err); + } + } + else { + setFollowUserButtonName((prev) => ({ ...prev, [username]: false })); try{ - // handleFollowUser(userId || "", token || ""); + handleUnfollowUser(username || "", token || ""); } catch (err) { console.error("Error following user:", err); } }; + }; useEffect(() => { async function fetchingData() { @@ -338,7 +354,7 @@ function GamePage() {
{review.username} - {authenticated && } + {authenticated && } {review.formatedDate} diff --git a/frontend/src/types/types.tsx b/frontend/src/types/types.tsx index 23a7611..b63ccd9 100644 --- a/frontend/src/types/types.tsx +++ b/frontend/src/types/types.tsx @@ -63,8 +63,24 @@ export type getReview = { formatedDate: string; }; +export type Review = { + id: number; + gameTitle: string; + rating: number; + reviewText: string; + createdAt: string; +}; + export type postReview = { gameId: number; rating: number; reviewText: string; } +export type FavoriteGame = { + id: number; + name: string; + cover?: { + image_id: string; + }; +}; + From 9bda94a9a093eca10736dd92ddf8ea15d10c5d2d Mon Sep 17 00:00:00 2001 From: Jacob Date: Fri, 17 Apr 2026 21:21:36 -0400 Subject: [PATCH 3/4] fixed some crap --- backend/src/restapi/gametime/urls.py | 5 +- backend/src/restapi/gametime/views.py | 49 ++++++-- frontend/src/api/endpoints.tsx | 147 +++++++++++++++++------ frontend/src/pages/gamePage/gamePage.tsx | 24 +++- 4 files changed, 175 insertions(+), 50 deletions(-) diff --git a/backend/src/restapi/gametime/urls.py b/backend/src/restapi/gametime/urls.py index 52ff6ba..c889b76 100644 --- a/backend/src/restapi/gametime/urls.py +++ b/backend/src/restapi/gametime/urls.py @@ -13,8 +13,11 @@ path('followers//', views.getFollowers), path('favorites//', views.getFavorites), path('user/account/favorites/', views.handleFavorites), + path('user/account/check/favorites//', views.checkFavorites), path('user/account/followed-games/', views.handleFollowGames), + path('user/account/check/followed-games//', views.checkIfFollowGame), path('user/account/followed-users/', views.followUser), - path('user/account/backlog/', views.handleBacklog) + path('user/account/backlog/', views.handleBacklog), + path('user/account/check/backlog//', views.checkBacklog) ] diff --git a/backend/src/restapi/gametime/views.py b/backend/src/restapi/gametime/views.py index 3a2136b..f23af08 100644 --- a/backend/src/restapi/gametime/views.py +++ b/backend/src/restapi/gametime/views.py @@ -1,3 +1,4 @@ +from urllib import request import requests from rest_framework.authtoken.models import Token @@ -200,7 +201,6 @@ def deleteReview(request): date=data['date'], ) review.delete() - review.save() content = { "Review has been deleted!" } @@ -237,7 +237,6 @@ def handleFavorites(request): return Response({"error: Could not fufil request."}, status=400) - @api_view(['POST', 'DELETE']) @permission_classes([IsAuthenticated]) def handleFollowGames(request): @@ -271,22 +270,46 @@ def handleFollowGames(request): return Response({"error: Could not unfollow game."}) +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +def checkIfFollowGame(request, id: int): + user = request.user + exists = FOLLOWGAME.objects.filter(followerID=user, gameID=id).exists() + if exists: + return Response(True, status=200) + else: + return Response(False, status=200) + + @api_view(['POST']) @permission_classes([IsAuthenticated]) def handleBacklog(request): if request.method == 'POST': user = request.user - backlogId = request.data.get("backlogID") - logged = BACKLOG.objects.filter( + backlogId = request.data.get("gameID") + logged = BACKLOG.objects.create( gameID=backlogId, userID=user ) - logged.delete() + logged.save() content = {"message": "Backlog"} return Response(content, status=200) return Response({"Couldn't backlog item"}) +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +def checkBacklog(request, id: int): + user = request.user + exists = BACKLOG.objects.filter(userID=user, gameID=id).exists() + if exists: + return Response(True, status=200) + else: + return Response(False, status=200) + + + + @api_view(['POST', 'DELETE', 'GET']) @permission_classes([IsAuthenticated]) def followUser(request): @@ -314,12 +337,10 @@ def followUser(request): "User has been unfollowed." } return Response(content) - if request.method == 'GET': - count = FOLLOWUSER.objects.filter(following=request.user).count() - return Response(count) return Response({"error: Could not follow user."}, status=400) + @api_view(['GET']) def getFollowers(request, user): if request.method == 'GET': @@ -327,6 +348,7 @@ def getFollowers(request, user): count = user_obj.followers.count() return Response({'followers': count}) + @api_view(['GET']) def getFavorites(request, user): if request.method == 'GET': @@ -334,3 +356,14 @@ def getFavorites(request, user): games = FAVORITES.objects.filter(userID=user_obj) content = gameSerializer(games, many=True).data return Response(content) + + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +def checkFavorites(request, id: int): + user = request.user + exists = FAVORITES.objects.filter(userID=user, gameID=id).exists() + if exists: + return Response(True, status=200) + else: + return Response(False, status=200) \ No newline at end of file diff --git a/frontend/src/api/endpoints.tsx b/frontend/src/api/endpoints.tsx index bdc0c4e..f7778b1 100644 --- a/frontend/src/api/endpoints.tsx +++ b/frontend/src/api/endpoints.tsx @@ -15,46 +15,6 @@ export default async function getGame(id: string) { } -export const getFollowers = async (username: string) => { - try { - const res = await fetch(`http://127.0.0.1:8000/gametime/followers/${username}/`, { method: 'GET' }); - if (!res.ok) { - throw new Error("Failed to fetch followers"); - } - const data = await res.json(); - return data // Assuming the response is an array of followed users - - } - catch (err) { - console.error("Error fetching followers:", err); - throw err; - } - -} - -export const getFavorites = async (username: string) => { - try { - const res = await fetch(`http://127.0.0.1:8000/gametime/favorites/${username}/`, { - method: 'GET', - }); - if (!res.ok) { - throw new Error("Failed to fetch favorites"); - } - const data = await res.json(); - - const favorites = data.map((item: { gameID: number }) => item.gameID).join(", "); - if (favorites.length === 0) { - return []; - } - const gameData = await getGame(favorites); - return gameData; // Assuming the response is an array of followed users - } - catch (err) { - console.error("Error fetching favorites:", err); - throw err; - } -} - @@ -181,6 +141,53 @@ export const handleUnfollowGame = (id: string, token: string) => { } } + +export const checkIfFollowingGame = async (token: string, gameID: string) => { + try { + const res = await fetch(`http://127.0.0.1:8000/gametime/user/account/check/followed-games/${gameID}/`, { + method: 'GET', + headers: { + 'Authorization': `Token ${token}` + }, + + }); + if (!res.ok) { + throw new Error("Failed to fetch followed users"); + } + const data = await res.json(); + return data; + } + catch (err) { + console.error("Error checking if following:", err); + return false; + } +}; + + + +export const getFavorites = async (username: string) => { + try { + const res = await fetch(`http://127.0.0.1:8000/gametime/favorites/${username}/`, { + method: 'GET', + }); + if (!res.ok) { + throw new Error("Failed to fetch favorites"); + } + const data = await res.json(); + + const favorites = data.map((item: { gameID: number }) => item.gameID).join(", "); + if (favorites.length === 0) { + return []; + } + const gameData = await getGame(favorites); + return gameData; // Assuming the response is an array of followed users + } + catch (err) { + console.error("Error fetching favorites:", err); + throw err; + } +} + export const handleAddFavoriteGame = (id: string, token: string) => { try { fetch(`http://127.0.0.1:8000/gametime/user/account/favorites/`, { @@ -239,6 +246,24 @@ export const handleUnfavoriteGame = (id: string, token: string) => { } } +export const checkIfFavorite = async (token: string, gameID: string) => { + try { + const res = await fetch(`http://127.0.0.1:8000/gametime/user/account/check/favorites/${gameID}/`, { + method: 'GET', + headers: { + 'Authorization': `Token ${token}` + }, + }); + + const data = await res.json(); + console.log("Favorite check result:", data); + return data; + } catch (err) { + console.error("Error checking if game is favorite:", err); + return false; + } +}; + export const handleFollowUser = (username: string, token: string) => { try { fetch(`http://127.0.0.1:8000/gametime/user/account/followed-users/`, { @@ -297,6 +322,26 @@ export const handleUnfollowUser = (username: string, token: string) => { } } +export const getFollowers = async (username: string) => { + try { + const res = await fetch(`http://127.0.0.1:8000/gametime/followers/${username}/`, { method: 'GET' }); + if (!res.ok) { + throw new Error("Failed to fetch followers"); + } + const data = await res.json(); + return data // Assuming the response is an array of followed users + + } + catch (err) { + console.error("Error fetching followers:", err); + throw err; + } + +} + + + + export const AddToBacklog = (id: string, token: string) => { try { @@ -326,3 +371,25 @@ export const AddToBacklog = (id: string, token: string) => { console.error("Error adding game to backlog:", err); } } + +export const checkIfInBacklog = async (token: string, gameID: string) => { + try { + const res = await fetch(`http://127.0.0.1:8000/gametime/user/account/check/backlog/${gameID}/`, { + method: 'GET', + headers: { + 'Authorization': `Token ${token}` + }, + }); + if (!res.ok) { + throw new Error("Failed to fetch backlog games"); + } + const data = await res.json(); + console.log("Backlog check result:", data); + return Boolean(data); + } + catch (err) { + console.error("Error checking if in backlog:", err); + throw err; + } +}; + diff --git a/frontend/src/pages/gamePage/gamePage.tsx b/frontend/src/pages/gamePage/gamePage.tsx index 38f55a3..f4f620f 100644 --- a/frontend/src/pages/gamePage/gamePage.tsx +++ b/frontend/src/pages/gamePage/gamePage.tsx @@ -8,7 +8,7 @@ import { useNavigate } from "react-router-dom"; import Authentication from "../../auth/endpoints"; -import getGame from "../../api/endpoints"; +import getGame, { checkIfFavorite, checkIfFollowingGame, checkIfInBacklog } from "../../api/endpoints"; import { handleFollowGame, handleUnfollowGame, handleSubmitReview, handleAddFavoriteGame, handleUnfavoriteGame, handleUnfollowUser, handleFollowUser, AddToBacklog } from "../../api/endpoints"; @@ -29,6 +29,9 @@ function GamePage() { const [backlogButtonName, setBacklogButtonName] = useState("add to backlog"); const [followUserButtonName, setFollowUserButtonName] = useState<{[key:string]: boolean}>({}); + + + const [authenticated, setAuthenticated] = useState(false); @@ -164,6 +167,24 @@ function GamePage() { const res = await Authentication(url, token || ""); + const checkBacklog = await checkIfInBacklog(token || "", id || ""); + if (checkBacklog === true) { + setBacklogButtonName("logged"); + } + + const favorite = await checkIfFavorite( token|| "", id || ""); + if (favorite === true) { + setFavoriteButtonName("unfavorite"); + } + + const followingGame = await checkIfFollowingGame(token || "", id || ""); + if (followingGame === true) { + setFollowButtonName("unfollow"); + } + + + + if (res?.status === 401) { setSubmitButtonName("sign in"); @@ -189,6 +210,7 @@ function GamePage() { } fetchingData(); getReviews(); + }, [id]); From 1cac23db3ea038acab1bcbca3cdb4b5e9bcc6057 Mon Sep 17 00:00:00 2001 From: Jacob Date: Mon, 20 Apr 2026 23:12:08 -0400 Subject: [PATCH 4/4] added more functionality --- backend/src/restapi/gametime/serializers.py | 12 +- backend/src/restapi/gametime/urls.py | 5 +- backend/src/restapi/gametime/views.py | 49 +++----- frontend/src/api/endpoints.tsx | 82 +++++-------- frontend/src/pages/Backlog/backlog.module.css | 79 ++++++++++++ frontend/src/pages/Backlog/backlog.tsx | 52 +++++++- frontend/src/pages/gamePage/gamePage.tsx | 115 +++++++++--------- frontend/src/types/types.tsx | 8 ++ 8 files changed, 251 insertions(+), 151 deletions(-) create mode 100644 frontend/src/pages/Backlog/backlog.module.css diff --git a/backend/src/restapi/gametime/serializers.py b/backend/src/restapi/gametime/serializers.py index 818887c..14c0e4b 100644 --- a/backend/src/restapi/gametime/serializers.py +++ b/backend/src/restapi/gametime/serializers.py @@ -1,5 +1,6 @@ from rest_framework import serializers -from .models import REVIEWS, FAVORITES +from .models import REVIEWS, FAVORITES, BACKLOG, USER + class reviewSerializer(serializers.ModelSerializer): @@ -18,3 +19,12 @@ class gameSerializer(serializers.ModelSerializer): class Meta: model = FAVORITES fields = ['gameID'] + + + +class backlogSerializer(serializers.ModelSerializer): + class Meta: + model = BACKLOG + fields = ['gameID'] + + diff --git a/backend/src/restapi/gametime/urls.py b/backend/src/restapi/gametime/urls.py index c889b76..229388e 100644 --- a/backend/src/restapi/gametime/urls.py +++ b/backend/src/restapi/gametime/urls.py @@ -13,11 +13,10 @@ path('followers//', views.getFollowers), path('favorites//', views.getFavorites), path('user/account/favorites/', views.handleFavorites), - path('user/account/check/favorites//', views.checkFavorites), path('user/account/followed-games/', views.handleFollowGames), - path('user/account/check/followed-games//', views.checkIfFollowGame), path('user/account/followed-users/', views.followUser), path('user/account/backlog/', views.handleBacklog), - path('user/account/check/backlog//', views.checkBacklog) + path('user/account/check/buttons//', views.checkButtons) + ] diff --git a/backend/src/restapi/gametime/views.py b/backend/src/restapi/gametime/views.py index f23af08..ad57464 100644 --- a/backend/src/restapi/gametime/views.py +++ b/backend/src/restapi/gametime/views.py @@ -8,7 +8,7 @@ from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response from .models import USER, REVIEWS, FAVORITES, FOLLOWGAME, FOLLOWUSER, BACKLOG -from .serializers import reviewSerializer, gameSerializer +from .serializers import reviewSerializer, gameSerializer, backlogSerializer # Create your views here. # get the client id from the .env file @@ -270,22 +270,12 @@ def handleFollowGames(request): return Response({"error: Could not unfollow game."}) -@api_view(['GET']) -@permission_classes([IsAuthenticated]) -def checkIfFollowGame(request, id: int): - user = request.user - exists = FOLLOWGAME.objects.filter(followerID=user, gameID=id).exists() - if exists: - return Response(True, status=200) - else: - return Response(False, status=200) - -@api_view(['POST']) +@api_view(['POST', 'GET']) @permission_classes([IsAuthenticated]) def handleBacklog(request): + user = request.user if request.method == 'POST': - user = request.user backlogId = request.data.get("gameID") logged = BACKLOG.objects.create( gameID=backlogId, @@ -294,23 +284,15 @@ def handleBacklog(request): logged.save() content = {"message": "Backlog"} return Response(content, status=200) - return Response({"Couldn't backlog item"}) - - -@api_view(['GET']) -@permission_classes([IsAuthenticated]) -def checkBacklog(request, id: int): - user = request.user - exists = BACKLOG.objects.filter(userID=user, gameID=id).exists() - if exists: - return Response(True, status=200) - else: - return Response(False, status=200) - + if request.method == 'GET': + backlogs = BACKLOG.objects.filter(userID=user) + content = backlogSerializer(backlogs, many=True).data + return Response(content, status=200) + return Response({"Couldn't backlog item"}) -@api_view(['POST', 'DELETE', 'GET']) +@api_view(['POST', 'DELETE']) @permission_classes([IsAuthenticated]) def followUser(request): followerID = request.user @@ -357,13 +339,12 @@ def getFavorites(request, user): content = gameSerializer(games, many=True).data return Response(content) - @api_view(['GET']) @permission_classes([IsAuthenticated]) -def checkFavorites(request, id: int): +def checkButtons(request, id: int): user = request.user - exists = FAVORITES.objects.filter(userID=user, gameID=id).exists() - if exists: - return Response(True, status=200) - else: - return Response(False, status=200) \ No newline at end of file + checkBacklog = BACKLOG.objects.filter(userID=user, gameID=id).exists() + checkFavorites = FAVORITES.objects.filter(userID=user, gameID=id).exists() + checkFollow = FOLLOWGAME.objects.filter(followerID=user, gameID=id).exists() + checks = [checkFollow, checkFavorites, checkBacklog] + return Response(checks, status=200) diff --git a/frontend/src/api/endpoints.tsx b/frontend/src/api/endpoints.tsx index f7778b1..d1c5d63 100644 --- a/frontend/src/api/endpoints.tsx +++ b/frontend/src/api/endpoints.tsx @@ -20,11 +20,6 @@ export default async function getGame(id: string) { export const handleSubmitReview = (token: string, id: string, reviewText: string, selectedRating: number) => { - - - - - if (reviewText.trim() === "" && selectedRating === 0) { return; @@ -72,9 +67,6 @@ export const handleSubmitReview = (token: string, id: string, reviewText: string - - - export const handleFollowGame = (id: string, token: string) => { @@ -142,26 +134,7 @@ export const handleUnfollowGame = (id: string, token: string) => { } -export const checkIfFollowingGame = async (token: string, gameID: string) => { - try { - const res = await fetch(`http://127.0.0.1:8000/gametime/user/account/check/followed-games/${gameID}/`, { - method: 'GET', - headers: { - 'Authorization': `Token ${token}` - }, - }); - if (!res.ok) { - throw new Error("Failed to fetch followed users"); - } - const data = await res.json(); - return data; - } - catch (err) { - console.error("Error checking if following:", err); - return false; - } -}; @@ -246,23 +219,7 @@ export const handleUnfavoriteGame = (id: string, token: string) => { } } -export const checkIfFavorite = async (token: string, gameID: string) => { - try { - const res = await fetch(`http://127.0.0.1:8000/gametime/user/account/check/favorites/${gameID}/`, { - method: 'GET', - headers: { - 'Authorization': `Token ${token}` - }, - }); - const data = await res.json(); - console.log("Favorite check result:", data); - return data; - } catch (err) { - console.error("Error checking if game is favorite:", err); - return false; - } -}; export const handleFollowUser = (username: string, token: string) => { try { @@ -341,8 +298,6 @@ export const getFollowers = async (username: string) => { - - export const AddToBacklog = (id: string, token: string) => { try { fetch(`http://127.0.0.1:8000/gametime/user/account/backlog/`, { @@ -372,23 +327,48 @@ export const AddToBacklog = (id: string, token: string) => { } } -export const checkIfInBacklog = async (token: string, gameID: string) => { +export const getBacklog = async (token: string) => { try { - const res = await fetch(`http://127.0.0.1:8000/gametime/user/account/check/backlog/${gameID}/`, { + const res = await fetch(`http://127.0.0.1:8000/gametime/user/account/backlog/`, { method: 'GET', headers: { - 'Authorization': `Token ${token}` + 'Authorization': `Token ${token}`, }, }); if (!res.ok) { throw new Error("Failed to fetch backlog games"); } const data = await res.json(); - console.log("Backlog check result:", data); - return Boolean(data); + const Backlog = data.map((item: { gameID: number }) => item.gameID).join(", "); + if (Backlog.length === 0) { + return []; + } + const backlogData = await getGame(Backlog); + return backlogData; + } + catch (err) { + console.error("Error fetching backlog games:", err); + throw err; + } +}; + + +export const checkButtons = async (id: string, token: string) => { + try { + const res = await fetch(`http://127.0.0.1:8000/gametime/user/account/check/buttons/${id}/`, { + method: 'GET', + headers: { + 'Authorization': `Token ${token}`, + }, + }); + if (!res.ok) { + throw new Error("Failed to fetch button states"); + } + const data = await res.json(); + return data; } catch (err) { - console.error("Error checking if in backlog:", err); + console.error("Error fetching backlog games:", err); throw err; } }; diff --git a/frontend/src/pages/Backlog/backlog.module.css b/frontend/src/pages/Backlog/backlog.module.css new file mode 100644 index 0000000..deffbbb --- /dev/null +++ b/frontend/src/pages/Backlog/backlog.module.css @@ -0,0 +1,79 @@ +.page { + display: flex; + justify-content: center; + align-items: center; + padding-top: 80px; + + min-height: 100vh; + background: linear-gradient(to bottom, #2b2e33, #1f2227); + background-size: cover; + background-position: center; + color: #e6edf3; + + align-items: flex-start; + padding-left: 2rem; + padding-right: 2rem; +} + + +.backlogScrollBox { + max-height: 360px; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 0.8rem; + padding-right: 0.35rem; +} + + +.panel { + background: #0f1115; + color: white; + padding: 22px; + border-radius: 16px; + border: 1px solid rgba(255, 255, 255, 0.08); +} + +.panel h3 { + margin-top: 0; + margin-bottom: 1rem; + color: white; +} + +.favoriteGrid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 12px; + padding: auto; +} + + + +.favoriteItem { + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 12px; + padding: 0.85rem; + text-align: center; + font-weight: 600; + color: #e6edf3; +} + +.title { + white-space: normal; + overflow-wrap: break-word; + word-break: break-word; + text-align: center; + max-width: 100%; + max-width: 250px; +} + +.image { + width: 100%; + max-width: 180px; + height: auto; + border-radius: 10px; + object-fit: cover; + + transition: transform 0.25s ease, filter 0.25s ease; +} \ No newline at end of file diff --git a/frontend/src/pages/Backlog/backlog.tsx b/frontend/src/pages/Backlog/backlog.tsx index 2ec1ec4..cff6e69 100644 --- a/frontend/src/pages/Backlog/backlog.tsx +++ b/frontend/src/pages/Backlog/backlog.tsx @@ -1,11 +1,53 @@ +import styles from "./backlog.module.css"; +import { useEffect, useState } from "react"; +import type { BacklogGame } from "../../types/types"; +import { getBacklog } from "../../api/endpoints"; + + + + +export default function Backlog() { + + +const [backlog, setBacklog] = useState([]); +const token = localStorage.getItem("token"); + +useEffect(() => { +async function fetchBacklog() { + const backlogData = await getBacklog(token || ""); + console.log("Backlog data:", backlogData); + setBacklog(backlogData); +} + + +fetchBacklog(); +}, []); + + + -function Backlog() { return ( -
-

Backlog

-

This is the backlog page.

+
+
+

Backlog

+ +
+ {backlog && backlog.map((game) => ( +
+

{game.name}

+ {game.cover && ( + {`${game.name} + )} + +
+ ))} +
+
); } -export default Backlog; + diff --git a/frontend/src/pages/gamePage/gamePage.tsx b/frontend/src/pages/gamePage/gamePage.tsx index f4f620f..182e951 100644 --- a/frontend/src/pages/gamePage/gamePage.tsx +++ b/frontend/src/pages/gamePage/gamePage.tsx @@ -8,7 +8,7 @@ import { useNavigate } from "react-router-dom"; import Authentication from "../../auth/endpoints"; -import getGame, { checkIfFavorite, checkIfFollowingGame, checkIfInBacklog } from "../../api/endpoints"; +import getGame, { checkButtons } from "../../api/endpoints"; import { handleFollowGame, handleUnfollowGame, handleSubmitReview, handleAddFavoriteGame, handleUnfavoriteGame, handleUnfollowUser, handleFollowUser, AddToBacklog } from "../../api/endpoints"; @@ -27,19 +27,19 @@ function GamePage() { const [followButtonName, setFollowButtonName] = useState("follow"); const [favoriteButtonName, setFavoriteButtonName] = useState("favorite"); const [backlogButtonName, setBacklogButtonName] = useState("add to backlog"); - const [followUserButtonName, setFollowUserButtonName] = useState<{[key:string]: boolean}>({}); + const [followUserButtonName, setFollowUserButtonName] = useState<{ [key: string]: boolean }>({}); - - - const [authenticated, setAuthenticated] = useState(false); + + + const [authenticated, setAuthenticated] = useState(false); const navigate = useNavigate(); // placeholder till proper implementation const [reviews, setReviews] = useState([]); - + @@ -47,14 +47,14 @@ function GamePage() { - const getReviews = () => { - try{ + const getReviews = () => { + try { fetch(`http://127.0.0.1:8000/gametime/reviews/${id}/`) - .then((res) => res.json()) - .then((data) => { - setReviews(data); - }) - } + .then((res) => res.json()) + .then((data) => { + setReviews(data); + }) + } catch (err) { console.error("Error fetching reviews:", err); } @@ -73,17 +73,17 @@ function GamePage() { // later connect backend here handleSubmitReview(token || "", id || "", reviewText, selectedRating); - - setReviewText(""); - setSelectedRating(0); + + setReviewText(""); + setSelectedRating(0); }; - + const handleFollowGameButton = () => { if (followButtonName === "follow") { - try{ + try { handleFollowGame(id || "", token || ""); setFollowButtonName("unfollow"); } @@ -92,7 +92,7 @@ function GamePage() { } } else { - try{ + try { handleUnfollowGame(id || "", token || ""); setFollowButtonName("follow"); } @@ -104,7 +104,7 @@ function GamePage() { const handleFavoriteGameButton = () => { if (favoriteButtonName === "favorite") { - try{ + try { handleAddFavoriteGame(id || "", token || ""); setFavoriteButtonName("unfavorite"); } @@ -113,7 +113,7 @@ function GamePage() { } } else { - try{ + try { handleUnfavoriteGame(id || "", token || ""); setFavoriteButtonName("favorite"); } @@ -125,7 +125,7 @@ function GamePage() { const handleBacklogButton = () => { if (backlogButtonName === "add to backlog") { - try{ + try { AddToBacklog(id || "", token || ""); setBacklogButtonName("remove from backlog"); } @@ -140,25 +140,25 @@ function GamePage() { const isFollowing = followUserButtonName[username]; if (!isFollowing) { setFollowUserButtonName((prev) => ({ ...prev, [username]: true })); - try{ - console.log("we are ", localStorage.getItem("username"), "and we want to follow ", username); - handleFollowUser(username || "", token || ""); + try { + console.log("we are ", localStorage.getItem("username"), "and we want to follow ", username); + handleFollowUser(username || "", token || ""); - } - catch (err) { - console.error("Error following user:", err); - } + } + catch (err) { + console.error("Error following user:", err); + } } else { setFollowUserButtonName((prev) => ({ ...prev, [username]: false })); - try{ - handleUnfollowUser(username || "", token || ""); + try { + handleUnfollowUser(username || "", token || ""); - } - catch (err) { - console.error("Error following user:", err); - } - }; + } + catch (err) { + console.error("Error following user:", err); + } + }; }; useEffect(() => { @@ -167,23 +167,9 @@ function GamePage() { const res = await Authentication(url, token || ""); - const checkBacklog = await checkIfInBacklog(token || "", id || ""); - if (checkBacklog === true) { - setBacklogButtonName("logged"); - } - const favorite = await checkIfFavorite( token|| "", id || ""); - if (favorite === true) { - setFavoriteButtonName("unfavorite"); - } - const followingGame = await checkIfFollowingGame(token || "", id || ""); - if (followingGame === true) { - setFollowButtonName("unfollow"); - } - - if (res?.status === 401) { setSubmitButtonName("sign in"); @@ -191,16 +177,31 @@ function GamePage() { } else { setSubmitButtonName("submit review"); + setAuthenticated(true); + const buttonData = await checkButtons(id || "", token || ""); + if (buttonData[0] === true) { + setFollowButtonName("unfollow"); + } + + if (buttonData[1] === true) { + setFavoriteButtonName("unfavorite"); + } + + if (buttonData[2] === true) { + setBacklogButtonName("logged"); + } } if (!id) return; + + const gameinfo = await getGame(id); - + setGame(gameinfo[0]); - + } catch (err) { @@ -210,7 +211,7 @@ function GamePage() { } fetchingData(); getReviews(); - + }, [id]); @@ -234,7 +235,7 @@ function GamePage() { - + @@ -324,7 +325,7 @@ function GamePage() { alt={game.name} /> )} - +
{authenticated && ( } {authenticated && }
- +

Release Date: {time}

@@ -376,7 +377,7 @@ function GamePage() {
{review.username} - {authenticated && } + {authenticated && } {review.formatedDate} diff --git a/frontend/src/types/types.tsx b/frontend/src/types/types.tsx index b63ccd9..c063210 100644 --- a/frontend/src/types/types.tsx +++ b/frontend/src/types/types.tsx @@ -84,3 +84,11 @@ export type FavoriteGame = { }; }; +export type BacklogGame = { + id: number; + name: string; + cover?: { + image_id: string; + }; +}; +