Skip to content

Commit 9c24a38

Browse files
authored
Merge pull request #91 from codersforcauses/issue-70-Add_projects_to_member_pages
Issue 70 add projects to member pages
2 parents a8bfc95 + 550fa2e commit 9c24a38

9 files changed

Lines changed: 156 additions & 33 deletions

File tree

.github/workflows/ci-backend.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ jobs:
5858
env:
5959
EMAIL_PORT: 1025
6060
FRONTEND_URL: http://localhost:3000
61+
API_ALLOWED_HOSTS: localhost
6162
run: poetry run python manage.py migrate
6263

6364
- name: Run tests 🧪
@@ -66,6 +67,7 @@ jobs:
6667
JWT_SIGNING_KEY: NjMgNmYgNmQgNmQgNzUgNmUgNjkgNzQgNzkgNzMgNzAgNjkgNzIgNjkgNzQgNjYgNmYgNzUgNmUgNjQgNjEgNzQgNjkgNmYgNmU=
6768
EMAIL_PORT: 1025
6869
FRONTEND_URL: http://localhost:3000
70+
API_ALLOWED_HOSTS: localhost
6971
run: |
7072
poetry run python3 -m pip install coverage
7173
poetry run coverage run manage.py test

client/src/components/main/MemberProfile.tsx

Lines changed: 13 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,10 @@
11
"use client";
22

3+
import { Palette, Sparkles } from "lucide-react";
34
import Image from "next/image";
45
import { SocialIcon } from "react-social-icons";
56

6-
// unused atm, as the member isnt linked a project on the backend
7-
/* export type MemberProfileProject = {
8-
id: string;
9-
name: string;
10-
description?: string;
11-
href?: string;
12-
}; */
7+
import MemberProjectSection from "../ui/MemberProjectSection";
138

149
export type MemberProfileData = {
1510
name: string;
@@ -25,7 +20,6 @@ export type MemberProfileData = {
2520

2621
type MemberProfileProps = {
2722
member: MemberProfileData;
28-
//projects?: MemberProfileProject[];
2923
};
3024

3125
function initialsFromName(name: string) {
@@ -55,7 +49,7 @@ export function MemberProfile({ member }: MemberProfileProps) {
5549
/>
5650
) : (
5751
<div className="flex h-full w-full items-center justify-center font-jersey10 text-5xl text-muted-foreground">
58-
{initials}
52+
<p className="mb-2"> {initials} </p>
5953
</div>
6054
)}
6155
</div>
@@ -107,23 +101,16 @@ export function MemberProfile({ member }: MemberProfileProps) {
107101
</div>
108102
</div>
109103
</div>
110-
{/* Template for Projects section */}
111-
<div className="m-auto min-h-80 w-11/12">
112-
<h2 className="mt-7 text-center font-jersey10 text-5xl">Projects</h2>
113-
<div className="m-auto my-5 flex flex-wrap justify-center gap-8">
114-
{/* Div below is a single project card */}
115-
<div className="w-fit rounded-md p-5">
116-
<div className="mb-2 h-44 w-96 overflow-clip rounded-md p-5 text-neutral_1">
117-
{/* Image and/or Link to Project */}
118-
</div>
119-
<p className="max-w-96 font-firaCode text-xl font-semibold">
120-
{/* Project Title */}
121-
</p>
122-
<p className="line-clamp-1 max-w-96 font-firaCode text-[--light-3]">
123-
{/* Project description */}
124-
</p>
125-
</div>
126-
</div>
104+
<div className="m-auto mb-10 min-h-80 w-11/12">
105+
<h2 className="mt-7 flex justify-center text-center font-jersey10 text-5xl">
106+
Games
107+
<Sparkles size={32} className="ml-2 self-center text-yellow-300" />
108+
</h2>
109+
<MemberProjectSection id={window.location.pathname.slice(9)} />
110+
<h2 className="mt-7 flex justify-center text-center font-jersey10 text-5xl">
111+
Artwork
112+
<Palette size={32} className="ml-2 self-center text-yellow-300" />
113+
</h2>
127114
</div>
128115
</>
129116
);
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { ArrowUpRight } from "lucide-react";
2+
import Image from "next/image";
3+
import Link from "next/link";
4+
import React from "react";
5+
6+
import { useContributor } from "@/hooks/useContributor";
7+
8+
type MemberProjectSectionProps = {
9+
id: string;
10+
};
11+
12+
export default function MemberProjectSection(props: MemberProjectSectionProps) {
13+
const { data: games, isError, error } = useContributor(props.id);
14+
15+
{
16+
/* Error handling from Games Showcase page */
17+
}
18+
if (isError) {
19+
const errorMessage =
20+
error?.response?.status === 404
21+
? "Games not found."
22+
: "Failed to Load Games";
23+
return (
24+
<div className="mx-auto min-h-screen max-w-7xl px-6 py-16">
25+
<p
26+
className="my-10 text-center font-firaCode text-lg text-red-500"
27+
role="alert"
28+
>
29+
{errorMessage}
30+
</p>
31+
</div>
32+
);
33+
}
34+
35+
return (
36+
<div className="mb-12">
37+
{!games || games.length === 0 ? (
38+
<p className="my-10 text-center font-firaCode text-lg text-[--light-3]">
39+
No games available.
40+
</p>
41+
) : (
42+
<div className="m-auto my-5 flex flex-wrap justify-center gap-8">
43+
{games.map((game) => (
44+
<React.Fragment key={game.game_id}>
45+
<div className="w-fit rounded-md p-5">
46+
<div className="group mb-2 grid h-44 w-96 grid-cols-1 grid-rows-1 overflow-clip rounded-md">
47+
<Image
48+
src={game.game_data.thumbnail}
49+
alt={`${game.game_data.name} cover image`}
50+
width={384}
51+
height={176}
52+
className="group-hover:brightness-75 group-hover:duration-200"
53+
/>
54+
<Link
55+
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"
56+
href="#"
57+
onClick={() => window.open(`/games/${game.game_id}`)}
58+
>
59+
Visit Game <ArrowUpRight className="ml-1" />
60+
</Link>
61+
</div>
62+
<p className="max-w-96 font-firaCode text-xl font-semibold">
63+
{game.game_data.name}
64+
</p>
65+
<p className="line-clamp-1 max-w-96 font-firaCode text-[--light-3]">
66+
{game.game_data.description}
67+
</p>
68+
</div>
69+
</React.Fragment>
70+
))}
71+
</div>
72+
)}
73+
</div>
74+
);
75+
}

client/src/hooks/useContributor.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { useQuery } from "@tanstack/react-query";
2+
import { AxiosError } from "axios";
3+
4+
import api from "@/lib/api";
5+
6+
type ApiContributorGameData = {
7+
name: string;
8+
thumbnail: string;
9+
description: string;
10+
};
11+
12+
type ApiContributorGamesList = {
13+
game_id: number;
14+
role: string;
15+
game_data: ApiContributorGameData;
16+
};
17+
18+
export const useContributor = (member: string | string[] | undefined) => {
19+
return useQuery<ApiContributorGamesList[], AxiosError>({
20+
queryKey: ["contributor", member],
21+
queryFn: async () => {
22+
const response = await api.get(`/games/contributor/${member}/`);
23+
return response.data;
24+
},
25+
enabled: !!member,
26+
});
27+
};

client/src/pages/_app.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
55
import type { AppProps } from "next/app";
66
import { Fira_Code, Inter as FontSans, Jersey_10 } from "next/font/google";
77

8-
import Navbar from "@/components/main/Navbar";
98
import Footer from "@/components/main/Footer";
9+
import Navbar from "@/components/main/Navbar";
1010
import { ExplosionProvider } from "@/contexts/ExplosionContext";
1111

1212
const fontSans = FontSans({

client/src/pages/games/[id].tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export default function IndividualGamePage() {
8989
<main>
9090
<section className="w-full items-center justify-center bg-popover">
9191
<div className="mx-auto flex max-w-7xl justify-center p-0 sm:p-8">
92-
{gameEmbedID != "0" ? (
92+
{gameEmbedID ? (
9393
<div className="m-auto flex overflow-auto">
9494
<GameEmbed
9595
embedID={gameEmbedID}

server/game_dev/serializers.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,25 @@ def get_contributors(self, obj):
100100
return ShowcaseContributorSerializer(contributors, many=True).data
101101

102102

103+
class ContributorGameSerializer(serializers.ModelSerializer):
104+
game_id = serializers.IntegerField(source='game.id', read_only=True)
105+
role = serializers.CharField(read_only=True)
106+
game_data = serializers.SerializerMethodField()
107+
108+
class Meta:
109+
model = GameContributor
110+
fields = ['game_id', 'role', 'game_data']
111+
112+
def get_game_data(self, obj):
113+
game = obj.game
114+
request = self.context.get('request')
115+
return {
116+
'name': game.name,
117+
'description': game.description,
118+
'thumbnail': request.build_absolute_uri(game.thumbnail.url) if game.thumbnail and request else None
119+
}
120+
121+
103122
class SocialMediaSerializer(serializers.ModelSerializer):
104123
class Meta:
105124
model = SocialMedia

server/game_dev/urls.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
from django.urls import path
2-
from .views import EventListAPIView, EventDetailAPIView, GamesDetailAPIView, GameshowcaseAPIView, MemberAPIView, CommitteeAPIView
2+
from .views import ContributorGamesListAPIView, EventListAPIView, EventDetailAPIView
3+
from .views import GamesDetailAPIView, GameshowcaseAPIView, MemberAPIView, CommitteeAPIView
34

45
urlpatterns = [
56
path("events/", EventListAPIView.as_view(), name="events-list"),
67
path("events/<int:id>/", EventDetailAPIView.as_view()),
78
path("games/<int:id>/", GamesDetailAPIView.as_view()),
8-
path("gameshowcase/", GameshowcaseAPIView.as_view(), name="gameshowcase-api"), # Updated line for GameShowcase endpoint
9+
path("games/contributor/<int:member>/",
10+
ContributorGamesListAPIView.as_view()),
11+
# Updated line for GameShowcase endpoint
12+
path("gameshowcase/", GameshowcaseAPIView.as_view(), name="gameshowcase-api"),
913
path('members/<int:id>/', MemberAPIView.as_view()),
10-
path("about/", CommitteeAPIView.as_view())
14+
path("about/", CommitteeAPIView.as_view()),
15+
path('members/<int:id>/', MemberAPIView.as_view())
1116
]

server/game_dev/views.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from rest_framework import generics
2-
from .serializers import GamesSerializer, GameshowcaseSerializer, EventSerializer, MemberSerializer
3-
from .models import Game, GameShowcase, Event, Member, Committee
2+
from .serializers import ContributorGameSerializer, GamesSerializer, GameshowcaseSerializer, EventSerializer, MemberSerializer
3+
from .models import Game, GameContributor, GameShowcase, Event, Member, Committee
44
from django.utils import timezone
55
from rest_framework.views import APIView
66
from rest_framework.response import Response
@@ -70,6 +70,14 @@ def get(self, request):
7070
return Response(serializer.data)
7171

7272

73+
class ContributorGamesListAPIView(generics.ListAPIView):
74+
serializer_class = ContributorGameSerializer
75+
76+
def get_queryset(self):
77+
member_id = self.kwargs.get("member")
78+
return GameContributor.objects.filter(member=member_id)
79+
80+
7381
class MemberAPIView(generics.RetrieveAPIView):
7482
serializer_class = MemberSerializer
7583
lookup_field = "id"

0 commit comments

Comments
 (0)