Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
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="mt-[-165px] hidden place-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"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please don't use pixel units for css. The standard tailwind units are much more consistent and responsive.

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,
});
};
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