Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b8b75dd
Adding backend support for fetching games by their contributors (WIP)
Feb 1, 2026
894a048
Added backend support for fetching games by their contributors
Feb 1, 2026
5867c23
Added hook to fetch game data from backend by contributor id
Feb 1, 2026
5eb4f83
Project section in Member Profile Page now fetches data from backend
Feb 1, 2026
9025848
Added links to the game detail page for each game on the member profile
Feb 2, 2026
8ced90a
Separated sections for games and artwork on Member Profile page.
Feb 3, 2026
a33202f
Merge remote-tracking branch 'origin/main' into issue-70-Add_projects…
Feb 3, 2026
cc8148e
Removed 'line too long' error from game_dev urls.py
Feb 3, 2026
8fced48
Fix link to member pages in IndividualGamePage component
phillipnguyenn Feb 4, 2026
3374f54
Re-positioned member initials to be exactly in the centre of the frame
Feb 7, 2026
47aed70
Added documentation to backend
Feb 7, 2026
f9b6cc0
Removed duplicate GameShowcase path in urls.py
Feb 7, 2026
cd2451c
Prettified error messages in MemberProjectSection
Feb 7, 2026
8dd2a5b
Games link now opens in a new tab
Feb 7, 2026
0d04c12
Merge branch 'main' into issue-70-Add_projects_to_member_pages
Feb 7, 2026
e24bd1e
Removed unused commented code in MemberProfile.tsx
Feb 7, 2026
6e549a1
Merge branch 'main' into issue-70-Add_projects_to_member_pages
belledw Feb 14, 2026
59af88c
Fixing flake8 issue
Feb 14, 2026
8b83aa3
Removed use of pixels unit in MemberProjectSection
Feb 18, 2026
56b0832
Fixed mistake with media serving
SafetyInObscurity Feb 21, 2026
8199a7b
Correcting workflow
SafetyInObscurity Feb 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci-backend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 🧪
Expand All @@ -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
Expand Down
39 changes: 13 additions & 26 deletions client/src/components/main/MemberProfile.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -24,7 +19,6 @@ export type MemberProfileData = {

type MemberProfileProps = {
member: MemberProfileData;
//projects?: MemberProfileProject[];
};

function initialsFromName(name: string) {
Expand Down Expand Up @@ -54,7 +48,7 @@ export function MemberProfile({ member }: MemberProfileProps) {
/>
) : (
<div className="flex h-full w-full items-center justify-center font-jersey10 text-5xl text-muted-foreground">
{initials}
<p className="mb-2"> {initials} </p>
</div>
)}
</div>
Expand Down Expand Up @@ -106,23 +100,16 @@ export function MemberProfile({ member }: MemberProfileProps) {
</div>
</div>
</div>
{/* Template for Projects section */}
<div className="m-auto min-h-80 w-11/12">
<h2 className="mt-7 text-center font-jersey10 text-5xl">Projects</h2>
<div className="m-auto my-5 flex flex-wrap justify-center gap-8">
{/* Div below is a single project card */}
<div className="w-fit rounded-md p-5">
<div className="mb-2 h-44 w-96 overflow-clip rounded-md p-5 text-neutral_1">
{/* Image and/or Link to Project */}
</div>
<p className="max-w-96 font-firaCode text-xl font-semibold">
{/* Project Title */}
</p>
<p className="line-clamp-1 max-w-96 font-firaCode text-[--light-3]">
{/* Project description */}
</p>
</div>
</div>
<div className="m-auto mb-10 min-h-80 w-11/12">
<h2 className="mt-7 flex justify-center text-center font-jersey10 text-5xl">
Games
<Sparkles size={32} className="ml-2 self-center text-yellow-300" />
</h2>
<MemberProjectSection id={window.location.pathname.slice(9)} />
<h2 className="mt-7 flex justify-center text-center font-jersey10 text-5xl">
Artwork
<Palette size={32} className="ml-2 self-center text-yellow-300" />
</h2>
</div>
</>
);
Expand Down
87 changes: 87 additions & 0 deletions client/src/components/ui/MemberProjectSection.tsx
Original file line number Diff line number Diff line change
@@ -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}`;
}
Comment on lines +13 to +22
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is unnecessary. We can just use the URL sent by the backend, and not display an image if necessary.

We definitely shouldn't be hard coding URLs like "http://localhost:8000".


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 (
<div className="mx-auto min-h-screen max-w-7xl px-6 py-16">
<p
className="my-10 text-center font-firaCode text-lg text-red-500"
role="alert"
>
{errorMessage}
</p>
</div>
);
}

return (
<div className="mb-12">
{!games || games.length === 0 ? (
<p className="my-10 text-center font-firaCode text-lg text-[--light-3]">
No games available.
</p>
) : (
<div className="m-auto my-5 flex flex-wrap justify-center gap-8">
{games.map((game) => (
<React.Fragment key={game.game_id}>
<div className="w-fit rounded-md p-5">
<div className="group mb-2 grid h-44 w-96 grid-cols-1 grid-rows-1 overflow-clip rounded-md">
<Image
src={getGameCoverUrl(game.game_data.thumbnail)}
alt={`${game.game_data.name} cover image`}
width={384}
height={176}
className="group-hover:brightness-75 group-hover:duration-200"
/>
<Link
className="mb-16 hidden justify-self-center rounded-md bg-accent p-3 font-firaCode text-light_1 drop-shadow-md hover:underline group-hover:flex group-hover:blur-0 group-hover:duration-200"
href="#"
onClick={() => window.open(`/games/${game.game_id}`)}
>
Visit Game <ArrowUpRight className="ml-1" />
</Link>
</div>
<p className="max-w-96 font-firaCode text-xl font-semibold">
{game.game_data.name}
</p>
<p className="line-clamp-1 max-w-96 font-firaCode text-[--light-3]">
{game.game_data.description}
</p>
</div>
</React.Fragment>
))}
</div>
)}
</div>
);
}
27 changes: 27 additions & 0 deletions client/src/hooks/useContributor.ts
Original file line number Diff line number Diff line change
@@ -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<ApiContributorGamesList[], AxiosError>({
queryKey: ["contributor", member],
queryFn: async () => {
const response = await api.get(`/games/contributor/${member}/`);
return response.data;
},
enabled: !!member,
});
};
7 changes: 5 additions & 2 deletions server/api/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "https://localhost:8000/media/"
else:
MEDIA_URL = f"https://{ALLOWED_HOSTS[0]}/media/"

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

Expand Down
24 changes: 24 additions & 0 deletions server/game_dev/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 8 additions & 3 deletions server/game_dev/urls.py
Original file line number Diff line number Diff line change
@@ -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/<int:id>/", EventDetailAPIView.as_view()),
path("games/<int:id>/", GamesDetailAPIView.as_view()),
path("gameshowcase/", GameshowcaseAPIView.as_view(), name="gameshowcase-api"), # Updated line for GameShowcase endpoint
path("games/contributor/<int:member>/",
ContributorGamesListAPIView.as_view()),
# Updated line for GameShowcase endpoint
path("gameshowcase/", GameshowcaseAPIView.as_view(), name="gameshowcase-api"),
path('members/<int:id>/', MemberAPIView.as_view()),
path("about/", CommitteeAPIView.as_view())
path("about/", CommitteeAPIView.as_view()),
path('members/<int:id>/', MemberAPIView.as_view())
]
18 changes: 16 additions & 2 deletions server/game_dev/views.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -70,6 +70,20 @@ def get(self, request):
return Response(serializer.data)


class ContributorGamesListAPIView(APIView):
"""
GET /api/games/contributor/<member>/
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"
Expand Down
Loading