diff --git a/client/src/hooks/useCommittee.ts b/client/src/hooks/useCommittee.ts new file mode 100644 index 0000000..69c6fb8 --- /dev/null +++ b/client/src/hooks/useCommittee.ts @@ -0,0 +1,28 @@ +import { useQuery } from "@tanstack/react-query"; +import { AxiosError } from "axios"; + +import api from "@/lib/api"; + +export type ApiMember = { + name: string; + profile_picture: string; + pronouns: string; + about: string; +}; + +export function useCommittee() { + return useQuery({ + queryKey: ["role"], + queryFn: async () => { + const response = await api.get("/about/"); + console.log(response.data); + return response.data; + }, + retry: (failureCount, error) => { + if (error?.response?.status === 404) { + return false; + } + return failureCount < 3; + }, + }); +} diff --git a/client/src/pages/about.tsx b/client/src/pages/about.tsx new file mode 100644 index 0000000..996a569 --- /dev/null +++ b/client/src/pages/about.tsx @@ -0,0 +1,181 @@ +import Image from "next/image"; + +import { ApiMember, useCommittee } from "@/hooks/useCommittee"; + +export default function AboutPage() { + const { data: committee, isPending, error, isError } = useCommittee(); + + const topRow: ApiMember[] = []; + const bottomRow: ApiMember[] = []; + //lists that will be populated with member objects in the committee + const roleOrder = [ + "President", + "Vice President", + "Secretary", + "Treasurer", + "Marketing", + "Events OCM", + "Projects OCM", + "Fresher Rep", + ]; + + const about = ( + <> +
+
+
+

+ About Us +

+ +
+
+ /landing_placeholder.png +
+
+
+
+ {/* Our Committee Title Section - LIGHT - Full Width */} +
+
+

Our Committee

+
+
+ + ); + + if (isPending) { + for (let i = 0; i < 8; i++) { + if (i < 4) { + topRow.push({ + name: "Loading...", + pronouns: "", + profile_picture: "/landing_placeholder.png", + about: "", + }); + } else { + bottomRow.push({ + name: "Loading...", + pronouns: "", + profile_picture: "/landing_placeholder.png", + about: "", + }); + } + } + } else if (isError) { + const errorMessage = + error?.response?.status === 404 + ? "Committee Members not found." + : "Failed to load Committee Members."; + + return ( + <> + {about} +
+

+ {errorMessage} +

+
+ + ); + } else { + for (let i = 0; i < 8; i++) { + if (i < 4) { + topRow.push(committee[i]); + } else { + bottomRow.push(committee[i]); + } + } + } + + return ( +
+ {about} + {/* Portraits Section - DARK - Full Width */} +
+
+ {/* Top row - 4 Presidents */} +
+ {topRow.map((member, idx) => ( +
+
+ /landing_placeholder.png +
+
+

+ {member.name} {member.pronouns} +

+

+ {roleOrder[idx]} +

+
+
+ ))} +
+ + {/* Bottom row - 4 other roles */} +
+ {bottomRow.map((member, idx) => ( +
+
+ /landing_placeholder.png +
+
+

+ {member.name} {member.pronouns} +

+

+ {roleOrder[4 + idx]} +

+
+
+ ))} +
+
+
+
+ ); +} diff --git a/server/game_dev/admin.py b/server/game_dev/admin.py index ff6fcf2..d07b4da 100644 --- a/server/game_dev/admin.py +++ b/server/game_dev/admin.py @@ -1,9 +1,10 @@ from django.contrib import admin -from .models import Member, Game, Event, GameContributor, GameShowcase +from .models import Member, Game, Event, GameContributor, GameShowcase, Committee class MemberAdmin(admin.ModelAdmin): - pass + list_display = ("id", "name", "active", "profile_picture", "about", "pronouns") + search_fields = ["name", "about"] # Sample EventsAdmin Class made @@ -24,8 +25,13 @@ class GamesAdmin(admin.ModelAdmin): search_fields = ["name", "description"] +class CommitteeAdmin(admin.ModelAdmin): + raw_id_fields = ["id"] + + admin.site.register(Member, MemberAdmin) admin.site.register(Event, EventAdmin) admin.site.register(Game, GamesAdmin) admin.site.register(GameContributor, GameContributorAdmin) admin.site.register(GameShowcase, GameShowcaseAdmin) +admin.site.register(Committee, CommitteeAdmin) diff --git a/server/game_dev/migrations/0005_committee.py b/server/game_dev/migrations/0005_committee.py new file mode 100644 index 0000000..271dee1 --- /dev/null +++ b/server/game_dev/migrations/0005_committee.py @@ -0,0 +1,20 @@ +# Generated by Django 5.1.15 on 2026-01-09 09:46 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('game_dev', '0004_alter_event_date'), + ] + + operations = [ + migrations.CreateModel( + name='Committee', + fields=[ + ('id', models.OneToOneField(on_delete=django.db.models.deletion.DO_NOTHING, primary_key=True, serialize=False, to='game_dev.member')), + ], + ), + ] diff --git a/server/game_dev/migrations/0006_committee_role.py b/server/game_dev/migrations/0006_committee_role.py new file mode 100644 index 0000000..457afa8 --- /dev/null +++ b/server/game_dev/migrations/0006_committee_role.py @@ -0,0 +1,20 @@ +# Generated by Django 5.1.15 on 2026-01-09 09:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('game_dev', '0005_committee'), + ] + + operations = [ + migrations.AddField( + model_name='committee', + name='role', + field=models.CharField(choices=[('P', 'President'), ('VP', 'Vice-President'), ('SEC', 'Secretary'), + ('TRE', 'Treasurer'), ('MARK', 'Marketing'), ('EV', 'Events OCM'), + ('PRO', 'Projects OCM'), ('FRE', 'Fresher Rep')], default='FRE', max_length=9), + ), + ] diff --git a/server/game_dev/migrations/0007_alter_committee_id.py b/server/game_dev/migrations/0007_alter_committee_id.py new file mode 100644 index 0000000..fa9a484 --- /dev/null +++ b/server/game_dev/migrations/0007_alter_committee_id.py @@ -0,0 +1,19 @@ +# Generated by Django 5.1.15 on 2026-01-21 07:59 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('game_dev', '0006_committee_role'), + ] + + operations = [ + migrations.AlterField( + model_name='committee', + name='id', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='game_dev.member'), + ), + ] diff --git a/server/game_dev/migrations/0008_alter_committee_role.py b/server/game_dev/migrations/0008_alter_committee_role.py new file mode 100644 index 0000000..ecd84c0 --- /dev/null +++ b/server/game_dev/migrations/0008_alter_committee_role.py @@ -0,0 +1,20 @@ +# Generated by Django 5.1.15 on 2026-01-21 08:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('game_dev', '0007_alter_committee_id'), + ] + + operations = [ + migrations.AlterField( + model_name='committee', + name='role', + field=models.CharField(choices=[('P', 'President'), ('VP', 'Vice-President'), ('SEC', 'Secretary'), + ('TRE', 'Treasurer'), ('MARK', 'Marketing'), ('EV', 'Events OCM'), + ('PRO', 'Projects OCM'), ('FRE', 'Fresher Rep')], default='FRE', max_length=9, unique=True), + ), + ] diff --git a/server/game_dev/migrations/0009_merge_20260129_2104.py b/server/game_dev/migrations/0009_merge_20260129_2104.py new file mode 100644 index 0000000..c7d68a0 --- /dev/null +++ b/server/game_dev/migrations/0009_merge_20260129_2104.py @@ -0,0 +1,14 @@ +# Generated by Django 5.1.15 on 2026-01-29 13:04 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('game_dev', '0005_alter_member_profile_picture'), + ('game_dev', '0008_alter_committee_role'), + ] + + operations = [ + ] diff --git a/server/game_dev/migrations/0010_merge_20260131_1118.py b/server/game_dev/migrations/0010_merge_20260131_1118.py new file mode 100644 index 0000000..3502772 --- /dev/null +++ b/server/game_dev/migrations/0010_merge_20260131_1118.py @@ -0,0 +1,14 @@ +# Generated by Django 5.1.15 on 2026-01-31 03:18 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('game_dev', '0009_merge_20260129_2104'), + ('game_dev', '0009_merge_20260131_1044'), + ] + + operations = [ + ] diff --git a/server/game_dev/models.py b/server/game_dev/models.py index 8b81b7b..7947ce6 100644 --- a/server/game_dev/models.py +++ b/server/game_dev/models.py @@ -74,3 +74,24 @@ class GameShowcase(models.Model): def __str__(self): return f"{self.game.name}" + + +class Committee(models.Model): + id = models.OneToOneField(Member, on_delete=models.CASCADE, primary_key=True) + roles = { + "P": "President", + "VP": "Vice-President", + "SEC": "Secretary", + "TRE": "Treasurer", + "MARK": "Marketing", + "EV": "Events OCM", + "PRO": "Projects OCM", + "FRE": "Fresher Rep" + } + role = models.CharField(max_length=9, choices=roles, default="FRE", unique=True) + + def get_member(self): + return self.id + + def __str__(self): + return self.id.name diff --git a/server/game_dev/tests.py b/server/game_dev/tests.py index 92da547..96bdc43 100644 --- a/server/game_dev/tests.py +++ b/server/game_dev/tests.py @@ -1,9 +1,10 @@ from django.test import TestCase -from .models import Member, Event +from .models import Member, Event, Committee import datetime from django.core.files.uploadedfile import SimpleUploadedFile from django.utils import timezone from django.urls import reverse +from django.db.utils import IntegrityError class MemberModelTest(TestCase): @@ -73,6 +74,54 @@ def test_event_datetime_matches(self): self.assertEqual(event.date, self.event_datetime) +class CommitteeModelTest(TestCase): + def setUp(self): + self.member = Member.objects.create( + name="Linus Torvalds", + about="Linux creator", + pronouns="He/Him" + ) + try: + Member.objects.get(name="Linus Torvalds") + except Member.DoesNotExist: + self.fail("Member was not properly created before testing Committee model; check Member model") + self.committee = Committee.objects.create(id=self.member, role="P") + + def test_committee_creation(self): + try: + Committee.objects.get(id=self.member) + except Member.DoesNotExist: + self.fail("Committee Member was not properly created") + + def test_role_is_unique(self): + Member.objects.create( + name="Jane Doe", + about="Placeholder", + pronouns="She/Her" + ) + try: + Committee.objects.create(id=Member.objects.get(name="Jane Doe"), role="P") + self.fail("Committee Member with a duplicate role was created") + except IntegrityError: + return True + + def test_cascade_from_committee(self): + self.committee.delete() + try: + Member.objects.get(name=self.member.name) + except Member.DoesNotExist: + self.fail("Deleting Committee object deleted it's corresponding Member object (undesired behaviour)") + + def test_cascade_from_member(self): + tempRole = Committee.objects.get(id=self.member).role + self.member.delete() + try: + Committee.objects.get(role=tempRole) + self.fail("Deleting Member Object did not delete a possible corresponding Committee object (undesired behaviour)") + except Committee.DoesNotExist: + return True + + class EventListAPITest(TestCase): def setUp(self): self.url = reverse("events-list") diff --git a/server/game_dev/urls.py b/server/game_dev/urls.py index 1e7f682..45a1e36 100644 --- a/server/game_dev/urls.py +++ b/server/game_dev/urls.py @@ -1,10 +1,11 @@ from django.urls import path -from .views import EventListAPIView, EventDetailAPIView, GamesDetailAPIView, GameshowcaseAPIView, MemberAPIView +from .views import EventListAPIView, EventDetailAPIView, 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('members//', MemberAPIView.as_view()) + path('members//', MemberAPIView.as_view()), + path("about/", CommitteeAPIView.as_view()) ] diff --git a/server/game_dev/views.py b/server/game_dev/views.py index d78482c..40ea545 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 +from .models import Game, GameShowcase, Event, Member, Committee from django.utils import timezone from rest_framework.views import APIView from rest_framework.response import Response @@ -76,3 +76,23 @@ class MemberAPIView(generics.RetrieveAPIView): def get_queryset(self): return Member.objects.filter(active=True) + + +class CommitteeAPIView(generics.ListAPIView): + serializer_class = MemberSerializer + + def get_queryset(self): + outputList = [] + roleOrder = ("P", "VP", "SEC", "TRE", "MARK", "EVE", "PRO", "FRE") + placeholderMember = {"name": "Position not filled", "profile_picture": "url('/landing_placeholder.png')", + "about": "", "pronouns": ""} + for i in roleOrder: + try: + cur = Committee.objects.get(role=i).id + if cur.active: + outputList.append(cur) + else: + outputList.append(placeholderMember) + except Committee.DoesNotExist: + outputList.append(placeholderMember) + return outputList