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/.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/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/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/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/serializers.py b/backend/src/restapi/gametime/serializers.py index f7219a9..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 +from .models import REVIEWS, FAVORITES, BACKLOG, USER + class reviewSerializer(serializers.ModelSerializer): @@ -12,3 +13,18 @@ class reviewSerializer(serializers.ModelSerializer): class Meta: model = REVIEWS fields = ['username', 'review', 'rating', 'formatedDate'] + + +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 f3bccfc..229388e 100644 --- a/backend/src/restapi/gametime/urls.py +++ b/backend/src/restapi/gametime/urls.py @@ -10,8 +10,13 @@ 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('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), + path('user/account/backlog/', views.handleBacklog), + path('user/account/check/buttons//', views.checkButtons) + ] diff --git a/backend/src/restapi/gametime/views.py b/backend/src/restapi/gametime/views.py index 2a20647..ad57464 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 @@ -6,8 +7,8 @@ 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 .serializers import reviewSerializer +from .models import USER, REVIEWS, FAVORITES, FOLLOWGAME, FOLLOWUSER, BACKLOG +from .serializers import reviewSerializer, gameSerializer, backlogSerializer # Create your views here. # get the client id from the .env file @@ -200,7 +201,6 @@ def deleteReview(request): date=data['date'], ) review.delete() - review.save() content = { "Review has been deleted!" } @@ -208,69 +208,61 @@ 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,36 +270,81 @@ def unfollowGame(request): return Response({"error: Could not unfollow game."}) -@api_view(['POST']) + +@api_view(['POST', 'GET']) +@permission_classes([IsAuthenticated]) +def handleBacklog(request): + user = request.user + if request.method == 'POST': + backlogId = request.data.get("gameID") + logged = BACKLOG.objects.create( + gameID=backlogId, + userID=user + ) + logged.save() + content = {"message": "Backlog"} + return Response(content, 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']) @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 userfollows = FOLLOWUSER.objects.create( - followerID=data['followerID'], - 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 userfollows = FOLLOWUSER.objects.get( - followerID=data['followerID'], - 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) + + 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) + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +def checkButtons(request, id: int): + user = request.user + 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/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..d1c5d63 --- /dev/null +++ b/frontend/src/api/endpoints.tsx @@ -0,0 +1,375 @@ + +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 }), + }) + .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 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/`, { + 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 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 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 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 { + 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 getBacklog = async (token: string) => { + try { + const res = await fetch(`http://127.0.0.1:8000/gametime/user/account/backlog/`, { + method: 'GET', + headers: { + 'Authorization': `Token ${token}`, + }, + }); + if (!res.ok) { + throw new Error("Failed to fetch backlog games"); + } + const data = await res.json(); + 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 fetching backlog games:", err); + throw 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..0ea9688 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,17 @@ 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; + } @@ -92,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; } @@ -186,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 674d522..121a61c 100644 --- a/frontend/src/pages/Account/Account.tsx +++ b/frontend/src/pages/Account/Account.tsx @@ -1,21 +1,12 @@ 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"; +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]); @@ -128,9 +124,10 @@ export default function Account() {
{/* Main account information box */}
-

Account

+
+

Account

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

@@ -141,7 +138,7 @@ export default function Account() { Date Joined: {formattedJoinDate}

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

@@ -155,6 +152,16 @@ export default function Account() { > Sign Out + +
{/* Favorite games box */} @@ -162,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/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.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..182e951 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, { checkButtons } from "../../api/endpoints"; +import { handleFollowGame, handleUnfollowGame, handleSubmitReview, handleAddFavoriteGame, handleUnfavoriteGame, handleUnfollowUser, handleFollowUser, AddToBacklog } from "../../api/endpoints"; @@ -18,12 +22,24 @@ 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 [followUserButtonName, setFollowUserButtonName] = useState<{ [key: string]: boolean }>({}); + + + + + + const [authenticated, setAuthenticated] = useState(false); + const navigate = useNavigate(); // placeholder till proper implementation const [reviews, setReviews] = useState([]); - + @@ -31,51 +47,171 @@ 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); } }; + + + 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 = (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 { + handleUnfollowUser(username || "", token || ""); + + } + catch (err) { + console.error("Error following user:", err); + } + }; + }; + useEffect(() => { async function fetchingData() { try { 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); + 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]); + - 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)); } catch (err) { console.error("Game page authentication error:", err); - setButtonName("sign in"); + setSubmitButtonName("sign in"); } } fetchingData(); getReviews(); + }, [id]); @@ -99,59 +235,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); - } - }; @@ -242,6 +326,17 @@ function GamePage() { /> )} +
+ {authenticated && ( + + )} + {authenticated && } + {authenticated && } +
+ +

Release Date: {time}

@@ -282,6 +377,7 @@ function GamePage() {

{review.username} + {authenticated && } {review.formatedDate} @@ -308,7 +404,7 @@ function GamePage() {

Write a Review

-
+
@@ -332,7 +428,7 @@ function GamePage() { />
diff --git a/frontend/src/types/types.tsx b/frontend/src/types/types.tsx index 23a7611..c063210 100644 --- a/frontend/src/types/types.tsx +++ b/frontend/src/types/types.tsx @@ -63,8 +63,32 @@ 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; + }; +}; + +export type BacklogGame = { + id: number; + name: string; + cover?: { + image_id: string; + }; +}; +