diff --git a/client/src/components/main/MemberProfile.tsx b/client/src/components/main/MemberProfile.tsx index 13e9e434..23bb0cab 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; @@ -24,7 +19,6 @@ export type MemberProfileData = { type MemberProfileProps = { member: MemberProfileData; - //projects?: MemberProfileProject[]; }; function initialsFromName(name: string) { @@ -54,7 +48,7 @@ export function MemberProfile({ member }: MemberProfileProps) { /> ) : (
- {initials} +

{initials}

)} @@ -106,23 +100,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..1b5f7e9a --- /dev/null +++ b/client/src/components/ui/MemberProjectSection.tsx @@ -0,0 +1,87 @@ +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; +}; + +// From useGamesShowcase +function getGameCoverUrl( + game_cover_thumbnail: string | null | undefined, +): string { + if (!game_cover_thumbnail) return "/game_dev_club_logo.svg"; + if (game_cover_thumbnail.startsWith("http")) return game_cover_thumbnail; + // Use environment variable for Django backend base URL + const apiBaseUrl = + process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"; + return `${apiBaseUrl}${game_cover_thumbnail}`; +} + +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/game_dev/serializers.py b/server/game_dev/serializers.py index 4d278157..90db3a20 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 40ea5457..d1f7c42f 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"