From 7b36317e56b9e0c383f8112d727d3e8dadb80b9a Mon Sep 17 00:00:00 2001 From: Petr Kudryavtsev Date: Sat, 13 Dec 2025 07:37:48 +0000 Subject: [PATCH 1/5] initial changes: hook, backend and events page 1st version --- client/src/hooks/useEvents.ts | 37 +++++++++++++ client/src/pages/events/index.tsx | 88 +++++++++++++++++++++++++++++++ server/game_dev/urls.py | 3 +- server/game_dev/views.py | 9 ++++ 4 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 client/src/hooks/useEvents.ts create mode 100644 client/src/pages/events/index.tsx diff --git a/client/src/hooks/useEvents.ts b/client/src/hooks/useEvents.ts new file mode 100644 index 0000000..2b1ab9d --- /dev/null +++ b/client/src/hooks/useEvents.ts @@ -0,0 +1,37 @@ +import { useQuery } from "@tanstack/react-query"; +import { AxiosError } from "axios"; + +import api from "@/lib/api"; + +type ApiEvent = { + id: number; + name: string; + description: string; + publicationDate: string; + date: string; + startTime: string | null; + location: string; + cover_image: string | null; +}; + +type UiEvent = Omit & { + coverImage: string; +}; + +function transformApiEventToUiEvent(data: ApiEvent): UiEvent { + return { + ...data, + coverImage: data.cover_image ?? "/game_dev_club_logo.svg", + }; +} + +export function useEvents() { + return useQuery({ + queryKey: ["events"], + queryFn: async () => { + const response = await api.get("/events/"); + return response.data; + }, + select: (data) => data.map(transformApiEventToUiEvent), + }); +} diff --git a/client/src/pages/events/index.tsx b/client/src/pages/events/index.tsx new file mode 100644 index 0000000..0a06cc1 --- /dev/null +++ b/client/src/pages/events/index.tsx @@ -0,0 +1,88 @@ +import Image from "next/image"; +import Link from "next/link"; + +import { useEvents } from "@/hooks/useEvents"; + +function formatDateTime(dateString: string): string { + try { + const date = new Date(dateString); + return new Intl.DateTimeFormat("en-AU", { + year: "numeric", + month: "long", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }).format(date); + } catch { + return dateString; + } +} + +export default function EventsPage() { + const { data: events, isPending, isError } = useEvents(); + + if (isPending) { + return ( +
+

Loading events...

+
+ ); + } + + if (isError) { + return ( +
+

+ Failed to load events. +

+
+ ); + } + + if (!events || events.length === 0) { + return ( +
+

Events

+

No events available.

+
+ ); + } + + return ( +
+

Events

+
+ {events.map((event) => ( + +
+ {`Cover { + e.currentTarget.src = "/game_dev_club_logo.svg"; + }} + /> +
+
+

+ {event.name} +

+

+ {formatDateTime(event.date)} · {event.location} +

+

+ {event.description} +

+
+ + ))} +
+
+ ); +} diff --git a/server/game_dev/urls.py b/server/game_dev/urls.py index e387e58..65b8180 100644 --- a/server/game_dev/urls.py +++ b/server/game_dev/urls.py @@ -1,6 +1,7 @@ from django.urls import path -from .views import EventDetailAPIView +from .views import EventListAPIView, EventDetailAPIView urlpatterns = [ + path("events/", EventListAPIView.as_view()), path("events//", EventDetailAPIView.as_view()), ] diff --git a/server/game_dev/views.py b/server/game_dev/views.py index 71a747c..9c717b0 100644 --- a/server/game_dev/views.py +++ b/server/game_dev/views.py @@ -6,6 +6,15 @@ from .serializers import EventSerializer +class EventListAPIView(generics.ListAPIView): + """ + GET /api/events/ + Returns a list of all events + """ + queryset = Event.objects.all() + serializer_class = EventSerializer + + class EventDetailAPIView(generics.RetrieveAPIView): """ GET /api/events// From 90004317c6d246d63158832d15ff177bd3f8b7a7 Mon Sep 17 00:00:00 2001 From: Yosuke Inoue <24513446@student.uwa.edu.au> Date: Wed, 7 Jan 2026 08:00:08 +0000 Subject: [PATCH 2/5] Fix event fetch API to support filtering by type (past/upcoming) --- client/src/hooks/useEvents.ts | 10 +++++++--- client/src/pages/events/index.tsx | 19 +++++++++++++++++-- server/game_dev/views.py | 23 +++++++++++++++++++++-- 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/client/src/hooks/useEvents.ts b/client/src/hooks/useEvents.ts index 2b1ab9d..2cff78b 100644 --- a/client/src/hooks/useEvents.ts +++ b/client/src/hooks/useEvents.ts @@ -25,11 +25,15 @@ function transformApiEventToUiEvent(data: ApiEvent): UiEvent { }; } -export function useEvents() { +export type EventTypeFilter = "past" | "upcoming"; + +export function useEvents(type?: EventTypeFilter) { return useQuery({ - queryKey: ["events"], + queryKey: ["events", type ?? "all"], queryFn: async () => { - const response = await api.get("/events/"); + const response = await api.get("/events/", { + params: type ? { type } : {}, + }); return response.data; }, select: (data) => data.map(transformApiEventToUiEvent), diff --git a/client/src/pages/events/index.tsx b/client/src/pages/events/index.tsx index 0a06cc1..c22a66c 100644 --- a/client/src/pages/events/index.tsx +++ b/client/src/pages/events/index.tsx @@ -1,7 +1,8 @@ import Image from "next/image"; import Link from "next/link"; +import { useRouter } from "next/router"; -import { useEvents } from "@/hooks/useEvents"; +import { EventTypeFilter,useEvents } from "@/hooks/useEvents"; function formatDateTime(dateString: string): string { try { @@ -19,7 +20,20 @@ function formatDateTime(dateString: string): string { } export default function EventsPage() { - const { data: events, isPending, isError } = useEvents(); + const router = useRouter(); + + const typeParam = router.query.type; + const type = + typeof typeParam === "string" && + (typeParam === "past" || typeParam === "upcoming") + ? (typeParam as EventTypeFilter) + : undefined; + + const { + data: events, + isPending, + isError, + } = useEvents(router.isReady ? type : undefined); if (isPending) { return ( @@ -51,6 +65,7 @@ export default function EventsPage() { return (

Events

+
{events.map((event) => ( Date: Fri, 9 Jan 2026 05:15:52 +0000 Subject: [PATCH 3/5] Creating a toggle that can be used to switch between viewing past and future events --- client/src/pages/events/index.tsx | 176 +++++++++++++++++++++++------- 1 file changed, 138 insertions(+), 38 deletions(-) diff --git a/client/src/pages/events/index.tsx b/client/src/pages/events/index.tsx index c22a66c..47987f7 100644 --- a/client/src/pages/events/index.tsx +++ b/client/src/pages/events/index.tsx @@ -2,23 +2,60 @@ import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/router"; -import { EventTypeFilter,useEvents } from "@/hooks/useEvents"; +import { EventTypeFilter, useEvents } from "@/hooks/useEvents"; -function formatDateTime(dateString: string): string { +function formatTimeOnly(dateString: string): string { try { const date = new Date(dateString); return new Intl.DateTimeFormat("en-AU", { - year: "numeric", - month: "long", - day: "numeric", - hour: "2-digit", + hour: "numeric", minute: "2-digit", }).format(date); } catch { - return dateString; + return ""; } } +function formatMonthShort(dateString: string): string { + try { + const date = new Date(dateString); + return new Intl.DateTimeFormat("en-AU", { month: "short" }).format(date); + } catch { + return ""; + } +} + +function formatDay2(dateString: string): string { + try { + const date = new Date(dateString); + return String(date.getDate()).padStart(2, "0"); + } catch { + return ""; + } +} + +function formatYear(dateString: string): string { + try { + const date = new Date(dateString); + return String(date.getFullYear()); + } catch { + return ""; + } +} + +type EventsByYear = Record; + +function groupEventsByYear( + events: T[], +): EventsByYear { + return events.reduce((acc, event) => { + const year = new Date(event.date).getFullYear(); + if (!acc[year]) acc[year] = []; + acc[year].push(event); + return acc; + }, {} as EventsByYear); +} + export default function EventsPage() { const router = useRouter(); @@ -62,42 +99,105 @@ export default function EventsPage() { ); } + const eventsByYear = groupEventsByYear(events); + const sortedYears = Object.keys(eventsByYear) + .map(Number) + .sort((a, b) => b - a); + return (

Events

-
- {events.map((event) => ( - -
- {`Cover { - e.currentTarget.src = "/game_dev_club_logo.svg"; - }} - /> -
-
-

- {event.name} -

-

- {formatDateTime(event.date)} · {event.location} -

-

- {event.description} -

-
- - ))} +
+ +
+ + {sortedYears.map((year) => ( +
+

{year}

+ +
+ {eventsByYear[year].map((event) => ( + +
+
+
+ + {formatMonthShort(event.date)} + + + {formatDay2(event.date)} + + + {formatYear(event.date)} + +
+
+ +
+ {`Cover { + e.currentTarget.src = "/game_dev_club_logo.svg"; + }} + /> +
+ +
+

+ {event.name} +

+ +

+ Time:{" "} + {formatTimeOnly(event.date)} +

+ +

+ + Location: + {" "} + {event.location} +

+ +

+ {event.description} +

+
+
+ + ))} +
+
+ ))}
); } From 7b13adde3b00640de6e9e3be8f43230100c8678e Mon Sep 17 00:00:00 2001 From: Yosuke Inoue <24513446@student.uwa.edu.au> Date: Fri, 9 Jan 2026 13:52:37 +0000 Subject: [PATCH 4/5] Layout Adjustment --- client/src/components/main/Navbar.tsx | 2 +- client/src/pages/events/index.tsx | 165 +++++++++++--------------- 2 files changed, 73 insertions(+), 94 deletions(-) diff --git a/client/src/components/main/Navbar.tsx b/client/src/components/main/Navbar.tsx index b25a62b..faf8bfe 100644 --- a/client/src/components/main/Navbar.tsx +++ b/client/src/components/main/Navbar.tsx @@ -18,7 +18,7 @@ export default function Navbar() { return ( <> -
+
- {sortedYears.map((year) => ( -
-

{year}

- -
- {eventsByYear[year].map((event) => ( - -
-
-
- - {formatMonthShort(event.date)} - - - {formatDay2(event.date)} - - - {formatYear(event.date)} - -
-
- -
- {`Cover { - e.currentTarget.src = "/game_dev_club_logo.svg"; - }} - /> -
- -
-

- {event.name} -

- -

- Time:{" "} - {formatTimeOnly(event.date)} -

- -

- - Location: - {" "} - {event.location} -

- -

- {event.description} -

-
+
+ {sortedYears.map((year) => ( +
+
+
+
+ {year}
- - ))} -
-
- ))} + + +
+ {eventsByYear[year].map((event) => ( + +
+
+

+ {event.name} +

+ +
+
+ {formatDateTimeLine(event.date)} +
+
{event.location}
+
+ +

+ {event.description} +

+
+ +
+ {`Cover { + e.currentTarget.src = "/game_dev_club_logo.svg"; + }} + /> +
+
+ + ))} +
+
+
+ ))} +
); } From 38d492795a314d71d85814968325c4c49bfc85b0 Mon Sep 17 00:00:00 2001 From: Yosuke Inoue <24513446@student.uwa.edu.au> Date: Wed, 14 Jan 2026 09:33:43 +0000 Subject: [PATCH 5/5] Added test code for event list API --- server/game_dev/tests.py | 60 ++++++++++++++++++++++++++++++++++++++++ server/game_dev/urls.py | 2 +- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/server/game_dev/tests.py b/server/game_dev/tests.py index 411a639..7bcdefe 100644 --- a/server/game_dev/tests.py +++ b/server/game_dev/tests.py @@ -3,6 +3,7 @@ import datetime from django.core.files.uploadedfile import SimpleUploadedFile from django.utils import timezone +from django.urls import reverse class MemberModelTest(TestCase): @@ -70,3 +71,62 @@ def test_event_date_is_datetime(self): def test_event_datetime_matches(self): event = Event.objects.get(pk=self.event.pk) self.assertEqual(event.date, self.event_datetime) + + +class EventListAPITest(TestCase): + def setUp(self): + self.url = reverse("events-list") + + now = timezone.now() + + self.past_old = Event.objects.create( + name="Past Old Event", + date=now - datetime.timedelta(days=30), + description="past old", + publicationDate=now.date(), + location="Perth", + ) + self.past_new = Event.objects.create( + name="Old Event2", + date=now - datetime.timedelta(days=1), + description="past new", + publicationDate=now.date(), + location="Perth", + ) + self.up_soon = Event.objects.create( + name="Upcoming Soon", + date=now + datetime.timedelta(days=1), + description="up soon 1 day", + publicationDate=now.date(), + location="Perth", + ) + self.up_later = Event.objects.create( + name="Upcoming Later", + date=now + datetime.timedelta(days=30), + description="up later", + publicationDate=now.date(), + location="Perth", + ) + + def _ids(self, resp_json): + return [item["id"] for item in resp_json] + + def test_past(self): + res = self.client.get(self.url, {"type": "past"}) + self.assertEqual(res.status_code, 200) + + ids = self._ids(res.json()) + + self.assertEqual(ids, [self.past_new.id, self.past_old.id]) + + def test_upcoming(self): + res = self.client.get(self.url, {"type": "upcoming"}) + self.assertEqual(res.status_code, 200) + + ids = self._ids(res.json()) + + self.assertEqual(ids, [self.up_soon.id, self.up_later.id]) + + def test_invalid_type(self): + res = self.client.get(self.url, {"type": "invalid"}) + self.assertEqual(res.status_code, 400) diff --git a/server/game_dev/urls.py b/server/game_dev/urls.py index 65b8180..d42d6a8 100644 --- a/server/game_dev/urls.py +++ b/server/game_dev/urls.py @@ -2,6 +2,6 @@ from .views import EventListAPIView, EventDetailAPIView urlpatterns = [ - path("events/", EventListAPIView.as_view()), + path("events/", EventListAPIView.as_view(), name="events-list"), path("events//", EventDetailAPIView.as_view()), ]