diff --git a/client/public/frame.png b/client/public/frame.png new file mode 100644 index 0000000..4a427a5 Binary files /dev/null and b/client/public/frame.png differ 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..83c2d9a --- /dev/null +++ b/client/src/pages/about.tsx @@ -0,0 +1,182 @@ +import { ImageIcon } from "lucide-react"; +import Image from "next/image"; + +import { ApiMember, useCommittee } from "@/hooks/useCommittee"; + +export default function AboutPage() { + //const router = useRouter(); + //const { id } = router.query; + // don't know if necessary + + 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", + ]; + + if (isPending) { + for (let i = 0; i < 8; i++) { + if (i < 4) { + topRow.push({ + name: "Loading...", + pronouns: "", + profile_picture: "", + about: "", + }); + } else { + bottomRow.push({ + name: "Loading...", + pronouns: "", + profile_picture: "", + about: "", + }); + } + } + } else if (isError) { + const errorMessage = + error?.response?.status === 404 + ? "Committee Page not found." + : "Failed to load Committee Page."; + + return ( +
+

+ {errorMessage} +

+
+ ); + } else { + for (let i = 0; i < 8; i++) { + if (i < 4) { + topRow.push(committee[i]); + } else { + bottomRow.push(committee[i]); + } + } + } + + return ( +
+
+
+
+

+ About Us +

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

Our Committee

+
+
+ + {/* Portraits Section - DARK - Full Width */} +
+
+ {/* Top row - 4 Presidents */} +
+ {topRow.map((member, idx) => ( +
+
+ +
+
+

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

+

+ {roleOrder[idx]} +

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

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

+

+ {roleOrder[4 + idx]} +

+
+
+ ))} +
+
+
+
+ ); +} diff --git a/server/game_dev/admin.py b/server/game_dev/admin.py index 46d358c..1974bc4 100644 --- a/server/game_dev/admin.py +++ b/server/game_dev/admin.py @@ -1,5 +1,5 @@ from django.contrib import admin -from .models import Member, Event +from .models import Member, Event, Committee class MemberAdmin(admin.ModelAdmin): @@ -10,5 +10,10 @@ class EventAdmin(admin.ModelAdmin): list_display = ("name", "date", "location", "publicationDate") +class CommitteeAdmin(admin.ModelAdmin): + pass + + admin.site.register(Member, MemberAdmin) admin.site.register(Event, EventAdmin) +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/models.py b/server/game_dev/models.py index 6398070..40f33cb 100644 --- a/server/game_dev/models.py +++ b/server/game_dev/models.py @@ -22,3 +22,24 @@ class Event(models.Model): def __str__(self): return self.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 d42d6a8..e5b5562 100644 --- a/server/game_dev/urls.py +++ b/server/game_dev/urls.py @@ -1,7 +1,8 @@ from django.urls import path -from .views import EventListAPIView, EventDetailAPIView +from .views import EventListAPIView, EventDetailAPIView, CommitteeAPIView urlpatterns = [ path("events/", EventListAPIView.as_view(), name="events-list"), path("events//", EventDetailAPIView.as_view()), + path("about/", CommitteeAPIView.as_view()) ] diff --git a/server/game_dev/views.py b/server/game_dev/views.py index 89ce3a9..3cef3d1 100644 --- a/server/game_dev/views.py +++ b/server/game_dev/views.py @@ -2,8 +2,8 @@ # Create your views here. from rest_framework import generics -from .models import Event -from .serializers import EventSerializer +from .models import Event, Committee +from .serializers import EventSerializer, MemberSerializer from django.utils import timezone from rest_framework.exceptions import ValidationError from rest_framework.pagination import PageNumberPagination @@ -51,3 +51,17 @@ class EventDetailAPIView(generics.RetrieveAPIView): def get_queryset(self): return Event.objects.filter(id=self.kwargs["id"]) + + +class CommitteeAPIView(generics.ListAPIView): + serializer_class = MemberSerializer + + def get_queryset(self): + outputList = [] + roleOrder = ("P", "VP", "SEC", "TRE", "MARK", "EVE", "PRO", "FRE") + for i in roleOrder: + try: + outputList.append(Committee.objects.get(role=i).id) + except Committee.DoesNotExist: + outputList.append({"name": "Position not filled", "profile_picture": "", "about": "", "pronouns": ""}) + return outputList