diff --git a/.github/workflows/ci-backend.yml b/.github/workflows/ci-backend.yml index 9f10b043..2f6583d6 100644 --- a/.github/workflows/ci-backend.yml +++ b/.github/workflows/ci-backend.yml @@ -58,6 +58,7 @@ jobs: env: EMAIL_PORT: 1025 FRONTEND_URL: http://localhost:3000 + API_ALLOWED_HOSTS: localhost run: poetry run python manage.py migrate - name: Run tests 🧪 @@ -66,6 +67,7 @@ jobs: JWT_SIGNING_KEY: NjMgNmYgNmQgNmQgNzUgNmUgNjkgNzQgNzkgNzMgNzAgNjkgNzIgNjkgNzQgNjYgNmYgNzUgNmUgNjQgNjEgNzQgNjkgNmYgNmU= EMAIL_PORT: 1025 FRONTEND_URL: http://localhost:3000 + API_ALLOWED_HOSTS: localhost run: | poetry run python3 -m pip install coverage poetry run coverage run manage.py test diff --git a/client/src/components/main/MemberProfile.tsx b/client/src/components/main/MemberProfile.tsx index 859d4b6b..c66780b3 100644 --- a/client/src/components/main/MemberProfile.tsx +++ b/client/src/components/main/MemberProfile.tsx @@ -1,15 +1,10 @@ "use client"; +import { Palette, Sparkles } from "lucide-react"; import Image from "next/image"; import { SocialIcon } from "react-social-icons"; -// unused atm, as the member isnt linked a project on the backend -/* export type MemberProfileProject = { - id: string; - name: string; - description?: string; - href?: string; -}; */ +import MemberProjectSection from "../ui/MemberProjectSection"; export type MemberProfileData = { name: string; @@ -25,7 +20,6 @@ export type MemberProfileData = { type MemberProfileProps = { member: MemberProfileData; - //projects?: MemberProfileProject[]; }; function initialsFromName(name: string) { @@ -55,7 +49,7 @@ export function MemberProfile({ member }: MemberProfileProps) { /> ) : (
- {initials} +

{initials}

)} @@ -107,23 +101,16 @@ export function MemberProfile({ member }: MemberProfileProps) { - {/* Template for Projects section */} -
-

Projects

-
- {/* Div below is a single project card */} -
-
- {/* Image and/or Link to Project */} -
-

- {/* Project Title */} -

-

- {/* Project description */} -

-
-
+
+

+ Games + +

+ +

+ Artwork + +

); diff --git a/client/src/components/ui/MemberProjectSection.tsx b/client/src/components/ui/MemberProjectSection.tsx new file mode 100644 index 00000000..046ca556 --- /dev/null +++ b/client/src/components/ui/MemberProjectSection.tsx @@ -0,0 +1,75 @@ +import { ArrowUpRight } from "lucide-react"; +import Image from "next/image"; +import Link from "next/link"; +import React from "react"; + +import { useContributor } from "@/hooks/useContributor"; + +type MemberProjectSectionProps = { + id: string; +}; + +export default function MemberProjectSection(props: MemberProjectSectionProps) { + const { data: games, isError, error } = useContributor(props.id); + + { + /* Error handling from Games Showcase page */ + } + if (isError) { + const errorMessage = + error?.response?.status === 404 + ? "Games not found." + : "Failed to Load Games"; + return ( +
+

+ {errorMessage} +

+
+ ); + } + + return ( +
+ {!games || games.length === 0 ? ( +

+ No games available. +

+ ) : ( +
+ {games.map((game) => ( + +
+
+ {`${game.game_data.name} + window.open(`/games/${game.game_id}`)} + > + Visit Game + +
+

+ {game.game_data.name} +

+

+ {game.game_data.description} +

+
+
+ ))} +
+ )} +
+ ); +} diff --git a/client/src/hooks/useContributor.ts b/client/src/hooks/useContributor.ts new file mode 100644 index 00000000..d29e7843 --- /dev/null +++ b/client/src/hooks/useContributor.ts @@ -0,0 +1,27 @@ +import { useQuery } from "@tanstack/react-query"; +import { AxiosError } from "axios"; + +import api from "@/lib/api"; + +type ApiContributorGameData = { + name: string; + thumbnail: string; + description: string; +}; + +type ApiContributorGamesList = { + game_id: number; + role: string; + game_data: ApiContributorGameData; +}; + +export const useContributor = (member: string | string[] | undefined) => { + return useQuery({ + queryKey: ["contributor", member], + queryFn: async () => { + const response = await api.get(`/games/contributor/${member}/`); + return response.data; + }, + enabled: !!member, + }); +}; diff --git a/server/api/settings.py b/server/api/settings.py index 1f4df893..699f1a5a 100644 --- a/server/api/settings.py +++ b/server/api/settings.py @@ -148,8 +148,11 @@ # The directory to store images and other media MEDIA_ROOT = BASE_DIR/"media" -# The path to serve images and other media -MEDIA_URL = "/media/" +# The url to serve images and other media +if DEBUG: + MEDIA_URL = "http://localhost:8000/media/" +else: + MEDIA_URL = f"https://{ALLOWED_HOSTS[0]}/media/" DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" diff --git a/server/game_dev/serializers.py b/server/game_dev/serializers.py index c576aaa3..cea76ec0 100644 --- a/server/game_dev/serializers.py +++ b/server/game_dev/serializers.py @@ -82,6 +82,30 @@ def get_contributors(self, obj): return ShowcaseContributorSerializer(contributors, many=True).data +class ContributorGameDataSerializer(serializers.ModelSerializer): + # Serializes data in Game model to display on a contributor's profile. + + class Meta: + model = Game + fields = ('name', 'thumbnail', + 'description') + + +class ContributorGameSerializer(serializers.ModelSerializer): + # Matches games in the GameContributor model to the information about them in the Game model. + game_id = serializers.IntegerField(source='game.id', read_only=True) + role = serializers.CharField(read_only=True) + game_data = serializers.SerializerMethodField() + + class Meta: + model = GameContributor + fields = ['game_id', 'role', 'game_data'] + + def get_game_data(self, obj): + game_data = Game.objects.get(id=obj.game_id) + return ContributorGameDataSerializer(game_data).data + + class SocialMediaSerializer(serializers.ModelSerializer): class Meta: model = SocialMedia diff --git a/server/game_dev/urls.py b/server/game_dev/urls.py index 45a1e362..7e103bf9 100644 --- a/server/game_dev/urls.py +++ b/server/game_dev/urls.py @@ -1,11 +1,16 @@ from django.urls import path -from .views import EventListAPIView, EventDetailAPIView, GamesDetailAPIView, GameshowcaseAPIView, MemberAPIView, CommitteeAPIView +from .views import ContributorGamesListAPIView, EventListAPIView, EventDetailAPIView +from .views import GamesDetailAPIView, GameshowcaseAPIView, MemberAPIView, CommitteeAPIView urlpatterns = [ path("events/", EventListAPIView.as_view(), name="events-list"), path("events//", EventDetailAPIView.as_view()), path("games//", GamesDetailAPIView.as_view()), - path("gameshowcase/", GameshowcaseAPIView.as_view(), name="gameshowcase-api"), # Updated line for GameShowcase endpoint + path("games/contributor//", + ContributorGamesListAPIView.as_view()), + # Updated line for GameShowcase endpoint + path("gameshowcase/", GameshowcaseAPIView.as_view(), name="gameshowcase-api"), path('members//', MemberAPIView.as_view()), - path("about/", CommitteeAPIView.as_view()) + path("about/", CommitteeAPIView.as_view()), + path('members//', MemberAPIView.as_view()) ] diff --git a/server/game_dev/views.py b/server/game_dev/views.py index 086119cf..5fc8520f 100644 --- a/server/game_dev/views.py +++ b/server/game_dev/views.py @@ -1,6 +1,6 @@ from rest_framework import generics -from .serializers import GamesSerializer, GameshowcaseSerializer, EventSerializer, MemberSerializer -from .models import Game, GameShowcase, Event, Member, Committee +from .serializers import ContributorGameSerializer, GamesSerializer, GameshowcaseSerializer, EventSerializer, MemberSerializer +from .models import Game, GameContributor, GameShowcase, Event, Member, Committee from django.utils import timezone from rest_framework.views import APIView from rest_framework.response import Response @@ -70,6 +70,20 @@ def get(self, request): return Response(serializer.data) +class ContributorGamesListAPIView(APIView): + """ + GET /api/games/contributor// + Returns the games a particular member has contributed to. + """ + lookup_url_kwarg = "member" + + def get(self, request, member): + contributions = GameContributor.objects.filter( + member=self.kwargs["member"]) + serializer = ContributorGameSerializer(contributions, many=True) + return Response(serializer.data) + + class MemberAPIView(generics.RetrieveAPIView): serializer_class = MemberSerializer lookup_field = "id"