From 66f1c0d931452f5e0bcbc1d4970191157073cdc8 Mon Sep 17 00:00:00 2001 From: Karl_Sue Date: Tue, 25 Nov 2025 23:30:43 +0000 Subject: [PATCH 001/122] feat: add Art model --- server/game_dev/models.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/server/game_dev/models.py b/server/game_dev/models.py index 9c26a564..54e9f298 100644 --- a/server/game_dev/models.py +++ b/server/game_dev/models.py @@ -10,3 +10,14 @@ class Member(models.Model): def __str__(self): return str(self.name) + + +class Art(models.Model): + name = models.CharField(null=False, max_length=200) + description = models.CharField(max_length=200,) + source_game = models.ForeignKey(Games, on_delete=models.CASCADE, related_name='art_pieces') #Need implement Games model + path_to_media = models.CharField(null=False) + active = models.BooleanField(null=False) + + def __str__(self): + return str(self.name) From ddbf780db22c50870d255a7ccdb4f7ca614fdbc2 Mon Sep 17 00:00:00 2001 From: Karl_Sue Date: Wed, 26 Nov 2025 11:02:02 +0000 Subject: [PATCH 002/122] refactor: keep model register minimal --- server/game_dev/admin.py | 5 ++++- server/game_dev/models.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/server/game_dev/admin.py b/server/game_dev/admin.py index 4185d360..69865a64 100644 --- a/server/game_dev/admin.py +++ b/server/game_dev/admin.py @@ -1,3 +1,6 @@ -# from django.contrib import admin +from django.contrib import admin + +from .models import Art # Register your models here. +admin.site.register(Art) diff --git a/server/game_dev/models.py b/server/game_dev/models.py index 54e9f298..6fa4bb1b 100644 --- a/server/game_dev/models.py +++ b/server/game_dev/models.py @@ -15,7 +15,7 @@ def __str__(self): class Art(models.Model): name = models.CharField(null=False, max_length=200) description = models.CharField(max_length=200,) - source_game = models.ForeignKey(Games, on_delete=models.CASCADE, related_name='art_pieces') #Need implement Games model + # source_game = models.ForeignKey(Games, on_delete=models.CASCADE, related_name='art_pieces') #Need implement Games model path_to_media = models.CharField(null=False) active = models.BooleanField(null=False) From b26d12de0d3bc241da422f1d20519817f75d8e2d Mon Sep 17 00:00:00 2001 From: David Du <24074639@student.uwa.edu.au> Date: Fri, 28 Nov 2025 17:43:29 +0000 Subject: [PATCH 003/122] Add ArtContributor model with API endpoints --- server/api/urls.py | 5 +- server/game_dev/admin.py | 24 ++++++- .../migrations/0002_art_artcontributor.py | 68 +++++++++++++++++++ server/game_dev/models.py | 14 +++- server/game_dev/serializers.py | 11 +++ server/game_dev/urls.py | 10 +++ server/game_dev/views.py | 9 ++- 7 files changed, 133 insertions(+), 8 deletions(-) create mode 100644 server/game_dev/migrations/0002_art_artcontributor.py create mode 100644 server/game_dev/serializers.py create mode 100644 server/game_dev/urls.py diff --git a/server/api/urls.py b/server/api/urls.py index 1347bb73..c6a16187 100644 --- a/server/api/urls.py +++ b/server/api/urls.py @@ -20,5 +20,6 @@ urlpatterns = [ path("admin/", admin.site.urls), - path("api/healthcheck/", include(("api.healthcheck.urls"))), -] + path("api/healthcheck/", include("api.healthcheck.urls")), + path("api/game-dev/", include("game_dev.urls")), +] \ No newline at end of file diff --git a/server/game_dev/admin.py b/server/game_dev/admin.py index 69865a64..2cfeab2e 100644 --- a/server/game_dev/admin.py +++ b/server/game_dev/admin.py @@ -1,6 +1,24 @@ from django.contrib import admin +from .models import Member, Art, ArtContributor -from .models import Art -# Register your models here. -admin.site.register(Art) +@admin.register(Member) +class MemberAdmin(admin.ModelAdmin): + list_display = ['name', 'active', 'pronouns'] + list_filter = ['active'] + search_fields = ['name'] + + +@admin.register(Art) +class ArtAdmin(admin.ModelAdmin): + list_display = ['name', 'active'] + list_filter = ['active'] + search_fields = ['name'] + + +@admin.register(ArtContributor) +class ArtContributorAdmin(admin.ModelAdmin): + list_display = ['art', 'member', 'role'] + list_filter = ['role'] + search_fields = ['member__name', 'art__name'] + autocomplete_fields = ['art', 'member'] \ No newline at end of file diff --git a/server/game_dev/migrations/0002_art_artcontributor.py b/server/game_dev/migrations/0002_art_artcontributor.py new file mode 100644 index 00000000..138685d9 --- /dev/null +++ b/server/game_dev/migrations/0002_art_artcontributor.py @@ -0,0 +1,68 @@ +# Generated by Django 5.1.14 on 2025-11-28 17:32 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("game_dev", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="Art", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=200)), + ("description", models.CharField(max_length=200)), + ("path_to_media", models.CharField(max_length=500)), + ("active", models.BooleanField()), + ], + ), + migrations.CreateModel( + name="ArtContributor", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("role", models.CharField(max_length=100)), + ( + "art", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="contributors", + to="game_dev.art", + ), + ), + ( + "member", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="art_contributions", + to="game_dev.member", + ), + ), + ], + options={ + "verbose_name": "Art Contributor", + "verbose_name_plural": "Art Contributors", + "unique_together": {("art", "member")}, + }, + ), + ] diff --git a/server/game_dev/models.py b/server/game_dev/models.py index 6fa4bb1b..ea132d6b 100644 --- a/server/game_dev/models.py +++ b/server/game_dev/models.py @@ -16,8 +16,20 @@ class Art(models.Model): name = models.CharField(null=False, max_length=200) description = models.CharField(max_length=200,) # source_game = models.ForeignKey(Games, on_delete=models.CASCADE, related_name='art_pieces') #Need implement Games model - path_to_media = models.CharField(null=False) + path_to_media = models.CharField(null=False, max_length=500) active = models.BooleanField(null=False) def __str__(self): return str(self.name) + +class ArtContributor(models.Model): + art = models.ForeignKey('Art', on_delete=models.CASCADE, related_name='contributors') + member = models.ForeignKey('Member', on_delete=models.CASCADE, related_name='art_contributions') + role = models.CharField(max_length=100) + class Meta: + unique_together = ('art', 'member') + verbose_name = 'Art Contributor' + verbose_name_plural = 'Art Contributors' + + def __str__(self): + return f"{self.member.name} - {self.art.name} ({self.role})" diff --git a/server/game_dev/serializers.py b/server/game_dev/serializers.py new file mode 100644 index 00000000..768a4bfa --- /dev/null +++ b/server/game_dev/serializers.py @@ -0,0 +1,11 @@ +from rest_framework import serializers +from .models import ArtContributor + + +class ArtContributorSerializer(serializers.ModelSerializer): + member_name = serializers.CharField(source='member.name', read_only=True) + art_name = serializers.CharField(source='art.name', read_only=True) + + class Meta: + model = ArtContributor + fields = ['id', 'art', 'member', 'member_name', 'art_name', 'role'] \ No newline at end of file diff --git a/server/game_dev/urls.py b/server/game_dev/urls.py new file mode 100644 index 00000000..d1799496 --- /dev/null +++ b/server/game_dev/urls.py @@ -0,0 +1,10 @@ +from django.urls import path, include +from rest_framework.routers import DefaultRouter +from .views import ArtContributorViewSet + +router = DefaultRouter() +router.register(r'art-contributors', ArtContributorViewSet, basename='artcontributor') + +urlpatterns = [ + path('', include(router.urls)), +] \ No newline at end of file diff --git a/server/game_dev/views.py b/server/game_dev/views.py index fd0e0449..264448f7 100644 --- a/server/game_dev/views.py +++ b/server/game_dev/views.py @@ -1,3 +1,8 @@ -# from django.shortcuts import render +from rest_framework import viewsets +from .models import ArtContributor +from .serializers import ArtContributorSerializer -# Create your views here. + +class ArtContributorViewSet(viewsets.ModelViewSet): + queryset = ArtContributor.objects.all() + serializer_class = ArtContributorSerializer \ No newline at end of file From 8ac4927eec7caa6c8085a1568bad2be7c58ef2dd Mon Sep 17 00:00:00 2001 From: Han Minh Tran Date: Fri, 5 Dec 2025 20:19:11 +0800 Subject: [PATCH 004/122] art-page-frontend --- client/src/pages/artwork/[id].tsx | 223 +++++++++++++++++++++++++++++ client/src/pages/artwork/index.tsx | 131 +++++++++++++++++ client/src/styles/globals.css | 25 ++++ 3 files changed, 379 insertions(+) create mode 100644 client/src/pages/artwork/[id].tsx create mode 100644 client/src/pages/artwork/index.tsx diff --git a/client/src/pages/artwork/[id].tsx b/client/src/pages/artwork/[id].tsx new file mode 100644 index 00000000..897ccead --- /dev/null +++ b/client/src/pages/artwork/[id].tsx @@ -0,0 +1,223 @@ +import { GetServerSideProps } from "next"; +import Image from "next/image"; +import Link from "next/link"; + +type Contributor = { + id: string; + name: string; + instagramUrl?: string; + discordUrl?: string; +}; +type Artwork = { + id: string; + name: string; + description: string; + sourceGame: string; + pathToMedia: string; + active: boolean; + createdAt: string; + contributors?: Contributor[]; +}; + +interface ArtworkPageProps { + artwork: Artwork; +} + +export default function ArtworkPage({ artwork }: ArtworkPageProps) { + return ( +
+
+ TODO add Header +
+
+
+ < Gallery +
+
+
+
+
+ + + +
+
+
+
+
+ {artwork.name} +
+
+
+ + {artwork.description} + +
+
+
+
+
+
+ Contributors +
+
+
+ {artwork.contributors?.map((contributor) => ( +
+
+ {contributor.name} +
+
+ {contributor.discordUrl ? ( +
+ + + +
+ ) : ( + "" + )} + {contributor.instagramUrl ? ( +
+ + + +
+ ) : ( + "" + )} +
+
+ ))} +
+
+
+
+
+
+ +
+
+ Game Image +
+
+
+ TODO add footer +
+
+ ); +} + +export const getServerSideProps: GetServerSideProps = async ( + context, +) => { + const { id } = context.params as { id: string }; + console.log("Fetching artwork with id:", id); + // const res = await fetch(`https://your-backend.com/api/artwork/${id}`); + const artwork: Artwork = { + id: "abc of art", + name: "title of art", + description: "description of art", + sourceGame: "", + pathToMedia: "", + active: false, + createdAt: new Date().toISOString(), + contributors: [ + { id: "1", name: "Contributor 1", discordUrl: "discordUrl" }, + { id: "2", name: "Contributor 2", instagramUrl: "instagramUrl" }, + ], + }; + + return { props: { artwork } }; +}; diff --git a/client/src/pages/artwork/index.tsx b/client/src/pages/artwork/index.tsx new file mode 100644 index 00000000..b3e6d926 --- /dev/null +++ b/client/src/pages/artwork/index.tsx @@ -0,0 +1,131 @@ +import { GetServerSideProps } from "next"; +import Link from "next/link"; + +type Artwork = { + id: string; + name: string; + description: string; + sourceGame: string; + pathToMedia: string; + active: boolean; + createdAt: string; +}; + +interface ArtworksPageProps { + artworks: Artwork[]; +} + +const PLACEHOLDER_ICON = ( +
+ + + +
+); + +function renderArtworkCard(artwork: Artwork) { + return ( + +
+ {PLACEHOLDER_ICON} +
+ + ); +} + +export default function ArtworksPage({ artworks }: ArtworksPageProps) { + return ( +
+
+ TODO add Header +
+
+
+ FEATURED: +
+ SOME GAME +
+
+ {PLACEHOLDER_ICON} +
+
+
+
+ More about us → +
+
+
+
+ +
+
+ {artworks.map((artwork) => renderArtworkCard(artwork))} +
+
+
+ TODO add footer +
+
+ ); +} + +export const getServerSideProps: GetServerSideProps< + ArtworksPageProps +> = async () => { + // const res = await fetch(`https://your-backend.com/api/artwork/${id}`); + const artworks: Artwork[] = []; + for (let i = 0; i < 12; i++) { + const artwork: Artwork = { + id: i + "", + name: "title of art" + i, + description: "description of art", + sourceGame: "", + pathToMedia: "", + active: false, + createdAt: new Date().toISOString(), + }; + artworks.push(artwork); + } + + return { props: { artworks } }; +}; diff --git a/client/src/styles/globals.css b/client/src/styles/globals.css index 616c2b14..9079808f 100644 --- a/client/src/styles/globals.css +++ b/client/src/styles/globals.css @@ -33,6 +33,12 @@ --ring: 236 47% 7%; --radius: 0.5rem; + + --dark-2: #090A19; + --neutral-1: #1B1F4C; + --light-1: #FFFFFF; + --light-2: #CED1FE; + --light-3: #9CA4FD; } } @@ -41,3 +47,22 @@ @apply bg-background text-foreground; } } + +.bg-neutral-1 { + background-color: var(--neutral-1); +} +.bg-dark-2 { + background-color: var(--dark-2); +} +.bg-light-2 { + background-color: var(--light-2); +} +.text-light-1 { + color: var(--light-1); +} +.outline-neutral-1 { + outline-color: var(--neutral-1); +} +.text-light-3 { + color: var(--light-3); +} \ No newline at end of file From 8b98a06352b5aaa6c1a2e7ebe30dd27ef9e199e8 Mon Sep 17 00:00:00 2001 From: Han Minh Tran Date: Fri, 5 Dec 2025 20:19:11 +0800 Subject: [PATCH 005/122] art-page-frontend --- client/public/placeholder1293x405.svg | 1 + client/src/pages/artwork/[id].tsx | 225 ++++++++++++++++++++++++++ client/src/pages/artwork/index.tsx | 131 +++++++++++++++ client/src/styles/globals.css | 25 +++ 4 files changed, 382 insertions(+) create mode 100644 client/public/placeholder1293x405.svg create mode 100644 client/src/pages/artwork/[id].tsx create mode 100644 client/src/pages/artwork/index.tsx diff --git a/client/public/placeholder1293x405.svg b/client/public/placeholder1293x405.svg new file mode 100644 index 00000000..34f928c8 --- /dev/null +++ b/client/public/placeholder1293x405.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/pages/artwork/[id].tsx b/client/src/pages/artwork/[id].tsx new file mode 100644 index 00000000..732f645b --- /dev/null +++ b/client/src/pages/artwork/[id].tsx @@ -0,0 +1,225 @@ +import { GetServerSideProps } from "next"; +import Image from "next/image"; +import Link from "next/link"; + +type Contributor = { + id: string; + name: string; + instagramUrl?: string; + discordUrl?: string; +}; +type Artwork = { + id: string; + name: string; + description: string; + sourceGame: string; + pathToMedia: string; + active: boolean; + createdAt: string; + contributors?: Contributor[]; +}; + +interface ArtworkPageProps { + artwork: Artwork; +} + +export default function ArtworkPage({ artwork }: ArtworkPageProps) { + return ( +
+
+ TODO add Header +
+
+
+ < Gallery +
+
+
+
+
+ + + +
+
+
+
+
+ {artwork.name} +
+
+
+ + {artwork.description} + +
+
+
+
+
+
+ Contributors +
+
+
+ {artwork.contributors?.map((contributor) => ( +
+
+ {contributor.name} +
+
+ {contributor.discordUrl ? ( +
+ + + +
+ ) : ( + "" + )} + {contributor.instagramUrl ? ( +
+ + + +
+ ) : ( + "" + )} +
+
+ ))} +
+
+
+
+
+
+ +
+
+ Game Image +
+
+
+ TODO add footer +
+
+ ); +} + +export const getServerSideProps: GetServerSideProps = async ( + context, +) => { + const { id } = context.params as { id: string }; + console.log("Fetching artwork with id:", id); + // const res = await fetch(`https://your-backend.com/api/artwork/${id}`); + const artwork: Artwork = { + id: "abc of art", + name: "title of art", + description: "description of art", + sourceGame: "", + pathToMedia: "", + active: false, + createdAt: new Date().toISOString(), + contributors: [ + { id: "1", name: "Contributor 1", discordUrl: "discordUrl" }, + { id: "2", name: "Contributor 2", instagramUrl: "instagramUrl" }, + ], + }; + + return { props: { artwork } }; +}; diff --git a/client/src/pages/artwork/index.tsx b/client/src/pages/artwork/index.tsx new file mode 100644 index 00000000..b3e6d926 --- /dev/null +++ b/client/src/pages/artwork/index.tsx @@ -0,0 +1,131 @@ +import { GetServerSideProps } from "next"; +import Link from "next/link"; + +type Artwork = { + id: string; + name: string; + description: string; + sourceGame: string; + pathToMedia: string; + active: boolean; + createdAt: string; +}; + +interface ArtworksPageProps { + artworks: Artwork[]; +} + +const PLACEHOLDER_ICON = ( +
+ + + +
+); + +function renderArtworkCard(artwork: Artwork) { + return ( + +
+ {PLACEHOLDER_ICON} +
+ + ); +} + +export default function ArtworksPage({ artworks }: ArtworksPageProps) { + return ( +
+
+ TODO add Header +
+
+
+ FEATURED: +
+ SOME GAME +
+
+ {PLACEHOLDER_ICON} +
+
+
+
+ More about us → +
+
+
+
+ +
+
+ {artworks.map((artwork) => renderArtworkCard(artwork))} +
+
+
+ TODO add footer +
+
+ ); +} + +export const getServerSideProps: GetServerSideProps< + ArtworksPageProps +> = async () => { + // const res = await fetch(`https://your-backend.com/api/artwork/${id}`); + const artworks: Artwork[] = []; + for (let i = 0; i < 12; i++) { + const artwork: Artwork = { + id: i + "", + name: "title of art" + i, + description: "description of art", + sourceGame: "", + pathToMedia: "", + active: false, + createdAt: new Date().toISOString(), + }; + artworks.push(artwork); + } + + return { props: { artworks } }; +}; diff --git a/client/src/styles/globals.css b/client/src/styles/globals.css index 616c2b14..9079808f 100644 --- a/client/src/styles/globals.css +++ b/client/src/styles/globals.css @@ -33,6 +33,12 @@ --ring: 236 47% 7%; --radius: 0.5rem; + + --dark-2: #090A19; + --neutral-1: #1B1F4C; + --light-1: #FFFFFF; + --light-2: #CED1FE; + --light-3: #9CA4FD; } } @@ -41,3 +47,22 @@ @apply bg-background text-foreground; } } + +.bg-neutral-1 { + background-color: var(--neutral-1); +} +.bg-dark-2 { + background-color: var(--dark-2); +} +.bg-light-2 { + background-color: var(--light-2); +} +.text-light-1 { + color: var(--light-1); +} +.outline-neutral-1 { + outline-color: var(--neutral-1); +} +.text-light-3 { + color: var(--light-3); +} \ No newline at end of file From 7f91e0b507cce7ff90d4ece816fea4e81faa48b9 Mon Sep 17 00:00:00 2001 From: Karl_Sue Date: Sat, 6 Dec 2025 06:43:13 +0000 Subject: [PATCH 006/122] refactor: keep admin register simple --- server/game_dev/admin.py | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/server/game_dev/admin.py b/server/game_dev/admin.py index 2cfeab2e..0e8d557c 100644 --- a/server/game_dev/admin.py +++ b/server/game_dev/admin.py @@ -2,23 +2,8 @@ from .models import Member, Art, ArtContributor -@admin.register(Member) -class MemberAdmin(admin.ModelAdmin): - list_display = ['name', 'active', 'pronouns'] - list_filter = ['active'] - search_fields = ['name'] +admin.site.register(Member) +admin.site.register(Art) -@admin.register(Art) -class ArtAdmin(admin.ModelAdmin): - list_display = ['name', 'active'] - list_filter = ['active'] - search_fields = ['name'] - - -@admin.register(ArtContributor) -class ArtContributorAdmin(admin.ModelAdmin): - list_display = ['art', 'member', 'role'] - list_filter = ['role'] - search_fields = ['member__name', 'art__name'] - autocomplete_fields = ['art', 'member'] \ No newline at end of file +admin.site.register(ArtContributor) \ No newline at end of file From aeedeec5920eb87632fb57488d1d89d5934340dc Mon Sep 17 00:00:00 2001 From: Karl_Sue Date: Sat, 6 Dec 2025 06:44:01 +0000 Subject: [PATCH 007/122] fix: space error --- server/game_dev/models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/game_dev/models.py b/server/game_dev/models.py index ea132d6b..0735d3f3 100644 --- a/server/game_dev/models.py +++ b/server/game_dev/models.py @@ -22,14 +22,16 @@ class Art(models.Model): def __str__(self): return str(self.name) + class ArtContributor(models.Model): art = models.ForeignKey('Art', on_delete=models.CASCADE, related_name='contributors') member = models.ForeignKey('Member', on_delete=models.CASCADE, related_name='art_contributions') role = models.CharField(max_length=100) + class Meta: unique_together = ('art', 'member') verbose_name = 'Art Contributor' verbose_name_plural = 'Art Contributors' - + def __str__(self): return f"{self.member.name} - {self.art.name} ({self.role})" From 31954afefb1a9bb8180057441f2c0a19b95bdbb7 Mon Sep 17 00:00:00 2001 From: Han Minh Tran Date: Wed, 10 Dec 2025 10:57:58 +0800 Subject: [PATCH 008/122] Load arts from backend --- client/next.config.mjs | 10 +++ client/src/pages/artwork/[id].tsx | 96 ++++++++++------------------- client/src/pages/artwork/index.tsx | 48 ++++++--------- client/src/styles/globals.css | 3 + client/src/types/art-contributor.ts | 9 +++ client/src/types/art.ts | 8 +++ client/src/types/base-dto.ts | 3 + server/game_dev/serializers.py | 16 ++++- server/game_dev/urls.py | 4 +- server/game_dev/views.py | 14 ++++- 10 files changed, 113 insertions(+), 98 deletions(-) create mode 100644 client/src/types/art-contributor.ts create mode 100644 client/src/types/art.ts create mode 100644 client/src/types/base-dto.ts diff --git a/client/next.config.mjs b/client/next.config.mjs index c5b60fed..1402c100 100644 --- a/client/next.config.mjs +++ b/client/next.config.mjs @@ -19,6 +19,16 @@ const config = { // pollIntervalMs: 1000 // } // : undefined, + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: '**', + port: '', + pathname: '**' + } + ] + } }; export default config; diff --git a/client/src/pages/artwork/[id].tsx b/client/src/pages/artwork/[id].tsx index 732f645b..ce89b26e 100644 --- a/client/src/pages/artwork/[id].tsx +++ b/client/src/pages/artwork/[id].tsx @@ -2,28 +2,19 @@ import { GetServerSideProps } from "next"; import Image from "next/image"; import Link from "next/link"; -type Contributor = { - id: string; - name: string; - instagramUrl?: string; - discordUrl?: string; -}; -type Artwork = { - id: string; - name: string; - description: string; - sourceGame: string; - pathToMedia: string; - active: boolean; - createdAt: string; - contributors?: Contributor[]; -}; +import api from "@/lib/api"; +import { Art } from "@/types/art"; +import { ArtContributor } from "@/types/art-contributor"; interface ArtworkPageProps { - artwork: Artwork; + artwork: Art; + contributors: ArtContributor[]; } -export default function ArtworkPage({ artwork }: ArtworkPageProps) { +export default function ArtworkPage({ + artwork, + contributors, +}: ArtworkPageProps) { return (
< Gallery
-
+
-
- - - +
+ Artwork image
- {artwork.contributors?.map((contributor) => ( + {contributors?.map((contributor) => (
- {contributor.name} + {contributor.member_name}
- {contributor.discordUrl ? ( + {contributor.discordUrl && (
- ) : ( - "" )} - {contributor.instagramUrl ? ( + {contributor.instagramUrl && (
- ) : ( - "" )}
@@ -205,21 +184,14 @@ export const getServerSideProps: GetServerSideProps = async ( context, ) => { const { id } = context.params as { id: string }; - console.log("Fetching artwork with id:", id); - // const res = await fetch(`https://your-backend.com/api/artwork/${id}`); - const artwork: Artwork = { - id: "abc of art", - name: "title of art", - description: "description of art", - sourceGame: "", - pathToMedia: "", - active: false, - createdAt: new Date().toISOString(), - contributors: [ - { id: "1", name: "Contributor 1", discordUrl: "discordUrl" }, - { id: "2", name: "Contributor 2", instagramUrl: "instagramUrl" }, - ], - }; - - return { props: { artwork } }; + const artResponse = await api.get(`game-dev/arts/${id}`); + const artwork = artResponse.data; + const contributorsResponse = await api.get( + `game-dev/art-contributors`, + ); + const contributors: ArtContributor[] = contributorsResponse.data.filter( + (x) => x.art_id === Number(id), + ); + // TODO [HanMinh] to filter on backend + return { props: { artwork, contributors } }; }; diff --git a/client/src/pages/artwork/index.tsx b/client/src/pages/artwork/index.tsx index b3e6d926..d0ca4af0 100644 --- a/client/src/pages/artwork/index.tsx +++ b/client/src/pages/artwork/index.tsx @@ -1,18 +1,12 @@ import { GetServerSideProps } from "next"; +import Image from "next/image"; import Link from "next/link"; -type Artwork = { - id: string; - name: string; - description: string; - sourceGame: string; - pathToMedia: string; - active: boolean; - createdAt: string; -}; +import api from "@/lib/api"; +import { Art } from "@/types/art"; interface ArtworksPageProps { - artworks: Artwork[]; + artworks: Art[]; } const PLACEHOLDER_ICON = ( @@ -32,18 +26,27 @@ const PLACEHOLDER_ICON = (
); -function renderArtworkCard(artwork: Artwork) { +function renderArtworkCard(artwork: Art) { return (
- {PLACEHOLDER_ICON} +
+ Artwork image +
); @@ -112,20 +115,7 @@ export default function ArtworksPage({ artworks }: ArtworksPageProps) { export const getServerSideProps: GetServerSideProps< ArtworksPageProps > = async () => { - // const res = await fetch(`https://your-backend.com/api/artwork/${id}`); - const artworks: Artwork[] = []; - for (let i = 0; i < 12; i++) { - const artwork: Artwork = { - id: i + "", - name: "title of art" + i, - description: "description of art", - sourceGame: "", - pathToMedia: "", - active: false, - createdAt: new Date().toISOString(), - }; - artworks.push(artwork); - } - - return { props: { artworks } }; + const res = await api.get("game-dev/arts"); + const arts = res.data; + return { props: { artworks: arts } }; }; diff --git a/client/src/styles/globals.css b/client/src/styles/globals.css index 9079808f..53eb51bc 100644 --- a/client/src/styles/globals.css +++ b/client/src/styles/globals.css @@ -65,4 +65,7 @@ } .text-light-3 { color: var(--light-3); +} +.border-light-2 { + border-color: var(--light-2); } \ No newline at end of file diff --git a/client/src/types/art-contributor.ts b/client/src/types/art-contributor.ts new file mode 100644 index 00000000..9ccc389f --- /dev/null +++ b/client/src/types/art-contributor.ts @@ -0,0 +1,9 @@ +import { BaseDto } from "./base-dto"; + +export interface ArtContributor extends BaseDto { + art_id: number; + member_name: string; + role: string; + instagramUrl?: string; // TODO [HanMinh] to refine where to get these info + discordUrl?: string; +} diff --git a/client/src/types/art.ts b/client/src/types/art.ts new file mode 100644 index 00000000..3587cbe4 --- /dev/null +++ b/client/src/types/art.ts @@ -0,0 +1,8 @@ +import { BaseDto } from "./base-dto"; + +export interface Art extends BaseDto { + name: string; + description: string; + path_to_media: string; + active: boolean; +} diff --git a/client/src/types/base-dto.ts b/client/src/types/base-dto.ts new file mode 100644 index 00000000..9e3b6872 --- /dev/null +++ b/client/src/types/base-dto.ts @@ -0,0 +1,3 @@ +export interface BaseDto { + id: number; +} diff --git a/server/game_dev/serializers.py b/server/game_dev/serializers.py index 768a4bfa..5c21cbc9 100644 --- a/server/game_dev/serializers.py +++ b/server/game_dev/serializers.py @@ -1,11 +1,21 @@ from rest_framework import serializers -from .models import ArtContributor +from .models import Art, ArtContributor, Member class ArtContributorSerializer(serializers.ModelSerializer): member_name = serializers.CharField(source='member.name', read_only=True) - art_name = serializers.CharField(source='art.name', read_only=True) + art_id = serializers.IntegerField(source="art.id", read_only=True) class Meta: model = ArtContributor - fields = ['id', 'art', 'member', 'member_name', 'art_name', 'role'] \ No newline at end of file + fields = ['id', 'art_id', 'member', 'member_name', 'role'] + +class ArtSerializer(serializers.ModelSerializer): + class Meta: + model = Art + fields = ['id', 'name', 'description', 'path_to_media', 'active'] + +class MemberSerializer(serializers.ModelSerializer): + class Meta: + model = Member + fields = ['name'] diff --git a/server/game_dev/urls.py b/server/game_dev/urls.py index d1799496..b6f94c27 100644 --- a/server/game_dev/urls.py +++ b/server/game_dev/urls.py @@ -1,9 +1,11 @@ from django.urls import path, include from rest_framework.routers import DefaultRouter -from .views import ArtContributorViewSet +from .views import ArtContributorViewSet, ArtViewSet, MemberViewSet router = DefaultRouter() router.register(r'art-contributors', ArtContributorViewSet, basename='artcontributor') +router.register(r'arts', ArtViewSet, basename="art") +router.register(r'members', MemberViewSet, basename="member") urlpatterns = [ path('', include(router.urls)), diff --git a/server/game_dev/views.py b/server/game_dev/views.py index 264448f7..bf68916a 100644 --- a/server/game_dev/views.py +++ b/server/game_dev/views.py @@ -1,8 +1,16 @@ from rest_framework import viewsets -from .models import ArtContributor -from .serializers import ArtContributorSerializer +from .models import Art, ArtContributor, Member +from .serializers import ArtContributorSerializer, ArtSerializer, MemberSerializer class ArtContributorViewSet(viewsets.ModelViewSet): queryset = ArtContributor.objects.all() - serializer_class = ArtContributorSerializer \ No newline at end of file + serializer_class = ArtContributorSerializer + +class ArtViewSet(viewsets.ModelViewSet): + queryset = Art.objects.all() + serializer_class = ArtSerializer + +class MemberViewSet(viewsets.ModelViewSet): + queryset = Member.objects.all() + serializer_class = MemberSerializer \ No newline at end of file From 9e67fa6cb81bcf030663ae37f57b4eb81b7e18f4 Mon Sep 17 00:00:00 2001 From: Han Minh Tran Date: Wed, 10 Dec 2025 11:08:38 +0800 Subject: [PATCH 009/122] Change field name style --- client/src/pages/artwork/[id].tsx | 4 ++-- client/src/types/art-contributor.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/src/pages/artwork/[id].tsx b/client/src/pages/artwork/[id].tsx index ce89b26e..a8f61707 100644 --- a/client/src/pages/artwork/[id].tsx +++ b/client/src/pages/artwork/[id].tsx @@ -102,7 +102,7 @@ export default function ArtworkPage({ {contributor.member_name}
- {contributor.discordUrl && ( + {contributor.discord_url && (
)} - {contributor.instagramUrl && ( + {contributor.instagram_url && (
Date: Wed, 10 Dec 2025 11:16:33 +0800 Subject: [PATCH 010/122] Readd art field to create data --- server/game_dev/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/game_dev/serializers.py b/server/game_dev/serializers.py index 5c21cbc9..54c5c7a1 100644 --- a/server/game_dev/serializers.py +++ b/server/game_dev/serializers.py @@ -8,7 +8,7 @@ class ArtContributorSerializer(serializers.ModelSerializer): class Meta: model = ArtContributor - fields = ['id', 'art_id', 'member', 'member_name', 'role'] + fields = ['id', 'art', 'art_id', 'member', 'member_name', 'role'] class ArtSerializer(serializers.ModelSerializer): class Meta: From c10ee23476e2b299a6e94ee1035a7d23c01d918c Mon Sep 17 00:00:00 2001 From: Han Minh Tran Date: Wed, 10 Dec 2025 13:48:46 +0800 Subject: [PATCH 011/122] Make the image height corresponding to the text --- client/src/pages/artwork/[id].tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/pages/artwork/[id].tsx b/client/src/pages/artwork/[id].tsx index a8f61707..00527cde 100644 --- a/client/src/pages/artwork/[id].tsx +++ b/client/src/pages/artwork/[id].tsx @@ -40,7 +40,7 @@ export default function ArtworkPage({
Date: Wed, 10 Dec 2025 15:08:38 +0800 Subject: [PATCH 012/122] Filter on backend + include contributors into art --- client/src/pages/artwork/[id].tsx | 18 +++--------------- client/src/types/art.ts | 2 ++ server/api/settings.py | 1 + server/game_dev/serializers.py | 6 +++--- server/game_dev/views.py | 3 +++ 5 files changed, 12 insertions(+), 18 deletions(-) diff --git a/client/src/pages/artwork/[id].tsx b/client/src/pages/artwork/[id].tsx index 00527cde..0aa8f7c5 100644 --- a/client/src/pages/artwork/[id].tsx +++ b/client/src/pages/artwork/[id].tsx @@ -4,17 +4,12 @@ import Link from "next/link"; import api from "@/lib/api"; import { Art } from "@/types/art"; -import { ArtContributor } from "@/types/art-contributor"; interface ArtworkPageProps { artwork: Art; - contributors: ArtContributor[]; } -export default function ArtworkPage({ - artwork, - contributors, -}: ArtworkPageProps) { +export default function ArtworkPage({ artwork }: ArtworkPageProps) { return (
- {contributors?.map((contributor) => ( + {artwork.contributors?.map((contributor) => (
= async ( const { id } = context.params as { id: string }; const artResponse = await api.get(`game-dev/arts/${id}`); const artwork = artResponse.data; - const contributorsResponse = await api.get( - `game-dev/art-contributors`, - ); - const contributors: ArtContributor[] = contributorsResponse.data.filter( - (x) => x.art_id === Number(id), - ); - // TODO [HanMinh] to filter on backend - return { props: { artwork, contributors } }; + return { props: { artwork } }; }; diff --git a/client/src/types/art.ts b/client/src/types/art.ts index 3587cbe4..5a5397c7 100644 --- a/client/src/types/art.ts +++ b/client/src/types/art.ts @@ -1,3 +1,4 @@ +import { ArtContributor } from "./art-contributor"; import { BaseDto } from "./base-dto"; export interface Art extends BaseDto { @@ -5,4 +6,5 @@ export interface Art extends BaseDto { description: string; path_to_media: string; active: boolean; + contributors: ArtContributor[]; } diff --git a/server/api/settings.py b/server/api/settings.py index 424f34e5..f460957d 100644 --- a/server/api/settings.py +++ b/server/api/settings.py @@ -49,6 +49,7 @@ "django.contrib.messages", "django.contrib.staticfiles", "django_extensions", + "django_filters", "rest_framework", "corsheaders", "api.healthcheck", diff --git a/server/game_dev/serializers.py b/server/game_dev/serializers.py index 54c5c7a1..48901998 100644 --- a/server/game_dev/serializers.py +++ b/server/game_dev/serializers.py @@ -4,16 +4,16 @@ class ArtContributorSerializer(serializers.ModelSerializer): member_name = serializers.CharField(source='member.name', read_only=True) - art_id = serializers.IntegerField(source="art.id", read_only=True) class Meta: model = ArtContributor - fields = ['id', 'art', 'art_id', 'member', 'member_name', 'role'] + fields = ['id', 'art', 'member', 'member_name', 'role'] class ArtSerializer(serializers.ModelSerializer): + contributors = ArtContributorSerializer(many = True, read_only = True) class Meta: model = Art - fields = ['id', 'name', 'description', 'path_to_media', 'active'] + fields = ['id', 'name', 'description', 'path_to_media', 'active', 'contributors'] class MemberSerializer(serializers.ModelSerializer): class Meta: diff --git a/server/game_dev/views.py b/server/game_dev/views.py index bf68916a..86d0f0ad 100644 --- a/server/game_dev/views.py +++ b/server/game_dev/views.py @@ -1,11 +1,14 @@ from rest_framework import viewsets from .models import Art, ArtContributor, Member from .serializers import ArtContributorSerializer, ArtSerializer, MemberSerializer +from django_filters.rest_framework import DjangoFilterBackend class ArtContributorViewSet(viewsets.ModelViewSet): queryset = ArtContributor.objects.all() serializer_class = ArtContributorSerializer + filter_backends = [DjangoFilterBackend] + filterset_fields=['art'] class ArtViewSet(viewsets.ModelViewSet): queryset = Art.objects.all() From 1f2ce0cca3b308c3af817f5a921c3c6a0fe6c239 Mon Sep 17 00:00:00 2001 From: Han Minh Tran Date: Wed, 10 Dec 2025 15:34:28 +0800 Subject: [PATCH 013/122] Add Pagination on backend --- client/src/pages/artwork/index.tsx | 13 +++++++------ client/src/types/page-response.ts | 6 ++++++ server/api/settings.py | 5 +++++ 3 files changed, 18 insertions(+), 6 deletions(-) create mode 100644 client/src/types/page-response.ts diff --git a/client/src/pages/artwork/index.tsx b/client/src/pages/artwork/index.tsx index d0ca4af0..8a3586f5 100644 --- a/client/src/pages/artwork/index.tsx +++ b/client/src/pages/artwork/index.tsx @@ -4,9 +4,10 @@ import Link from "next/link"; import api from "@/lib/api"; import { Art } from "@/types/art"; +import { PageResult } from "@/types/page-response"; interface ArtworksPageProps { - artworks: Art[]; + pages: PageResult; } const PLACEHOLDER_ICON = ( @@ -52,7 +53,7 @@ function renderArtworkCard(artwork: Art) { ); } -export default function ArtworksPage({ artworks }: ArtworksPageProps) { +export default function ArtworksPage({ pages }: ArtworksPageProps) { return (
- {artworks.map((artwork) => renderArtworkCard(artwork))} + {pages.results.map((artwork) => renderArtworkCard(artwork))}
= async () => { - const res = await api.get("game-dev/arts"); - const arts = res.data; - return { props: { artworks: arts } }; + const res = await api.get>("game-dev/arts"); + const pages = res.data; + return { props: { pages } }; }; diff --git a/client/src/types/page-response.ts b/client/src/types/page-response.ts new file mode 100644 index 00000000..e5fa692e --- /dev/null +++ b/client/src/types/page-response.ts @@ -0,0 +1,6 @@ +export interface PageResult { + count: number; + next: string; + previous: string; + results: T[]; +} diff --git a/server/api/settings.py b/server/api/settings.py index f460957d..4664e69a 100644 --- a/server/api/settings.py +++ b/server/api/settings.py @@ -154,3 +154,8 @@ MEDIA_URL = "/media/" DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +REST_FRAMEWORK = { + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', + 'PAGE_SIZE': 100, +} \ No newline at end of file From eed05cef3c3c0d0ef6e31362cede3ca87bf471ca Mon Sep 17 00:00:00 2001 From: Han Minh Tran Date: Wed, 10 Dec 2025 18:02:26 +0800 Subject: [PATCH 014/122] Improve frontend to support responsive --- client/src/pages/artwork/[id].tsx | 208 +++++++++++++++++------------- 1 file changed, 115 insertions(+), 93 deletions(-) diff --git a/client/src/pages/artwork/[id].tsx b/client/src/pages/artwork/[id].tsx index 0aa8f7c5..b5c464d4 100644 --- a/client/src/pages/artwork/[id].tsx +++ b/client/src/pages/artwork/[id].tsx @@ -1,6 +1,7 @@ import { GetServerSideProps } from "next"; import Image from "next/image"; import Link from "next/link"; +import { JSX } from "react"; import api from "@/lib/api"; import { Art } from "@/types/art"; @@ -9,6 +10,84 @@ interface ArtworkPageProps { artwork: Art; } +const DISCORD_ICON = ( +
+ + + +
+); +const INSTAGRAM_ICON = ( +
+ + + +
+); + +function iconWithUrl(icon: JSX.Element, url: string) { + return {icon}; +} + +function displayContributors(artwork: Art) { + return ( +
+
+
+
+ Contributors +
+
+
+ {artwork.contributors?.map((contributor) => ( +
+
+ {contributor.member_name} +
+
+ {contributor.discord_url && + iconWithUrl(DISCORD_ICON, contributor.discord_url)} + {contributor.instagram_url && + iconWithUrl(INSTAGRAM_ICON, contributor.instagram_url)} +
+
+ ))} +
+
+
+ ); +} + export default function ArtworkPage({ artwork }: ArtworkPageProps) { return (
< Gallery
-
-
-
- Artwork image -
+
+
+ Artwork image
-
+
-
-
-
-
- Contributors -
-
-
- {artwork.contributors?.map((contributor) => ( -
-
- {contributor.member_name} -
-
- {contributor.discord_url && ( -
- - - -
- )} - {contributor.instagram_url && ( -
- - - -
- )} -
-
- ))} -
-
-
+ {displayContributors(artwork)} +
+
+
+
+
+ {artwork.name} +
+
+
+ + {artwork.description} +
+ {displayContributors(artwork)}
@@ -158,10 +180,10 @@ export default function ArtworkPage({ artwork }: ArtworkPageProps) { Game Image
From a1371d62d02753a709e2abf06569a94082d76bc1 Mon Sep 17 00:00:00 2001 From: Han Minh Tran Date: Wed, 10 Dec 2025 18:05:13 +0800 Subject: [PATCH 015/122] Improve resize image --- client/src/pages/artwork/[id].tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/pages/artwork/[id].tsx b/client/src/pages/artwork/[id].tsx index b5c464d4..7cb0cea6 100644 --- a/client/src/pages/artwork/[id].tsx +++ b/client/src/pages/artwork/[id].tsx @@ -121,7 +121,7 @@ export default function ArtworkPage({ artwork }: ArtworkPageProps) { alt="Artwork image" width={500} height={500} - className="relative block sm:h-auto sm:max-w-full md:max-h-full md:w-auto" + className="relative block sm:h-auto sm:max-w-full md:max-h-full" />
Date: Wed, 10 Dec 2025 18:06:13 +0800 Subject: [PATCH 016/122] Back button padding --- client/src/pages/artwork/[id].tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/pages/artwork/[id].tsx b/client/src/pages/artwork/[id].tsx index 7cb0cea6..95adacb5 100644 --- a/client/src/pages/artwork/[id].tsx +++ b/client/src/pages/artwork/[id].tsx @@ -102,7 +102,7 @@ export default function ArtworkPage({ artwork }: ArtworkPageProps) {
Date: Sat, 13 Dec 2025 03:48:37 +0000 Subject: [PATCH 017/122] feature: improve responsive layout --- client/src/components/ui/goBack.tsx | 34 +++++++++++++++++++++++++ client/src/components/ui/imageFrame.tsx | 30 ++++++++++++++++++++++ client/src/pages/artwork/[id].tsx | 7 ++--- client/src/pages/artwork/index.tsx | 29 ++++++++++----------- 4 files changed, 81 insertions(+), 19 deletions(-) create mode 100644 client/src/components/ui/goBack.tsx create mode 100644 client/src/components/ui/imageFrame.tsx diff --git a/client/src/components/ui/goBack.tsx b/client/src/components/ui/goBack.tsx new file mode 100644 index 00000000..4273aad5 --- /dev/null +++ b/client/src/components/ui/goBack.tsx @@ -0,0 +1,34 @@ +import Link from "next/link"; + +const ButtonGallery = () => { + return ( + + + + ); +}; + +export default ButtonGallery; diff --git a/client/src/components/ui/imageFrame.tsx b/client/src/components/ui/imageFrame.tsx new file mode 100644 index 00000000..6d66335c --- /dev/null +++ b/client/src/components/ui/imageFrame.tsx @@ -0,0 +1,30 @@ +import Image from "next/image"; +import React from "react"; + +interface CardProps { + imageSrc?: string; + imageAlt?: string; + children?: React.ReactNode; +} + +const Card = ({ imageSrc, imageAlt = "Artwork", children }: CardProps) => { + return ( +
+
+ {imageSrc ? ( + {imageAlt} + ) : ( + children || No Image + )} +
+
+ ); +}; + +export default Card; diff --git a/client/src/pages/artwork/[id].tsx b/client/src/pages/artwork/[id].tsx index 732f645b..0afcd2a0 100644 --- a/client/src/pages/artwork/[id].tsx +++ b/client/src/pages/artwork/[id].tsx @@ -1,6 +1,7 @@ import { GetServerSideProps } from "next"; import Image from "next/image"; -import Link from "next/link"; + +import ButtonGallery from "@/components/ui/goBack"; type Contributor = { id: string; @@ -37,13 +38,13 @@ export default function ArtworkPage({ artwork }: ArtworkPageProps) {
- < Gallery +
-
- {PLACEHOLDER_ICON} -
+ + {!artwork.pathToMedia && PLACEHOLDER_ICON} + ); } @@ -80,17 +81,13 @@ export default function ArtworksPage({ artworks }: ArtworksPageProps) { data-layer="Auto Layout Horizontal" className="AutoLayoutHorizontal items-start justify-start gap-6" > -
-
- More about us → -
-
+ More about us → +
From 3b5607e4d186fccf1bb89f3ca52dff7f79470932 Mon Sep 17 00:00:00 2001 From: Karl_Sue Date: Sat, 13 Dec 2025 05:03:41 +0000 Subject: [PATCH 018/122] feat: add Art hook --- client/src/hooks/useArtworkData.ts | 18 ++++++++++++++++++ client/src/pages/artwork/index.tsx | 20 ++++++++++++++++---- 2 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 client/src/hooks/useArtworkData.ts diff --git a/client/src/hooks/useArtworkData.ts b/client/src/hooks/useArtworkData.ts new file mode 100644 index 00000000..1008d07e --- /dev/null +++ b/client/src/hooks/useArtworkData.ts @@ -0,0 +1,18 @@ +import { Art } from "@/types/art"; + +export const generateMockArtworks = (count: number): Art[] => { + const artworks: Art[] = []; + for (let i = 1; i <= count; i++) { + artworks.push({ + id: i, + name: `Artwork ${i}`, + description: "Mock artwork description", + //source_game: "Mock Game", + path_to_media: "", + active: true, + contributors: [], + //created_at: new Date().toISOString(), + }); + } + return artworks; +}; diff --git a/client/src/pages/artwork/index.tsx b/client/src/pages/artwork/index.tsx index f5f7d8c9..e34fbddf 100644 --- a/client/src/pages/artwork/index.tsx +++ b/client/src/pages/artwork/index.tsx @@ -3,6 +3,7 @@ import Link from "next/link"; import { Button } from "@/components/ui/button"; import Card from "@/components/ui/imageFrame"; +import { generateMockArtworks } from "@/hooks/useArtworkData"; import api from "@/lib/api"; import { Art } from "@/types/art"; import { PageResult } from "@/types/page-response"; @@ -112,13 +113,24 @@ export const getServerSideProps: GetServerSideProps< const res = await api.get>("game-dev/arts"); return { props: { artworks: res.data } }; } catch { + // return { + // props: { + // artworks: { + // count: 0, + // next: null as unknown as string, + // previous: null as unknown as string, + // results: [] as Art[], + // }, + // }, + // }; ==> use when successfully populate db + const mockArtworks = generateMockArtworks(12); return { props: { artworks: { - count: 0, - next: null as unknown as string, - previous: null as unknown as string, - results: [] as Art[], + count: mockArtworks.length, + next: "", + previous: "", + results: mockArtworks, }, }, }; From 2e9ef677e925232df5d78aa1b114074db132e620 Mon Sep 17 00:00:00 2001 From: Karl_Sue Date: Sat, 13 Dec 2025 05:18:23 +0000 Subject: [PATCH 019/122] feat: add Artwork hook --- client/src/hooks/useArtworkData.ts | 31 ++++++++++++++++++++++++++++++ client/src/pages/artwork/[id].tsx | 13 ++++++++++--- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/client/src/hooks/useArtworkData.ts b/client/src/hooks/useArtworkData.ts index 1008d07e..eca1ceee 100644 --- a/client/src/hooks/useArtworkData.ts +++ b/client/src/hooks/useArtworkData.ts @@ -16,3 +16,34 @@ export const generateMockArtworks = (count: number): Art[] => { } return artworks; }; + +export const generateMockArtwork = (id: string): Art => { + return { + id: Number(id), + name: "Mock Artwork Title", + description: + "Lorem ipsum dolor sit amet. Non numquam dicta nam autem dicta 33 error molestias et repellat consequatur eum iste expedita est dolorem libero et quas provident!", + //source_game: "Mock Game", + path_to_media: "/placeholder1293x405.svg", + active: true, + //created_at: new Date().toISOString(), + contributors: [ + { + id: 1, + art_id: Number(id), + member_name: "Contributor 1", + role: "user1", + discord_url: "https://discord.com", + instagram_url: "", + }, + { + id: 2, + art_id: Number(id), + member_name: "Contributor 2", + role: "user2", + discord_url: "", + instagram_url: "https://instagram.com", + }, + ], + }; +}; diff --git a/client/src/pages/artwork/[id].tsx b/client/src/pages/artwork/[id].tsx index d37ad25e..15d1681a 100644 --- a/client/src/pages/artwork/[id].tsx +++ b/client/src/pages/artwork/[id].tsx @@ -3,6 +3,7 @@ import Image from "next/image"; import { JSX } from "react"; import ButtonGallery from "@/components/ui/goBack"; +import { generateMockArtwork } from "@/hooks/useArtworkData"; import api from "@/lib/api"; import { Art } from "@/types/art"; @@ -201,7 +202,13 @@ export const getServerSideProps: GetServerSideProps = async ( context, ) => { const { id } = context.params as { id: string }; - const artResponse = await api.get(`game-dev/arts/${id}`); - const artwork = artResponse.data; - return { props: { artwork } }; + try { + const artResponse = await api.get(`game-dev/arts/${id}`); + const artwork = artResponse.data; + return { props: { artwork } }; + } catch { + // Return mock data when API fails or DB is empty + const mockArtwork = generateMockArtwork(id); + return { props: { artwork: mockArtwork } }; + } }; From 3d3811954caf12ae5b08247ec8dcc6e1c764cad3 Mon Sep 17 00:00:00 2001 From: Karl_Sue Date: Sat, 13 Dec 2025 05:31:21 +0000 Subject: [PATCH 020/122] feat: add placeholder art --- client/src/hooks/useArtworkData.ts | 2 +- client/src/pages/artwork/[id].tsx | 37 ++++++++++++++++++++++++------ 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/client/src/hooks/useArtworkData.ts b/client/src/hooks/useArtworkData.ts index eca1ceee..44d0136c 100644 --- a/client/src/hooks/useArtworkData.ts +++ b/client/src/hooks/useArtworkData.ts @@ -24,7 +24,7 @@ export const generateMockArtwork = (id: string): Art => { description: "Lorem ipsum dolor sit amet. Non numquam dicta nam autem dicta 33 error molestias et repellat consequatur eum iste expedita est dolorem libero et quas provident!", //source_game: "Mock Game", - path_to_media: "/placeholder1293x405.svg", + path_to_media: "", active: true, //created_at: new Date().toISOString(), contributors: [ diff --git a/client/src/pages/artwork/[id].tsx b/client/src/pages/artwork/[id].tsx index 15d1681a..ac8ecc3a 100644 --- a/client/src/pages/artwork/[id].tsx +++ b/client/src/pages/artwork/[id].tsx @@ -117,13 +117,36 @@ export default function ArtworkPage({ artwork }: ArtworkPageProps) { className="Frame1099 bg-neutral-1 justify-start md:flex" >
- Artwork image + {artwork.path_to_media ? ( + Artwork image + ) : ( + // in case fail to load image or no image in db yet +
+
+ + + +
+
+ )}
Date: Sat, 13 Dec 2025 14:42:47 +0800 Subject: [PATCH 021/122] Fix flake8 --- server/api/settings.py | 2 +- server/api/urls.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/api/settings.py b/server/api/settings.py index 4664e69a..46902758 100644 --- a/server/api/settings.py +++ b/server/api/settings.py @@ -158,4 +158,4 @@ REST_FRAMEWORK = { 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'PAGE_SIZE': 100, -} \ No newline at end of file +} diff --git a/server/api/urls.py b/server/api/urls.py index c6a16187..073e0e7d 100644 --- a/server/api/urls.py +++ b/server/api/urls.py @@ -21,5 +21,5 @@ urlpatterns = [ path("admin/", admin.site.urls), path("api/healthcheck/", include("api.healthcheck.urls")), - path("api/game-dev/", include("game_dev.urls")), -] \ No newline at end of file + path("api/game-dev/", include("game_dev.urls")), +] From 45afd6d96391e76b11db3c46e1368afc04eee624 Mon Sep 17 00:00:00 2001 From: Karl_Sue Date: Sat, 13 Dec 2025 06:52:52 +0000 Subject: [PATCH 022/122] fix: match Prettier code style --- client/src/styles/globals.css | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/client/src/styles/globals.css b/client/src/styles/globals.css index 53eb51bc..43a74218 100644 --- a/client/src/styles/globals.css +++ b/client/src/styles/globals.css @@ -34,11 +34,11 @@ --radius: 0.5rem; - --dark-2: #090A19; - --neutral-1: #1B1F4C; - --light-1: #FFFFFF; - --light-2: #CED1FE; - --light-3: #9CA4FD; + --dark-2: #090a19; + --neutral-1: #1b1f4c; + --light-1: #ffffff; + --light-2: #ced1fe; + --light-3: #9ca4fd; } } @@ -68,4 +68,4 @@ } .border-light-2 { border-color: var(--light-2); -} \ No newline at end of file +} From b7273d5751968658f4d1df9179e0cb66a630ba28 Mon Sep 17 00:00:00 2001 From: Han Minh Tran Date: Sat, 13 Dec 2025 14:54:42 +0800 Subject: [PATCH 023/122] Fix flake8 --- server/game_dev/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/game_dev/admin.py b/server/game_dev/admin.py index 0e8d557c..b148cace 100644 --- a/server/game_dev/admin.py +++ b/server/game_dev/admin.py @@ -6,4 +6,4 @@ admin.site.register(Art) -admin.site.register(ArtContributor) \ No newline at end of file +admin.site.register(ArtContributor) From 53e30c3b3b279672e711d93f24faf62e4013e2b6 Mon Sep 17 00:00:00 2001 From: Han Minh Tran Date: Sat, 20 Dec 2025 18:18:56 +0800 Subject: [PATCH 024/122] Refactor code for reuseability --- .../ui/{goBack.tsx => go-back-button.tsx} | 12 +++++--- .../ui/{imageFrame.tsx => image-card.tsx} | 6 ++-- .../src/components/ui/image-placeholder.tsx | 26 +++++++++++++++++ ...{useArtworkData.ts => use-artwork-data.ts} | 0 client/src/pages/artwork/[id].tsx | 28 ++++--------------- client/src/pages/artwork/index.tsx | 18 +++--------- 6 files changed, 46 insertions(+), 44 deletions(-) rename client/src/components/ui/{goBack.tsx => go-back-button.tsx} (79%) rename client/src/components/ui/{imageFrame.tsx => image-card.tsx} (87%) create mode 100644 client/src/components/ui/image-placeholder.tsx rename client/src/hooks/{useArtworkData.ts => use-artwork-data.ts} (100%) diff --git a/client/src/components/ui/goBack.tsx b/client/src/components/ui/go-back-button.tsx similarity index 79% rename from client/src/components/ui/goBack.tsx rename to client/src/components/ui/go-back-button.tsx index 4273aad5..22109a4a 100644 --- a/client/src/components/ui/goBack.tsx +++ b/client/src/components/ui/go-back-button.tsx @@ -1,8 +1,12 @@ import Link from "next/link"; -const ButtonGallery = () => { +interface GoBackButtonProps { + url: string; + label: string; +} +const GoBackButton = ({ url, label }: GoBackButtonProps) => { return ( - +
-

Gallery

+

{label}

); }; -export default ButtonGallery; +export default GoBackButton; diff --git a/client/src/components/ui/imageFrame.tsx b/client/src/components/ui/image-card.tsx similarity index 87% rename from client/src/components/ui/imageFrame.tsx rename to client/src/components/ui/image-card.tsx index 6d66335c..f3ca51b1 100644 --- a/client/src/components/ui/imageFrame.tsx +++ b/client/src/components/ui/image-card.tsx @@ -1,13 +1,13 @@ import Image from "next/image"; import React from "react"; -interface CardProps { +interface ImageCard { imageSrc?: string; imageAlt?: string; children?: React.ReactNode; } -const Card = ({ imageSrc, imageAlt = "Artwork", children }: CardProps) => { +const ImageCard = ({ imageSrc, imageAlt = "Image", children }: ImageCard) => { return (
@@ -27,4 +27,4 @@ const Card = ({ imageSrc, imageAlt = "Artwork", children }: CardProps) => { ); }; -export default Card; +export default ImageCard; diff --git a/client/src/components/ui/image-placeholder.tsx b/client/src/components/ui/image-placeholder.tsx new file mode 100644 index 00000000..b7e25e58 --- /dev/null +++ b/client/src/components/ui/image-placeholder.tsx @@ -0,0 +1,26 @@ +import React from "react"; + +const ImagePlaceholder = () => { + return ( +
+
+ + + +
+
+ ); +}; +export default ImagePlaceholder; diff --git a/client/src/hooks/useArtworkData.ts b/client/src/hooks/use-artwork-data.ts similarity index 100% rename from client/src/hooks/useArtworkData.ts rename to client/src/hooks/use-artwork-data.ts diff --git a/client/src/pages/artwork/[id].tsx b/client/src/pages/artwork/[id].tsx index ac8ecc3a..8e3b89b7 100644 --- a/client/src/pages/artwork/[id].tsx +++ b/client/src/pages/artwork/[id].tsx @@ -2,8 +2,9 @@ import { GetServerSideProps } from "next"; import Image from "next/image"; import { JSX } from "react"; -import ButtonGallery from "@/components/ui/goBack"; -import { generateMockArtwork } from "@/hooks/useArtworkData"; +import GoBackButton from "@/components/ui/go-back-button"; +import ImagePlaceholder from "@/components/ui/image-placeholder"; +import { generateMockArtwork } from "@/hooks/use-artwork-data"; import api from "@/lib/api"; import { Art } from "@/types/art"; @@ -109,7 +110,7 @@ export default function ArtworkPage({ artwork }: ArtworkPageProps) { data-layer="< Gallery" className="Gallery text-light-1 h-10 justify-start font-['DM_Sans'] text-3xl font-bold leading-10 tracking-tight" > - +
) : ( - // in case fail to load image or no image in db yet -
-
- - - -
-
+ )}
- {!artwork.path_to_media && PLACEHOLDER_ICON} - + ); } @@ -113,16 +113,6 @@ export const getServerSideProps: GetServerSideProps< const res = await api.get>("game-dev/arts"); return { props: { artworks: res.data } }; } catch { - // return { - // props: { - // artworks: { - // count: 0, - // next: null as unknown as string, - // previous: null as unknown as string, - // results: [] as Art[], - // }, - // }, - // }; ==> use when successfully populate db const mockArtworks = generateMockArtworks(12); return { props: { From b234e606c3f33f683df289d1edf6bcf77cad7420 Mon Sep 17 00:00:00 2001 From: Han Minh Tran Date: Sat, 20 Dec 2025 19:18:58 +0800 Subject: [PATCH 025/122] Error message --- .../src/components/ui/modal/error-modal.tsx | 45 +++++++++++++++++++ client/src/pages/artwork/[id].tsx | 34 +++++++------- client/src/pages/artwork/index.tsx | 30 +++++-------- client/src/styles/globals.css | 4 ++ 4 files changed, 80 insertions(+), 33 deletions(-) create mode 100644 client/src/components/ui/modal/error-modal.tsx diff --git a/client/src/components/ui/modal/error-modal.tsx b/client/src/components/ui/modal/error-modal.tsx new file mode 100644 index 00000000..d5ff49db --- /dev/null +++ b/client/src/components/ui/modal/error-modal.tsx @@ -0,0 +1,45 @@ +import React, { useState } from "react"; + +interface ErrorModalProps { + message: string | null; + onClose: () => void; +} + +const ErrorModal = ({ message, onClose = () => {} }: ErrorModalProps) => { + const [isVisible, setIsVisible] = useState(true); + if (!isVisible || !message) { + return null; + } + + function onModalClose() { + setIsVisible(false); + onClose(); + } + + return ( + // Backdrop overlay +
+ {/* Modal content container */} +
e.stopPropagation()} // Prevent closing when clicking inside the modal + > +

Error

+

{message}

+
+ +
+
+
+ ); +}; + +export default ErrorModal; diff --git a/client/src/pages/artwork/[id].tsx b/client/src/pages/artwork/[id].tsx index 8e3b89b7..29c6a369 100644 --- a/client/src/pages/artwork/[id].tsx +++ b/client/src/pages/artwork/[id].tsx @@ -1,15 +1,17 @@ import { GetServerSideProps } from "next"; import Image from "next/image"; +import { useRouter } from "next/navigation"; import { JSX } from "react"; import GoBackButton from "@/components/ui/go-back-button"; import ImagePlaceholder from "@/components/ui/image-placeholder"; -import { generateMockArtwork } from "@/hooks/use-artwork-data"; +import ErrorModal from "@/components/ui/modal/error-modal"; import api from "@/lib/api"; import { Art } from "@/types/art"; interface ArtworkPageProps { - artwork: Art; + artwork?: Art; + error?: string; } const DISCORD_ICON = ( @@ -90,7 +92,11 @@ function displayContributors(artwork: Art) { ); } -export default function ArtworkPage({ artwork }: ArtworkPageProps) { +export default function ArtworkPage({ artwork, error }: ArtworkPageProps) { + const router = useRouter(); + if (error) { + return router.back()} />; + } return (
- {artwork.path_to_media ? ( + {artwork!.path_to_media ? ( Artwork image - {artwork.name} + {artwork!.name}
- {artwork.description} + {artwork!.description}
- {displayContributors(artwork)} + {displayContributors(artwork!)}
@@ -163,7 +169,7 @@ export default function ArtworkPage({ artwork }: ArtworkPageProps) { data-layer="Art Name" className="ArtName text-light-3 flex justify-center font-['Jersey_10'] text-8xl font-normal leading-[76px] tracking-wide" > - {artwork.name} + {artwork!.name}
- {artwork.description} + {artwork!.description}
- {displayContributors(artwork)} + {displayContributors(artwork!)}
@@ -211,9 +217,7 @@ export const getServerSideProps: GetServerSideProps = async ( const artResponse = await api.get(`game-dev/arts/${id}`); const artwork = artResponse.data; return { props: { artwork } }; - } catch { - // Return mock data when API fails or DB is empty - const mockArtwork = generateMockArtwork(id); - return { props: { artwork: mockArtwork } }; + } catch (err: { message: string }) { + return { props: { error: err.message || "Failed to load artwork." } }; } }; diff --git a/client/src/pages/artwork/index.tsx b/client/src/pages/artwork/index.tsx index ab5542d6..6388ccd9 100644 --- a/client/src/pages/artwork/index.tsx +++ b/client/src/pages/artwork/index.tsx @@ -1,15 +1,17 @@ import { GetServerSideProps } from "next"; import Link from "next/link"; +import { useRouter } from "next/navigation"; import { Button } from "@/components/ui/button"; import ImageCard from "@/components/ui/image-card"; -import { generateMockArtworks } from "@/hooks/use-artwork-data"; +import ErrorModal from "@/components/ui/modal/error-modal"; import api from "@/lib/api"; import { Art } from "@/types/art"; import { PageResult } from "@/types/page-response"; interface ArtworksPageProps { - artworks: PageResult; + artworks?: PageResult; + error?: string; } const PLACEHOLDER_ICON = ( @@ -48,7 +50,11 @@ function renderArtworkCard(artwork: Art) { ); } -export default function ArtworksPage({ artworks }: ArtworksPageProps) { +export default function ArtworksPage({ artworks, error }: ArtworksPageProps) { + const router = useRouter(); + if (error) { + return router.back()} />; + } return (
- {artworks.results.map((artwork) => renderArtworkCard(artwork))} + {artworks!.results.map((artwork) => renderArtworkCard(artwork))}
= async () => { try { const res = await api.get>("game-dev/arts"); return { props: { artworks: res.data } }; - } catch { - const mockArtworks = generateMockArtworks(12); - return { - props: { - artworks: { - count: mockArtworks.length, - next: "", - previous: "", - results: mockArtworks, - }, - }, - }; + } catch (err: { message: string }) { + return { props: { error: err.message || "Failed to load artworks." } }; } }; diff --git a/client/src/styles/globals.css b/client/src/styles/globals.css index 43a74218..81287554 100644 --- a/client/src/styles/globals.css +++ b/client/src/styles/globals.css @@ -39,6 +39,7 @@ --light-1: #ffffff; --light-2: #ced1fe; --light-3: #9ca4fd; + --error: #fa5c5c; } } @@ -69,3 +70,6 @@ .border-light-2 { border-color: var(--light-2); } +.bg-error { + background-color: var(--error); +} \ No newline at end of file From cc4ac457527c9a56558344a1772d7c4b45a21b85 Mon Sep 17 00:00:00 2001 From: Han Minh Tran Date: Sat, 20 Dec 2025 19:22:47 +0800 Subject: [PATCH 026/122] Remove mock data on Frontend --- client/src/hooks/use-artwork-data.ts | 49 ---------------------------- 1 file changed, 49 deletions(-) delete mode 100644 client/src/hooks/use-artwork-data.ts diff --git a/client/src/hooks/use-artwork-data.ts b/client/src/hooks/use-artwork-data.ts deleted file mode 100644 index 44d0136c..00000000 --- a/client/src/hooks/use-artwork-data.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Art } from "@/types/art"; - -export const generateMockArtworks = (count: number): Art[] => { - const artworks: Art[] = []; - for (let i = 1; i <= count; i++) { - artworks.push({ - id: i, - name: `Artwork ${i}`, - description: "Mock artwork description", - //source_game: "Mock Game", - path_to_media: "", - active: true, - contributors: [], - //created_at: new Date().toISOString(), - }); - } - return artworks; -}; - -export const generateMockArtwork = (id: string): Art => { - return { - id: Number(id), - name: "Mock Artwork Title", - description: - "Lorem ipsum dolor sit amet. Non numquam dicta nam autem dicta 33 error molestias et repellat consequatur eum iste expedita est dolorem libero et quas provident!", - //source_game: "Mock Game", - path_to_media: "", - active: true, - //created_at: new Date().toISOString(), - contributors: [ - { - id: 1, - art_id: Number(id), - member_name: "Contributor 1", - role: "user1", - discord_url: "https://discord.com", - instagram_url: "", - }, - { - id: 2, - art_id: Number(id), - member_name: "Contributor 2", - role: "user2", - discord_url: "", - instagram_url: "https://instagram.com", - }, - ], - }; -}; From f0c5e4cd4e3f01bdb6e8a64bfb5cd48a3ab39eba Mon Sep 17 00:00:00 2001 From: Han Minh Tran Date: Sat, 20 Dec 2025 19:37:40 +0800 Subject: [PATCH 027/122] Solve conflict and adapt code --- client/src/components/main/Navbar.tsx | 2 +- client/src/pages/artwork/[id].tsx | 8 +------- client/src/pages/artwork/index.tsx | 8 +------- server/game_dev/admin.py | 2 +- 4 files changed, 4 insertions(+), 16 deletions(-) diff --git a/client/src/components/main/Navbar.tsx b/client/src/components/main/Navbar.tsx index b25a62b1..67a0dbef 100644 --- a/client/src/components/main/Navbar.tsx +++ b/client/src/components/main/Navbar.tsx @@ -18,7 +18,7 @@ export default function Navbar() { return ( <> -
+
-
- TODO add Header -
= async ( ) => { const { id } = context.params as { id: string }; try { - const artResponse = await api.get(`game-dev/arts/${id}`); + const artResponse = await api.get(`arts/${id}`); const artwork = artResponse.data; return { props: { artwork } }; } catch (err: { message: string }) { diff --git a/client/src/pages/artwork/index.tsx b/client/src/pages/artwork/index.tsx index 6388ccd9..a29a78d4 100644 --- a/client/src/pages/artwork/index.tsx +++ b/client/src/pages/artwork/index.tsx @@ -57,12 +57,6 @@ export default function ArtworksPage({ artworks, error }: ArtworksPageProps) { } return (
-
- TODO add Header -
= async () => { try { - const res = await api.get>("game-dev/arts"); + const res = await api.get>("arts"); return { props: { artworks: res.data } }; } catch (err: { message: string }) { return { props: { error: err.message || "Failed to load artworks." } }; diff --git a/server/game_dev/admin.py b/server/game_dev/admin.py index 58e968dd..c2d8a5e2 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 Art, ArtContributor, Member, Event class MemberAdmin(admin.ModelAdmin): From 08f0865b2b3a32515e39569db9c2ff4323513c5e Mon Sep 17 00:00:00 2001 From: Han Minh Tran Date: Sat, 20 Dec 2025 19:41:04 +0800 Subject: [PATCH 028/122] fix flake8 on backend --- server/game_dev/serializers.py | 1 + server/game_dev/views.py | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/game_dev/serializers.py b/server/game_dev/serializers.py index 659373c2..d9173974 100644 --- a/server/game_dev/serializers.py +++ b/server/game_dev/serializers.py @@ -15,6 +15,7 @@ class Meta: "location", ] + class ArtContributorSerializer(serializers.ModelSerializer): member_name = serializers.CharField(source='member.name', read_only=True) diff --git a/server/game_dev/views.py b/server/game_dev/views.py index 70e5bfb9..313093f6 100644 --- a/server/game_dev/views.py +++ b/server/game_dev/views.py @@ -18,7 +18,6 @@ def get_queryset(self): return Event.objects.filter(id=self.kwargs["id"]) - class ArtContributorViewSet(viewsets.ModelViewSet): queryset = ArtContributor.objects.all() serializer_class = ArtContributorSerializer @@ -33,4 +32,4 @@ class ArtViewSet(viewsets.ModelViewSet): class MemberViewSet(viewsets.ModelViewSet): queryset = Member.objects.all() - serializer_class = MemberSerializer \ No newline at end of file + serializer_class = MemberSerializer From 23b65ea0fa38a3aa92fe5db5fc9c30d5bdb44a11 Mon Sep 17 00:00:00 2001 From: Han Minh Tran Date: Sat, 20 Dec 2025 19:43:12 +0800 Subject: [PATCH 029/122] Fix Prettier and type check --- client/src/pages/artwork/[id].tsx | 6 ++++-- client/src/pages/artwork/index.tsx | 6 ++++-- client/src/styles/globals.css | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/client/src/pages/artwork/[id].tsx b/client/src/pages/artwork/[id].tsx index cf0f0cad..f61b3ed9 100644 --- a/client/src/pages/artwork/[id].tsx +++ b/client/src/pages/artwork/[id].tsx @@ -211,7 +211,9 @@ export const getServerSideProps: GetServerSideProps = async ( const artResponse = await api.get(`arts/${id}`); const artwork = artResponse.data; return { props: { artwork } }; - } catch (err: { message: string }) { - return { props: { error: err.message || "Failed to load artwork." } }; + } catch (err: unknown) { + return { + props: { error: (err as Error).message || "Failed to load artwork." }, + }; } }; diff --git a/client/src/pages/artwork/index.tsx b/client/src/pages/artwork/index.tsx index a29a78d4..4211949a 100644 --- a/client/src/pages/artwork/index.tsx +++ b/client/src/pages/artwork/index.tsx @@ -110,7 +110,9 @@ export const getServerSideProps: GetServerSideProps< try { const res = await api.get>("arts"); return { props: { artworks: res.data } }; - } catch (err: { message: string }) { - return { props: { error: err.message || "Failed to load artworks." } }; + } catch (err: unknown) { + return { + props: { error: (err as Error).message || "Failed to load artworks." }, + }; } }; diff --git a/client/src/styles/globals.css b/client/src/styles/globals.css index 0836bc29..8cf2f38f 100644 --- a/client/src/styles/globals.css +++ b/client/src/styles/globals.css @@ -91,4 +91,4 @@ } .bg-error { background-color: var(--error); -} \ No newline at end of file +} From 6cc9d1c677bab95f63e803275bd2b7fb1351f1f4 Mon Sep 17 00:00:00 2001 From: Han Minh Tran Date: Sat, 20 Dec 2025 19:49:32 +0800 Subject: [PATCH 030/122] Correct django-filter version --- server/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/pyproject.toml b/server/pyproject.toml index dd0e67ee..fb713c85 100644 --- a/server/pyproject.toml +++ b/server/pyproject.toml @@ -16,7 +16,7 @@ gunicorn = "^23.0.0" python-dotenv = "^1.0.1" django-extensions = "^3.2.3" pillow = "^11.3.0" -django-filter = "^25.2" +django-filter = "^24.3" [tool.poetry.group.dev.dependencies] From 39306efc944e4af5e0c5fcf0f5d12e53fb38114c Mon Sep 17 00:00:00 2001 From: Han Minh Tran Date: Sat, 20 Dec 2025 19:54:06 +0800 Subject: [PATCH 031/122] Commit poetry.lock --- server/poetry.lock | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/server/poetry.lock b/server/poetry.lock index 282ace1b..aab552a0 100644 --- a/server/poetry.lock +++ b/server/poetry.lock @@ -91,20 +91,17 @@ Django = ">=3.2" [[package]] name = "django-filter" -version = "25.2" +version = "24.3" description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically." optional = false -python-versions = ">=3.10" +python-versions = ">=3.8" files = [ - {file = "django_filter-25.2-py3-none-any.whl", hash = "sha256:9c0f8609057309bba611062fe1b720b4a873652541192d232dd28970383633e3"}, - {file = "django_filter-25.2.tar.gz", hash = "sha256:760e984a931f4468d096f5541787efb8998c61217b73006163bf2f9523fe8f23"}, + {file = "django_filter-24.3-py3-none-any.whl", hash = "sha256:c4852822928ce17fb699bcfccd644b3574f1a2d80aeb2b4ff4f16b02dd49dc64"}, + {file = "django_filter-24.3.tar.gz", hash = "sha256:d8ccaf6732afd21ca0542f6733b11591030fa98669f8d15599b358e24a2cd9c3"}, ] [package.dependencies] -Django = ">=5.2" - -[package.extras] -drf = ["djangorestframework"] +Django = ">=4.2" [[package]] name = "djangorestframework" From d564d057bcda81823441b37177ea7b2d150d2fee Mon Sep 17 00:00:00 2001 From: Han Minh Tran Date: Sat, 20 Dec 2025 19:55:22 +0800 Subject: [PATCH 032/122] Commit poetry.lock --- server/poetry.lock | 39 ++++++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/server/poetry.lock b/server/poetry.lock index aab552a0..202b5e94 100644 --- a/server/poetry.lock +++ b/server/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. [[package]] name = "asgiref" @@ -6,6 +6,7 @@ version = "3.8.1" description = "ASGI specs, helper code, and adapters" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, @@ -20,6 +21,7 @@ version = "2.15.8" description = "An abstract syntax tree for Python with inference support." optional = false python-versions = ">=3.7.2" +groups = ["dev"] files = [ {file = "astroid-2.15.8-py3-none-any.whl", hash = "sha256:1aa149fc5c6589e3d0ece885b4491acd80af4f087baafa3fb5203b113e68cd3c"}, {file = "astroid-2.15.8.tar.gz", hash = "sha256:6c107453dffee9055899705de3c9ead36e74119cee151e5a9aaf7f0b0e020a6a"}, @@ -35,6 +37,8 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +markers = "sys_platform == \"win32\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -46,13 +50,14 @@ version = "5.1.15" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.10" +groups = ["main"] files = [ {file = "django-5.1.15-py3-none-any.whl", hash = "sha256:117871e58d6eda37f09870b7d73a3d66567b03aecd515b386b1751177c413432"}, {file = "django-5.1.15.tar.gz", hash = "sha256:46a356b5ff867bece73fc6365e081f21c569973403ee7e9b9a0316f27d0eb947"}, ] [package.dependencies] -asgiref = ">=3.8.1" +asgiref = ">=3.8.1,<4" sqlparse = ">=0.3.1" tzdata = {version = "*", markers = "sys_platform == \"win32\""} @@ -66,6 +71,7 @@ version = "4.4.0" description = "django-cors-headers is a Django application for handling the server headers required for Cross-Origin Resource Sharing (CORS)." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "django_cors_headers-4.4.0-py3-none-any.whl", hash = "sha256:5c6e3b7fe870876a1efdfeb4f433782c3524078fa0dc9e0195f6706ce7a242f6"}, {file = "django_cors_headers-4.4.0.tar.gz", hash = "sha256:92cf4633e22af67a230a1456cb1b7a02bb213d6536d2dcb2a4a24092ea9cebc2"}, @@ -81,6 +87,7 @@ version = "3.2.3" description = "Extensions for Django" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "django-extensions-3.2.3.tar.gz", hash = "sha256:44d27919d04e23b3f40231c4ab7af4e61ce832ef46d610cc650d53e68328410a"}, {file = "django_extensions-3.2.3-py3-none-any.whl", hash = "sha256:9600b7562f79a92cbf1fde6403c04fee314608fefbb595502e34383ae8203401"}, @@ -95,6 +102,7 @@ version = "24.3" description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "django_filter-24.3-py3-none-any.whl", hash = "sha256:c4852822928ce17fb699bcfccd644b3574f1a2d80aeb2b4ff4f16b02dd49dc64"}, {file = "django_filter-24.3.tar.gz", hash = "sha256:d8ccaf6732afd21ca0542f6733b11591030fa98669f8d15599b358e24a2cd9c3"}, @@ -109,6 +117,7 @@ version = "3.15.2" description = "Web APIs for Django, made easy." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "djangorestframework-3.15.2-py3-none-any.whl", hash = "sha256:2b8871b062ba1aefc2de01f773875441a961fefbf79f5eed1e32b2f096944b20"}, {file = "djangorestframework-3.15.2.tar.gz", hash = "sha256:36fe88cd2d6c6bec23dca9804bab2ba5517a8bb9d8f47ebc68981b56840107ad"}, @@ -123,6 +132,7 @@ version = "6.1.0" description = "the modular source code checker: pep8 pyflakes and co" optional = false python-versions = ">=3.8.1" +groups = ["dev"] files = [ {file = "flake8-6.1.0-py2.py3-none-any.whl", hash = "sha256:ffdfce58ea94c6580c77888a86506937f9a1a227dfcd15f245d694ae20a6b6e5"}, {file = "flake8-6.1.0.tar.gz", hash = "sha256:d5b3857f07c030bdb5bf41c7f53799571d75c4491748a3adcd47de929e34cd23"}, @@ -139,6 +149,7 @@ version = "1.4" description = "Plugin to catch bad style specific to Django Projects." optional = false python-versions = ">=3.7.2,<4.0.0" +groups = ["dev"] files = [ {file = "flake8_django-1.4.tar.gz", hash = "sha256:4debba883084191568e3187416d1d6bdd4abd826da988f197a3c36572e9f30de"}, ] @@ -153,6 +164,7 @@ version = "1.5.1" description = "Let your Python tests travel through time" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "freezegun-1.5.1-py3-none-any.whl", hash = "sha256:bf111d7138a8abe55ab48a71755673dbaa4ab87f4cff5634a4442dfec34c15f1"}, {file = "freezegun-1.5.1.tar.gz", hash = "sha256:b29dedfcda6d5e8e083ce71b2b542753ad48cfec44037b3fc79702e2980a89e9"}, @@ -167,6 +179,7 @@ version = "23.0.0" description = "WSGI HTTP Server for UNIX" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d"}, {file = "gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec"}, @@ -188,6 +201,7 @@ version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, @@ -199,6 +213,7 @@ version = "1.10.0" description = "A fast and thorough lazy object proxy." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "lazy-object-proxy-1.10.0.tar.gz", hash = "sha256:78247b6d45f43a52ef35c25b5581459e85117225408a4128a3daf8bf9648ac69"}, {file = "lazy_object_proxy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:855e068b0358ab916454464a884779c7ffa312b8925c6f7401e952dcf3b89977"}, @@ -245,6 +260,7 @@ version = "0.7.0" description = "McCabe checker, plugin for flake8" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, @@ -256,6 +272,7 @@ version = "24.1" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, @@ -267,6 +284,7 @@ version = "11.3.0" description = "Python Imaging Library (Fork)" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860"}, {file = "pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad"}, @@ -382,7 +400,7 @@ fpx = ["olefile"] mic = ["olefile"] test-arrow = ["pyarrow"] tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "trove-classifiers (>=2024.10.12)"] -typing = ["typing-extensions"] +typing = ["typing-extensions ; python_version < \"3.10\""] xmp = ["defusedxml"] [[package]] @@ -391,6 +409,7 @@ version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -406,6 +425,7 @@ version = "2.11.1" description = "Python style guide checker" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, @@ -417,6 +437,7 @@ version = "3.1.0" description = "passive checker of Python programs" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pyflakes-3.1.0-py2.py3-none-any.whl", hash = "sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774"}, {file = "pyflakes-3.1.0.tar.gz", hash = "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc"}, @@ -428,6 +449,7 @@ version = "8.3.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, @@ -448,6 +470,7 @@ version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -462,6 +485,7 @@ version = "1.0.1" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, @@ -476,6 +500,7 @@ version = "1.16.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["main"] files = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, @@ -487,6 +512,7 @@ version = "0.5.1" description = "A non-validating SQL parser." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "sqlparse-0.5.1-py3-none-any.whl", hash = "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4"}, {file = "sqlparse-0.5.1.tar.gz", hash = "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e"}, @@ -502,6 +528,8 @@ version = "2024.1" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" +groups = ["main"] +markers = "sys_platform == \"win32\"" files = [ {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, @@ -513,6 +541,7 @@ version = "1.16.0" description = "Module for decorators, wrappers and monkey patching." optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, @@ -587,6 +616,6 @@ files = [ ] [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "^3.12" -content-hash = "c5308f28a61ca954f8c322f9d9c4ae9412b82c10eb7a262c8ee438554aeed16f" +content-hash = "3056497732593ecab7864ba1cc8d96295741aeeafedf53d57c757311f6a7e009" From b0a3062be540534cf5695ceb1860d558511e4404 Mon Sep 17 00:00:00 2001 From: Han Minh Tran Date: Sat, 20 Dec 2025 19:57:16 +0800 Subject: [PATCH 033/122] Correct script order --- .../{0002_art_artcontributor.py => 0005_art_artcontributor.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename server/game_dev/migrations/{0002_art_artcontributor.py => 0005_art_artcontributor.py} (100%) diff --git a/server/game_dev/migrations/0002_art_artcontributor.py b/server/game_dev/migrations/0005_art_artcontributor.py similarity index 100% rename from server/game_dev/migrations/0002_art_artcontributor.py rename to server/game_dev/migrations/0005_art_artcontributor.py From d1c5ee9df6631245b7d867566f4ddcfefcec17ec Mon Sep 17 00:00:00 2001 From: Han Minh Tran Date: Sat, 20 Dec 2025 19:58:50 +0800 Subject: [PATCH 034/122] Correct script order --- server/game_dev/migrations/0005_art_artcontributor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/game_dev/migrations/0005_art_artcontributor.py b/server/game_dev/migrations/0005_art_artcontributor.py index 138685d9..f3d0c905 100644 --- a/server/game_dev/migrations/0005_art_artcontributor.py +++ b/server/game_dev/migrations/0005_art_artcontributor.py @@ -7,7 +7,7 @@ class Migration(migrations.Migration): dependencies = [ - ("game_dev", "0001_initial"), + ("game_dev", "0004_alter_event_date"), ] operations = [ From c820c7d2c48890c9d3608f4121896173c67d9d6b Mon Sep 17 00:00:00 2001 From: David <24074639@student.uwa.edu.au> Date: Tue, 6 Jan 2026 19:42:18 +0000 Subject: [PATCH 035/122] feat: implement individual art pages with full functionality - Changed Art model from CharField to ImageField for media - Added ImageField with upload_to='art_images/' - Created migration 0006 for field rename - Added discord_url and instagram_url to ArtContributor model - Created migration 0007 for social media fields - Updated serializers for both Art and ArtContributor models - Updated TypeScript types for Art model - Added artwork index and detail pages with dynamic routing - Replaced hardcoded SVG icons with react-social-icons library - Configured Next.js to allow localhost images - Fixed duplicate color definitions in globals.css - Updated Tailwind config for consistent HSL colors - Created test data script with sample images and social links --- client/next.config.mjs | 33 +++------ client/src/pages/artwork/[id].tsx | 70 ++++++------------- client/src/pages/artwork/index.tsx | 7 +- client/src/styles/globals.css | 9 +-- client/src/types/art.ts | 2 +- client/tailwind.config.ts | 2 + .../0006_rename_path_to_media_to_media.py | 25 +++++++ ...0007_add_social_links_to_artcontributor.py | 23 ++++++ server/game_dev/models.py | 4 +- server/game_dev/serializers.py | 4 +- 10 files changed, 91 insertions(+), 88 deletions(-) create mode 100644 server/game_dev/migrations/0006_rename_path_to_media_to_media.py create mode 100644 server/game_dev/migrations/0007_add_social_links_to_artcontributor.py diff --git a/client/next.config.mjs b/client/next.config.mjs index 28739846..c8a0f711 100644 --- a/client/next.config.mjs +++ b/client/next.config.mjs @@ -1,12 +1,6 @@ -// import os from "node:os"; -// import isInsideContainer from "is-inside-container"; - -// const isWindowsDevContainer = () => -// os.release().toLowerCase().includes("microsoft") && isInsideContainer(); - /** @type {import('next').NextConfig} */ -const config = { +const nextConfig = { reactStrictMode: true, turbopack: { root: import.meta.dirname, @@ -14,24 +8,15 @@ const config = { outputFileTracingRoot: import.meta.dirname, images: { domains: ["localhost"], - }, - // Turns on file change polling for the Windows Dev Container - // Doesn't work currently for turbopack, so file changes will not automatically update the client. - // watchOptions: isWindowsDevContainer() - // ? { - // pollIntervalMs: 1000 - // } - // : undefined, - images: { remotePatterns: [ { - protocol: 'https', - hostname: '**', - port: '', - pathname: '**' - } - ] - } + protocol: 'http', + hostname: 'localhost', + port: '8000', + pathname: '/media/**', + }, + ], + }, }; -export default config; +export default nextConfig; diff --git a/client/src/pages/artwork/[id].tsx b/client/src/pages/artwork/[id].tsx index f61b3ed9..5fa25927 100644 --- a/client/src/pages/artwork/[id].tsx +++ b/client/src/pages/artwork/[id].tsx @@ -1,7 +1,7 @@ import { GetServerSideProps } from "next"; import Image from "next/image"; import { useRouter } from "next/navigation"; -import { JSX } from "react"; +import { SocialIcon } from "react-social-icons"; import GoBackButton from "@/components/ui/go-back-button"; import ImagePlaceholder from "@/components/ui/image-placeholder"; @@ -14,46 +14,6 @@ interface ArtworkPageProps { error?: string; } -const DISCORD_ICON = ( -
- - - -
-); -const INSTAGRAM_ICON = ( -
- - - -
-); - -function iconWithUrl(icon: JSX.Element, url: string) { - return {icon}; -} - function displayContributors(artwork: Art) { return (
@@ -78,11 +38,25 @@ function displayContributors(artwork: Art) {
{contributor.member_name}
-
- {contributor.discord_url && - iconWithUrl(DISCORD_ICON, contributor.discord_url)} - {contributor.instagram_url && - iconWithUrl(INSTAGRAM_ICON, contributor.instagram_url)} +
+ {contributor.discord_url && ( + + )} + {contributor.instagram_url && ( + + )}
))} @@ -118,9 +92,9 @@ export default function ArtworkPage({ artwork, error }: ArtworkPageProps) { className="Frame1099 bg-neutral-1 justify-start md:flex" >
- {artwork!.path_to_media ? ( + {artwork!.media ? ( Artwork image - - {!artwork.path_to_media && PLACEHOLDER_ICON} + + {!artwork.media && PLACEHOLDER_ICON} ); diff --git a/client/src/styles/globals.css b/client/src/styles/globals.css index 8cf2f38f..dd23d87d 100644 --- a/client/src/styles/globals.css +++ b/client/src/styles/globals.css @@ -16,12 +16,14 @@ --neutral-5: hsl(203 82% 18%); --light-1: hsl(0 0% 100%); --light-2: hsl(236 18% 98%); + --light-3: hsl(236 96% 80%); /* light purple */ --light-alt: hsl(260 46% 90%); /* light green */ --light-alt-2: hsl(183 100% 79%); --logo-blue-2: hsl(237 66% 77%); --logo-blue-1: hsl(236 62% 95%); + --error: hsl(0 93% 67%); /* Colours to be used for components */ --background: 236 47% 7%; @@ -52,13 +54,6 @@ --input: 235 47% 20%; --ring: 236 47% 7%; --radius: 0.5rem; - - --dark-2: #090a19; - --neutral-1: #1b1f4c; - --light-1: #ffffff; - --light-2: #ced1fe; - --light-3: #9ca4fd; - --error: #fa5c5c; } } diff --git a/client/src/types/art.ts b/client/src/types/art.ts index 5a5397c7..f00c2979 100644 --- a/client/src/types/art.ts +++ b/client/src/types/art.ts @@ -4,7 +4,7 @@ import { BaseDto } from "./base-dto"; export interface Art extends BaseDto { name: string; description: string; - path_to_media: string; + media: string; active: boolean; contributors: ArtContributor[]; } diff --git a/client/tailwind.config.ts b/client/tailwind.config.ts index 915464d8..d6a49201 100644 --- a/client/tailwind.config.ts +++ b/client/tailwind.config.ts @@ -69,10 +69,12 @@ const config = { neutral_5: "var(--neutral-5)", light_1: "var(--light-1)", light_2: "var(--light-2)", + light_3: "var(--light-3)", light_alt: "var(--light-alt)", light_alt_2: "var(--light-alt-2)", logo_blue_2: "var(--logo-blue-2)", logo_blue_1: "var(--logo-blue-1)", + error: "var(--error)", }, borderRadius: { lg: "var(--radius)", diff --git a/server/game_dev/migrations/0006_rename_path_to_media_to_media.py b/server/game_dev/migrations/0006_rename_path_to_media_to_media.py new file mode 100644 index 00000000..9b9b54ee --- /dev/null +++ b/server/game_dev/migrations/0006_rename_path_to_media_to_media.py @@ -0,0 +1,25 @@ +# Generated manually for changing path_to_media to media (ImageField) + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("game_dev", "0005_art_artcontributor"), + ] + + operations = [ + # First, rename the field + migrations.RenameField( + model_name="art", + old_name="path_to_media", + new_name="media", + ), + # Then, alter the field to ImageField + migrations.AlterField( + model_name="art", + name="media", + field=models.ImageField(upload_to='art_images/'), + ), + ] diff --git a/server/game_dev/migrations/0007_add_social_links_to_artcontributor.py b/server/game_dev/migrations/0007_add_social_links_to_artcontributor.py new file mode 100644 index 00000000..e3e8cba4 --- /dev/null +++ b/server/game_dev/migrations/0007_add_social_links_to_artcontributor.py @@ -0,0 +1,23 @@ +# Generated manually for adding discord_url and instagram_url to ArtContributor + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("game_dev", "0006_rename_path_to_media_to_media"), + ] + + operations = [ + migrations.AddField( + model_name="artcontributor", + name="discord_url", + field=models.URLField(max_length=500, blank=True, null=True), + ), + migrations.AddField( + model_name="artcontributor", + name="instagram_url", + field=models.URLField(max_length=500, blank=True, null=True), + ), + ] diff --git a/server/game_dev/models.py b/server/game_dev/models.py index abf0ff87..6bd3aa43 100644 --- a/server/game_dev/models.py +++ b/server/game_dev/models.py @@ -28,7 +28,7 @@ class Art(models.Model): name = models.CharField(null=False, max_length=200) description = models.CharField(max_length=200,) # source_game = models.ForeignKey(Games, on_delete=models.CASCADE, related_name='art_pieces') #Need implement Games model - path_to_media = models.CharField(null=False, max_length=500) + media = models.ImageField(upload_to='art_images/', null=False) active = models.BooleanField(null=False) def __str__(self): @@ -39,6 +39,8 @@ class ArtContributor(models.Model): art = models.ForeignKey('Art', on_delete=models.CASCADE, related_name='contributors') member = models.ForeignKey('Member', on_delete=models.CASCADE, related_name='art_contributions') role = models.CharField(max_length=100) + discord_url = models.URLField(max_length=500, blank=True, null=True) + instagram_url = models.URLField(max_length=500, blank=True, null=True) class Meta: unique_together = ('art', 'member') diff --git a/server/game_dev/serializers.py b/server/game_dev/serializers.py index d9173974..aa0ed057 100644 --- a/server/game_dev/serializers.py +++ b/server/game_dev/serializers.py @@ -21,7 +21,7 @@ class ArtContributorSerializer(serializers.ModelSerializer): class Meta: model = ArtContributor - fields = ['id', 'art', 'member', 'member_name', 'role'] + fields = ['id', 'art', 'member', 'member_name', 'role', 'discord_url', 'instagram_url'] class ArtSerializer(serializers.ModelSerializer): @@ -29,7 +29,7 @@ class ArtSerializer(serializers.ModelSerializer): class Meta: model = Art - fields = ['id', 'name', 'description', 'path_to_media', 'active', 'contributors'] + fields = ['id', 'name', 'description', 'media', 'active', 'contributors'] class MemberSerializer(serializers.ModelSerializer): From 21121714e1c5ffe068ed89257311dfa91253f0bb Mon Sep 17 00:00:00 2001 From: David <24074639@student.uwa.edu.au> Date: Tue, 6 Jan 2026 19:58:54 +0000 Subject: [PATCH 036/122] fix: remove null=True from URLField to pass flake8 - Changed discord_url and instagram_url to use blank=True with default='' - Removed null=True to follow Django best practices for string fields - Updated migration 0007 accordingly --- .../migrations/0007_add_social_links_to_artcontributor.py | 4 ++-- server/game_dev/models.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/server/game_dev/migrations/0007_add_social_links_to_artcontributor.py b/server/game_dev/migrations/0007_add_social_links_to_artcontributor.py index e3e8cba4..a7949306 100644 --- a/server/game_dev/migrations/0007_add_social_links_to_artcontributor.py +++ b/server/game_dev/migrations/0007_add_social_links_to_artcontributor.py @@ -13,11 +13,11 @@ class Migration(migrations.Migration): migrations.AddField( model_name="artcontributor", name="discord_url", - field=models.URLField(max_length=500, blank=True, null=True), + field=models.URLField(max_length=500, blank=True, default=''), ), migrations.AddField( model_name="artcontributor", name="instagram_url", - field=models.URLField(max_length=500, blank=True, null=True), + field=models.URLField(max_length=500, blank=True, default=''), ), ] diff --git a/server/game_dev/models.py b/server/game_dev/models.py index 6bd3aa43..d2cf4d79 100644 --- a/server/game_dev/models.py +++ b/server/game_dev/models.py @@ -39,8 +39,8 @@ class ArtContributor(models.Model): art = models.ForeignKey('Art', on_delete=models.CASCADE, related_name='contributors') member = models.ForeignKey('Member', on_delete=models.CASCADE, related_name='art_contributions') role = models.CharField(max_length=100) - discord_url = models.URLField(max_length=500, blank=True, null=True) - instagram_url = models.URLField(max_length=500, blank=True, null=True) + discord_url = models.URLField(max_length=500, blank=True, default='') + instagram_url = models.URLField(max_length=500, blank=True, default='') class Meta: unique_together = ('art', 'member') From 6640e7fdbfb1b0bb66b70d1cdb6d021ece57019b Mon Sep 17 00:00:00 2001 From: David <24074639@student.uwa.edu.au> Date: Tue, 6 Jan 2026 20:03:06 +0000 Subject: [PATCH 037/122] style: fix prettier formatting in error-modal.tsx --- client/src/components/ui/modal/error-modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/ui/modal/error-modal.tsx b/client/src/components/ui/modal/error-modal.tsx index d5ff49db..ba1c2404 100644 --- a/client/src/components/ui/modal/error-modal.tsx +++ b/client/src/components/ui/modal/error-modal.tsx @@ -31,7 +31,7 @@ const ErrorModal = ({ message, onClose = () => {} }: ErrorModalProps) => {

{message}

{contributor.discord_url && ( - + className="flex h-[30px] w-[30px] items-center justify-center rounded-full bg-[#9CA4FD] transition-opacity hover:opacity-80" + > + + )} {contributor.instagram_url && ( - + className="flex h-[30px] w-[30px] items-center justify-center rounded-full bg-[#9CA4FD] transition-opacity hover:opacity-80" + > + + )}
From 4277b4dd95b91c0cd0d4866aa6af66b88c66e4d3 Mon Sep 17 00:00:00 2001 From: Peitong Du <101039613+DDuu123321@users.noreply.github.com> Date: Wed, 7 Jan 2026 13:54:03 +0800 Subject: [PATCH 039/122] Fix import formatting in [id].tsx --- client/src/pages/artwork/[id].tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/pages/artwork/[id].tsx b/client/src/pages/artwork/[id].tsx index 941cf373..abcf31bd 100644 --- a/client/src/pages/artwork/[id].tsx +++ b/client/src/pages/artwork/[id].tsx @@ -1,4 +1,4 @@ -import { Instagram,MessageSquare } from "lucide-react"; +import { Instagram, MessageSquare } from "lucide-react"; import { GetServerSideProps } from "next"; import Image from "next/image"; import { useRouter } from "next/navigation"; From 959c3e18d0dcc7b13a8c30d4b85864955c30fb5c Mon Sep 17 00:00:00 2001 From: Karl_Sue <24595816@student.uwa.edu.au> Date: Fri, 9 Jan 2026 08:56:54 +0000 Subject: [PATCH 040/122] feature: add mock data for dev --- client/src/hooks/use-artwork-data.ts | 49 ++++++++++++++++++++++++++++ client/src/pages/artwork/index.tsx | 19 +++++++++-- 2 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 client/src/hooks/use-artwork-data.ts diff --git a/client/src/hooks/use-artwork-data.ts b/client/src/hooks/use-artwork-data.ts new file mode 100644 index 00000000..da08c9e9 --- /dev/null +++ b/client/src/hooks/use-artwork-data.ts @@ -0,0 +1,49 @@ +import { Art } from "@/types/art"; + +export const generateMockArtworks = (count: number): Art[] => { + const artworks: Art[] = []; + for (let i = 1; i <= count; i++) { + artworks.push({ + id: i, + name: `Artwork ${i}`, + description: "Mock artwork description", + //source_game: "Mock Game", + media: "", + active: true, + contributors: [], + //created_at: new Date().toISOString(), + }); + } + return artworks; +}; + +export const generateMockArtwork = (id: string): Art => { + return { + id: Number(id), + name: "Mock Artwork Title", + description: + "Lorem ipsum dolor sit amet. Non numquam dicta nam autem dicta 33 error molestias et repellat consequatur eum iste expedita est dolorem libero et quas provident!", + //source_game: "Mock Game", + media: "", + active: true, + //created_at: new Date().toISOString(), + contributors: [ + { + id: 1, + art_id: Number(id), + member_name: "Contributor 1", + role: "user1", + discord_url: "https://discord.com", + instagram_url: "", + }, + { + id: 2, + art_id: Number(id), + member_name: "Contributor 2", + role: "user2", + discord_url: "", + instagram_url: "https://instagram.com", + }, + ], + }; +}; diff --git a/client/src/pages/artwork/index.tsx b/client/src/pages/artwork/index.tsx index 1f355e74..61b04b30 100644 --- a/client/src/pages/artwork/index.tsx +++ b/client/src/pages/artwork/index.tsx @@ -5,6 +5,7 @@ import { useRouter } from "next/navigation"; import { Button } from "@/components/ui/button"; import ImageCard from "@/components/ui/image-card"; import ErrorModal from "@/components/ui/modal/error-modal"; +import { generateMockArtworks } from "@/hooks/use-artwork-data"; import api from "@/lib/api"; import { Art } from "@/types/art"; import { PageResult } from "@/types/page-response"; @@ -107,9 +108,23 @@ export const getServerSideProps: GetServerSideProps< try { const res = await api.get>("arts"); return { props: { artworks: res.data } }; - } catch (err: unknown) { + //} catch (err: unknown) { + } catch { + // return { + // props: { error: (err as Error).message || "Failed to load artworks." }, + // }; + + // Fallback to mock data on error + const mockArtworks = generateMockArtworks(12); return { - props: { error: (err as Error).message || "Failed to load artworks." }, + props: { + artworks: { + results: mockArtworks, + count: mockArtworks.length, + next: "", + previous: "", + }, + }, }; } }; From 6a7a5e03554ef7a1779da5b0b45c8e118262d2f3 Mon Sep 17 00:00:00 2001 From: David <24074639@student.uwa.edu.au> Date: Fri, 9 Jan 2026 16:08:19 +0000 Subject: [PATCH 041/122] chore: update migrations and frontend components - Updated migration files formatting - Updated artwork pages and components - Updated go-back-button component - Updated styles and Tailwind config - Updated serializers --- client/src/components/ui/go-back-button.tsx | 4 +- client/src/pages/artwork/[id].tsx | 54 ++++++++++--------- client/src/pages/artwork/index.tsx | 4 +- client/src/styles/globals.css | 25 --------- client/tailwind.config.ts | 3 +- .../0006_rename_path_to_media_to_media.py | 2 - ...0007_add_social_links_to_artcontributor.py | 2 - server/game_dev/serializers.py | 3 +- 8 files changed, 38 insertions(+), 59 deletions(-) diff --git a/client/src/components/ui/go-back-button.tsx b/client/src/components/ui/go-back-button.tsx index 22109a4a..5f1ebce1 100644 --- a/client/src/components/ui/go-back-button.tsx +++ b/client/src/components/ui/go-back-button.tsx @@ -8,10 +8,10 @@ const GoBackButton = ({ url, label }: GoBackButtonProps) => { return ( + + +
diff --git a/client/src/styles/globals.css b/client/src/styles/globals.css index dd23d87d..db50e92e 100644 --- a/client/src/styles/globals.css +++ b/client/src/styles/globals.css @@ -87,3 +87,21 @@ .bg-error { background-color: var(--error); } + +@layer utilities { + @keyframes float { + 0% { + transform: translateY(0); + } + 50% { + transform: translateY(-20px); + } + 100% { + transform: translateY(0); + } + } + + .animate-float { + animation: float 3s infinite; + } +} \ No newline at end of file From 4c5c1ba9f4a5e89506e1516f3913751056d23751 Mon Sep 17 00:00:00 2001 From: David Du <24074639@student.uwa.edu.au> Date: Wed, 14 Jan 2026 09:39:35 +0000 Subject: [PATCH 044/122] Refactor: Move social links from ArtContributor to Member model and remove django-filter dependency --- server/api/settings.py | 1 - server/game_dev/models.py | 12 +++++++----- server/game_dev/urls.py | 5 ++--- server/pyproject.toml | 1 - 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/server/api/settings.py b/server/api/settings.py index 46902758..8b0ee42c 100644 --- a/server/api/settings.py +++ b/server/api/settings.py @@ -49,7 +49,6 @@ "django.contrib.messages", "django.contrib.staticfiles", "django_extensions", - "django_filters", "rest_framework", "corsheaders", "api.healthcheck", diff --git a/server/game_dev/models.py b/server/game_dev/models.py index d2cf4d79..76b91288 100644 --- a/server/game_dev/models.py +++ b/server/game_dev/models.py @@ -7,6 +7,8 @@ class Member(models.Model): profile_picture = models.ImageField(upload_to="profiles/", null=True) about = models.CharField(max_length=256, blank=True) pronouns = models.CharField(max_length=20, blank=True) + discord_url = models.URLField(max_length=500, blank=True, null=True) + instagram_url = models.URLField(max_length=500, blank=True, null=True) def __str__(self): return str(self.name) @@ -28,8 +30,8 @@ class Art(models.Model): name = models.CharField(null=False, max_length=200) description = models.CharField(max_length=200,) # source_game = models.ForeignKey(Games, on_delete=models.CASCADE, related_name='art_pieces') #Need implement Games model - media = models.ImageField(upload_to='art_images/', null=False) - active = models.BooleanField(null=False) + media = models.ImageField(upload_to='art/', null=False) + active = models.BooleanField(default=True) def __str__(self): return str(self.name) @@ -39,11 +41,11 @@ class ArtContributor(models.Model): art = models.ForeignKey('Art', on_delete=models.CASCADE, related_name='contributors') member = models.ForeignKey('Member', on_delete=models.CASCADE, related_name='art_contributions') role = models.CharField(max_length=100) - discord_url = models.URLField(max_length=500, blank=True, default='') - instagram_url = models.URLField(max_length=500, blank=True, default='') class Meta: - unique_together = ('art', 'member') + constraints = [ + models.UniqueConstraint(fields=['art', 'member'], name='unique_art_member') + ] verbose_name = 'Art Contributor' verbose_name_plural = 'Art Contributors' diff --git a/server/game_dev/urls.py b/server/game_dev/urls.py index 58559694..dadcd6dd 100644 --- a/server/game_dev/urls.py +++ b/server/game_dev/urls.py @@ -1,4 +1,4 @@ -from django.urls import path, include +from django.urls import path from rest_framework.routers import DefaultRouter from .views import EventDetailAPIView, ArtContributorViewSet, ArtViewSet, MemberViewSet @@ -9,5 +9,4 @@ urlpatterns = [ path("events//", EventDetailAPIView.as_view()), - path('', include(router.urls)), -] +] + router.urls diff --git a/server/pyproject.toml b/server/pyproject.toml index fb713c85..5ec547cb 100644 --- a/server/pyproject.toml +++ b/server/pyproject.toml @@ -16,7 +16,6 @@ gunicorn = "^23.0.0" python-dotenv = "^1.0.1" django-extensions = "^3.2.3" pillow = "^11.3.0" -django-filter = "^24.3" [tool.poetry.group.dev.dependencies] From 2951ead1ad24bca4718c66be5900e6aeff4bb85a Mon Sep 17 00:00:00 2001 From: David Du <24074639@student.uwa.edu.au> Date: Fri, 16 Jan 2026 15:29:45 +0000 Subject: [PATCH 045/122] Fix: Remove hardcoded social media fields to comply with schema design - Remove discord_url and instagram_url from Member model - Remove social media fields from ArtContributor model - Delete incorrect migration 0007 (social links on ArtContributor) - Update ArtContributorSerializer to remove social media field references - Change unique_together to UniqueConstraint (modern Django approach) - Add default=True to Art.active field - Keep ArtContributor as pure junction table (art, member, role only) This change aligns with the original schema design where social media links are managed through a separate SocialMedia table (in development on another branch). Addresses James's code review feedback. --- ...0007_add_social_links_to_artcontributor.py | 21 -------- ...artcontributor_unique_together_and_more.py | 33 ++++++++++++ server/game_dev/models.py | 2 - server/game_dev/serializers.py | 2 +- server/poetry.lock | 51 ++----------------- 5 files changed, 38 insertions(+), 71 deletions(-) delete mode 100644 server/game_dev/migrations/0007_add_social_links_to_artcontributor.py create mode 100644 server/game_dev/migrations/0007_alter_artcontributor_unique_together_and_more.py diff --git a/server/game_dev/migrations/0007_add_social_links_to_artcontributor.py b/server/game_dev/migrations/0007_add_social_links_to_artcontributor.py deleted file mode 100644 index 9656e5bc..00000000 --- a/server/game_dev/migrations/0007_add_social_links_to_artcontributor.py +++ /dev/null @@ -1,21 +0,0 @@ -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("game_dev", "0006_rename_path_to_media_to_media"), - ] - - operations = [ - migrations.AddField( - model_name="artcontributor", - name="discord_url", - field=models.URLField(max_length=500, blank=True, default=''), - ), - migrations.AddField( - model_name="artcontributor", - name="instagram_url", - field=models.URLField(max_length=500, blank=True, default=''), - ), - ] diff --git a/server/game_dev/migrations/0007_alter_artcontributor_unique_together_and_more.py b/server/game_dev/migrations/0007_alter_artcontributor_unique_together_and_more.py new file mode 100644 index 00000000..3c917f6b --- /dev/null +++ b/server/game_dev/migrations/0007_alter_artcontributor_unique_together_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 5.1.15 on 2026-01-16 15:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("game_dev", "0006_rename_path_to_media_to_media"), + ] + + operations = [ + migrations.AlterUniqueTogether( + name="artcontributor", + unique_together=set(), + ), + migrations.AlterField( + model_name="art", + name="active", + field=models.BooleanField(default=True), + ), + migrations.AlterField( + model_name="art", + name="media", + field=models.ImageField(upload_to="art/"), + ), + migrations.AddConstraint( + model_name="artcontributor", + constraint=models.UniqueConstraint( + fields=("art", "member"), name="unique_art_member" + ), + ), + ] diff --git a/server/game_dev/models.py b/server/game_dev/models.py index 76b91288..58c2e1fe 100644 --- a/server/game_dev/models.py +++ b/server/game_dev/models.py @@ -7,8 +7,6 @@ class Member(models.Model): profile_picture = models.ImageField(upload_to="profiles/", null=True) about = models.CharField(max_length=256, blank=True) pronouns = models.CharField(max_length=20, blank=True) - discord_url = models.URLField(max_length=500, blank=True, null=True) - instagram_url = models.URLField(max_length=500, blank=True, null=True) def __str__(self): return str(self.name) diff --git a/server/game_dev/serializers.py b/server/game_dev/serializers.py index fc58e490..dd12bcb5 100644 --- a/server/game_dev/serializers.py +++ b/server/game_dev/serializers.py @@ -22,7 +22,7 @@ class ArtContributorSerializer(serializers.ModelSerializer): class Meta: model = ArtContributor - fields = ['id', 'art_id', 'member', 'member_name', 'role', 'discord_url', 'instagram_url'] + fields = ['id', 'art_id', 'member', 'member_name', 'role'] class ArtSerializer(serializers.ModelSerializer): diff --git a/server/poetry.lock b/server/poetry.lock index 202b5e94..8e6deac4 100644 --- a/server/poetry.lock +++ b/server/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. [[package]] name = "asgiref" @@ -6,7 +6,6 @@ version = "3.8.1" description = "ASGI specs, helper code, and adapters" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, @@ -21,7 +20,6 @@ version = "2.15.8" description = "An abstract syntax tree for Python with inference support." optional = false python-versions = ">=3.7.2" -groups = ["dev"] files = [ {file = "astroid-2.15.8-py3-none-any.whl", hash = "sha256:1aa149fc5c6589e3d0ece885b4491acd80af4f087baafa3fb5203b113e68cd3c"}, {file = "astroid-2.15.8.tar.gz", hash = "sha256:6c107453dffee9055899705de3c9ead36e74119cee151e5a9aaf7f0b0e020a6a"}, @@ -37,8 +35,6 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["dev"] -markers = "sys_platform == \"win32\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -50,7 +46,6 @@ version = "5.1.15" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.10" -groups = ["main"] files = [ {file = "django-5.1.15-py3-none-any.whl", hash = "sha256:117871e58d6eda37f09870b7d73a3d66567b03aecd515b386b1751177c413432"}, {file = "django-5.1.15.tar.gz", hash = "sha256:46a356b5ff867bece73fc6365e081f21c569973403ee7e9b9a0316f27d0eb947"}, @@ -71,7 +66,6 @@ version = "4.4.0" description = "django-cors-headers is a Django application for handling the server headers required for Cross-Origin Resource Sharing (CORS)." optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "django_cors_headers-4.4.0-py3-none-any.whl", hash = "sha256:5c6e3b7fe870876a1efdfeb4f433782c3524078fa0dc9e0195f6706ce7a242f6"}, {file = "django_cors_headers-4.4.0.tar.gz", hash = "sha256:92cf4633e22af67a230a1456cb1b7a02bb213d6536d2dcb2a4a24092ea9cebc2"}, @@ -87,7 +81,6 @@ version = "3.2.3" description = "Extensions for Django" optional = false python-versions = ">=3.6" -groups = ["main"] files = [ {file = "django-extensions-3.2.3.tar.gz", hash = "sha256:44d27919d04e23b3f40231c4ab7af4e61ce832ef46d610cc650d53e68328410a"}, {file = "django_extensions-3.2.3-py3-none-any.whl", hash = "sha256:9600b7562f79a92cbf1fde6403c04fee314608fefbb595502e34383ae8203401"}, @@ -96,28 +89,12 @@ files = [ [package.dependencies] Django = ">=3.2" -[[package]] -name = "django-filter" -version = "24.3" -description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "django_filter-24.3-py3-none-any.whl", hash = "sha256:c4852822928ce17fb699bcfccd644b3574f1a2d80aeb2b4ff4f16b02dd49dc64"}, - {file = "django_filter-24.3.tar.gz", hash = "sha256:d8ccaf6732afd21ca0542f6733b11591030fa98669f8d15599b358e24a2cd9c3"}, -] - -[package.dependencies] -Django = ">=4.2" - [[package]] name = "djangorestframework" version = "3.15.2" description = "Web APIs for Django, made easy." optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "djangorestframework-3.15.2-py3-none-any.whl", hash = "sha256:2b8871b062ba1aefc2de01f773875441a961fefbf79f5eed1e32b2f096944b20"}, {file = "djangorestframework-3.15.2.tar.gz", hash = "sha256:36fe88cd2d6c6bec23dca9804bab2ba5517a8bb9d8f47ebc68981b56840107ad"}, @@ -132,7 +109,6 @@ version = "6.1.0" description = "the modular source code checker: pep8 pyflakes and co" optional = false python-versions = ">=3.8.1" -groups = ["dev"] files = [ {file = "flake8-6.1.0-py2.py3-none-any.whl", hash = "sha256:ffdfce58ea94c6580c77888a86506937f9a1a227dfcd15f245d694ae20a6b6e5"}, {file = "flake8-6.1.0.tar.gz", hash = "sha256:d5b3857f07c030bdb5bf41c7f53799571d75c4491748a3adcd47de929e34cd23"}, @@ -149,7 +125,6 @@ version = "1.4" description = "Plugin to catch bad style specific to Django Projects." optional = false python-versions = ">=3.7.2,<4.0.0" -groups = ["dev"] files = [ {file = "flake8_django-1.4.tar.gz", hash = "sha256:4debba883084191568e3187416d1d6bdd4abd826da988f197a3c36572e9f30de"}, ] @@ -164,7 +139,6 @@ version = "1.5.1" description = "Let your Python tests travel through time" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "freezegun-1.5.1-py3-none-any.whl", hash = "sha256:bf111d7138a8abe55ab48a71755673dbaa4ab87f4cff5634a4442dfec34c15f1"}, {file = "freezegun-1.5.1.tar.gz", hash = "sha256:b29dedfcda6d5e8e083ce71b2b542753ad48cfec44037b3fc79702e2980a89e9"}, @@ -179,7 +153,6 @@ version = "23.0.0" description = "WSGI HTTP Server for UNIX" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d"}, {file = "gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec"}, @@ -201,7 +174,6 @@ version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" -groups = ["dev"] files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, @@ -213,7 +185,6 @@ version = "1.10.0" description = "A fast and thorough lazy object proxy." optional = false python-versions = ">=3.8" -groups = ["dev"] files = [ {file = "lazy-object-proxy-1.10.0.tar.gz", hash = "sha256:78247b6d45f43a52ef35c25b5581459e85117225408a4128a3daf8bf9648ac69"}, {file = "lazy_object_proxy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:855e068b0358ab916454464a884779c7ffa312b8925c6f7401e952dcf3b89977"}, @@ -260,7 +231,6 @@ version = "0.7.0" description = "McCabe checker, plugin for flake8" optional = false python-versions = ">=3.6" -groups = ["dev"] files = [ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, @@ -272,7 +242,6 @@ version = "24.1" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" -groups = ["main", "dev"] files = [ {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, @@ -284,7 +253,6 @@ version = "11.3.0" description = "Python Imaging Library (Fork)" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860"}, {file = "pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad"}, @@ -400,7 +368,7 @@ fpx = ["olefile"] mic = ["olefile"] test-arrow = ["pyarrow"] tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "trove-classifiers (>=2024.10.12)"] -typing = ["typing-extensions ; python_version < \"3.10\""] +typing = ["typing-extensions"] xmp = ["defusedxml"] [[package]] @@ -409,7 +377,6 @@ version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" -groups = ["dev"] files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -425,7 +392,6 @@ version = "2.11.1" description = "Python style guide checker" optional = false python-versions = ">=3.8" -groups = ["dev"] files = [ {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, @@ -437,7 +403,6 @@ version = "3.1.0" description = "passive checker of Python programs" optional = false python-versions = ">=3.8" -groups = ["dev"] files = [ {file = "pyflakes-3.1.0-py2.py3-none-any.whl", hash = "sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774"}, {file = "pyflakes-3.1.0.tar.gz", hash = "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc"}, @@ -449,7 +414,6 @@ version = "8.3.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" -groups = ["dev"] files = [ {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, @@ -470,7 +434,6 @@ version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main"] files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -485,7 +448,6 @@ version = "1.0.1" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, @@ -500,7 +462,6 @@ version = "1.16.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -groups = ["main"] files = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, @@ -512,7 +473,6 @@ version = "0.5.1" description = "A non-validating SQL parser." optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "sqlparse-0.5.1-py3-none-any.whl", hash = "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4"}, {file = "sqlparse-0.5.1.tar.gz", hash = "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e"}, @@ -528,8 +488,6 @@ version = "2024.1" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" -groups = ["main"] -markers = "sys_platform == \"win32\"" files = [ {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, @@ -541,7 +499,6 @@ version = "1.16.0" description = "Module for decorators, wrappers and monkey patching." optional = false python-versions = ">=3.6" -groups = ["dev"] files = [ {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, @@ -616,6 +573,6 @@ files = [ ] [metadata] -lock-version = "2.1" +lock-version = "2.0" python-versions = "^3.12" -content-hash = "3056497732593ecab7864ba1cc8d96295741aeeafedf53d57c757311f6a7e009" +content-hash = "f804c2f3998772b91e34ad214e5fcafe900bec97675f73046d3bcc79aba0f7db" From 1138ccb0f01766001928bd14e844196f7fa65c4d Mon Sep 17 00:00:00 2001 From: David Du <24074639@student.uwa.edu.au> Date: Fri, 16 Jan 2026 15:36:49 +0000 Subject: [PATCH 046/122] Resolve merge conflict in Navbar: keep z-100 from main --- client/src/components/main/Navbar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/main/Navbar.tsx b/client/src/components/main/Navbar.tsx index 67a0dbef..8567f937 100644 --- a/client/src/components/main/Navbar.tsx +++ b/client/src/components/main/Navbar.tsx @@ -18,7 +18,7 @@ export default function Navbar() { return ( <> -
+
Date: Fri, 16 Jan 2026 15:41:11 +0000 Subject: [PATCH 047/122] Fix: Remove django_filters import and usage from views.py --- server/game_dev/views.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/server/game_dev/views.py b/server/game_dev/views.py index 313093f6..9aa13adb 100644 --- a/server/game_dev/views.py +++ b/server/game_dev/views.py @@ -1,10 +1,6 @@ -# from django.shortcuts import render - -# Create your views here. from rest_framework import generics, viewsets from .models import Event, Art, ArtContributor, Member from .serializers import EventSerializer, ArtContributorSerializer, ArtSerializer, MemberSerializer -from django_filters.rest_framework import DjangoFilterBackend class EventDetailAPIView(generics.RetrieveAPIView): @@ -21,8 +17,6 @@ def get_queryset(self): class ArtContributorViewSet(viewsets.ModelViewSet): queryset = ArtContributor.objects.all() serializer_class = ArtContributorSerializer - filter_backends = [DjangoFilterBackend] - filterset_fields = ['art'] class ArtViewSet(viewsets.ModelViewSet): @@ -32,4 +26,4 @@ class ArtViewSet(viewsets.ModelViewSet): class MemberViewSet(viewsets.ModelViewSet): queryset = Member.objects.all() - serializer_class = MemberSerializer + serializer_class = MemberSerializer \ No newline at end of file From 8e9de276ac599b7b1e0987fb23538075f30f3c18 Mon Sep 17 00:00:00 2001 From: Peitong Du <101039613+DDuu123321@users.noreply.github.com> Date: Fri, 16 Jan 2026 23:43:29 +0800 Subject: [PATCH 048/122] Update views.py --- server/game_dev/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/game_dev/views.py b/server/game_dev/views.py index 9aa13adb..cea2a84d 100644 --- a/server/game_dev/views.py +++ b/server/game_dev/views.py @@ -26,4 +26,5 @@ class ArtViewSet(viewsets.ModelViewSet): class MemberViewSet(viewsets.ModelViewSet): queryset = Member.objects.all() - serializer_class = MemberSerializer \ No newline at end of file + serializer_class = MemberSerializer + From 85ea95d8125e8b1e1f57fbd6a2ca792d2d44bb11 Mon Sep 17 00:00:00 2001 From: David Du <24074639@student.uwa.edu.au> Date: Fri, 16 Jan 2026 16:00:45 +0000 Subject: [PATCH 049/122] Fix: Add newline at end of views.py --- server/game_dev/views.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/game_dev/views.py b/server/game_dev/views.py index cea2a84d..e4693138 100644 --- a/server/game_dev/views.py +++ b/server/game_dev/views.py @@ -1,3 +1,6 @@ +# from django.shortcuts import render + +# Create your views here. from rest_framework import generics, viewsets from .models import Event, Art, ArtContributor, Member from .serializers import EventSerializer, ArtContributorSerializer, ArtSerializer, MemberSerializer @@ -27,4 +30,3 @@ class ArtViewSet(viewsets.ModelViewSet): class MemberViewSet(viewsets.ModelViewSet): queryset = Member.objects.all() serializer_class = MemberSerializer - From 163cc05634ffa95b79ac4ef116fcd5c4ae03929f Mon Sep 17 00:00:00 2001 From: Karl_Sue <24595816@student.uwa.edu.au> Date: Sat, 17 Jan 2026 02:55:09 +0000 Subject: [PATCH 050/122] refactor: remove pagination --- server/api/settings.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/server/api/settings.py b/server/api/settings.py index 8b0ee42c..424f34e5 100644 --- a/server/api/settings.py +++ b/server/api/settings.py @@ -153,8 +153,3 @@ MEDIA_URL = "/media/" DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" - -REST_FRAMEWORK = { - 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', - 'PAGE_SIZE': 100, -} From 57a7a844fe0a76e331f7af907afd644bf4b1f6aa Mon Sep 17 00:00:00 2001 From: Karl_Sue <24595816@student.uwa.edu.au> Date: Sat, 17 Jan 2026 03:03:23 +0000 Subject: [PATCH 051/122] refactor: change from viewset to a standard RetrieveAPIView --- server/game_dev/views.py | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/server/game_dev/views.py b/server/game_dev/views.py index e4693138..d38423ce 100644 --- a/server/game_dev/views.py +++ b/server/game_dev/views.py @@ -1,9 +1,9 @@ # from django.shortcuts import render # Create your views here. -from rest_framework import generics, viewsets -from .models import Event, Art, ArtContributor, Member -from .serializers import EventSerializer, ArtContributorSerializer, ArtSerializer, MemberSerializer +from rest_framework import generics +from .models import Event, Art +from .serializers import EventSerializer, ArtSerializer class EventDetailAPIView(generics.RetrieveAPIView): @@ -17,16 +17,12 @@ def get_queryset(self): return Event.objects.filter(id=self.kwargs["id"]) -class ArtContributorViewSet(viewsets.ModelViewSet): - queryset = ArtContributor.objects.all() - serializer_class = ArtContributorSerializer - - -class ArtViewSet(viewsets.ModelViewSet): - queryset = Art.objects.all() +class ArtDetailAPIView(generics.RetrieveAPIView): + """ + GET /api/artworks// + """ serializer_class = ArtSerializer + lookup_url_kwarg = "id" - -class MemberViewSet(viewsets.ModelViewSet): - queryset = Member.objects.all() - serializer_class = MemberSerializer + def get_queryset(self): + return Art.objects.filter(id=self.kwargs["id"]) From 752ed72626330d6a55865cdc62f72d4b525c285d Mon Sep 17 00:00:00 2001 From: Karl_Sue <24595816@student.uwa.edu.au> Date: Sat, 17 Jan 2026 03:07:38 +0000 Subject: [PATCH 052/122] bug: remove leftover code: --- server/game_dev/urls.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/server/game_dev/urls.py b/server/game_dev/urls.py index dadcd6dd..9fb36b62 100644 --- a/server/game_dev/urls.py +++ b/server/game_dev/urls.py @@ -1,11 +1,8 @@ from django.urls import path from rest_framework.routers import DefaultRouter -from .views import EventDetailAPIView, ArtContributorViewSet, ArtViewSet, MemberViewSet +from .views import EventDetailAPIView router = DefaultRouter() -router.register(r'art-contributors', ArtContributorViewSet, basename='artcontributor') -router.register(r'arts', ArtViewSet, basename="art") -router.register(r'members', MemberViewSet, basename="member") urlpatterns = [ path("events//", EventDetailAPIView.as_view()), From ec5f54a16623b0b918c993f6fbf39b183a3e4659 Mon Sep 17 00:00:00 2001 From: Han Minh Tran Date: Sat, 10 Jan 2026 11:40:59 +0800 Subject: [PATCH 053/122] Create backend model and API to retrieve featured art pieces. --- server/game_dev/admin.py | 3 +- .../game_dev/migrations/0008_art_showcase.py | 44 +++++++++++++++++++ server/game_dev/models.py | 12 +++++ server/game_dev/serializers.py | 10 ++++- server/game_dev/urls.py | 3 +- server/game_dev/views.py | 14 +++++- 6 files changed, 81 insertions(+), 5 deletions(-) create mode 100644 server/game_dev/migrations/0008_art_showcase.py diff --git a/server/game_dev/admin.py b/server/game_dev/admin.py index c2d8a5e2..24b9932a 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 Art, ArtContributor, Member, Event +from .models import Art, ArtContributor, ArtShowcase, Member, Event class MemberAdmin(admin.ModelAdmin): @@ -14,3 +14,4 @@ class EventAdmin(admin.ModelAdmin): admin.site.register(Event, EventAdmin) admin.site.register(Art) admin.site.register(ArtContributor) +admin.site.register(ArtShowcase) diff --git a/server/game_dev/migrations/0008_art_showcase.py b/server/game_dev/migrations/0008_art_showcase.py new file mode 100644 index 00000000..a27bd6fe --- /dev/null +++ b/server/game_dev/migrations/0008_art_showcase.py @@ -0,0 +1,44 @@ +# Create Art showcase model + +import django +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("game_dev", "0007_alter_artcontributor_unique_together_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="ArtShowcase", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("description", models.CharField(max_length=200)), + ( + "art", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="showcase", + to="game_dev.art", + ), + ) + ], + ), + migrations.AddConstraint( + model_name='artshowcase', + constraint=models.UniqueConstraint( + fields=['art'], + name='unique_artshowcase_per_art' + ) + ) + ] diff --git a/server/game_dev/models.py b/server/game_dev/models.py index 58c2e1fe..46aa1358 100644 --- a/server/game_dev/models.py +++ b/server/game_dev/models.py @@ -49,3 +49,15 @@ class Meta: def __str__(self): return f"{self.member.name} - {self.art.name} ({self.role})" + + +class ArtShowcase(models.Model): + description = models.CharField(max_length=200) + art = models.ForeignKey(Art, on_delete=models.CASCADE, related_name='showcase') + class Meta: + constraints = [ + models.UniqueConstraint(fields=['art'], name='unique_artshowcase_per_art', violation_error_message='Each art piece can only have one showcase.') + ] + + def __str__(self): + return f"ArtShowcase[Art={str(self.art.name)}, Description={self.description}]" \ No newline at end of file diff --git a/server/game_dev/serializers.py b/server/game_dev/serializers.py index dd12bcb5..0a2e37dc 100644 --- a/server/game_dev/serializers.py +++ b/server/game_dev/serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from .models import Event, Art, ArtContributor, Member +from .models import ArtShowcase, Event, Art, ArtContributor, Member class EventSerializer(serializers.ModelSerializer): @@ -37,3 +37,11 @@ class MemberSerializer(serializers.ModelSerializer): class Meta: model = Member fields = ['name'] + + +class ArtShowcaseSerializer(serializers.ModelSerializer): + art_name = serializers.CharField(source='art.name', read_only=True) + + class Meta: + model = ArtShowcase + fields = ['id', 'description', 'art', 'art_name'] diff --git a/server/game_dev/urls.py b/server/game_dev/urls.py index 9fb36b62..df0f1454 100644 --- a/server/game_dev/urls.py +++ b/server/game_dev/urls.py @@ -1,9 +1,10 @@ from django.urls import path from rest_framework.routers import DefaultRouter -from .views import EventDetailAPIView +from .views import EventDetailAPIView, FeatureArtAPIView router = DefaultRouter() urlpatterns = [ path("events//", EventDetailAPIView.as_view()), + path('arts/featured/', FeatureArtAPIView.as_view()), ] + router.urls diff --git a/server/game_dev/views.py b/server/game_dev/views.py index d38423ce..d4df8ce4 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, Art -from .serializers import EventSerializer, ArtSerializer +from .models import Event, Art, ArtShowcase +from .serializers import EventSerializer, ArtSerializer, ArtShowcaseSerializer class EventDetailAPIView(generics.RetrieveAPIView): @@ -26,3 +26,13 @@ class ArtDetailAPIView(generics.RetrieveAPIView): def get_queryset(self): return Art.objects.filter(id=self.kwargs["id"]) + + +class FeatureArtAPIView(generics.ListAPIView): + """ + GET /api/arts/featured/ + """ + serializer_class = ArtSerializer + + def get_queryset(self): + return Art.objects.filter(showcase__isnull=False) \ No newline at end of file From 2a1cea8cd939b0558f4cf96999d7b6431843c91f Mon Sep 17 00:00:00 2001 From: Karl_Sue <24595816@student.uwa.edu.au> Date: Sat, 17 Jan 2026 03:59:19 +0000 Subject: [PATCH 054/122] refactor: remove redundant file --- client/src/components/ui/featureArt.tsx | 90 ------------------------- 1 file changed, 90 deletions(-) delete mode 100644 client/src/components/ui/featureArt.tsx diff --git a/client/src/components/ui/featureArt.tsx b/client/src/components/ui/featureArt.tsx deleted file mode 100644 index e8417cb0..00000000 --- a/client/src/components/ui/featureArt.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import Image from "next/image"; -import React from "react"; - -interface FeatureArtProps { - title?: string; - category?: string; - imageSrc?: string; - imageAlt?: string; -} - -export default function FeatureArt({ - title = "Featured Artwork", - category = "Digital Art", - imageSrc, - imageAlt = "Featured artwork", -}: FeatureArtProps) { - return ( -
- {/* Image */} -
- {imageSrc ? ( - {imageAlt} - ) : ( - - - - - - - - - - - - - - - - )} -
- - {/* Text Content */} -
-

{title}

- {category} -
-
- ); -} From 7afaaa9122f98199c9390eb0831ffd3e5ac82663 Mon Sep 17 00:00:00 2001 From: Karl_Sue <24595816@student.uwa.edu.au> Date: Sat, 17 Jan 2026 04:20:30 +0000 Subject: [PATCH 055/122] bug: fix the unused components error --- client/src/pages/artwork/index.tsx | 52 +++++------------------------- 1 file changed, 8 insertions(+), 44 deletions(-) diff --git a/client/src/pages/artwork/index.tsx b/client/src/pages/artwork/index.tsx index 4b765b7e..0a4b3427 100644 --- a/client/src/pages/artwork/index.tsx +++ b/client/src/pages/artwork/index.tsx @@ -2,7 +2,6 @@ import { GetServerSideProps } from "next"; import Link from "next/link"; import { useRouter } from "next/navigation"; -import { Button } from "@/components/ui/button"; import ImageCard from "@/components/ui/image-card"; import ErrorModal from "@/components/ui/modal/error-modal"; import { generateMockArtworks } from "@/hooks/use-artwork-data"; @@ -53,53 +52,18 @@ export default function ArtworksPage({ artworks, error }: ArtworksPageProps) { if (error) { return router.back()} />; } + return ( -
-
-
+
+
+

FEATURED -
- SOME GAME -

-
- {PLACEHOLDER_ICON} -
-
- - - -
-
+ -
-
- {artworks!.results.map((artwork) => renderArtworkCard(artwork))} +
+ {artworks?.results.map(renderArtworkCard)}
-
-
- TODO add footer -
+
); } From 665ac3d6ba2c678848e4dd356b8c212e44ba4f4b Mon Sep 17 00:00:00 2001 From: Karl_Sue <24595816@student.uwa.edu.au> Date: Wed, 21 Jan 2026 10:52:57 +0000 Subject: [PATCH 056/122] refactor: redesign artshowcase --- client/src/components/ui/image-card.tsx | 60 +++++++++++---- client/src/pages/artwork/index.tsx | 98 ++++++++++++++++++++++++- 2 files changed, 140 insertions(+), 18 deletions(-) diff --git a/client/src/components/ui/image-card.tsx b/client/src/components/ui/image-card.tsx index f3ca51b1..31353bf2 100644 --- a/client/src/components/ui/image-card.tsx +++ b/client/src/components/ui/image-card.tsx @@ -1,26 +1,58 @@ import Image from "next/image"; import React from "react"; -interface ImageCard { +interface ImageCardProps { imageSrc?: string; imageAlt?: string; + /** Optional content rendered on the front (over the image or placeholder). */ children?: React.ReactNode; + /** Optional content rendered on the back when hovering/focused. */ + backContent?: React.ReactNode; } -const ImageCard = ({ imageSrc, imageAlt = "Image", children }: ImageCard) => { +const ImageCard = ({ + imageSrc, + imageAlt = "Image", + children, + backContent, +}: ImageCardProps) => { + const hasBack = Boolean(backContent); + const cardFlipClass = hasBack + ? " group-hover:[transform:rotateY(180deg)]" + : ""; + return ( -
-
- {imageSrc ? ( - {imageAlt} - ) : ( - children || No Image +
+
+
+ {imageSrc ? ( + <> + {imageAlt} + {children && ( +
+ {children} +
+ )} + + ) : ( +
+ {children || No Image} +
+ )} +
+ + {hasBack && ( +
+ {backContent} +
)}
diff --git a/client/src/pages/artwork/index.tsx b/client/src/pages/artwork/index.tsx index 0a4b3427..9bdfb3aa 100644 --- a/client/src/pages/artwork/index.tsx +++ b/client/src/pages/artwork/index.tsx @@ -40,7 +40,94 @@ function renderArtworkCard(artwork: Art) { className="Frame1120" title={artwork.name} > - + +
+

+ {artwork.name} +

+

+ from GAME NAME +

+

+ {artwork.description || "No description available."} +

+
+ + {artwork.contributors.length > 0 && ( +
+

+ Contributors +

+
+ {artwork.contributors.map((contributor) => ( + + ))} +
+
+ )} +
+ } + > {!artwork.media && PLACEHOLDER_ICON} @@ -56,12 +143,15 @@ export default function ArtworksPage({ artworks, error }: ArtworksPageProps) { return (
-

+

FEATURED

- {artworks?.results.map(renderArtworkCard)} + {artworks?.results.slice(0, 3).map(renderArtworkCard)}
@@ -81,7 +171,7 @@ export const getServerSideProps: GetServerSideProps< // }; // Fallback to mock data on error - const mockArtworks = generateMockArtworks(12); + const mockArtworks = generateMockArtworks(3); return { props: { artworks: { From 387fbf0c8d3f971395bd86195f91f463991b1403 Mon Sep 17 00:00:00 2001 From: Karl_Sue <24595816@student.uwa.edu.au> Date: Wed, 21 Jan 2026 11:58:52 +0000 Subject: [PATCH 057/122] fix: styling --- client/src/components/ui/image-card.tsx | 30 +++++++++++++++++-------- client/src/hooks/use-artwork-data.ts | 19 +++++++++++++++- client/src/pages/artwork/index.tsx | 20 +++++++++-------- 3 files changed, 50 insertions(+), 19 deletions(-) diff --git a/client/src/components/ui/image-card.tsx b/client/src/components/ui/image-card.tsx index 31353bf2..9d22efca 100644 --- a/client/src/components/ui/image-card.tsx +++ b/client/src/components/ui/image-card.tsx @@ -16,17 +16,23 @@ const ImageCard = ({ children, backContent, }: ImageCardProps) => { - const hasBack = Boolean(backContent); - const cardFlipClass = hasBack - ? " group-hover:[transform:rotateY(180deg)]" - : ""; + const [isFlipped, setIsFlipped] = React.useState(false); return ( -
+
backContent && setIsFlipped(true)} + onMouseLeave={() => backContent && setIsFlipped(false)} > -
+
{imageSrc ? ( <> - {hasBack && ( -
+ {backContent && ( +
{backContent}
)} diff --git a/client/src/hooks/use-artwork-data.ts b/client/src/hooks/use-artwork-data.ts index da08c9e9..b8019096 100644 --- a/client/src/hooks/use-artwork-data.ts +++ b/client/src/hooks/use-artwork-data.ts @@ -10,7 +10,24 @@ export const generateMockArtworks = (count: number): Art[] => { //source_game: "Mock Game", media: "", active: true, - contributors: [], + contributors: [ + { + id: i * 10 + 1, + art_id: i, + member_name: "Contributor 1", + role: "artist", + discord_url: "https://discord.com", + instagram_url: "https://instagram.com", + }, + { + id: i * 10 + 2, + art_id: i, + member_name: "Contributor 2", + role: "designer", + discord_url: "https://discord.com", + instagram_url: "https://instagram.com", + }, + ], //created_at: new Date().toISOString(), }); } diff --git a/client/src/pages/artwork/index.tsx b/client/src/pages/artwork/index.tsx index 9bdfb3aa..9ed7ba99 100644 --- a/client/src/pages/artwork/index.tsx +++ b/client/src/pages/artwork/index.tsx @@ -59,17 +59,17 @@ function renderArtworkCard(artwork: Art) { {artwork.contributors.length > 0 && (
-

+

Contributors

-
+
{artwork.contributors.map((contributor) => (
- {contributor.member_name} -
+ {contributor.member_name} +
{contributor.instagram_url && ( Date: Sat, 24 Jan 2026 03:56:35 +0000 Subject: [PATCH 058/122] feat: add interactive artwork cards with flip animation and responsive navigation --- client/src/components/ui/image-card.tsx | 31 +++- client/src/pages/artwork/index.tsx | 192 ++++++++++++------------ 2 files changed, 125 insertions(+), 98 deletions(-) diff --git a/client/src/components/ui/image-card.tsx b/client/src/components/ui/image-card.tsx index 9d22efca..32fba7e0 100644 --- a/client/src/components/ui/image-card.tsx +++ b/client/src/components/ui/image-card.tsx @@ -1,4 +1,5 @@ import Image from "next/image"; +import { useRouter } from "next/router"; import React from "react"; interface ImageCardProps { @@ -8,6 +9,8 @@ interface ImageCardProps { children?: React.ReactNode; /** Optional content rendered on the back when hovering/focused. */ backContent?: React.ReactNode; + /** Optional href for navigation when clicking the front face */ + href?: string; } const ImageCard = ({ @@ -15,19 +18,41 @@ const ImageCard = ({ imageAlt = "Image", children, backContent, + href, }: ImageCardProps) => { + const router = useRouter(); const [isFlipped, setIsFlipped] = React.useState(false); + const [isMobile, setIsMobile] = React.useState(false); + + React.useEffect(() => { + const checkMobile = () => { + setIsMobile(window.innerWidth < 768); + }; + + checkMobile(); + window.addEventListener("resize", checkMobile); + return () => window.removeEventListener("resize", checkMobile); + }, []); + + const handleClick = () => { + // On mobile, navigate directly if href is provided + if (isMobile && href) { + router.push(href); + } else if (backContent) { + // On desktop, toggle flip state + setIsFlipped(!isFlipped); + } + }; return (
backContent && setIsFlipped(true)} - onMouseLeave={() => backContent && setIsFlipped(false)} + onClick={handleClick} >
- -
-

- {artwork.name} -

-

- from GAME NAME -

-

- {artwork.description || "No description available."} -

-
+ imageSrc={artwork.media || undefined} + imageAlt={artwork.name} + href={`/artwork/${artwork.id}`} + backContent={ +
+
+

+ {artwork.name} +

+

+ from GAME NAME +

+

+ {artwork.description || "No description available."} +

+
- {artwork.contributors.length > 0 && ( -
-

- Contributors -

-
+ )} + + e.stopPropagation()} + > + VIEW FULL DETAILS + +
+ } + > + {!artwork.media && PLACEHOLDER_ICON} + ); } @@ -152,7 +154,7 @@ export default function ArtworksPage({ artworks, error }: ArtworksPageProps) { FEATURED -
+
{artworks?.results.slice(0, 3).map(renderArtworkCard)}
From ab4a7eeb2d94cec81c3f392b6d387999485d0caa Mon Sep 17 00:00:00 2001 From: Karl_Sue <24595816@student.uwa.edu.au> Date: Sat, 24 Jan 2026 04:45:52 +0000 Subject: [PATCH 059/122] refactor: remove discord + insta url for frontend --- client/src/hooks/use-artwork-data.ts | 8 ---- client/src/pages/artwork/[id].tsx | 25 +------------ client/src/pages/artwork/index.tsx | 55 +--------------------------- 3 files changed, 2 insertions(+), 86 deletions(-) diff --git a/client/src/hooks/use-artwork-data.ts b/client/src/hooks/use-artwork-data.ts index b8019096..60a65adf 100644 --- a/client/src/hooks/use-artwork-data.ts +++ b/client/src/hooks/use-artwork-data.ts @@ -16,16 +16,12 @@ export const generateMockArtworks = (count: number): Art[] => { art_id: i, member_name: "Contributor 1", role: "artist", - discord_url: "https://discord.com", - instagram_url: "https://instagram.com", }, { id: i * 10 + 2, art_id: i, member_name: "Contributor 2", role: "designer", - discord_url: "https://discord.com", - instagram_url: "https://instagram.com", }, ], //created_at: new Date().toISOString(), @@ -50,16 +46,12 @@ export const generateMockArtwork = (id: string): Art => { art_id: Number(id), member_name: "Contributor 1", role: "user1", - discord_url: "https://discord.com", - instagram_url: "", }, { id: 2, art_id: Number(id), member_name: "Contributor 2", role: "user2", - discord_url: "", - instagram_url: "https://instagram.com", }, ], }; diff --git a/client/src/pages/artwork/[id].tsx b/client/src/pages/artwork/[id].tsx index 3b2261eb..764210d9 100644 --- a/client/src/pages/artwork/[id].tsx +++ b/client/src/pages/artwork/[id].tsx @@ -1,4 +1,3 @@ -import { Instagram, MessageSquare } from "lucide-react"; import { GetServerSideProps } from "next"; import Image from "next/image"; import { useRouter } from "next/navigation"; @@ -37,32 +36,10 @@ function displayContributors(artwork: Art) { className="ContributorsList relative flex flex-col gap-3 p-3" > {artwork.contributors?.map((contributor) => ( -
+
{contributor.member_name}
-
- {contributor.discord_url && ( - - - - )} - {contributor.instagram_url && ( - - - - )} -
))}
diff --git a/client/src/pages/artwork/index.tsx b/client/src/pages/artwork/index.tsx index 9ed17cba..78fc9885 100644 --- a/client/src/pages/artwork/index.tsx +++ b/client/src/pages/artwork/index.tsx @@ -61,62 +61,9 @@ function renderArtworkCard(artwork: Art) { {artwork.contributors.map((contributor) => ( ))}
From 0a6c08c5a83dc7b178dd1cb951f4ed87d777edb6 Mon Sep 17 00:00:00 2001 From: Karl_Sue <24595816@student.uwa.edu.au> Date: Sat, 24 Jan 2026 05:05:33 +0000 Subject: [PATCH 060/122] refactor: remove basedto + page-reponse --- client/src/pages/artwork/index.tsx | 8 +++++++- client/src/types/art-contributor.ts | 5 ++--- client/src/types/art.ts | 4 ++-- client/src/types/base-dto.ts | 3 --- client/src/types/page-response.ts | 6 ------ 5 files changed, 11 insertions(+), 15 deletions(-) delete mode 100644 client/src/types/base-dto.ts delete mode 100644 client/src/types/page-response.ts diff --git a/client/src/pages/artwork/index.tsx b/client/src/pages/artwork/index.tsx index 78fc9885..4e717e27 100644 --- a/client/src/pages/artwork/index.tsx +++ b/client/src/pages/artwork/index.tsx @@ -7,7 +7,13 @@ import ErrorModal from "@/components/ui/modal/error-modal"; import { generateMockArtworks } from "@/hooks/use-artwork-data"; import api from "@/lib/api"; import { Art } from "@/types/art"; -import { PageResult } from "@/types/page-response"; + +export interface PageResult { + count: number; + next: string; + previous: string; + results: T[]; +} interface ArtworksPageProps { artworks?: PageResult; diff --git a/client/src/types/art-contributor.ts b/client/src/types/art-contributor.ts index ed38150d..600a941e 100644 --- a/client/src/types/art-contributor.ts +++ b/client/src/types/art-contributor.ts @@ -1,6 +1,5 @@ -import { BaseDto } from "./base-dto"; - -export interface ArtContributor extends BaseDto { +export interface ArtContributor { + id: number; art_id: number; member_name: string; role: string; diff --git a/client/src/types/art.ts b/client/src/types/art.ts index f00c2979..ebb22e13 100644 --- a/client/src/types/art.ts +++ b/client/src/types/art.ts @@ -1,7 +1,7 @@ import { ArtContributor } from "./art-contributor"; -import { BaseDto } from "./base-dto"; -export interface Art extends BaseDto { +export interface Art { + id: number; name: string; description: string; media: string; diff --git a/client/src/types/base-dto.ts b/client/src/types/base-dto.ts deleted file mode 100644 index 9e3b6872..00000000 --- a/client/src/types/base-dto.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface BaseDto { - id: number; -} diff --git a/client/src/types/page-response.ts b/client/src/types/page-response.ts deleted file mode 100644 index e5fa692e..00000000 --- a/client/src/types/page-response.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface PageResult { - count: number; - next: string; - previous: string; - results: T[]; -} From 08b4de62071eec860ea48d8e28f0b6dd0faab990 Mon Sep 17 00:00:00 2001 From: Karl_Sue <24595816@student.uwa.edu.au> Date: Sat, 24 Jan 2026 05:25:43 +0000 Subject: [PATCH 061/122] styling: using font-sans instead of font-jersey10 --- client/src/pages/artwork/[id].tsx | 6 +++--- client/src/pages/artwork/index.tsx | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/client/src/pages/artwork/[id].tsx b/client/src/pages/artwork/[id].tsx index 764210d9..70408b76 100644 --- a/client/src/pages/artwork/[id].tsx +++ b/client/src/pages/artwork/[id].tsx @@ -26,7 +26,7 @@ function displayContributors(artwork: Art) { >
Contributors
@@ -96,7 +96,7 @@ export default function ArtworkPage({ artwork, error }: ArtworkPageProps) {
{artwork!.name}
@@ -120,7 +120,7 @@ export default function ArtworkPage({ artwork, error }: ArtworkPageProps) {
{artwork!.name}
diff --git a/client/src/pages/artwork/index.tsx b/client/src/pages/artwork/index.tsx index 4e717e27..ea30552b 100644 --- a/client/src/pages/artwork/index.tsx +++ b/client/src/pages/artwork/index.tsx @@ -47,7 +47,7 @@ function renderArtworkCard(artwork: Art) { backContent={
-

+

{artwork.name}

@@ -60,7 +60,7 @@ function renderArtworkCard(artwork: Art) { {artwork.contributors.length > 0 && (

-

+

Contributors

@@ -78,7 +78,7 @@ function renderArtworkCard(artwork: Art) { e.stopPropagation()} > VIEW FULL DETAILS @@ -102,7 +102,7 @@ export default function ArtworksPage({ artworks, error }: ArtworksPageProps) {

FEATURED

From ca9ec3921e09cfed8bf196ccd3b9d3df74dfb308 Mon Sep 17 00:00:00 2001 From: David Du <24074639@student.uwa.edu.au> Date: Tue, 27 Jan 2026 06:20:33 +0000 Subject: [PATCH 062/122] Fix: Remove deprecated social media fields and unify font usage --- client/src/pages/artwork/[id].tsx | 8 +++---- client/src/pages/artwork/index.tsx | 6 ++--- client/src/types/art-contributor.ts | 2 -- client/tailwind.config.ts | 1 - server/poetry.lock | 36 +++++++++++++++++++++++++---- 5 files changed, 39 insertions(+), 14 deletions(-) diff --git a/client/src/pages/artwork/[id].tsx b/client/src/pages/artwork/[id].tsx index 70408b76..064e8e18 100644 --- a/client/src/pages/artwork/[id].tsx +++ b/client/src/pages/artwork/[id].tsx @@ -37,7 +37,7 @@ function displayContributors(artwork: Art) { > {artwork.contributors?.map((contributor) => (
-
+
{contributor.member_name}
@@ -64,7 +64,7 @@ export default function ArtworkPage({ artwork, error }: ArtworkPageProps) { >
@@ -108,7 +108,7 @@ export default function ArtworkPage({ artwork, error }: ArtworkPageProps) { data-layer="Artwork Description" className="justify-start self-stretch" > - + {artwork!.description}
@@ -129,7 +129,7 @@ export default function ArtworkPage({ artwork, error }: ArtworkPageProps) { className="DescriptionSectionMobile flex-col items-start justify-start pt-7" >
- + {artwork!.description}
diff --git a/client/src/pages/artwork/index.tsx b/client/src/pages/artwork/index.tsx index ea30552b..d8d0b38f 100644 --- a/client/src/pages/artwork/index.tsx +++ b/client/src/pages/artwork/index.tsx @@ -50,10 +50,10 @@ function renderArtworkCard(artwork: Art) {

{artwork.name}

-

+

from GAME NAME

-

+

{artwork.description || "No description available."}

@@ -67,7 +67,7 @@ function renderArtworkCard(artwork: Art) { {artwork.contributors.map((contributor) => (
{contributor.member_name}
diff --git a/client/src/types/art-contributor.ts b/client/src/types/art-contributor.ts index 600a941e..8afd03b5 100644 --- a/client/src/types/art-contributor.ts +++ b/client/src/types/art-contributor.ts @@ -3,6 +3,4 @@ export interface ArtContributor { art_id: number; member_name: string; role: string; - instagram_url?: string; // TODO [HanMinh] to refine where to get these info - discord_url?: string; } diff --git a/client/tailwind.config.ts b/client/tailwind.config.ts index 9c4bd797..7698c77a 100644 --- a/client/tailwind.config.ts +++ b/client/tailwind.config.ts @@ -22,7 +22,6 @@ const config = { fontFamily: { sans: ["var(--font-sans)", ...fontFamily.sans], jersey10: ["Jersey 10", ...fontFamily.sans], - dmSans: ["DM Sans", ...fontFamily.sans], firaCode: ["var(--font-firaCode)", ...fontFamily.sans], }, diff --git a/server/poetry.lock b/server/poetry.lock index 8e6deac4..3c9c8704 100644 --- a/server/poetry.lock +++ b/server/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. [[package]] name = "asgiref" @@ -6,6 +6,7 @@ version = "3.8.1" description = "ASGI specs, helper code, and adapters" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, @@ -20,6 +21,7 @@ version = "2.15.8" description = "An abstract syntax tree for Python with inference support." optional = false python-versions = ">=3.7.2" +groups = ["dev"] files = [ {file = "astroid-2.15.8-py3-none-any.whl", hash = "sha256:1aa149fc5c6589e3d0ece885b4491acd80af4f087baafa3fb5203b113e68cd3c"}, {file = "astroid-2.15.8.tar.gz", hash = "sha256:6c107453dffee9055899705de3c9ead36e74119cee151e5a9aaf7f0b0e020a6a"}, @@ -35,6 +37,8 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +markers = "sys_platform == \"win32\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -46,6 +50,7 @@ version = "5.1.15" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.10" +groups = ["main"] files = [ {file = "django-5.1.15-py3-none-any.whl", hash = "sha256:117871e58d6eda37f09870b7d73a3d66567b03aecd515b386b1751177c413432"}, {file = "django-5.1.15.tar.gz", hash = "sha256:46a356b5ff867bece73fc6365e081f21c569973403ee7e9b9a0316f27d0eb947"}, @@ -66,6 +71,7 @@ version = "4.4.0" description = "django-cors-headers is a Django application for handling the server headers required for Cross-Origin Resource Sharing (CORS)." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "django_cors_headers-4.4.0-py3-none-any.whl", hash = "sha256:5c6e3b7fe870876a1efdfeb4f433782c3524078fa0dc9e0195f6706ce7a242f6"}, {file = "django_cors_headers-4.4.0.tar.gz", hash = "sha256:92cf4633e22af67a230a1456cb1b7a02bb213d6536d2dcb2a4a24092ea9cebc2"}, @@ -81,6 +87,7 @@ version = "3.2.3" description = "Extensions for Django" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "django-extensions-3.2.3.tar.gz", hash = "sha256:44d27919d04e23b3f40231c4ab7af4e61ce832ef46d610cc650d53e68328410a"}, {file = "django_extensions-3.2.3-py3-none-any.whl", hash = "sha256:9600b7562f79a92cbf1fde6403c04fee314608fefbb595502e34383ae8203401"}, @@ -95,6 +102,7 @@ version = "3.15.2" description = "Web APIs for Django, made easy." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "djangorestframework-3.15.2-py3-none-any.whl", hash = "sha256:2b8871b062ba1aefc2de01f773875441a961fefbf79f5eed1e32b2f096944b20"}, {file = "djangorestframework-3.15.2.tar.gz", hash = "sha256:36fe88cd2d6c6bec23dca9804bab2ba5517a8bb9d8f47ebc68981b56840107ad"}, @@ -109,6 +117,7 @@ version = "6.1.0" description = "the modular source code checker: pep8 pyflakes and co" optional = false python-versions = ">=3.8.1" +groups = ["dev"] files = [ {file = "flake8-6.1.0-py2.py3-none-any.whl", hash = "sha256:ffdfce58ea94c6580c77888a86506937f9a1a227dfcd15f245d694ae20a6b6e5"}, {file = "flake8-6.1.0.tar.gz", hash = "sha256:d5b3857f07c030bdb5bf41c7f53799571d75c4491748a3adcd47de929e34cd23"}, @@ -125,6 +134,7 @@ version = "1.4" description = "Plugin to catch bad style specific to Django Projects." optional = false python-versions = ">=3.7.2,<4.0.0" +groups = ["dev"] files = [ {file = "flake8_django-1.4.tar.gz", hash = "sha256:4debba883084191568e3187416d1d6bdd4abd826da988f197a3c36572e9f30de"}, ] @@ -139,6 +149,7 @@ version = "1.5.1" description = "Let your Python tests travel through time" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "freezegun-1.5.1-py3-none-any.whl", hash = "sha256:bf111d7138a8abe55ab48a71755673dbaa4ab87f4cff5634a4442dfec34c15f1"}, {file = "freezegun-1.5.1.tar.gz", hash = "sha256:b29dedfcda6d5e8e083ce71b2b542753ad48cfec44037b3fc79702e2980a89e9"}, @@ -153,6 +164,7 @@ version = "23.0.0" description = "WSGI HTTP Server for UNIX" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d"}, {file = "gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec"}, @@ -174,6 +186,7 @@ version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, @@ -185,6 +198,7 @@ version = "1.10.0" description = "A fast and thorough lazy object proxy." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "lazy-object-proxy-1.10.0.tar.gz", hash = "sha256:78247b6d45f43a52ef35c25b5581459e85117225408a4128a3daf8bf9648ac69"}, {file = "lazy_object_proxy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:855e068b0358ab916454464a884779c7ffa312b8925c6f7401e952dcf3b89977"}, @@ -231,6 +245,7 @@ version = "0.7.0" description = "McCabe checker, plugin for flake8" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, @@ -242,6 +257,7 @@ version = "24.1" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, @@ -253,6 +269,7 @@ version = "11.3.0" description = "Python Imaging Library (Fork)" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860"}, {file = "pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad"}, @@ -368,7 +385,7 @@ fpx = ["olefile"] mic = ["olefile"] test-arrow = ["pyarrow"] tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "trove-classifiers (>=2024.10.12)"] -typing = ["typing-extensions"] +typing = ["typing-extensions ; python_version < \"3.10\""] xmp = ["defusedxml"] [[package]] @@ -377,6 +394,7 @@ version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -392,6 +410,7 @@ version = "2.11.1" description = "Python style guide checker" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, @@ -403,6 +422,7 @@ version = "3.1.0" description = "passive checker of Python programs" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pyflakes-3.1.0-py2.py3-none-any.whl", hash = "sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774"}, {file = "pyflakes-3.1.0.tar.gz", hash = "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc"}, @@ -414,6 +434,7 @@ version = "8.3.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, @@ -434,6 +455,7 @@ version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -448,6 +470,7 @@ version = "1.0.1" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, @@ -462,6 +485,7 @@ version = "1.16.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["main"] files = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, @@ -473,6 +497,7 @@ version = "0.5.1" description = "A non-validating SQL parser." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "sqlparse-0.5.1-py3-none-any.whl", hash = "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4"}, {file = "sqlparse-0.5.1.tar.gz", hash = "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e"}, @@ -488,6 +513,8 @@ version = "2024.1" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" +groups = ["main"] +markers = "sys_platform == \"win32\"" files = [ {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, @@ -499,6 +526,7 @@ version = "1.16.0" description = "Module for decorators, wrappers and monkey patching." optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, @@ -573,6 +601,6 @@ files = [ ] [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "^3.12" -content-hash = "f804c2f3998772b91e34ad214e5fcafe900bec97675f73046d3bcc79aba0f7db" +content-hash = "f804c2f3998772b91e34ad214e5fcafe900bec97675f73046d3bcc79aba0f7db" \ No newline at end of file From 1f5695780dccfb0d00364a9386df1e06e8cd6e0f Mon Sep 17 00:00:00 2001 From: David Du <24074639@student.uwa.edu.au> Date: Fri, 30 Jan 2026 14:19:16 +0000 Subject: [PATCH 063/122] Merge conflicting migrations --- .../game_dev/migrations/0008_merge_20260130_2216.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 server/game_dev/migrations/0008_merge_20260130_2216.py diff --git a/server/game_dev/migrations/0008_merge_20260130_2216.py b/server/game_dev/migrations/0008_merge_20260130_2216.py new file mode 100644 index 00000000..fda3f25b --- /dev/null +++ b/server/game_dev/migrations/0008_merge_20260130_2216.py @@ -0,0 +1,13 @@ +# Generated by Django 6.0 on 2026-01-30 14:16 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("game_dev", "0005_alter_member_profile_picture"), + ("game_dev", "0007_alter_artcontributor_unique_together_and_more"), + ] + + operations = [] From fd39a1124f53479777d29c19a3e77ebdaa9c84ec Mon Sep 17 00:00:00 2001 From: David Du <24074639@student.uwa.edu.au> Date: Fri, 30 Jan 2026 14:24:35 +0000 Subject: [PATCH 064/122] Fix flake8 formatting error --- server/game_dev/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/server/game_dev/views.py b/server/game_dev/views.py index d696728b..465c8a1e 100644 --- a/server/game_dev/views.py +++ b/server/game_dev/views.py @@ -63,6 +63,7 @@ class ArtDetailAPIView(generics.RetrieveAPIView): def get_queryset(self): return Art.objects.all() + class MemberAPIView(generics.RetrieveAPIView): serializer_class = MemberSerializer lookup_field = "id" From bd8ad2b38295959867e1fe031a0bc795c7afaa98 Mon Sep 17 00:00:00 2001 From: Tuan Khanh Hoang Date: Sat, 31 Jan 2026 03:45:54 +0000 Subject: [PATCH 065/122] add: migration --- .../game_dev/migrations/0010_merge_20260131_1145.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 server/game_dev/migrations/0010_merge_20260131_1145.py diff --git a/server/game_dev/migrations/0010_merge_20260131_1145.py b/server/game_dev/migrations/0010_merge_20260131_1145.py new file mode 100644 index 00000000..b998ef79 --- /dev/null +++ b/server/game_dev/migrations/0010_merge_20260131_1145.py @@ -0,0 +1,13 @@ +# Generated by Django 5.1.15 on 2026-01-31 03:45 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("game_dev", "0008_merge_20260130_2216"), + ("game_dev", "0009_merge_20260131_1044"), + ] + + operations = [] From 7b047efebbf010f423ed40cdb86c92498bfa6a07 Mon Sep 17 00:00:00 2001 From: David Du <24074639@student.uwa.edu.au> Date: Mon, 2 Feb 2026 12:40:16 +0000 Subject: [PATCH 066/122] Fix API response for featured arts - Add explicit art_id field - Add member_id for member page linking - Add showcase_description - Remove redundant art_id from contributors --- server/game_dev/serializers.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/server/game_dev/serializers.py b/server/game_dev/serializers.py index f4eeba19..af6d9b87 100644 --- a/server/game_dev/serializers.py +++ b/server/game_dev/serializers.py @@ -15,7 +15,6 @@ class Meta: "location", ] - # This is child serializer of GameSerializer class GameContributorSerializer(serializers.ModelSerializer): member_id = serializers.IntegerField(source="member.id") # to link contributors to their member/[id] page @@ -69,20 +68,26 @@ def get_contributors(self, obj): class ArtContributorSerializer(serializers.ModelSerializer): + member_id = serializers.IntegerField(source='member.id', read_only=True) member_name = serializers.CharField(source='member.name', read_only=True) - art_id = serializers.IntegerField(source='art.id', read_only=True) class Meta: model = ArtContributor - fields = ['id', 'art_id', 'member', 'member_name', 'role'] + fields = ['id', 'member_id', 'member_name', 'role'] class ArtSerializer(serializers.ModelSerializer): + art_id = serializers.IntegerField(source='id', read_only=True) contributors = ArtContributorSerializer(many=True, read_only=True) + showcase_description = serializers.SerializerMethodField() class Meta: model = Art - fields = ['id', 'name', 'description', 'media', 'active', 'contributors'] + fields = ['art_id', 'name', 'description', 'media', 'active', 'contributors', 'showcase_description'] + + def get_showcase_description(self, obj): + showcase = obj.showcase.first() + return showcase.description if showcase else None class MemberSerializer(serializers.ModelSerializer): From ae429062c30b5a61f9714df02fec04942d11baf9 Mon Sep 17 00:00:00 2001 From: Karl_Sue <24595816@student.uwa.edu.au> Date: Wed, 4 Feb 2026 05:12:11 +0000 Subject: [PATCH 067/122] merge: issue-40/backend --- ...ge_0008_art_showcase_0010_merge_20260131_1145.py | 13 +++++++++++++ server/game_dev/models.py | 2 +- server/game_dev/serializers.py | 1 + 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 server/game_dev/migrations/0011_merge_0008_art_showcase_0010_merge_20260131_1145.py diff --git a/server/game_dev/migrations/0011_merge_0008_art_showcase_0010_merge_20260131_1145.py b/server/game_dev/migrations/0011_merge_0008_art_showcase_0010_merge_20260131_1145.py new file mode 100644 index 00000000..2d09bb12 --- /dev/null +++ b/server/game_dev/migrations/0011_merge_0008_art_showcase_0010_merge_20260131_1145.py @@ -0,0 +1,13 @@ +# Generated by Django 5.2.9 on 2026-02-04 05:07 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("game_dev", "0008_art_showcase"), + ("game_dev", "0010_merge_20260131_1145"), + ] + + operations = [] diff --git a/server/game_dev/models.py b/server/game_dev/models.py index 094f3cd9..403ab1c6 100644 --- a/server/game_dev/models.py +++ b/server/game_dev/models.py @@ -79,7 +79,7 @@ def __str__(self): class Art(models.Model): name = models.CharField(null=False, max_length=200) description = models.CharField(max_length=200,) - source_game = models.ForeignKey(Game, on_delete=models.CASCADE, related_name='art_pieces') + source_game = models.ForeignKey(Game, on_delete=models.CASCADE, related_name='art_pieces', null=True, blank=True) media = models.ImageField(upload_to='art/', null=False) active = models.BooleanField(default=True) diff --git a/server/game_dev/serializers.py b/server/game_dev/serializers.py index af6d9b87..b342ee43 100644 --- a/server/game_dev/serializers.py +++ b/server/game_dev/serializers.py @@ -15,6 +15,7 @@ class Meta: "location", ] + # This is child serializer of GameSerializer class GameContributorSerializer(serializers.ModelSerializer): member_id = serializers.IntegerField(source="member.id") # to link contributors to their member/[id] page From fb56dfcf1fb7c9a63d6f38b000b02dd7ea1730e7 Mon Sep 17 00:00:00 2001 From: Karl_Sue <24595816@student.uwa.edu.au> Date: Wed, 4 Feb 2026 05:15:09 +0000 Subject: [PATCH 068/122] add: migration --- .../0012_art_source_game_and_more.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 server/game_dev/migrations/0012_art_source_game_and_more.py diff --git a/server/game_dev/migrations/0012_art_source_game_and_more.py b/server/game_dev/migrations/0012_art_source_game_and_more.py new file mode 100644 index 00000000..365573fb --- /dev/null +++ b/server/game_dev/migrations/0012_art_source_game_and_more.py @@ -0,0 +1,34 @@ +# Generated by Django 5.2.9 on 2026-02-04 05:13 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("game_dev", "0011_merge_0008_art_showcase_0010_merge_20260131_1145"), + ] + + operations = [ + migrations.AddField( + model_name="art", + name="source_game", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="art_pieces", + to="game_dev.game", + ), + ), + migrations.AlterConstraint( + model_name="artshowcase", + name="unique_artshowcase_per_art", + constraint=models.UniqueConstraint( + fields=("art",), + name="unique_artshowcase_per_art", + violation_error_message="Each art piece can only have one showcase.", + ), + ), + ] From 2f50442f8b9086bdf768a25c955e31119248c3c2 Mon Sep 17 00:00:00 2001 From: Karl_Sue <24595816@student.uwa.edu.au> Date: Wed, 4 Feb 2026 05:45:57 +0000 Subject: [PATCH 069/122] refactor: change api response --- client/src/hooks/use-artwork-data.ts | 22 ++++++++++------------ client/src/pages/artwork/index.tsx | 8 ++++---- client/src/types/art-contributor.ts | 2 +- client/src/types/art.ts | 3 ++- 4 files changed, 17 insertions(+), 18 deletions(-) diff --git a/client/src/hooks/use-artwork-data.ts b/client/src/hooks/use-artwork-data.ts index 60a65adf..ebca4aa7 100644 --- a/client/src/hooks/use-artwork-data.ts +++ b/client/src/hooks/use-artwork-data.ts @@ -4,27 +4,26 @@ export const generateMockArtworks = (count: number): Art[] => { const artworks: Art[] = []; for (let i = 1; i <= count; i++) { artworks.push({ - id: i, + art_id: i, name: `Artwork ${i}`, description: "Mock artwork description", - //source_game: "Mock Game", - media: "", + media: `http://localhost:8000/media/art/mock_artwork_${i}.png`, active: true, contributors: [ { id: i * 10 + 1, - art_id: i, + member_id: i * 10 + 1, member_name: "Contributor 1", role: "artist", }, { id: i * 10 + 2, - art_id: i, + member_id: i * 10 + 2, member_name: "Contributor 2", role: "designer", }, ], - //created_at: new Date().toISOString(), + showcase_description: `Showcase description for artwork ${i}`, }); } return artworks; @@ -32,27 +31,26 @@ export const generateMockArtworks = (count: number): Art[] => { export const generateMockArtwork = (id: string): Art => { return { - id: Number(id), + art_id: Number(id), name: "Mock Artwork Title", description: "Lorem ipsum dolor sit amet. Non numquam dicta nam autem dicta 33 error molestias et repellat consequatur eum iste expedita est dolorem libero et quas provident!", - //source_game: "Mock Game", - media: "", + media: `http://localhost:8000/media/art/mock_artwork_${id}.png`, active: true, - //created_at: new Date().toISOString(), contributors: [ { id: 1, - art_id: Number(id), + member_id: 1, member_name: "Contributor 1", role: "user1", }, { id: 2, - art_id: Number(id), + member_id: 2, member_name: "Contributor 2", role: "user2", }, ], + showcase_description: "Featured artwork showcase description", }; }; diff --git a/client/src/pages/artwork/index.tsx b/client/src/pages/artwork/index.tsx index d8d0b38f..93817207 100644 --- a/client/src/pages/artwork/index.tsx +++ b/client/src/pages/artwork/index.tsx @@ -40,10 +40,10 @@ const PLACEHOLDER_ICON = ( function renderArtworkCard(artwork: Art) { return (
@@ -77,7 +77,7 @@ function renderArtworkCard(artwork: Art) { )} e.stopPropagation()} > @@ -119,7 +119,7 @@ export const getServerSideProps: GetServerSideProps< ArtworksPageProps > = async () => { try { - const res = await api.get>("arts"); + const res = await api.get>("arts/featured"); return { props: { artworks: res.data } }; //} catch (err: unknown) { } catch { diff --git a/client/src/types/art-contributor.ts b/client/src/types/art-contributor.ts index 8afd03b5..27e97b40 100644 --- a/client/src/types/art-contributor.ts +++ b/client/src/types/art-contributor.ts @@ -1,6 +1,6 @@ export interface ArtContributor { id: number; - art_id: number; + member_id: number; member_name: string; role: string; } diff --git a/client/src/types/art.ts b/client/src/types/art.ts index ebb22e13..874cb817 100644 --- a/client/src/types/art.ts +++ b/client/src/types/art.ts @@ -1,10 +1,11 @@ import { ArtContributor } from "./art-contributor"; export interface Art { - id: number; + art_id: number; name: string; description: string; media: string; active: boolean; contributors: ArtContributor[]; + showcase_description: string; } From 0aa032de25399359d0b4d1cd04c6df703b1ccd13 Mon Sep 17 00:00:00 2001 From: David Du <24074639@student.uwa.edu.au> Date: Wed, 4 Feb 2026 06:43:23 +0000 Subject: [PATCH 070/122] added source_game fields to the API --- server/game_dev/serializers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/game_dev/serializers.py b/server/game_dev/serializers.py index af6d9b87..c64d2fa4 100644 --- a/server/game_dev/serializers.py +++ b/server/game_dev/serializers.py @@ -78,12 +78,14 @@ class Meta: class ArtSerializer(serializers.ModelSerializer): art_id = serializers.IntegerField(source='id', read_only=True) + source_game_id = serializers.IntegerField(source='source_game.id', read_only=True) + source_game_name = serializers.CharField(source='source_game.name', read_only=True) contributors = ArtContributorSerializer(many=True, read_only=True) showcase_description = serializers.SerializerMethodField() class Meta: model = Art - fields = ['art_id', 'name', 'description', 'media', 'active', 'contributors', 'showcase_description'] + fields = ['art_id', 'name', 'description', 'media', 'active', 'source_game_id', 'source_game_name', 'contributors', 'showcase_description'] def get_showcase_description(self, obj): showcase = obj.showcase.first() From 77930669a7bf67a03dfa101c90a14494b06759f7 Mon Sep 17 00:00:00 2001 From: Karl_Sue <24595816@student.uwa.edu.au> Date: Wed, 4 Feb 2026 06:48:22 +0000 Subject: [PATCH 071/122] add: migration --- .../game_dev/migrations/0013_merge_20260204_1441.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 server/game_dev/migrations/0013_merge_20260204_1441.py diff --git a/server/game_dev/migrations/0013_merge_20260204_1441.py b/server/game_dev/migrations/0013_merge_20260204_1441.py new file mode 100644 index 00000000..1578b046 --- /dev/null +++ b/server/game_dev/migrations/0013_merge_20260204_1441.py @@ -0,0 +1,13 @@ +# Generated by Django 5.2.9 on 2026-02-04 06:41 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("game_dev", "0010_merge_20260131_1118"), + ("game_dev", "0012_art_source_game_and_more"), + ] + + operations = [] From 7ac5eeba7bcba6921b24a4e7bd7180c4b5ac7bea Mon Sep 17 00:00:00 2001 From: Karl_Sue <24595816@student.uwa.edu.au> Date: Wed, 4 Feb 2026 08:29:45 +0000 Subject: [PATCH 072/122] fix: eslint type --- client/src/pages/artwork/[id].tsx | 56 +++++++++++++++++++++--------- client/src/pages/artwork/index.tsx | 48 +++++++++++++++++++------ 2 files changed, 76 insertions(+), 28 deletions(-) diff --git a/client/src/pages/artwork/[id].tsx b/client/src/pages/artwork/[id].tsx index 064e8e18..1171c6e9 100644 --- a/client/src/pages/artwork/[id].tsx +++ b/client/src/pages/artwork/[id].tsx @@ -1,6 +1,6 @@ import { GetServerSideProps } from "next"; import Image from "next/image"; -import { useRouter } from "next/navigation"; +import { useRouter } from "next/router"; import GoBackButton from "@/components/ui/go-back-button"; import ImagePlaceholder from "@/components/ui/image-placeholder"; @@ -50,9 +50,20 @@ function displayContributors(artwork: Art) { export default function ArtworkPage({ artwork, error }: ArtworkPageProps) { const router = useRouter(); + if (error) { return router.back()} />; } + + if (!artwork) { + return ( + router.push("/artwork")} + /> + ); + } + return (
+
- {artwork!.media ? ( + {artwork.media ? ( Artwork image )}
+
- {artwork!.name} + {artwork.name}
+
- {artwork!.description} + {artwork.description}
- {displayContributors(artwork!)} + + {displayContributors(artwork)}
+
- {artwork!.name} + {artwork.name}
+
- {artwork!.description} + {artwork.description}
- {displayContributors(artwork!)} + + {displayContributors(artwork)}
@@ -152,23 +170,27 @@ export default function ArtworkPage({ artwork, error }: ArtworkPageProps) { />
-
- TODO add footer -
); } +type FeaturedResponse = Art[] | { results: Art[] }; + export const getServerSideProps: GetServerSideProps = async ( context, ) => { const { id } = context.params as { id: string }; + try { - const artResponse = await api.get(`arts/${id}`); - const artwork = artResponse.data; + // We only have this endpoint, so reuse it and pick the item by art_id + const res = await api.get("arts/featured"); + const data = res.data; + + const list: Art[] = Array.isArray(data) ? data : (data?.results ?? []); + const artwork = list.find((a) => String(a.art_id) === String(id)); + + if (!artwork) return { notFound: true }; + return { props: { artwork } }; } catch (err: unknown) { return { diff --git a/client/src/pages/artwork/index.tsx b/client/src/pages/artwork/index.tsx index 93817207..e3e8a82d 100644 --- a/client/src/pages/artwork/index.tsx +++ b/client/src/pages/artwork/index.tsx @@ -1,6 +1,6 @@ import { GetServerSideProps } from "next"; import Link from "next/link"; -import { useRouter } from "next/navigation"; +import { useRouter } from "next/router"; // ✅ pages router (not next/navigation) import ImageCard from "@/components/ui/image-card"; import ErrorModal from "@/components/ui/modal/error-modal"; @@ -20,6 +20,13 @@ interface ArtworksPageProps { error?: string; } +function hasResultsArray(value: unknown): value is { results: T[] } { + if (typeof value !== "object" || value === null) return false; + + const v = value as Record; + return Array.isArray(v.results); +} + const PLACEHOLDER_ICON = (
router.back()} />; } + const featuredArtworks = artworks?.results?.slice(0, 3) ?? []; + return (
@@ -108,7 +117,7 @@ export default function ArtworksPage({ artworks, error }: ArtworksPageProps) {
- {artworks?.results.slice(0, 3).map(renderArtworkCard)} + {featuredArtworks.map(renderArtworkCard)}
@@ -119,15 +128,31 @@ export const getServerSideProps: GetServerSideProps< ArtworksPageProps > = async () => { try { - const res = await api.get>("arts/featured"); - return { props: { artworks: res.data } }; - //} catch (err: unknown) { - } catch { - // return { - // props: { error: (err as Error).message || "Failed to load artworks." }, - // }; - - // Fallback to mock data on error + const res = await api.get("arts/featured"); + const data = res.data as unknown; + + // Accept either: PageResult OR Art[] + const results: Art[] | null = Array.isArray(data) + ? (data as Art[]) + : hasResultsArray(data) + ? data.results + : null; + + // If API didn't throw but returned an unexpected shape, trigger fallback + if (!results) throw new Error("Invalid arts/featured response shape"); + + return { + props: { + artworks: { + results, + count: results.length, + next: "", + previous: "", + }, + }, + }; + } catch (err) { + // Fallback to mock data on any error (network, 500, invalid shape, etc.) const mockArtworks = generateMockArtworks(3); return { props: { @@ -137,6 +162,7 @@ export const getServerSideProps: GetServerSideProps< next: "", previous: "", }, + error: err instanceof Error ? err.message : undefined, }, }; } From e130d813bdccb13362aba7d914febd8f4e2ba004 Mon Sep 17 00:00:00 2001 From: Tuan Khanh Hoang Date: Sat, 7 Feb 2026 04:35:50 +0000 Subject: [PATCH 073/122] refactor: use svgs instead of inline declaration --- client/public/go-back-icon.svg | 10 +++++++++ client/public/placeholder-icon.svg | 6 +++++ client/src/components/ui/go-back-button.tsx | 22 ++++++------------- .../src/components/ui/image-placeholder.tsx | 19 ++++++---------- client/src/pages/artwork/index.tsx | 19 ++++++---------- 5 files changed, 37 insertions(+), 39 deletions(-) create mode 100644 client/public/go-back-icon.svg create mode 100644 client/public/placeholder-icon.svg diff --git a/client/public/go-back-icon.svg b/client/public/go-back-icon.svg new file mode 100644 index 00000000..e920f5a5 --- /dev/null +++ b/client/public/go-back-icon.svg @@ -0,0 +1,10 @@ + + + + diff --git a/client/public/placeholder-icon.svg b/client/public/placeholder-icon.svg new file mode 100644 index 00000000..6879e787 --- /dev/null +++ b/client/public/placeholder-icon.svg @@ -0,0 +1,6 @@ + + + diff --git a/client/src/components/ui/go-back-button.tsx b/client/src/components/ui/go-back-button.tsx index 5f1ebce1..53ca3501 100644 --- a/client/src/components/ui/go-back-button.tsx +++ b/client/src/components/ui/go-back-button.tsx @@ -1,3 +1,4 @@ +import Image from "next/image"; import Link from "next/link"; interface GoBackButtonProps { @@ -12,22 +13,13 @@ const GoBackButton = ({ url, label }: GoBackButtonProps) => { type="button" >
- + />

{label}

diff --git a/client/src/components/ui/image-placeholder.tsx b/client/src/components/ui/image-placeholder.tsx index b7e25e58..c694f378 100644 --- a/client/src/components/ui/image-placeholder.tsx +++ b/client/src/components/ui/image-placeholder.tsx @@ -1,3 +1,4 @@ +import Image from "next/image"; import React from "react"; const ImagePlaceholder = () => { @@ -7,18 +8,12 @@ const ImagePlaceholder = () => { className="PlaceholderImage bg-light-2 flex h-[500px] w-[500px] items-center justify-center rounded-[10px]" >
- - - + Placeholder icon
); diff --git a/client/src/pages/artwork/index.tsx b/client/src/pages/artwork/index.tsx index e3e8a82d..a6504e63 100644 --- a/client/src/pages/artwork/index.tsx +++ b/client/src/pages/artwork/index.tsx @@ -1,4 +1,5 @@ import { GetServerSideProps } from "next"; +import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/router"; // ✅ pages router (not next/navigation) @@ -29,18 +30,12 @@ function hasResultsArray(value: unknown): value is { results: T[] } { const PLACEHOLDER_ICON = (
- - - + Placeholder icon
); From 8853c7793c4298359686ae4e4f0aaa6f80276992 Mon Sep 17 00:00:00 2001 From: Tuan Khanh Hoang Date: Sat, 7 Feb 2026 04:46:50 +0000 Subject: [PATCH 074/122] refactor: using AddConstraint in migration instead of AlterConstraint --- server/game_dev/migrations/0012_art_source_game_and_more.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/game_dev/migrations/0012_art_source_game_and_more.py b/server/game_dev/migrations/0012_art_source_game_and_more.py index 365573fb..03ee5d7f 100644 --- a/server/game_dev/migrations/0012_art_source_game_and_more.py +++ b/server/game_dev/migrations/0012_art_source_game_and_more.py @@ -22,9 +22,8 @@ class Migration(migrations.Migration): to="game_dev.game", ), ), - migrations.AlterConstraint( + migrations.AddConstraint( model_name="artshowcase", - name="unique_artshowcase_per_art", constraint=models.UniqueConstraint( fields=("art",), name="unique_artshowcase_per_art", From a12b0722da302c902f7dc6178c39980461f42f8b Mon Sep 17 00:00:00 2001 From: Tuan Khanh Hoang Date: Sat, 7 Feb 2026 05:57:06 +0000 Subject: [PATCH 075/122] feature: connect frontend + backend --- client/src/pages/artwork/[id].tsx | 25 ++++++++++++++++++++----- client/src/pages/artwork/index.tsx | 23 +++++++++++++++++++++-- client/src/types/art.ts | 2 ++ server/game_dev/admin.py | 13 +++++++++++-- 4 files changed, 54 insertions(+), 9 deletions(-) diff --git a/client/src/pages/artwork/[id].tsx b/client/src/pages/artwork/[id].tsx index 1171c6e9..410a5672 100644 --- a/client/src/pages/artwork/[id].tsx +++ b/client/src/pages/artwork/[id].tsx @@ -35,13 +35,28 @@ function displayContributors(artwork: Art) { data-layer="Contributors List" className="ContributorsList relative flex flex-col gap-3 p-3" > - {artwork.contributors?.map((contributor) => ( -
-
- {contributor.member_name} + {artwork.contributors.length > 0 && ( +
+
+ {artwork.contributors.map((contributor) => ( + + ))}
- ))} + )}
diff --git a/client/src/pages/artwork/index.tsx b/client/src/pages/artwork/index.tsx index a6504e63..1e5b71b0 100644 --- a/client/src/pages/artwork/index.tsx +++ b/client/src/pages/artwork/index.tsx @@ -53,7 +53,20 @@ function renderArtworkCard(artwork: Art) { {artwork.name}

- from GAME NAME + {artwork.source_game_name ? ( + <> + from{" "} + e.stopPropagation()} + > + {artwork.source_game_name} + + + ) : ( + "No associated game" + )}

{artwork.description || "No description available."} @@ -71,7 +84,13 @@ function renderArtworkCard(artwork: Art) { key={contributor.id} className="font-sans text-[15px] text-light_1" > - {contributor.member_name} + e.stopPropagation()} + > + {contributor.member_name} +

))}
diff --git a/client/src/types/art.ts b/client/src/types/art.ts index 874cb817..79d6ef5e 100644 --- a/client/src/types/art.ts +++ b/client/src/types/art.ts @@ -6,6 +6,8 @@ export interface Art { description: string; media: string; active: boolean; + source_game_id: number | null; + source_game_name: string | null; contributors: ArtContributor[]; showcase_description: string; } diff --git a/server/game_dev/admin.py b/server/game_dev/admin.py index ab5118de..6daed890 100644 --- a/server/game_dev/admin.py +++ b/server/game_dev/admin.py @@ -29,12 +29,21 @@ class CommitteeAdmin(admin.ModelAdmin): raw_id_fields = ["id"] +class ArtContributorInline(admin.TabularInline): + model = ArtContributor + extra = 1 + + +class ArtAdmin(admin.ModelAdmin): + inlines = [ArtContributorInline] + + 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(Art) -admin.site.register(ArtContributor) +admin.site.register(Art, ArtAdmin) +# admin.site.register(ArtContributor) admin.site.register(ArtShowcase) admin.site.register(Committee, CommitteeAdmin) From 626ac0540c49e5d8ab2d01934224aec9326db854 Mon Sep 17 00:00:00 2001 From: Tuan Khanh Hoang Date: Sat, 7 Feb 2026 06:03:47 +0000 Subject: [PATCH 076/122] feature: add tests for art model --- server/game_dev/tests.py | 214 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 213 insertions(+), 1 deletion(-) diff --git a/server/game_dev/tests.py b/server/game_dev/tests.py index 96bdc434..34993c67 100644 --- a/server/game_dev/tests.py +++ b/server/game_dev/tests.py @@ -1,5 +1,5 @@ from django.test import TestCase -from .models import Member, Event, Committee +from .models import Member, Event, Committee, Game, Art, ArtContributor, ArtShowcase import datetime from django.core.files.uploadedfile import SimpleUploadedFile from django.utils import timezone @@ -193,3 +193,215 @@ def test_default_is_upcoming(self): def test_invalid_type(self): res = self.client.get(self.url, {"type": "invalid"}) self.assertEqual(res.status_code, 400) + + +class ArtModelTest(TestCase): + def setUp(self): + # Create a game for source_game foreign key + self.game = Game.objects.create( + name="Test Game", + description="A test game", + completion=Game.CompletionStatus.WIP, + hostURL="https://example.com", + ) + + # Create an art piece with media + image_file = SimpleUploadedFile( + "test_art.jpg", + b"dummy art image data", + content_type="image/jpeg", + ) + self.art = Art.objects.create( + name="Test Artwork", + description="A beautiful test artwork", + source_game=self.game, + media=image_file, + ) + + def test_art_creation(self): + try: + Art.objects.get(name="Test Artwork") + except Art.DoesNotExist: + self.fail("Art was not properly created") + + def test_art_is_active_by_default(self): + self.assertTrue(self.art.active) + + def test_media_is_saved_in_correct_folder(self): + self.assertTrue(self.art.media.name.startswith("art/")) + + def test_media_field_not_empty(self): + self.assertIsNotNone(self.art.media) + + def test_source_game_relationship(self): + art = Art.objects.get(pk=self.art.pk) + self.assertEqual(art.source_game, self.game) + + def test_art_without_source_game(self): + # Test that art can be created without a source game + image_file = SimpleUploadedFile( + "test_art_no_game.jpg", + b"dummy art image data", + content_type="image/jpeg", + ) + art_no_game = Art.objects.create( + name="Independent Artwork", + description="Art with no game", + media=image_file, + ) + self.assertIsNone(art_no_game.source_game) + + def test_cascade_from_game(self): + # When game is deleted, art should remain (SET_NULL behavior would be ideal, but currently CASCADE) + art_id = self.art.id + self.game.delete() + # Since source_game has CASCADE, the art should be deleted + with self.assertRaises(Art.DoesNotExist): + Art.objects.get(id=art_id) + + +class ArtContributorModelTest(TestCase): + def setUp(self): + # Create member + self.member1 = Member.objects.create( + name="John Artist", + about="A talented artist", + pronouns="He/Him" + ) + self.member2 = Member.objects.create( + name="Jane Designer", + about="A creative designer", + pronouns="She/Her" + ) + + # Create art + image_file = SimpleUploadedFile( + "test_art.jpg", + b"dummy art image data", + content_type="image/jpeg", + ) + self.art = Art.objects.create( + name="Collaborative Artwork", + description="Art with multiple contributors", + media=image_file, + ) + + # Create art contributor + self.art_contributor = ArtContributor.objects.create( + art=self.art, + member=self.member1, + role="Lead Artist" + ) + + def test_art_contributor_creation(self): + try: + ArtContributor.objects.get(art=self.art, member=self.member1) + except ArtContributor.DoesNotExist: + self.fail("ArtContributor was not properly created") + + def test_art_contributor_unique_constraint(self): + # Try to create duplicate art-member pair + with self.assertRaises(IntegrityError): + ArtContributor.objects.create( + art=self.art, + member=self.member1, + role="Another Role" + ) + + def test_multiple_contributors_for_same_art(self): + # Should be able to add different members to same art + ArtContributor.objects.create( + art=self.art, + member=self.member2, + role="Character Designer" + ) + contributors = ArtContributor.objects.filter(art=self.art) + self.assertEqual(contributors.count(), 2) + + def test_cascade_from_art(self): + # When art is deleted, art contributors should be deleted + contributor_id = self.art_contributor.id + self.art.delete() + with self.assertRaises(ArtContributor.DoesNotExist): + ArtContributor.objects.get(id=contributor_id) + + def test_cascade_from_member(self): + # When member is deleted, art contributors should be deleted + contributor_id = self.art_contributor.id + self.member1.delete() + with self.assertRaises(ArtContributor.DoesNotExist): + ArtContributor.objects.get(id=contributor_id) + + def test_art_contributor_role(self): + contributor = ArtContributor.objects.get(pk=self.art_contributor.pk) + self.assertEqual(contributor.role, "Lead Artist") + + +class ArtShowcaseModelTest(TestCase): + def setUp(self): + # Create art pieces + image_file1 = SimpleUploadedFile( + "test_art1.jpg", + b"dummy art image data", + content_type="image/jpeg", + ) + self.art1 = Art.objects.create( + name="Showcased Artwork", + description="This art is showcased", + media=image_file1, + ) + + image_file2 = SimpleUploadedFile( + "test_art2.jpg", + b"dummy art image data 2", + content_type="image/jpeg", + ) + self.art2 = Art.objects.create( + name="Another Artwork", + description="This art is also showcased", + media=image_file2, + ) + + # Create showcase + self.showcase = ArtShowcase.objects.create( + art=self.art1, + description="Featured artwork of the month" + ) + + def test_art_showcase_creation(self): + try: + ArtShowcase.objects.get(art=self.art1) + except ArtShowcase.DoesNotExist: + self.fail("ArtShowcase was not properly created") + + def test_art_showcase_unique_constraint(self): + # Try to create another showcase for the same art + with self.assertRaises(IntegrityError): + ArtShowcase.objects.create( + art=self.art1, + description="Another showcase for same art" + ) + + def test_multiple_showcases_for_different_arts(self): + # Should be able to create showcases for different art pieces + ArtShowcase.objects.create( + art=self.art2, + description="Another featured artwork" + ) + showcases = ArtShowcase.objects.all() + self.assertEqual(showcases.count(), 2) + + def test_cascade_from_art(self): + # When art is deleted, its showcase should be deleted + showcase_id = self.showcase.id + self.art1.delete() + with self.assertRaises(ArtShowcase.DoesNotExist): + ArtShowcase.objects.get(id=showcase_id) + + def test_showcase_description(self): + showcase = ArtShowcase.objects.get(pk=self.showcase.pk) + self.assertEqual(showcase.description, "Featured artwork of the month") + + def test_art_showcase_relationship(self): + showcase = ArtShowcase.objects.get(pk=self.showcase.pk) + self.assertEqual(showcase.art, self.art1) From 319b1a24d83d9bc4a51afd62c8b454ced5af89fd Mon Sep 17 00:00:00 2001 From: Tuan Khanh Hoang Date: Sat, 7 Feb 2026 06:22:39 +0000 Subject: [PATCH 077/122] feature: add fallback when server fail --- client/src/components/ui/image-card.tsx | 26 ++++++++++++++++++------- client/src/hooks/use-artwork-data.ts | 6 ++++++ client/src/pages/artwork/index.tsx | 8 ++++---- client/src/types/art.ts | 1 + 4 files changed, 30 insertions(+), 11 deletions(-) diff --git a/client/src/components/ui/image-card.tsx b/client/src/components/ui/image-card.tsx index 32fba7e0..d868429a 100644 --- a/client/src/components/ui/image-card.tsx +++ b/client/src/components/ui/image-card.tsx @@ -5,12 +5,11 @@ import React from "react"; interface ImageCardProps { imageSrc?: string; imageAlt?: string; - /** Optional content rendered on the front (over the image or placeholder). */ children?: React.ReactNode; - /** Optional content rendered on the back when hovering/focused. */ backContent?: React.ReactNode; - /** Optional href for navigation when clicking the front face */ href?: string; + disableFlip?: boolean; + placeholder?: React.ReactNode; } const ImageCard = ({ @@ -19,10 +18,13 @@ const ImageCard = ({ children, backContent, href, + disableFlip = false, + placeholder, }: ImageCardProps) => { const router = useRouter(); const [isFlipped, setIsFlipped] = React.useState(false); const [isMobile, setIsMobile] = React.useState(false); + const [hasImageError, setHasImageError] = React.useState(false); React.useEffect(() => { const checkMobile = () => { @@ -38,7 +40,7 @@ const ImageCard = ({ // On mobile, navigate directly if href is provided if (isMobile && href) { router.push(href); - } else if (backContent) { + } else if (backContent && !disableFlip && !hasImageError) { // On desktop, toggle flip state setIsFlipped(!isFlipped); } @@ -47,7 +49,11 @@ const ImageCard = ({ return (
- {imageSrc ? ( + {imageSrc && !hasImageError ? ( <> { + setHasImageError(true); + setIsFlipped(false); + }} /> {children && (
@@ -75,7 +85,9 @@ const ImageCard = ({ ) : (
- {children || No Image} + {placeholder || children || ( + No Image + )}
)}
diff --git a/client/src/hooks/use-artwork-data.ts b/client/src/hooks/use-artwork-data.ts index ebca4aa7..e64c7295 100644 --- a/client/src/hooks/use-artwork-data.ts +++ b/client/src/hooks/use-artwork-data.ts @@ -9,6 +9,8 @@ export const generateMockArtworks = (count: number): Art[] => { description: "Mock artwork description", media: `http://localhost:8000/media/art/mock_artwork_${i}.png`, active: true, + source_game_id: null, + source_game_name: null, contributors: [ { id: i * 10 + 1, @@ -24,6 +26,7 @@ export const generateMockArtworks = (count: number): Art[] => { }, ], showcase_description: `Showcase description for artwork ${i}`, + isMock: true, }); } return artworks; @@ -37,6 +40,8 @@ export const generateMockArtwork = (id: string): Art => { "Lorem ipsum dolor sit amet. Non numquam dicta nam autem dicta 33 error molestias et repellat consequatur eum iste expedita est dolorem libero et quas provident!", media: `http://localhost:8000/media/art/mock_artwork_${id}.png`, active: true, + source_game_id: null, + source_game_name: null, contributors: [ { id: 1, @@ -52,5 +57,6 @@ export const generateMockArtwork = (id: string): Art => { }, ], showcase_description: "Featured artwork showcase description", + isMock: true, }; }; diff --git a/client/src/pages/artwork/index.tsx b/client/src/pages/artwork/index.tsx index 1e5b71b0..40300007 100644 --- a/client/src/pages/artwork/index.tsx +++ b/client/src/pages/artwork/index.tsx @@ -46,6 +46,8 @@ function renderArtworkCard(artwork: Art) { imageSrc={artwork.media || undefined} imageAlt={artwork.name} href={`/artwork/${artwork.art_id}`} + disableFlip={artwork.isMock === true} + placeholder={PLACEHOLDER_ICON} backContent={
@@ -106,15 +108,13 @@ function renderArtworkCard(artwork: Art) {
} - > - {!artwork.media && PLACEHOLDER_ICON} - + /> ); } export default function ArtworksPage({ artworks, error }: ArtworksPageProps) { const router = useRouter(); - if (error) { + if (error && !artworks?.results?.length) { return router.back()} />; } diff --git a/client/src/types/art.ts b/client/src/types/art.ts index 79d6ef5e..38436e89 100644 --- a/client/src/types/art.ts +++ b/client/src/types/art.ts @@ -10,4 +10,5 @@ export interface Art { source_game_name: string | null; contributors: ArtContributor[]; showcase_description: string; + isMock?: boolean; } From 1246ac5caa9d75b432fac99067e9e3d9c65ef369 Mon Sep 17 00:00:00 2001 From: Karl_Sue <24595816@student.uwa.edu.au> Date: Wed, 11 Feb 2026 05:28:57 +0000 Subject: [PATCH 078/122] feature: add api endpoint /api/arts/[id] --- server/game_dev/urls.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/server/game_dev/urls.py b/server/game_dev/urls.py index 18761af2..f00e4713 100644 --- a/server/game_dev/urls.py +++ b/server/game_dev/urls.py @@ -1,10 +1,15 @@ from django.urls import path -from .views import EventListAPIView, EventDetailAPIView, GamesDetailAPIView, GameshowcaseAPIView, MemberAPIView, CommitteeAPIView, FeatureArtAPIView +from .views import ( + EventListAPIView, EventDetailAPIView, GamesDetailAPIView, + GameshowcaseAPIView, MemberAPIView, CommitteeAPIView, + FeatureArtAPIView, ArtDetailAPIView +) urlpatterns = [ path("events/", EventListAPIView.as_view(), name="events-list"), path("events//", EventDetailAPIView.as_view()), path('arts/featured/', FeatureArtAPIView.as_view()), + path('arts//', ArtDetailAPIView.as_view(), name='art-detail'), path("games//", GamesDetailAPIView.as_view()), path("gameshowcase/", GameshowcaseAPIView.as_view(), name="gameshowcase-api"), # Updated line for GameShowcase endpoint path('members//', MemberAPIView.as_view()), From f72ee8a156ea67b0c88cf2b9cce69d93d545c8f3 Mon Sep 17 00:00:00 2001 From: Karl_Sue <24595816@student.uwa.edu.au> Date: Wed, 11 Feb 2026 05:38:40 +0000 Subject: [PATCH 079/122] fix: eslint error --- client/src/components/ui/ContributorsList.tsx | 59 +++++++++++++++++++ .../{go-back-button.tsx => GoBackButton.tsx} | 0 .../ui/{image-card.tsx => ImageCard.tsx} | 0 client/src/pages/artwork/[id].tsx | 59 ++----------------- client/src/pages/artwork/index.tsx | 8 +-- 5 files changed, 69 insertions(+), 57 deletions(-) create mode 100644 client/src/components/ui/ContributorsList.tsx rename client/src/components/ui/{go-back-button.tsx => GoBackButton.tsx} (100%) rename client/src/components/ui/{image-card.tsx => ImageCard.tsx} (100%) diff --git a/client/src/components/ui/ContributorsList.tsx b/client/src/components/ui/ContributorsList.tsx new file mode 100644 index 00000000..457857f0 --- /dev/null +++ b/client/src/components/ui/ContributorsList.tsx @@ -0,0 +1,59 @@ +import Link from "next/link"; + +import { ArtContributor } from "@/types/art-contributor"; + +interface ContributorsListProps { + contributors: ArtContributor[]; +} + +export default function ContributorsList({ + contributors, +}: ContributorsListProps) { + if (contributors.length === 0) { + return null; + } + + return ( +
+
+
+ Contributors +
+
+
+
+
+ {contributors.map((contributor) => ( +
+ e.stopPropagation()} + > + {contributor.member_name} + + {" - "} + {contributor.role} +
+ ))} +
+
+
+
+ ); +} diff --git a/client/src/components/ui/go-back-button.tsx b/client/src/components/ui/GoBackButton.tsx similarity index 100% rename from client/src/components/ui/go-back-button.tsx rename to client/src/components/ui/GoBackButton.tsx diff --git a/client/src/components/ui/image-card.tsx b/client/src/components/ui/ImageCard.tsx similarity index 100% rename from client/src/components/ui/image-card.tsx rename to client/src/components/ui/ImageCard.tsx diff --git a/client/src/pages/artwork/[id].tsx b/client/src/pages/artwork/[id].tsx index 410a5672..28c7e755 100644 --- a/client/src/pages/artwork/[id].tsx +++ b/client/src/pages/artwork/[id].tsx @@ -2,7 +2,8 @@ import { GetServerSideProps } from "next"; import Image from "next/image"; import { useRouter } from "next/router"; -import GoBackButton from "@/components/ui/go-back-button"; +import ContributorsList from "@/components/ui/ContributorsList"; +import GoBackButton from "@/components/ui/GoBackButton"; import ImagePlaceholder from "@/components/ui/image-placeholder"; import ErrorModal from "@/components/ui/modal/error-modal"; import api from "@/lib/api"; @@ -13,56 +14,6 @@ interface ArtworkPageProps { error?: string; } -function displayContributors(artwork: Art) { - return ( -
-
-
-
- Contributors -
-
-
- {artwork.contributors.length > 0 && ( -
-
- {artwork.contributors.map((contributor) => ( - - ))} -
-
- )} -
-
-
- ); -} - export default function ArtworkPage({ artwork, error }: ArtworkPageProps) { const router = useRouter(); @@ -110,7 +61,9 @@ export default function ArtworkPage({ artwork, error }: ArtworkPageProps) { alt="Artwork image" width={500} height={500} + sizes="(max-width: 768px) 100vw, 50vw" className="relative block sm:h-auto sm:max-w-full md:max-h-full" + priority={false} /> ) : ( @@ -143,7 +96,7 @@ export default function ArtworkPage({ artwork, error }: ArtworkPageProps) {
- {displayContributors(artwork)} +
@@ -167,7 +120,7 @@ export default function ArtworkPage({ artwork, error }: ArtworkPageProps) {
- {displayContributors(artwork)} +
diff --git a/client/src/pages/artwork/index.tsx b/client/src/pages/artwork/index.tsx index 40300007..d27a188c 100644 --- a/client/src/pages/artwork/index.tsx +++ b/client/src/pages/artwork/index.tsx @@ -1,9 +1,9 @@ import { GetServerSideProps } from "next"; import Image from "next/image"; import Link from "next/link"; -import { useRouter } from "next/router"; // ✅ pages router (not next/navigation) +import { useRouter } from "next/router"; -import ImageCard from "@/components/ui/image-card"; +import ImageCard from "@/components/ui/ImageCard"; import ErrorModal from "@/components/ui/modal/error-modal"; import { generateMockArtworks } from "@/hooks/use-artwork-data"; import api from "@/lib/api"; @@ -58,13 +58,13 @@ function renderArtworkCard(artwork: Art) { {artwork.source_game_name ? ( <> from{" "} - e.stopPropagation()} > {artwork.source_game_name} - + ) : ( "No associated game" From 0e2e2b417fcd953bfb4a69dc3d01fd8c2827d579 Mon Sep 17 00:00:00 2001 From: Tuan Khanh Hoang Date: Fri, 13 Feb 2026 08:32:04 +0000 Subject: [PATCH 080/122] add art doc --- documentation/admin-dashboard/art.md | 33 ++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 documentation/admin-dashboard/art.md diff --git a/documentation/admin-dashboard/art.md b/documentation/admin-dashboard/art.md new file mode 100644 index 00000000..383a559f --- /dev/null +++ b/documentation/admin-dashboard/art.md @@ -0,0 +1,33 @@ +# admin-dashboard / art + +## Purpose +Add and manage artwork items shown on the site, including optional links to a source game and featured artwork sections. + +## Create a new Art entry (Admin: Art) +Fields to fill: +- `Name`: The public title displayed on artwork cards and detail pages. +- `Description`: Short description shown on cards and details. +- `Source game` (optional): Link this art to a game. If set, the artwork card shows “from ” and links to that game. If empty, the card shows “No associated game.” +- `Media`: The actual artwork image file. Required. +- `Active`: Currently not used in the frontend filtering; it does not hide or show items by itself. + +Contributor entries (inline “Art Contributors”): +- `Member`: The club member who contributed the art. +- `Role`: Their role (e.g. Artist, Designer, Illustrator). Each member can only be added once per art item. + +## Feature artwork on the site (Admin: Art Showcase) +To make art appear in the “Featured” section: +- Create an Art Showcase entry. +- Fields: + - Art: Select the artwork to feature (one showcase per artwork). + - Description: Text shown as `showcase_description` in the API. + +Important behavior: +- The frontend “Featured” section only pulls artworks that have a related Art Showcase entry. If you want to remove a featured item, delete its Art Showcase record. + +## Removing or hiding art +- To unfeature: delete the Art Showcase entry for that artwork. +- To remove entirely: delete the Art item (also removes its contributors and showcase). + +## itch.io data +- Not applicable for art entries. No itch.io fields are used for artwork. \ No newline at end of file From 03f7e1a78ec960b7a34e7523e6745a2b6c0f6daf Mon Sep 17 00:00:00 2001 From: Tuan Khanh Hoang Date: Sat, 14 Feb 2026 04:02:35 +0000 Subject: [PATCH 081/122] fix: art documentation - provide more details --- documentation/admin-dashboard/art.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/documentation/admin-dashboard/art.md b/documentation/admin-dashboard/art.md index 383a559f..7e4ba5c5 100644 --- a/documentation/admin-dashboard/art.md +++ b/documentation/admin-dashboard/art.md @@ -7,7 +7,7 @@ Add and manage artwork items shown on the site, including optional links to a so Fields to fill: - `Name`: The public title displayed on artwork cards and detail pages. - `Description`: Short description shown on cards and details. -- `Source game` (optional): Link this art to a game. If set, the artwork card shows “from ” and links to that game. If empty, the card shows “No associated game.” +- `Source game` (optional): Link this art to a game. If set, the artwork card shows “from game” and links to that game. If empty, the card shows “No associated game.” - `Media`: The actual artwork image file. Required. - `Active`: Currently not used in the frontend filtering; it does not hide or show items by itself. @@ -19,8 +19,9 @@ Contributor entries (inline “Art Contributors”): To make art appear in the “Featured” section: - Create an Art Showcase entry. - Fields: - - Art: Select the artwork to feature (one showcase per artwork). - - Description: Text shown as `showcase_description` in the API. + - `Art`: Select the artwork to feature (one showcase per artwork). + - `Description`: Text shown as `showcase_description` in the API. + - When featuring new art, delete the old featured item; otherwise the app picks the lowest `id` (ascending order). Important behavior: - The frontend “Featured” section only pulls artworks that have a related Art Showcase entry. If you want to remove a featured item, delete its Art Showcase record. From e99cb331c5ff9dfaeddea610c6aaf4136e92d057 Mon Sep 17 00:00:00 2001 From: Tuan Khanh Hoang Date: Sat, 14 Feb 2026 04:13:56 +0000 Subject: [PATCH 082/122] refactor: move file location to /client, next to src and public --- {documentation => client/documentation}/admin-dashboard/art.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {documentation => client/documentation}/admin-dashboard/art.md (100%) diff --git a/documentation/admin-dashboard/art.md b/client/documentation/admin-dashboard/art.md similarity index 100% rename from documentation/admin-dashboard/art.md rename to client/documentation/admin-dashboard/art.md From af9d74125e7546c93a8ad7422a86cf1c4b790205 Mon Sep 17 00:00:00 2001 From: Katariah Date: Sat, 14 Feb 2026 07:57:47 +0000 Subject: [PATCH 083/122] changed to match db names --- client/documentation/admin-dashboard.md | 63 +++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 client/documentation/admin-dashboard.md diff --git a/client/documentation/admin-dashboard.md b/client/documentation/admin-dashboard.md new file mode 100644 index 00000000..e11d488a --- /dev/null +++ b/client/documentation/admin-dashboard.md @@ -0,0 +1,63 @@ +# Admin Dashboard Documentation + +for admin dashboard docco pages: + +- each page should contain a list of the fields that need to be filled out to create a new entry and a short explanation of each +- explain any noteworthy cases where filling out a field a certain way causes a major difference on the frontend. e.g. rendering an itch widget vs a non itch widget +- for fields that need information to be filled from itch.io, a description of where to get this information from should be included +- the process for adding, managing, and removing admin accounts who can access the admin site + +## Events + +The events section of the admin dashboard allows administrators to create and manage events that will be displayed on the frontend. +Existing events will be listed in a table, and administrators can click on an event to edit its details or delete it if it is no longer relevant. +To create a new event, administrators can click on the "Add Event" button in the top right, which will take them to a form where they can fill out the necessary information about the event. + +When creating a new event, there are several fields that need to be filled out. + +### Name + +Required + +**Name:** The name of the event. This is what will be displayed on the frontend and should be descriptive enough to give users an idea of what the event is about. + +### Date and Time + +Required + +**Date:** The date and time of the event. It has two fields, **Date** and **Time**. +The **Date** field should be given in DD/MM/YYYY format, while the **Time** field should be given in HH:MM:SS format. This information is crucial for users to know when the event is taking place. + +### Description + +**Description:** A brief description of the event. This should provide users with more information about what to expect from the event and why they should attend, or alternatively what happened at the event if it is a past event. + +### Publication Date + +Required + +**Publication Date:** The date when the event was published on the frontend. This is important for users to know when the event was announced and can help them plan accordingly. + +### Cover Image + +Required + +**Cover Image:** An image that represents the event. This should be visually appealing and relevant to the event to attract users' attention. It will be displayed on the frontend alongside the event details. + +### Location + +Required + +**Location:** The location of the event. This can be a physical location or an online platform where the event will take place. Providing this information is essential for users to know where they need to go to attend the event. + +## Members + +## Committees + +## Game Contributors + +## Games + +## Art + +## Game Showcases From 5f30aa9d423b73557653141d4c56e8205111b29e Mon Sep 17 00:00:00 2001 From: Katariah Date: Sat, 14 Feb 2026 08:01:15 +0000 Subject: [PATCH 084/122] bolded rthe rquired label and added descriptions for game contributors --- client/documentation/admin-dashboard.md | 42 ++++++++++++++++++------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/client/documentation/admin-dashboard.md b/client/documentation/admin-dashboard.md index e11d488a..f20f14d8 100644 --- a/client/documentation/admin-dashboard.md +++ b/client/documentation/admin-dashboard.md @@ -17,38 +17,38 @@ When creating a new event, there are several fields that need to be filled out. ### Name -Required +**Required** -**Name:** The name of the event. This is what will be displayed on the frontend and should be descriptive enough to give users an idea of what the event is about. +The name of the event. This is what will be displayed on the frontend and should be descriptive enough to give users an idea of what the event is about. ### Date and Time -Required +**Required** -**Date:** The date and time of the event. It has two fields, **Date** and **Time**. +The date and time of the event. It has two fields, **Date** and **Time**. The **Date** field should be given in DD/MM/YYYY format, while the **Time** field should be given in HH:MM:SS format. This information is crucial for users to know when the event is taking place. ### Description -**Description:** A brief description of the event. This should provide users with more information about what to expect from the event and why they should attend, or alternatively what happened at the event if it is a past event. +A brief description of the event. This should provide users with more information about what to expect from the event and why they should attend, or alternatively what happened at the event if it is a past event. ### Publication Date -Required +**Required** -**Publication Date:** The date when the event was published on the frontend. This is important for users to know when the event was announced and can help them plan accordingly. +The date when the event was published on the frontend. This is important for users to know when the event was announced and can help them plan accordingly. ### Cover Image -Required +**Required** -**Cover Image:** An image that represents the event. This should be visually appealing and relevant to the event to attract users' attention. It will be displayed on the frontend alongside the event details. +An image that represents the event. This should be visually appealing and relevant to the event to attract users' attention. It will be displayed on the frontend alongside the event details. ### Location -Required +**Required** -**Location:** The location of the event. This can be a physical location or an online platform where the event will take place. Providing this information is essential for users to know where they need to go to attend the event. +The location of the event. This can be a physical location or an online platform where the event will take place. Providing this information is essential for users to know where they need to go to attend the event. ## Members @@ -56,6 +56,26 @@ Required ## Game Contributors +### Game + +**Required** + +Search for a game that is already in the database and select it. If the game is not in the database, you will need to add it first before you can add a contributor to it. + +### Member + +**Required** + +Search for a member that is already in the database and select them. If the member is not in the database, you will need to add them first before you can add them as a contributor to a game. + +### Role + +**Required** + +The description of the role that the member played in the development of the game. This should be a brief description that gives users an idea of what the member contributed to the game. + +E.g. "Lead Developer", "Artist", "Composer", etc. + ## Games ## Art From 7e445c9eaa5f86ef35dcc406716e2e018618bea2 Mon Sep 17 00:00:00 2001 From: Tuan Khanh Hoang Date: Sat, 14 Feb 2026 08:01:41 +0000 Subject: [PATCH 085/122] refactor: Art class for consistency with Game team issue #81 --- server/game_dev/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/game_dev/models.py b/server/game_dev/models.py index 4872091f..ce88f8d3 100644 --- a/server/game_dev/models.py +++ b/server/game_dev/models.py @@ -79,7 +79,7 @@ def __str__(self): class Art(models.Model): name = models.CharField(null=False, max_length=200) description = models.CharField(max_length=200,) - source_game = models.ForeignKey(Game, on_delete=models.CASCADE, related_name='art_pieces', null=True, blank=True) + source_game = models.ForeignKey('Game', on_delete=models.CASCADE, related_name='game_artwork') media = models.ImageField(upload_to='art/', null=False) active = models.BooleanField(default=True) From cfe92cb06e80f1b954927a6d5fcae548b33bb5b6 Mon Sep 17 00:00:00 2001 From: Games4Doritos Date: Wed, 18 Feb 2026 21:42:36 +0800 Subject: [PATCH 086/122] Reworked how pronouns and names are arranged -Rather than name and pronouns being part of the same rectangle, they are their own independent blocks that will wrap if appropriate - Added placeholder text for committee portrait pronouns, where it will say "No Pronouns" if the pronouns attribute is blank. We can definitely change if needed --- client/src/pages/about.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/client/src/pages/about.tsx b/client/src/pages/about.tsx index fee93f7e..7fc3ce29 100644 --- a/client/src/pages/about.tsx +++ b/client/src/pages/about.tsx @@ -84,9 +84,9 @@ export default function AboutPage() { )}
-
-

- +

+

+ {committeeMember.pk === 0 ? ( <>{committeeMember.name} ) : ( @@ -95,9 +95,13 @@ export default function AboutPage() { )} - {committeeMember.pronouns} + + {committeeMember.pronouns === "" + ? "No Pronouns" + : committeeMember.pronouns} +

-

+

{roleOrder[id]}

From ab66a47a4c301747d1323cb45cf0aeec062fe1da Mon Sep 17 00:00:00 2001 From: Games4Doritos Date: Thu, 19 Feb 2026 17:19:15 +0800 Subject: [PATCH 087/122] Undid ternary logic for pronouns - Removed ternary logic for pronouns which showed a placeholder of "No Pronouns" if a committee member's pronouns was empty. This was simply replaced with empty:hidden, so the pronoun element just won't display if they're empty --- client/src/pages/about.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/client/src/pages/about.tsx b/client/src/pages/about.tsx index 7fc3ce29..92753d80 100644 --- a/client/src/pages/about.tsx +++ b/client/src/pages/about.tsx @@ -95,10 +95,8 @@ export default function AboutPage() { )} - - {committeeMember.pronouns === "" - ? "No Pronouns" - : committeeMember.pronouns} + + {committeeMember.pronouns}

From 60308808d0a3f9452f15170335f96dc3acf72b70 Mon Sep 17 00:00:00 2001 From: samjjacko Date: Sat, 21 Feb 2026 09:15:12 +0800 Subject: [PATCH 088/122] changed the python version constraints in the pyproject.toml to avoid using 3.14, which is incompatible with django 5.1.15 --- server/poetry.lock | 6 +++--- server/pyproject.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/server/poetry.lock b/server/poetry.lock index 34f6c925..1dc664f7 100644 --- a/server/poetry.lock +++ b/server/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. [[package]] name = "asgiref" @@ -586,5 +586,5 @@ files = [ [metadata] lock-version = "2.1" -python-versions = "^3.12" -content-hash = "9576347c536499de99b323235e5722ecff72a250598b689f042441da6d57411c" +python-versions = "^3.12 <3.14" +content-hash = "34c827f5703228d41f7b807ccc2b5445c3a1dfc907729ba4c288a345201709b2" diff --git a/server/pyproject.toml b/server/pyproject.toml index 356eb1e2..25d0d82d 100644 --- a/server/pyproject.toml +++ b/server/pyproject.toml @@ -7,7 +7,7 @@ readme = "README.md" package-mode = false [tool.poetry.dependencies] -python = "^3.12" +python = "^3.12 <3.14" Django = "^5.1" djangorestframework = "^3.15.1" django-cors-headers = "^4.3.1" From a38fe17d8be28f097561905a61c079f49634aa22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CKushPatel-18=E2=80=9D?= <“kushpatel.personal@gmail.comgit config --global user.email “kushpatel.personal@gmail.com> Date: Fri, 20 Feb 2026 13:30:11 +0000 Subject: [PATCH 089/122] linked landing page buttons to game and art showcase respectively --- client/src/pages/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/pages/index.tsx b/client/src/pages/index.tsx index 2cab849f..1fa92fad 100644 --- a/client/src/pages/index.tsx +++ b/client/src/pages/index.tsx @@ -180,10 +180,10 @@ export default function Landing() {

- + - + From c8854b293c662a7d4fdc330f27e72a0768349ec3 Mon Sep 17 00:00:00 2001 From: belledw Date: Sat, 14 Feb 2026 13:55:44 +0800 Subject: [PATCH 090/122] Added member profile documentation --- client/documentation/admin-dashboard/members.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 client/documentation/admin-dashboard/members.md diff --git a/client/documentation/admin-dashboard/members.md b/client/documentation/admin-dashboard/members.md new file mode 100644 index 00000000..bc960abd --- /dev/null +++ b/client/documentation/admin-dashboard/members.md @@ -0,0 +1,15 @@ +## Member Profiles + +Profiles of club members can be added and edited at the row 'Member' of the GAME_DEV section on the main admin page. + +### Fields + +**Name:** Required field for the member's name. A character field (includes letters, numbers and symbols) of maximum length 200 characters. + +**Active:** Checkbox to represent whether a member is an active participant in the club. If the checkbox is not ticked then the member's profile will not be displayed on the website. + +**Profile Picture:** Optional field to upload a profile picture. Must be an image file, and will display best if the image is at least 128 by 128 px in size. If no profile picture is provided then the member's initials will be displayed instead. + +**About:** Optional field for a bio. A character field of maximum length 256 characters. + +**Pronouns:** Optional field for the member's pronouns. A character field of maximum length 20 characters. From ba34f30a96e8b3e834b32e08a7ace0fab8842d85 Mon Sep 17 00:00:00 2001 From: belledw Date: Fri, 20 Feb 2026 02:04:39 +0000 Subject: [PATCH 091/122] Added social media link information to the member profile documentation --- client/documentation/admin-dashboard/members.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/documentation/admin-dashboard/members.md b/client/documentation/admin-dashboard/members.md index bc960abd..f8fde6ad 100644 --- a/client/documentation/admin-dashboard/members.md +++ b/client/documentation/admin-dashboard/members.md @@ -13,3 +13,5 @@ Profiles of club members can be added and edited at the row 'Member' of the GAME **About:** Optional field for a bio. A character field of maximum length 256 characters. **Pronouns:** Optional field for the member's pronouns. A character field of maximum length 20 characters. + +**Social media links:** Optional section to display links to the member's social media profiles. Requires a link to the profile (character field of maximum length 2083) and, optionally, the profile username (character field of maximum length 200). If a username is not supplied then only a social media icon will be displayed with the link attached, otherwise the username will be placed next to the relevant icon. The type of icon to be displayed (e.g. instagram, linkedin, generic link) is inferred from the social media link provided. From 7952656f99b2549c3b111ff466e0fb19e73a3c18 Mon Sep 17 00:00:00 2001 From: saltyypringle Date: Wed, 28 Jan 2026 21:14:33 +0800 Subject: [PATCH 092/122] 404 page stuffs --- client/src/pages/404.tsx | 703 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 703 insertions(+) create mode 100644 client/src/pages/404.tsx diff --git a/client/src/pages/404.tsx b/client/src/pages/404.tsx new file mode 100644 index 00000000..ff717ce6 --- /dev/null +++ b/client/src/pages/404.tsx @@ -0,0 +1,703 @@ +"use client"; + +import Link from "next/link"; +import { useEffect,useState } from "react"; + +import { Button } from "@/components/ui/button"; + +interface Trivia { + question: string; + answer: string; +} + +const TRIVIA: Trivia[] = [ + { + question: + "What 1997 N64 video game, widely cited as one of the greatest of all time, features James Bond up against a criminal syndicate and is named after the 1995 film in the Bond franchise?", + answer: "GoldenEye", + }, + { + question: + 'Originally given the Japanese title "Puckman," what 1980s arcade game was inducted into the Guinness Book of Records as the "Most Successful Coin-Operated Game" in 2005?', + answer: "Pac-Man", + }, + { + question: + "In July 2023, video game company EA announced that players will be able to explore Wakanda in an upcoming open-world video game based on the adventures of what Marvel superhero?", + answer: "Black Panther", + }, + { + question: + "Which spooky 2001 GameCube game starring Mario's brother got a reboot for Nintendo Switch in 2019?", + answer: "Luigi's Mansion", + }, + { + question: + 'In May 2022, the government of what European nation banned its employees from using American gaming terms such as "e-sports," instead using their domestic language counterparts?', + answer: "France", + }, + { + question: + "In 2011, the World Health Organization included VGA, an addiction to what activity, among its mental health disorders for the first time?", + answer: "Video Games", + }, + { + question: + "What Will Wright created video game series, released in 2000 (with sequels in 2004, 2009, and 2014), saw players watching and directing characters to mundane things like eating, sleeping, and cleaning their houses?", + answer: "The Sims", + }, + { + question: + "Regarded as one of the greatest video games of all time, what 1981 arcade game features the titular amphibian trying to cross a road and a river?", + answer: "Frogger", + }, + { + question: + "What is thought to be the first video game, created in 1958 and becoming popular in the 1970s?", + answer: "Pong", + }, + { + question: + "Which video game console released in 2006 pioneered the use of motion controls in its gameplay?", + answer: "Nintendo Wii", + }, + { + question: + 'Making his debut in 1990s "Super Mario World," what is the name of the enemy-eating, egg-throwing green dinosaur who serves as a sidekick to Mario and Luigi?', + answer: "Yoshi", + }, + { + question: + "In 2014, Google partnered with Game Freak and Nintendo as part of an April Fool's Day prank to create a new version of Google Maps. This prank inspired what massively popular 2016 video game?", + answer: "Pokemon Go", + }, + { + question: + "What video game came with the Nintendo Entertainment System when it was released in the late 1980s, and was meant to be played with the NES Zapper gun?", + answer: "Duck Hunt", + }, + { + question: + "Introduced on Wii consoles, Nintendo gamers can make their own in-game character by creating an avatar known by what three-letter name that sounds similar to a pronoun?", + answer: "Mii", + }, + { + question: + 'What word completes the title of the 2017 game "Super Mario" what, for the Nintendo Switch? The word in question is also a vehicle manufactured by Honda.', + answer: "Odyssey", + }, + { + question: + 'What Konami game from September 1998 was initially released to the European arcade audience under the name "Dancing Stage?"', + answer: "Dance Dance Revolution", + }, + { + question: + "In Mario Kart, the power-up that seeks out the player in first position and explodes on impact is a shell that is what color?", + answer: "Blue", + }, + { + question: + "The company that created Fortnite, EPIC, also created a game engine that is licensed to other game creators, named what?", + answer: "Unreal", + }, + { + question: + "Smoke on the Water is a fictional medical-marijuana shop that can be purchased by Franklin with money in what heist-y video game franchise?", + answer: "Grand Theft Auto", + }, + { + question: + "Which PlayStation platformer released in 1996 has you play as the titular character—a marsupial prone to mayhem who was captured by Dr. Neo Cortex?", + answer: "Crash Bandicoot", + }, + { + question: + "The Warthog is the nickname of the M12 Force Application Light Reconnaissance Vehicle, a fictional armored vehicle that appears in what video game series?", + answer: "Halo", + }, + { + question: + "Pocket, Light, Color, and Advance were all styles or variants of what video game hardware system?", + answer: "Game Boy", + }, + { + question: + "Released in 2004 by Blizzard Entertainment and set in the fictional universe of Azeroth, what is the name of the computer game that became the world's most popular MMORPG?", + answer: "World of Warcraft", + }, + { + question: + "What object does Mario typically leap onto after completing a level in the earliest iterations of his franchise?", + answer: "Flag pole", + }, + { + question: + "What third-person shooter video game developed by Nintendo was first released in 2015 and features characters known as inklings?", + answer: "Splatoon", + }, + { + question: + "In Super Mario Kart, the first game in Nintendo's racing franchise, which of the playable characters has the shortest name?", + answer: "Toad", + }, + { + question: + "When the kids online say \"LoL,\" they're either laughing or referencing what online battle arena game that's been sponsored by Mastercard since 2018?", + answer: "League of Legends", + }, + { + question: + "What 2009 game, developed by Mojang, is an open sandbox in which players often build structures and battle creepers and zombies?", + answer: "Minecraft", + }, + { + question: + 'A 2017 Guerrilla Games game, published on the PlayStation 4, that features Aloy battling giant machines with her bow, is what "Zero Dawn"?', + answer: "Horizon Zero Dawn", + }, + { + question: + "Harry must collect treasures including gold and diamonds without landing in quicksand in what classic 1982 Atari game with an exclamation point in the title?", + answer: "Pitfall!", + }, + { + question: + "What word—which shares its name with a popular soda brand—describes a 2D bitmap image, such as a video game character, that's integrated into a larger scene?", + answer: "Sprite", + }, + { + question: + "In what franchise-launching 1985 educational video game was a user required to have a warrant for each arrest while traveling to locales like Oslo and Cairo?", + answer: "Where in the World Is Carmen San Diego?", + }, + { + question: + 'An egg-shaped wind instrument dating back to ancient times appears in the title of what 1998 installment in the "Legend of Zelda" franchise?', + answer: "Ocarina of Time", + }, + { + question: + "2021 saw the release of what sixth game in the Halo franchise, continuing the adventures of Master Chief? Its name sounds as if the game's story will continue in perpetuity.", + answer: "Halo Infinite", + }, + { + question: + 'Mendicant Bias and Offensive Bias are fictional AIs in what "holy" video game franchise that shares its name with a Beyonce song?', + answer: "Halo", + }, + { + question: + "What 2018 video games are set in 1899 and follow the story of outlaws Arthur Morgan and John Marston?", + answer: "Red Dead Redemption 2", + }, + { + question: + 'Used while playing "Contra," the original Konami code gave you 30 extra of what video game things?', + answer: "Extra Lives", + }, + { + question: + 'What card game related to the "Warcraft" universe did Blizzard release in 2014?', + answer: "Hearthstone", + }, + { + question: + "A sleek black convertible known as the Regalia is the car Noctis and his friends use to travel across Eos in the 15th installment of what alliterative video game franchise?", + answer: "Final Fantasy", + }, + { + question: + 'Dressed in purple and black with an upside-down "L" on his cap, what skinny and mustachioed character made his debut in the 2000 Nintendo 64 game, "Mario Tennis?"', + answer: "Waluigi", + }, + { + question: + 'Although early versions of the game featured a character named "Ivan the Space Biker," the game\'s maker (Valve) eventually settled on "Gordon Freeman" as the hero. What was the game?', + answer: "Half-Life", + }, + { + question: + "What video game character is described as a young, energetic, violet creature with orange medium-sized wings, large curved horns, and a spiral-shaped spike on his tail?", + answer: "Spyro the Dragon", + }, + { + question: + "What name is shared by a sci-fi video game franchise, a Beyonce song, and the tiara worn by Kate Middleton on her wedding day?", + answer: "Halo", + }, + { + question: + "In the timeless Oregon Trail video games, you were often given three options to get across rivers: caulk and float, take a ferry, and what four-letter third choice?", + answer: "Ford", + }, + { + question: + "What Star Wars console video game released at the end of 2020 focuses on space combat inspired by the movie franchise?", + answer: "Star Wars: Squadrons", + }, + { + question: + "What 2021 installment in the Call of Duty video game franchise shares its name with one of America's largest investment management firms?", + answer: "Vanguard", + }, + { + question: + 'What fantasy kingdom is the main setting for the "Legend of Zelda" video game series?', + answer: "Hyrule", + }, + { + question: + "Chuck E. Cheese was originally founded by Nolan Bushnell, who also co-founded what video game company known for its 2600?", + answer: "Atari", + }, + { + question: + 'The third entry in an extremely popular post-apocalyptic video game franchise was set in an area known as "Capital Wasteland," the ruins of Washington, DC. What is the name of this franchise?', + answer: "Fallout", + }, + { + question: + 'In 2008, the open world racing game was pioneered with the release of what "Paradise"?', + answer: "Burnout Paradise", + }, + { + question: + "Imane Anys, whose millions of followers love to watch her play League of Legends and Fortnite, is better known by what name?", + answer: "Pokimane", + }, + { + question: + "Sun, Moon, Diamond, Pearl, and SoulSilver have all been names of games in what iconic video game franchise?", + answer: "Pokemon", + }, + { + question: + "Crash is a video game character who is a genetically mutated type of what marsupial?", + answer: "Bandicoot", + }, + { + question: + "What first-person shooter video game developed by Valve and published for Microsoft Windows in 1998 launched a globally successful franchise?", + answer: "Half-Life", + }, + { + question: + "The first Star Wars video game, made for the Atari 2600, was based on which film in the original trilogy?", + answer: "The Empire Strikes Back", + }, + { + question: + "What is the name of the best-selling video game franchise to come out of Disney's home-grown intellectual property?", + answer: "Kingdom Hearts", + }, + { + question: + "In the original 1980 edition of Pac-Man, the four ghosts were named Blinky, Inky, Pinky, and what name that doesn't rhyme with the rest?", + answer: "Clyde", + }, + { + question: + "What simulation video game franchise was originally developed by Will Wright and launched in 1989 for the Macintosh computer?", + answer: "SimCity", + }, + { + question: + 'Tingle is a "short, paunchy 35-year-old" obsessed with "forest fairies." In what video game franchise did Tingle debut?', + answer: "The Legend of Zelda", + }, + { + question: + '"Ultimate" and "Melee" are two of the iterations in the Super Smash Bros. franchise. What is the one additional word that follows the name of a title in the series?', + answer: "Brawl", + }, + { + question: + "Tony Hawk is one of the world's most skilled skateboarders. He is also the face of one of the best-selling video games of the late '90s entitled \"Tony Hawk's\" what, released September 29, 1999?", + answer: "Pro Skater", + }, + { + question: + "The first game in the Final Fantasy video game franchise was released for what console?", + answer: "Nintendo Entertainment System (NES)", + }, + { + question: + "What is the name of the twin brother of Solid Snake, the protagonist of the Metal Gear franchise?", + answer: "Liquid Snake", + }, + { + question: + "In January 2021, a short squeeze orchestrated by Reddit users caused a skyrocketing of the price of what retail chain that sells video games and consumer electronics?", + answer: "GameStop", + }, + { + question: + 'The 1995 point-and-click adventure game "I Have No Mouth, and I Must Scream" is based on the short story of the same name by what sci-fi author?', + answer: "Harlan Ellison", + }, + { + question: + 'A 2022 Lego Star Wars game that lets players reenact all 9 mainline Star Wars films is "Lego Star Wars:" The what Saga?', + answer: "Skywalker", + }, + { + question: + "Skyrim is the fifth installment of what epic open-world videogame series by Bethesda Softworks?", + answer: "The Elder Scrolls", + }, + { + question: + 'What "S" videogame series co-created and published by Electronic Arts allows users to create and customize virtual human beings?', + answer: "The Sims", + }, + { + question: + "Which Legend of Zelda game that picks up after Ocarina of Time was released for N64 in 2000 and remade for Nintendo 3DS in 2015?", + answer: "Majora's Mask", + }, + { + question: + "What video game franchise debuted in 2001 as a hybrid real-time strategy and puzzle video game centered on part-collecting for a crashed rocket ship?", + answer: "Pikmin", + }, + { + question: + 'What was the name of the franchise of educational video games from the 1990s that featured a green protagonist and titles like "In Search of Spot"?', + answer: "Math Blaster!", + }, + { + question: + 'What video game franchise technically included "Dr. Kawashima" in the title? The first installment debuted in 2005 on the Nintendo DS.', + answer: "Brain Age", + }, + { + question: + "What is the name of the series of Star Wars video games that began on the Nintendo 64 console in 1998?", + answer: "Rogue Squadron", + }, + { + question: + "What classic open-ended PC game of 1993 may have been inspired by a Jules Verne novel whose characters were marooned on an island?", + answer: "Myst", + }, + { + question: + 'According to Apple, the second most popular free game downloaded on iPhones in 2018 was an "endless play style" game where you try to get a ball down platforms. What is the game?', + answer: "Helix Jump", + }, + { + question: + "What is the name of the largest body of water on the Fortnite Battle Royale map?", + answer: "Loot Lake", + }, + { + question: + "What Pokémon holds the title as the first listed creature in the Pokédex and is considered a hybrid grass-poison type?", + answer: "Bulbasaur", + }, + { + question: + "What regulatory group assigns content ratings and suggested age ratings for video games? (4-letter initialism)", + answer: "ESRB", + }, + { + question: + "Blathers is the name of the nocturnal, museum-curating owl in what series of Nintendo video games?", + answer: "Animal Crossing", + }, + { + question: + "In what video game universe, created by Capcom, would you find a character named Jill Valentine?", + answer: "Resident Evil", + }, + { + question: + "What 2021 game in the Metroid franchise, released on the Nintendo Switch, features Samus Aran investigating a mysterious transmission on the planet ZDR?", + answer: "Metroid Dread", + }, + { + question: + "The website Ranker named GLaDOS, a fictional artificially intelligent computer system, the greatest video game villain of all time. GLaDOS was introduced in what groundbreaking computer game?", + answer: "Portal", + }, + { + question: + '"Korobeiniki," a folk song about a peddler and a girl haggling, is best known outside Russia as the theme music for what video game?', + answer: "Tetris", + }, + { + question: + 'Air Man, Cut Man, Ring Man, and Drill Man are all villains in what "M.M." video game franchise?', + answer: "Mega Man", + }, + { + question: + "Larry, Morton, Wendy, Iggy, Roy, Lemmy, and Ludwig are all video game villains that report to which young commander?", + answer: "Bowser Jr", + }, + { + question: + 'A reference to its popular Angry Birds franchise, what Finnish video game company sometimes uses the slogan "Angry since 2009?"', + answer: "Rovio", + }, + { + question: + "It's one of the longest-running series in video game history. The four ghosts in Pac-Man are called Inky, Blinky, Pinky, and what name that breaks the pattern?", + answer: "Clyde", + }, + { + question: + "The very first game in the Madden NFL video game franchise was named John Madden Football and was released June 1st in what year?", + answer: "1988", + }, + { + question: + "What gothic video game franchise debuted in 1986 with Simon Belmont as protagonist, a member of the Belmont clan of vampire hunters?", + answer: "Castlevania", + }, + { + question: + 'What was the "metallic" golf video game played with a trackball that was popularized in bars across America?', + answer: "Golden Tee", + }, + { + question: + "What beat-em-up video game franchise, featuring twin brother martial artists Billy and Jimmy, was later turned into a poorly received 1994 movie?", + answer: "Double Dragon", + }, + { + question: + "According to market research company NPD Group, which video game console sold the most units in the United States in 2008?", + answer: "Nintendo Wii", + }, + { + question: + 'What popular mobile puzzle game involves the collection of characters who are described as "friends without the R"?', + answer: "Best Fiends", + }, + { + question: + "Which side-scrolling platformer by Ubisoft debuted in 1995 and tasked players with navigating levels like The Dream Forest?", + answer: "Rayman", + }, + { + question: + 'In Mario\'s first appearance in the video game "Donkey Kong", what J-word was his official name before later transitioning to Mario?', + answer: "Jumpman", + }, + { + question: + "What is the name of the talking animatronic toy that resembled a bear and reached peak popularity in the mid-1980s?", + answer: "Teddy Ruxpin", + }, + { + question: + 'The sci-fi novel "Ready Player One" features what 1979 Atari 2600 game in the book\'s final challenge?', + answer: "Adventure", + }, + { + question: + 'What was the name of the princess Mario rescues in Nintendo Gameboy\'s "Super Mario Land" (1989)?', + answer: "Daisy", + }, + { + question: + '"Dachshund & Friends," "Lab & Friends," and "Chihuahua & Friends" are the three versions of the 2005 U.S. release of what video game?', + answer: "Nintendogs", + }, +]; + +export default function Custom404() { + const [gameQuestions, setGameQuestions] = useState([]); + const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0); + const [score, setScore] = useState(0); + const [gameActive, setGameActive] = useState(false); + const [timeLeft, setTimeLeft] = useState(30); + const [answered, setAnswered] = useState(false); + const [selectedAnswer, setSelectedAnswer] = useState(null); + const [options, setOptions] = useState([]); + + const currentTrivia = gameQuestions[currentQuestionIndex]; + + const generateQuestions = () => { + const shuffled = TRIVIA.sort(() => Math.random() - 0.5).slice(0, 10); + setGameQuestions(shuffled); + }; + + useEffect(() => { + if (!currentTrivia) return; + + const wrongAnswers = TRIVIA.filter((t) => t.answer !== currentTrivia.answer) + .sort(() => Math.random() - 0.5) + .slice(0, 3) + .map((t) => t.answer); + + const allOptions = [currentTrivia.answer, ...wrongAnswers].sort( + () => Math.random() - 0.5, + ); + setOptions(allOptions); + }, [currentQuestionIndex, currentTrivia]); + + useEffect(() => { + if (!gameActive || timeLeft <= 0) return; + + const timer = setInterval(() => { + setTimeLeft((prev) => { + if (prev <= 1) { + setGameActive(false); + return 0; + } + return prev - 1; + }); + }, 1000); + + return () => clearInterval(timer); + }, [gameActive, timeLeft]); + + const startGame = () => { + generateQuestions(); + setGameActive(true); + setCurrentQuestionIndex(0); + setScore(0); + setTimeLeft(30); + setAnswered(false); + setSelectedAnswer(null); + }; + + const handleAnswer = (answer: string) => { + if (answered) return; + + setSelectedAnswer(answer); + setAnswered(true); + + if (answer === currentTrivia.answer) { + setScore((prev) => prev + 1); + } + + setTimeout(() => { + if (currentQuestionIndex < gameQuestions.length - 1) { + setCurrentQuestionIndex((prev) => prev + 1); + setAnswered(false); + setSelectedAnswer(null); + } else { + setGameActive(false); + } + }, 1000); + }; + + const getButtonClass = (option: string) => { + const baseClass = + "w-full p-3 text-left rounded border transition-all cursor-pointer"; + + if (!answered) { + return `${baseClass} bg-card border-border text-foreground hover:border-accent`; + } + + if (option === currentTrivia.answer) { + return `${baseClass} bg-accent border-accent text-accent-foreground font-semibold`; + } + + if (option === selectedAnswer && option !== currentTrivia.answer) { + return `${baseClass} bg-secondary border-secondary text-secondary-foreground`; + } + + return `${baseClass} bg-muted border-border text-muted-foreground`; + }; + + return ( +
+
+

+ 404 +

+

+ Page Not Found +

+ +

+ Test your game knowledge with quick rapid-fire instead!!! +

+ + {!gameActive ? ( +
+ {gameQuestions.length === 0 ? ( + <> +

+ Answer 10 random gaming trivia questions in 30 seconds! +

+ + + ) : ( + <> +

+ Game Over! +

+

+ Final Score:{" "} + + {score} + {" "} + / 10 +

+ + + )} +
+ ) : ( +
+
+

+ Question {currentQuestionIndex + 1} / 10 +

+
+ {timeLeft}s +
+
+ +
+

+ {currentTrivia.question} +

+ +
+ {options.map((option, idx) => ( + + ))} +
+ +

+ Score:{" "} + + {score} + {" "} + / 10 +

+
+
+ )} + + + + +
+
+ ); +} From 21ec32fdb3597a531cda72e59ca4de50dbb74d47 Mon Sep 17 00:00:00 2001 From: saltyypringle Date: Sat, 31 Jan 2026 09:52:31 +0800 Subject: [PATCH 093/122] fixed small styling --- client/src/pages/404.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/client/src/pages/404.tsx b/client/src/pages/404.tsx index ff717ce6..51ed8ada 100644 --- a/client/src/pages/404.tsx +++ b/client/src/pages/404.tsx @@ -1,7 +1,7 @@ "use client"; import Link from "next/link"; -import { useEffect,useState } from "react"; +import { useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; @@ -684,9 +684,8 @@ export default function Custom404() {

Score:{" "} - {score} - {" "} - / 10 + {score} / 10 +

From 5b81c472bd17c2297399fef670f6ba01f15e828f Mon Sep 17 00:00:00 2001 From: saltyypringle Date: Sat, 31 Jan 2026 12:11:09 +0800 Subject: [PATCH 094/122] typo --- client/src/pages/404.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/pages/404.tsx b/client/src/pages/404.tsx index 51ed8ada..bedf1ec5 100644 --- a/client/src/pages/404.tsx +++ b/client/src/pages/404.tsx @@ -616,7 +616,7 @@ export default function Custom404() {

- Test your game knowledge with quick rapid-fire instead!!! + Test your game knowledge with some rapid-fire trivia instead!!!

{!gameActive ? ( From 981ffc8551a86cd5965f42acbcc849c87df237da Mon Sep 17 00:00:00 2001 From: saltyypringle Date: Sat, 7 Feb 2026 14:47:12 +0800 Subject: [PATCH 095/122] moved the trivia to a json file --- client/src/pages/404.tsx | 499 +-------------------------------------- client/src/trivia.json | 398 +++++++++++++++++++++++++++++++ 2 files changed, 399 insertions(+), 498 deletions(-) create mode 100644 client/src/trivia.json diff --git a/client/src/pages/404.tsx b/client/src/pages/404.tsx index bedf1ec5..76a904aa 100644 --- a/client/src/pages/404.tsx +++ b/client/src/pages/404.tsx @@ -4,510 +4,13 @@ import Link from "next/link"; import { useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; +import TRIVIA from "@/trivia.json"; interface Trivia { question: string; answer: string; } -const TRIVIA: Trivia[] = [ - { - question: - "What 1997 N64 video game, widely cited as one of the greatest of all time, features James Bond up against a criminal syndicate and is named after the 1995 film in the Bond franchise?", - answer: "GoldenEye", - }, - { - question: - 'Originally given the Japanese title "Puckman," what 1980s arcade game was inducted into the Guinness Book of Records as the "Most Successful Coin-Operated Game" in 2005?', - answer: "Pac-Man", - }, - { - question: - "In July 2023, video game company EA announced that players will be able to explore Wakanda in an upcoming open-world video game based on the adventures of what Marvel superhero?", - answer: "Black Panther", - }, - { - question: - "Which spooky 2001 GameCube game starring Mario's brother got a reboot for Nintendo Switch in 2019?", - answer: "Luigi's Mansion", - }, - { - question: - 'In May 2022, the government of what European nation banned its employees from using American gaming terms such as "e-sports," instead using their domestic language counterparts?', - answer: "France", - }, - { - question: - "In 2011, the World Health Organization included VGA, an addiction to what activity, among its mental health disorders for the first time?", - answer: "Video Games", - }, - { - question: - "What Will Wright created video game series, released in 2000 (with sequels in 2004, 2009, and 2014), saw players watching and directing characters to mundane things like eating, sleeping, and cleaning their houses?", - answer: "The Sims", - }, - { - question: - "Regarded as one of the greatest video games of all time, what 1981 arcade game features the titular amphibian trying to cross a road and a river?", - answer: "Frogger", - }, - { - question: - "What is thought to be the first video game, created in 1958 and becoming popular in the 1970s?", - answer: "Pong", - }, - { - question: - "Which video game console released in 2006 pioneered the use of motion controls in its gameplay?", - answer: "Nintendo Wii", - }, - { - question: - 'Making his debut in 1990s "Super Mario World," what is the name of the enemy-eating, egg-throwing green dinosaur who serves as a sidekick to Mario and Luigi?', - answer: "Yoshi", - }, - { - question: - "In 2014, Google partnered with Game Freak and Nintendo as part of an April Fool's Day prank to create a new version of Google Maps. This prank inspired what massively popular 2016 video game?", - answer: "Pokemon Go", - }, - { - question: - "What video game came with the Nintendo Entertainment System when it was released in the late 1980s, and was meant to be played with the NES Zapper gun?", - answer: "Duck Hunt", - }, - { - question: - "Introduced on Wii consoles, Nintendo gamers can make their own in-game character by creating an avatar known by what three-letter name that sounds similar to a pronoun?", - answer: "Mii", - }, - { - question: - 'What word completes the title of the 2017 game "Super Mario" what, for the Nintendo Switch? The word in question is also a vehicle manufactured by Honda.', - answer: "Odyssey", - }, - { - question: - 'What Konami game from September 1998 was initially released to the European arcade audience under the name "Dancing Stage?"', - answer: "Dance Dance Revolution", - }, - { - question: - "In Mario Kart, the power-up that seeks out the player in first position and explodes on impact is a shell that is what color?", - answer: "Blue", - }, - { - question: - "The company that created Fortnite, EPIC, also created a game engine that is licensed to other game creators, named what?", - answer: "Unreal", - }, - { - question: - "Smoke on the Water is a fictional medical-marijuana shop that can be purchased by Franklin with money in what heist-y video game franchise?", - answer: "Grand Theft Auto", - }, - { - question: - "Which PlayStation platformer released in 1996 has you play as the titular character—a marsupial prone to mayhem who was captured by Dr. Neo Cortex?", - answer: "Crash Bandicoot", - }, - { - question: - "The Warthog is the nickname of the M12 Force Application Light Reconnaissance Vehicle, a fictional armored vehicle that appears in what video game series?", - answer: "Halo", - }, - { - question: - "Pocket, Light, Color, and Advance were all styles or variants of what video game hardware system?", - answer: "Game Boy", - }, - { - question: - "Released in 2004 by Blizzard Entertainment and set in the fictional universe of Azeroth, what is the name of the computer game that became the world's most popular MMORPG?", - answer: "World of Warcraft", - }, - { - question: - "What object does Mario typically leap onto after completing a level in the earliest iterations of his franchise?", - answer: "Flag pole", - }, - { - question: - "What third-person shooter video game developed by Nintendo was first released in 2015 and features characters known as inklings?", - answer: "Splatoon", - }, - { - question: - "In Super Mario Kart, the first game in Nintendo's racing franchise, which of the playable characters has the shortest name?", - answer: "Toad", - }, - { - question: - "When the kids online say \"LoL,\" they're either laughing or referencing what online battle arena game that's been sponsored by Mastercard since 2018?", - answer: "League of Legends", - }, - { - question: - "What 2009 game, developed by Mojang, is an open sandbox in which players often build structures and battle creepers and zombies?", - answer: "Minecraft", - }, - { - question: - 'A 2017 Guerrilla Games game, published on the PlayStation 4, that features Aloy battling giant machines with her bow, is what "Zero Dawn"?', - answer: "Horizon Zero Dawn", - }, - { - question: - "Harry must collect treasures including gold and diamonds without landing in quicksand in what classic 1982 Atari game with an exclamation point in the title?", - answer: "Pitfall!", - }, - { - question: - "What word—which shares its name with a popular soda brand—describes a 2D bitmap image, such as a video game character, that's integrated into a larger scene?", - answer: "Sprite", - }, - { - question: - "In what franchise-launching 1985 educational video game was a user required to have a warrant for each arrest while traveling to locales like Oslo and Cairo?", - answer: "Where in the World Is Carmen San Diego?", - }, - { - question: - 'An egg-shaped wind instrument dating back to ancient times appears in the title of what 1998 installment in the "Legend of Zelda" franchise?', - answer: "Ocarina of Time", - }, - { - question: - "2021 saw the release of what sixth game in the Halo franchise, continuing the adventures of Master Chief? Its name sounds as if the game's story will continue in perpetuity.", - answer: "Halo Infinite", - }, - { - question: - 'Mendicant Bias and Offensive Bias are fictional AIs in what "holy" video game franchise that shares its name with a Beyonce song?', - answer: "Halo", - }, - { - question: - "What 2018 video games are set in 1899 and follow the story of outlaws Arthur Morgan and John Marston?", - answer: "Red Dead Redemption 2", - }, - { - question: - 'Used while playing "Contra," the original Konami code gave you 30 extra of what video game things?', - answer: "Extra Lives", - }, - { - question: - 'What card game related to the "Warcraft" universe did Blizzard release in 2014?', - answer: "Hearthstone", - }, - { - question: - "A sleek black convertible known as the Regalia is the car Noctis and his friends use to travel across Eos in the 15th installment of what alliterative video game franchise?", - answer: "Final Fantasy", - }, - { - question: - 'Dressed in purple and black with an upside-down "L" on his cap, what skinny and mustachioed character made his debut in the 2000 Nintendo 64 game, "Mario Tennis?"', - answer: "Waluigi", - }, - { - question: - 'Although early versions of the game featured a character named "Ivan the Space Biker," the game\'s maker (Valve) eventually settled on "Gordon Freeman" as the hero. What was the game?', - answer: "Half-Life", - }, - { - question: - "What video game character is described as a young, energetic, violet creature with orange medium-sized wings, large curved horns, and a spiral-shaped spike on his tail?", - answer: "Spyro the Dragon", - }, - { - question: - "What name is shared by a sci-fi video game franchise, a Beyonce song, and the tiara worn by Kate Middleton on her wedding day?", - answer: "Halo", - }, - { - question: - "In the timeless Oregon Trail video games, you were often given three options to get across rivers: caulk and float, take a ferry, and what four-letter third choice?", - answer: "Ford", - }, - { - question: - "What Star Wars console video game released at the end of 2020 focuses on space combat inspired by the movie franchise?", - answer: "Star Wars: Squadrons", - }, - { - question: - "What 2021 installment in the Call of Duty video game franchise shares its name with one of America's largest investment management firms?", - answer: "Vanguard", - }, - { - question: - 'What fantasy kingdom is the main setting for the "Legend of Zelda" video game series?', - answer: "Hyrule", - }, - { - question: - "Chuck E. Cheese was originally founded by Nolan Bushnell, who also co-founded what video game company known for its 2600?", - answer: "Atari", - }, - { - question: - 'The third entry in an extremely popular post-apocalyptic video game franchise was set in an area known as "Capital Wasteland," the ruins of Washington, DC. What is the name of this franchise?', - answer: "Fallout", - }, - { - question: - 'In 2008, the open world racing game was pioneered with the release of what "Paradise"?', - answer: "Burnout Paradise", - }, - { - question: - "Imane Anys, whose millions of followers love to watch her play League of Legends and Fortnite, is better known by what name?", - answer: "Pokimane", - }, - { - question: - "Sun, Moon, Diamond, Pearl, and SoulSilver have all been names of games in what iconic video game franchise?", - answer: "Pokemon", - }, - { - question: - "Crash is a video game character who is a genetically mutated type of what marsupial?", - answer: "Bandicoot", - }, - { - question: - "What first-person shooter video game developed by Valve and published for Microsoft Windows in 1998 launched a globally successful franchise?", - answer: "Half-Life", - }, - { - question: - "The first Star Wars video game, made for the Atari 2600, was based on which film in the original trilogy?", - answer: "The Empire Strikes Back", - }, - { - question: - "What is the name of the best-selling video game franchise to come out of Disney's home-grown intellectual property?", - answer: "Kingdom Hearts", - }, - { - question: - "In the original 1980 edition of Pac-Man, the four ghosts were named Blinky, Inky, Pinky, and what name that doesn't rhyme with the rest?", - answer: "Clyde", - }, - { - question: - "What simulation video game franchise was originally developed by Will Wright and launched in 1989 for the Macintosh computer?", - answer: "SimCity", - }, - { - question: - 'Tingle is a "short, paunchy 35-year-old" obsessed with "forest fairies." In what video game franchise did Tingle debut?', - answer: "The Legend of Zelda", - }, - { - question: - '"Ultimate" and "Melee" are two of the iterations in the Super Smash Bros. franchise. What is the one additional word that follows the name of a title in the series?', - answer: "Brawl", - }, - { - question: - "Tony Hawk is one of the world's most skilled skateboarders. He is also the face of one of the best-selling video games of the late '90s entitled \"Tony Hawk's\" what, released September 29, 1999?", - answer: "Pro Skater", - }, - { - question: - "The first game in the Final Fantasy video game franchise was released for what console?", - answer: "Nintendo Entertainment System (NES)", - }, - { - question: - "What is the name of the twin brother of Solid Snake, the protagonist of the Metal Gear franchise?", - answer: "Liquid Snake", - }, - { - question: - "In January 2021, a short squeeze orchestrated by Reddit users caused a skyrocketing of the price of what retail chain that sells video games and consumer electronics?", - answer: "GameStop", - }, - { - question: - 'The 1995 point-and-click adventure game "I Have No Mouth, and I Must Scream" is based on the short story of the same name by what sci-fi author?', - answer: "Harlan Ellison", - }, - { - question: - 'A 2022 Lego Star Wars game that lets players reenact all 9 mainline Star Wars films is "Lego Star Wars:" The what Saga?', - answer: "Skywalker", - }, - { - question: - "Skyrim is the fifth installment of what epic open-world videogame series by Bethesda Softworks?", - answer: "The Elder Scrolls", - }, - { - question: - 'What "S" videogame series co-created and published by Electronic Arts allows users to create and customize virtual human beings?', - answer: "The Sims", - }, - { - question: - "Which Legend of Zelda game that picks up after Ocarina of Time was released for N64 in 2000 and remade for Nintendo 3DS in 2015?", - answer: "Majora's Mask", - }, - { - question: - "What video game franchise debuted in 2001 as a hybrid real-time strategy and puzzle video game centered on part-collecting for a crashed rocket ship?", - answer: "Pikmin", - }, - { - question: - 'What was the name of the franchise of educational video games from the 1990s that featured a green protagonist and titles like "In Search of Spot"?', - answer: "Math Blaster!", - }, - { - question: - 'What video game franchise technically included "Dr. Kawashima" in the title? The first installment debuted in 2005 on the Nintendo DS.', - answer: "Brain Age", - }, - { - question: - "What is the name of the series of Star Wars video games that began on the Nintendo 64 console in 1998?", - answer: "Rogue Squadron", - }, - { - question: - "What classic open-ended PC game of 1993 may have been inspired by a Jules Verne novel whose characters were marooned on an island?", - answer: "Myst", - }, - { - question: - 'According to Apple, the second most popular free game downloaded on iPhones in 2018 was an "endless play style" game where you try to get a ball down platforms. What is the game?', - answer: "Helix Jump", - }, - { - question: - "What is the name of the largest body of water on the Fortnite Battle Royale map?", - answer: "Loot Lake", - }, - { - question: - "What Pokémon holds the title as the first listed creature in the Pokédex and is considered a hybrid grass-poison type?", - answer: "Bulbasaur", - }, - { - question: - "What regulatory group assigns content ratings and suggested age ratings for video games? (4-letter initialism)", - answer: "ESRB", - }, - { - question: - "Blathers is the name of the nocturnal, museum-curating owl in what series of Nintendo video games?", - answer: "Animal Crossing", - }, - { - question: - "In what video game universe, created by Capcom, would you find a character named Jill Valentine?", - answer: "Resident Evil", - }, - { - question: - "What 2021 game in the Metroid franchise, released on the Nintendo Switch, features Samus Aran investigating a mysterious transmission on the planet ZDR?", - answer: "Metroid Dread", - }, - { - question: - "The website Ranker named GLaDOS, a fictional artificially intelligent computer system, the greatest video game villain of all time. GLaDOS was introduced in what groundbreaking computer game?", - answer: "Portal", - }, - { - question: - '"Korobeiniki," a folk song about a peddler and a girl haggling, is best known outside Russia as the theme music for what video game?', - answer: "Tetris", - }, - { - question: - 'Air Man, Cut Man, Ring Man, and Drill Man are all villains in what "M.M." video game franchise?', - answer: "Mega Man", - }, - { - question: - "Larry, Morton, Wendy, Iggy, Roy, Lemmy, and Ludwig are all video game villains that report to which young commander?", - answer: "Bowser Jr", - }, - { - question: - 'A reference to its popular Angry Birds franchise, what Finnish video game company sometimes uses the slogan "Angry since 2009?"', - answer: "Rovio", - }, - { - question: - "It's one of the longest-running series in video game history. The four ghosts in Pac-Man are called Inky, Blinky, Pinky, and what name that breaks the pattern?", - answer: "Clyde", - }, - { - question: - "The very first game in the Madden NFL video game franchise was named John Madden Football and was released June 1st in what year?", - answer: "1988", - }, - { - question: - "What gothic video game franchise debuted in 1986 with Simon Belmont as protagonist, a member of the Belmont clan of vampire hunters?", - answer: "Castlevania", - }, - { - question: - 'What was the "metallic" golf video game played with a trackball that was popularized in bars across America?', - answer: "Golden Tee", - }, - { - question: - "What beat-em-up video game franchise, featuring twin brother martial artists Billy and Jimmy, was later turned into a poorly received 1994 movie?", - answer: "Double Dragon", - }, - { - question: - "According to market research company NPD Group, which video game console sold the most units in the United States in 2008?", - answer: "Nintendo Wii", - }, - { - question: - 'What popular mobile puzzle game involves the collection of characters who are described as "friends without the R"?', - answer: "Best Fiends", - }, - { - question: - "Which side-scrolling platformer by Ubisoft debuted in 1995 and tasked players with navigating levels like The Dream Forest?", - answer: "Rayman", - }, - { - question: - 'In Mario\'s first appearance in the video game "Donkey Kong", what J-word was his official name before later transitioning to Mario?', - answer: "Jumpman", - }, - { - question: - "What is the name of the talking animatronic toy that resembled a bear and reached peak popularity in the mid-1980s?", - answer: "Teddy Ruxpin", - }, - { - question: - 'The sci-fi novel "Ready Player One" features what 1979 Atari 2600 game in the book\'s final challenge?', - answer: "Adventure", - }, - { - question: - 'What was the name of the princess Mario rescues in Nintendo Gameboy\'s "Super Mario Land" (1989)?', - answer: "Daisy", - }, - { - question: - '"Dachshund & Friends," "Lab & Friends," and "Chihuahua & Friends" are the three versions of the 2005 U.S. release of what video game?', - answer: "Nintendogs", - }, -]; - export default function Custom404() { const [gameQuestions, setGameQuestions] = useState([]); const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0); diff --git a/client/src/trivia.json b/client/src/trivia.json new file mode 100644 index 00000000..7d3a8d21 --- /dev/null +++ b/client/src/trivia.json @@ -0,0 +1,398 @@ +[ + { + "question": "What 1997 N64 video game, widely cited as one of the greatest of all time, features James Bond up against a criminal syndicate and is named after the 1995 film in the Bond franchise?", + "answer": "GoldenEye" + }, + { + "question": "Originally given the Japanese title \"Puckman,\" what 1980s arcade game was inducted into the Guinness Book of Records as the \"Most Successful Coin-Operated Game\" in 2005?", + "answer": "Pac-Man" + }, + { + "question": "In July 2023, video game company EA announced that players will be able to explore Wakanda in an upcoming open-world video game based on the adventures of what Marvel superhero?", + "answer": "Black Panther" + }, + { + "question": "Which spooky 2001 GameCube game starring Mario's brother got a reboot for Nintendo Switch in 2019?", + "answer": "Luigi's Mansion" + }, + { + "question": "In May 2022, the government of what European nation banned its employees from using American gaming terms such as \"e-sports,\" instead using their domestic language counterparts?", + "answer": "France" + }, + { + "question": "In 2011, the World Health Organization included VGA, an addiction to what activity, among its mental health disorders for the first time?", + "answer": "Video Games" + }, + { + "question": "What Will Wright created video game series, released in 2000 (with sequels in 2004, 2009, and 2014), saw players watching and directing characters to mundane things like eating, sleeping, and cleaning their houses?", + "answer": "The Sims" + }, + { + "question": "Regarded as one of the greatest video games of all time, what 1981 arcade game features the titular amphibian trying to cross a road and a river?", + "answer": "Frogger" + }, + { + "question": "What is thought to be the first video game, created in 1958 and becoming popular in the 1970s?", + "answer": "Pong" + }, + { + "question": "Which video game console released in 2006 pioneered the use of motion controls in its gameplay?", + "answer": "Nintendo Wii" + }, + { + "question": "Making his debut in 1990s \"Super Mario World,\" what is the name of the enemy-eating, egg-throwing green dinosaur who serves as a sidekick to Mario and Luigi?", + "answer": "Yoshi" + }, + { + "question": "In 2014, Google partnered with Game Freak and Nintendo as part of an April Fool's Day prank to create a new version of Google Maps. This prank inspired what massively popular 2016 video game?", + "answer": "Pokemon Go" + }, + { + "question": "What video game came with the Nintendo Entertainment System when it was released in the late 1980s, and was meant to be played with the NES Zapper gun?", + "answer": "Duck Hunt" + }, + { + "question": "Introduced on Wii consoles, Nintendo gamers can make their own in-game character by creating an avatar known by what three-letter name that sounds similar to a pronoun?", + "answer": "Mii" + }, + { + "question": "What word completes the title of the 2017 game \"Super Mario\" what, for the Nintendo Switch? The word in question is also a vehicle manufactured by Honda.", + "answer": "Odyssey" + }, + { + "question": "What Konami game from September 1998 was initially released to the European arcade audience under the name \"Dancing Stage?\"", + "answer": "Dance Dance Revolution" + }, + { + "question": "In Mario Kart, the power-up that seeks out the player in first position and explodes on impact is a shell that is what color?", + "answer": "Blue" + }, + { + "question": "The company that created Fortnite, EPIC, also created a game engine that is licensed to other game creators, named what?", + "answer": "Unreal" + }, + { + "question": "Smoke on the Water is a fictional medical-marijuana shop that can be purchased by Franklin with money in what heist-y video game franchise?", + "answer": "Grand Theft Auto" + }, + { + "question": "Which PlayStation platformer released in 1996 has you play as the titular character—a marsupial prone to mayhem who was captured by Dr. Neo Cortex?", + "answer": "Crash Bandicoot" + }, + { + "question": "The Warthog is the nickname of the M12 Force Application Light Reconnaissance Vehicle, a fictional armored vehicle that appears in what video game series?", + "answer": "Halo" + }, + { + "question": "Pocket, Light, Color, and Advance were all styles or variants of what video game hardware system?", + "answer": "Game Boy" + }, + { + "question": "Released in 2004 by Blizzard Entertainment and set in the fictional universe of Azeroth, what is the name of the computer game that became the world's most popular MMORPG?", + "answer": "World of Warcraft" + }, + { + "question": "What object does Mario typically leap onto after completing a level in the earliest iterations of his franchise?", + "answer": "Flag pole" + }, + { + "question": "What third-person shooter video game developed by Nintendo was first released in 2015 and features characters known as inklings?", + "answer": "Splatoon" + }, + { + "question": "In Super Mario Kart, the first game in Nintendo's racing franchise, which of the playable characters has the shortest name?", + "answer": "Toad" + }, + { + "question": "When the kids online say \"LoL,\" they're either laughing or referencing what online battle arena game that's been sponsored by Mastercard since 2018?", + "answer": "League of Legends" + }, + { + "question": "What 2009 game, developed by Mojang, is an open sandbox in which players often build structures and battle creepers and zombies?", + "answer": "Minecraft" + }, + { + "question": "A 2017 Guerrilla Games game, published on the PlayStation 4, that features Aloy battling giant machines with her bow, is what \"Zero Dawn\"?", + "answer": "Horizon Zero Dawn" + }, + { + "question": "Harry must collect treasures including gold and diamonds without landing in quicksand in what classic 1982 Atari game with an exclamation point in the title?", + "answer": "Pitfall!" + }, + { + "question": "What word—which shares its name with a popular soda brand—describes a 2D bitmap image, such as a video game character, that's integrated into a larger scene?", + "answer": "Sprite" + }, + { + "question": "In what franchise-launching 1985 educational video game was a user required to have a warrant for each arrest while traveling to locales like Oslo and Cairo?", + "answer": "Where in the World Is Carmen San Diego?" + }, + { + "question": "An egg-shaped wind instrument dating back to ancient times appears in the title of what 1998 installment in the \"Legend of Zelda\" franchise?", + "answer": "Ocarina of Time" + }, + { + "question": "2021 saw the release of what sixth game in the Halo franchise, continuing the adventures of Master Chief? Its name sounds as if the game's story will continue in perpetuity.", + "answer": "Halo Infinite" + }, + { + "question": "Mendicant Bias and Offensive Bias are fictional AIs in what \"holy\" video game franchise that shares its name with a Beyonce song?", + "answer": "Halo" + }, + { + "question": "What 2018 video games are set in 1899 and follow the story of outlaws Arthur Morgan and John Marston?", + "answer": "Red Dead Redemption 2" + }, + { + "question": "Used while playing \"Contra,\" the original Konami code gave you 30 extra of what video game things?", + "answer": "Extra Lives" + }, + { + "question": "What card game related to the \"Warcraft\" universe did Blizzard release in 2014?", + "answer": "Hearthstone" + }, + { + "question": "A sleek black convertible known as the Regalia is the car Noctis and his friends use to travel across Eos in the 15th installment of what alliterative video game franchise?", + "answer": "Final Fantasy" + }, + { + "question": "Dressed in purple and black with an upside-down \"L\" on his cap, what skinny and mustachioed character made his debut in the 2000 Nintendo 64 game, \"Mario Tennis?\"", + "answer": "Waluigi" + }, + { + "question": "Although early versions of the game featured a character named \"Ivan the Space Biker,\" the game's maker (Valve) eventually settled on \"Gordon Freeman\" as the hero. What was the game?", + "answer": "Half-Life" + }, + { + "question": "What video game character is described as a young, energetic, violet creature with orange medium-sized wings, large curved horns, and a spiral-shaped spike on his tail?", + "answer": "Spyro the Dragon" + }, + { + "question": "What name is shared by a sci-fi video game franchise, a Beyonce song, and the tiara worn by Kate Middleton on her wedding day?", + "answer": "Halo" + }, + { + "question": "In the timeless Oregon Trail video games, you were often given three options to get across rivers: caulk and float, take a ferry, and what four-letter third choice?", + "answer": "Ford" + }, + { + "question": "What Star Wars console video game released at the end of 2020 focuses on space combat inspired by the movie franchise?", + "answer": "Star Wars: Squadrons" + }, + { + "question": "What 2021 installment in the Call of Duty video game franchise shares its name with one of America's largest investment management firms?", + "answer": "Vanguard" + }, + { + "question": "What fantasy kingdom is the main setting for the \"Legend of Zelda\" video game series?", + "answer": "Hyrule" + }, + { + "question": "Chuck E. Cheese was originally founded by Nolan Bushnell, who also co-founded what video game company known for its 2600?", + "answer": "Atari" + }, + { + "question": "The third entry in an extremely popular post-apocalyptic video game franchise was set in an area known as \"Capital Wasteland,\" the ruins of Washington, DC. What is the name of this franchise?", + "answer": "Fallout" + }, + { + "question": "In 2008, the open world racing game was pioneered with the release of what \"Paradise\"?", + "answer": "Burnout Paradise" + }, + { + "question": "Imane Anys, whose millions of followers love to watch her play League of Legends and Fortnite, is better known by what name?", + "answer": "Pokimane" + }, + { + "question": "Sun, Moon, Diamond, Pearl, and SoulSilver have all been names of games in what iconic video game franchise?", + "answer": "Pokemon" + }, + { + "question": "Crash is a video game character who is a genetically mutated type of what marsupial?", + "answer": "Bandicoot" + }, + { + "question": "What first-person shooter video game developed by Valve and published for Microsoft Windows in 1998 launched a globally successful franchise?", + "answer": "Half-Life" + }, + { + "question": "The first Star Wars video game, made for the Atari 2600, was based on which film in the original trilogy?", + "answer": "The Empire Strikes Back" + }, + { + "question": "What is the name of the best-selling video game franchise to come out of Disney's home-grown intellectual property?", + "answer": "Kingdom Hearts" + }, + { + "question": "In the original 1980 edition of Pac-Man, the four ghosts were named Blinky, Inky, Pinky, and what name that doesn't rhyme with the rest?", + "answer": "Clyde" + }, + { + "question": "What simulation video game franchise was originally developed by Will Wright and launched in 1989 for the Macintosh computer?", + "answer": "SimCity" + }, + { + "question": "Tingle is a \"short, paunchy 35-year-old\" obsessed with \"forest fairies.\" In what video game franchise did Tingle debut?", + "answer": "The Legend of Zelda" + }, + { + "question": "\"Ultimate\" and \"Melee\" are two of the iterations in the Super Smash Bros. franchise. What is the one additional word that follows the name of a title in the series?", + "answer": "Brawl" + }, + { + "question": "Tony Hawk is one of the world's most skilled skateboarders. He is also the face of one of the best-selling video games of the late '90s entitled \"Tony Hawk's\" what, released September 29, 1999?", + "answer": "Pro Skater" + }, + { + "question": "The first game in the Final Fantasy video game franchise was released for what console?", + "answer": "Nintendo Entertainment System (NES)" + }, + { + "question": "What is the name of the twin brother of Solid Snake, the protagonist of the Metal Gear franchise?", + "answer": "Liquid Snake" + }, + { + "question": "In January 2021, a short squeeze orchestrated by Reddit users caused a skyrocketing of the price of what retail chain that sells video games and consumer electronics?", + "answer": "GameStop" + }, + { + "question": "The 1995 point-and-click adventure game \"I Have No Mouth, and I Must Scream\" is based on the short story of the same name by what sci-fi author?", + "answer": "Harlan Ellison" + }, + { + "question": "A 2022 Lego Star Wars game that lets players reenact all 9 mainline Star Wars films is \"Lego Star Wars:\" The what Saga?", + "answer": "Skywalker" + }, + { + "question": "Skyrim is the fifth installment of what epic open-world videogame series by Bethesda Softworks?", + "answer": "The Elder Scrolls" + }, + { + "question": "What \"S\" videogame series co-created and published by Electronic Arts allows users to create and customize virtual human beings?", + "answer": "The Sims" + }, + { + "question": "Which Legend of Zelda game that picks up after Ocarina of Time was released for N64 in 2000 and remade for Nintendo 3DS in 2015?", + "answer": "Majora's Mask" + }, + { + "question": "What video game franchise debuted in 2001 as a hybrid real-time strategy and puzzle video game centered on part-collecting for a crashed rocket ship?", + "answer": "Pikmin" + }, + { + "question": "What was the name of the franchise of educational video games from the 1990s that featured a green protagonist and titles like \"In Search of Spot\"?", + "answer": "Math Blaster!" + }, + { + "question": "What video game franchise technically included \"Dr. Kawashima\" in the title? The first installment debuted in 2005 on the Nintendo DS.", + "answer": "Brain Age" + }, + { + "question": "What is the name of the series of Star Wars video games that began on the Nintendo 64 console in 1998?", + "answer": "Rogue Squadron" + }, + { + "question": "What classic open-ended PC game of 1993 may have been inspired by a Jules Verne novel whose characters were marooned on an island?", + "answer": "Myst" + }, + { + "question": "According to Apple, the second most popular free game downloaded on iPhones in 2018 was an \"endless play style\" game where you try to get a ball down platforms. What is the game?", + "answer": "Helix Jump" + }, + { + "question": "What is the name of the largest body of water on the Fortnite Battle Royale map?", + "answer": "Loot Lake" + }, + { + "question": "What Pokémon holds the title as the first listed creature in the Pokédex and is considered a hybrid grass-poison type?", + "answer": "Bulbasaur" + }, + { + "question": "What regulatory group assigns content ratings and suggested age ratings for video games? (4-letter initialism)", + "answer": "ESRB" + }, + { + "question": "Blathers is the name of the nocturnal, museum-curating owl in what series of Nintendo video games?", + "answer": "Animal Crossing" + }, + { + "question": "In what video game universe, created by Capcom, would you find a character named Jill Valentine?", + "answer": "Resident Evil" + }, + { + "question": "What 2021 game in the Metroid franchise, released on the Nintendo Switch, features Samus Aran investigating a mysterious transmission on the planet ZDR?", + "answer": "Metroid Dread" + }, + { + "question": "The website Ranker named GLaDOS, a fictional artificially intelligent computer system, the greatest video game villain of all time. GLaDOS was introduced in what groundbreaking computer game?", + "answer": "Portal" + }, + { + "question": "\"Korobeiniki,\" a folk song about a peddler and a girl haggling, is best known outside Russia as the theme music for what video game?", + "answer": "Tetris" + }, + { + "question": "Air Man, Cut Man, Ring Man, and Drill Man are all villains in what \"M.M.\" video game franchise?", + "answer": "Mega Man" + }, + { + "question": "Larry, Morton, Wendy, Iggy, Roy, Lemmy, and Ludwig are all video game villains that report to which young commander?", + "answer": "Bowser Jr" + }, + { + "question": "A reference to its popular Angry Birds franchise, what Finnish video game company sometimes uses the slogan \"Angry since 2009?\"", + "answer": "Rovio" + }, + { + "question": "It's one of the longest-running series in video game history. The four ghosts in Pac-Man are called Inky, Blinky, Pinky, and what name that breaks the pattern?", + "answer": "Clyde" + }, + { + "question": "The very first game in the Madden NFL video game franchise was named John Madden Football and was released June 1st in what year?", + "answer": "1988" + }, + { + "question": "What gothic video game franchise debuted in 1986 with Simon Belmont as protagonist, a member of the Belmont clan of vampire hunters?", + "answer": "Castlevania" + }, + { + "question": "What was the \"metallic\" golf video game played with a trackball that was popularized in bars across America?", + "answer": "Golden Tee" + }, + { + "question": "What beat-em-up video game franchise, featuring twin brother martial artists Billy and Jimmy, was later turned into a poorly received 1994 movie?", + "answer": "Double Dragon" + }, + { + "question": "According to market research company NPD Group, which video game console sold the most units in the United States in 2008?", + "answer": "Nintendo Wii" + }, + { + "question": "What popular mobile puzzle game involves the collection of characters who are described as \"friends without the R\"?", + "answer": "Best Fiends" + }, + { + "question": "Which side-scrolling platformer by Ubisoft debuted in 1995 and tasked players with navigating levels like The Dream Forest?", + "answer": "Rayman" + }, + { + "question": "In Mario's first appearance in the video game \"Donkey Kong\", what J-word was his official name before later transitioning to Mario?", + "answer": "Jumpman" + }, + { + "question": "What is the name of the talking animatronic toy that resembled a bear and reached peak popularity in the mid-1980s?", + "answer": "Teddy Ruxpin" + }, + { + "question": "The sci-fi novel \"Ready Player One\" features what 1979 Atari 2600 game in the book's final challenge?", + "answer": "Adventure" + }, + { + "question": "What was the name of the princess Mario rescues in Nintendo Gameboy's \"Super Mario Land\" (1989)?", + "answer": "Daisy" + }, + { + "question": "\"Dachshund & Friends,\" \"Lab & Friends,\" and \"Chihuahua & Friends\" are the three versions of the 2005 U.S. release of what video game?", + "answer": "Nintendogs" + } +] \ No newline at end of file From f9ede4f5e77f6c1c24a0263f0cd73aaf37c53f6f Mon Sep 17 00:00:00 2001 From: saltyypringle Date: Sat, 7 Feb 2026 14:59:52 +0800 Subject: [PATCH 096/122] prettier --- client/src/trivia.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/trivia.json b/client/src/trivia.json index 7d3a8d21..865c30e8 100644 --- a/client/src/trivia.json +++ b/client/src/trivia.json @@ -395,4 +395,4 @@ "question": "\"Dachshund & Friends,\" \"Lab & Friends,\" and \"Chihuahua & Friends\" are the three versions of the 2005 U.S. release of what video game?", "answer": "Nintendogs" } -] \ No newline at end of file +] From efdee097762d0a93f2fa72952787d6d873f270bb Mon Sep 17 00:00:00 2001 From: dipika Date: Sat, 7 Feb 2026 15:00:31 +0800 Subject: [PATCH 097/122] Update client/src/pages/404.tsx Co-authored-by: James Lee <82937700+SafetyInObscurity@users.noreply.github.com> --- client/src/pages/404.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/pages/404.tsx b/client/src/pages/404.tsx index 76a904aa..7069232b 100644 --- a/client/src/pages/404.tsx +++ b/client/src/pages/404.tsx @@ -139,7 +139,7 @@ export default function Custom404() { Game Over!

- Final Score:{" "} + Final Score: {score} {" "} From da1caa55c08ccacb659ef5c79d041cfbee02c6f6 Mon Sep 17 00:00:00 2001 From: dipika Date: Sat, 21 Feb 2026 08:11:34 +0800 Subject: [PATCH 098/122] Update client/src/pages/404.tsx Co-authored-by: Sam Jackson <114900452+samjjacko@users.noreply.github.com> --- client/src/pages/404.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/pages/404.tsx b/client/src/pages/404.tsx index 7069232b..b7bd7a1f 100644 --- a/client/src/pages/404.tsx +++ b/client/src/pages/404.tsx @@ -98,11 +98,11 @@ export default function Custom404() { } if (option === currentTrivia.answer) { - return `${baseClass} bg-accent border-accent text-accent-foreground font-semibold`; + return `${baseClass} bg-primary border-accent text-accent-foreground font-semibold`; } if (option === selectedAnswer && option !== currentTrivia.answer) { - return `${baseClass} bg-secondary border-secondary text-secondary-foreground`; + return `${baseClass} bg-accent border-secondary text-secondary-foreground`; } return `${baseClass} bg-muted border-border text-muted-foreground`; From 544b666e7ff252f442dec79595205a7e37e31a52 Mon Sep 17 00:00:00 2001 From: saltyypringle Date: Sat, 21 Feb 2026 10:25:14 +0800 Subject: [PATCH 099/122] fixed styling for mobile view --- client/src/pages/404.tsx | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/client/src/pages/404.tsx b/client/src/pages/404.tsx index b7bd7a1f..728cc1ce 100644 --- a/client/src/pages/404.tsx +++ b/client/src/pages/404.tsx @@ -91,7 +91,7 @@ export default function Custom404() { const getButtonClass = (option: string) => { const baseClass = - "w-full p-3 text-left rounded border transition-all cursor-pointer"; + "w-full rounded border p-2.5 text-left text-sm transition-all cursor-pointer md:p-3 md:text-base"; if (!answered) { return `${baseClass} bg-card border-border text-foreground hover:border-accent`; @@ -109,24 +109,24 @@ export default function Custom404() { }; return ( -

+
-

+

404

-

+

Page Not Found

-

+

Test your game knowledge with some rapid-fire trivia instead!!!

{!gameActive ? ( -
+
{gameQuestions.length === 0 ? ( <> -

+

Answer 10 random gaming trivia questions in 30 seconds!

) : ( -
-
-

+

+
+

Question {currentQuestionIndex + 1} / 10

@@ -166,12 +166,12 @@ export default function Custom404() {
-
-

+

+

{currentTrivia.question}

-
+
{options.map((option, idx) => (
); } diff --git a/client/src/pages/artwork/index.tsx b/client/src/pages/artwork/index.tsx index d27a188c..b7125dbe 100644 --- a/client/src/pages/artwork/index.tsx +++ b/client/src/pages/artwork/index.tsx @@ -51,10 +51,10 @@ function renderArtworkCard(artwork: Art) { backContent={
-

+

{artwork.name}

-

+

{artwork.source_game_name ? ( <> from{" "} @@ -70,21 +70,21 @@ function renderArtworkCard(artwork: Art) { "No associated game" )}

-

+

{artwork.description || "No description available."}

{artwork.contributors.length > 0 && (
-

+

Contributors

{artwork.contributors.map((contributor) => (
e.stopPropagation()} > VIEW FULL DETAILS @@ -125,7 +125,7 @@ export default function ArtworksPage({ artworks, error }: ArtworksPageProps) {

FEATURED

diff --git a/client/tailwind.config.ts b/client/tailwind.config.ts index 7698c77a..52306b8e 100644 --- a/client/tailwind.config.ts +++ b/client/tailwind.config.ts @@ -21,7 +21,7 @@ const config = { extend: { fontFamily: { sans: ["var(--font-sans)", ...fontFamily.sans], - jersey10: ["Jersey 10", ...fontFamily.sans], + jersey10: ["var(--font-jersey10)", ...fontFamily.sans], firaCode: ["var(--font-firaCode)", ...fontFamily.sans], }, @@ -70,12 +70,6 @@ const config = { neutral_4: "var(--neutral-4)", light_1: "var(--light-1)", light_2: "var(--light-2)", - light_3: "var(--light-3)", - light_alt: "var(--light-alt)", - light_alt_2: "var(--light-alt-2)", - logo_blue_2: "var(--logo-blue-2)", - logo_blue_1: "var(--logo-blue-1)", - error: "var(--error)", }, borderRadius: { lg: "var(--radius)", diff --git a/server/game_dev/serializers.py b/server/game_dev/serializers.py index 8cb8ae39..7b82a2da 100644 --- a/server/game_dev/serializers.py +++ b/server/game_dev/serializers.py @@ -82,6 +82,31 @@ def get_contributors(self, obj): return ShowcaseContributorSerializer(contributors, many=True).data +class SocialMediaSerializer(serializers.ModelSerializer): + class Meta: + model = SocialMedia + fields = [ + "link", + "socialMediaUserName", + ] + + +class MemberSerializer(serializers.ModelSerializer): + social_media = SocialMediaSerializer( + many=True, source="social_media_links", read_only=True) + + class Meta: + model = Member + fields = [ + "name", + "profile_picture", + "about", + "pronouns", + "social_media", + "pk" + ] + + class ArtContributorSerializer(serializers.ModelSerializer): member_id = serializers.IntegerField(source='member.id', read_only=True) member_name = serializers.CharField(source='member.name', read_only=True) @@ -113,28 +138,3 @@ class ArtShowcaseSerializer(serializers.ModelSerializer): class Meta: model = ArtShowcase fields = ['id', 'description', 'art', 'art_name'] - - -class SocialMediaSerializer(serializers.ModelSerializer): - class Meta: - model = SocialMedia - fields = [ - "link", - "socialMediaUserName", - ] - - -class MemberSerializer(serializers.ModelSerializer): - social_media = SocialMediaSerializer( - many=True, source="social_media_links", read_only=True) - - class Meta: - model = Member - fields = [ - "name", - "profile_picture", - "about", - "pronouns", - "social_media", - "pk" - ] From 25c468ffc14952f2278678e9b204a9c2e036e9ef Mon Sep 17 00:00:00 2001 From: Katariah Date: Sat, 21 Feb 2026 07:24:10 +0000 Subject: [PATCH 101/122] still needs a description potentially --- client/documentation/admin-dashboard/events.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 client/documentation/admin-dashboard/events.md diff --git a/client/documentation/admin-dashboard/events.md b/client/documentation/admin-dashboard/events.md new file mode 100644 index 00000000..0e87dca0 --- /dev/null +++ b/client/documentation/admin-dashboard/events.md @@ -0,0 +1,15 @@ +# Events + +## Fields + +**Name:** Required field for the event's name. A character field (includes letters, numbers and symbols) of maximum length 200 characters. + +**Date:** Required field for the event date and time. Must be a valid date and time. Uses either ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ) or American format (MM-DD-YYYY HH:MM) for date and time representation. + +**Description:** Optional field for a description of the event. A character field of maximum length 256 characters. + +**Publication Date:** Required field for the date the event is published. Must be a valid date (DD/MM/YYYY). Uses either ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ) or American format (MM-DD-YYYY HH:MM) for date and time representation. + +**Cover Image:** Optional field to upload a cover image for the event. Must be an image file. If no cover image is provided, a default club logo will be displayed instead. + +**Location:** Required field for the event location. A character field of maximum length 256 characters. From 98880aa3bf4be1c4a576aa4859d489de46552dd2 Mon Sep 17 00:00:00 2001 From: Tuan Khanh Hoang Date: Sat, 21 Feb 2026 07:29:32 +0000 Subject: [PATCH 102/122] fix: remove unnecessary test --- server/game_dev/tests.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/server/game_dev/tests.py b/server/game_dev/tests.py index 34993c67..7b4b6bf8 100644 --- a/server/game_dev/tests.py +++ b/server/game_dev/tests.py @@ -237,20 +237,6 @@ def test_source_game_relationship(self): art = Art.objects.get(pk=self.art.pk) self.assertEqual(art.source_game, self.game) - def test_art_without_source_game(self): - # Test that art can be created without a source game - image_file = SimpleUploadedFile( - "test_art_no_game.jpg", - b"dummy art image data", - content_type="image/jpeg", - ) - art_no_game = Art.objects.create( - name="Independent Artwork", - description="Art with no game", - media=image_file, - ) - self.assertIsNone(art_no_game.source_game) - def test_cascade_from_game(self): # When game is deleted, art should remain (SET_NULL behavior would be ideal, but currently CASCADE) art_id = self.art.id From 876d7cdd838c8d84f241d690e4abae1a2d71570d Mon Sep 17 00:00:00 2001 From: Tuan Khanh Hoang Date: Sat, 21 Feb 2026 07:35:59 +0000 Subject: [PATCH 103/122] fix: formatting --- client/src/components/ui/GoBackButton.tsx | 2 +- client/src/components/ui/modal/error-modal.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/components/ui/GoBackButton.tsx b/client/src/components/ui/GoBackButton.tsx index 53ca3501..64e114ae 100644 --- a/client/src/components/ui/GoBackButton.tsx +++ b/client/src/components/ui/GoBackButton.tsx @@ -9,7 +9,7 @@ const GoBackButton = ({ url, label }: GoBackButtonProps) => { return ( diff --git a/client/src/components/ui/image-placeholder.tsx b/client/src/components/ui/image-placeholder.tsx deleted file mode 100644 index c694f378..00000000 --- a/client/src/components/ui/image-placeholder.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import Image from "next/image"; -import React from "react"; - -const ImagePlaceholder = () => { - return ( -
-
- Placeholder icon -
-
- ); -}; -export default ImagePlaceholder; diff --git a/client/src/pages/artwork/[id].tsx b/client/src/pages/artwork/[id].tsx index 3baad5e8..dd1ab090 100644 --- a/client/src/pages/artwork/[id].tsx +++ b/client/src/pages/artwork/[id].tsx @@ -4,7 +4,6 @@ import { useRouter } from "next/router"; import ContributorsList from "@/components/ui/ContributorsList"; import GoBackButton from "@/components/ui/GoBackButton"; -import ImagePlaceholder from "@/components/ui/image-placeholder"; import ErrorModal from "@/components/ui/modal/error-modal"; import api from "@/lib/api"; import { Art } from "@/types/art"; @@ -55,19 +54,15 @@ export default function ArtworkPage({ artwork, error }: ArtworkPageProps) { data-layer="Artwork Image Panel" className="ArtworkImagePanel relative flex content-center justify-center" > - {artwork.media ? ( - Artwork image - ) : ( - - )} + Artwork image
Date: Sun, 1 Mar 2026 16:16:58 +0800 Subject: [PATCH 108/122] simplify individual art page and contributor list --- client/src/components/ui/ContributorsList.tsx | 55 +++----- client/src/components/ui/GoBackButton.tsx | 24 ---- client/src/components/ui/button.tsx | 1 + client/src/pages/artwork/[id].tsx | 120 ++++-------------- 4 files changed, 45 insertions(+), 155 deletions(-) delete mode 100644 client/src/components/ui/GoBackButton.tsx diff --git a/client/src/components/ui/ContributorsList.tsx b/client/src/components/ui/ContributorsList.tsx index 64e3b899..9c08c57f 100644 --- a/client/src/components/ui/ContributorsList.tsx +++ b/client/src/components/ui/ContributorsList.tsx @@ -14,46 +14,23 @@ export default function ContributorsList({ } return ( -
-
-
- Contributors -
-
-
-
-
- {contributors.map((contributor) => ( -
- e.stopPropagation()} - > - {contributor.member_name} - - {" - "} - {contributor.role} -
- ))} + <> +
Contributors
+
+ {contributors.map((contributor) => ( +
+ e.stopPropagation()} + > + {contributor.member_name} + + {" - "} + {contributor.role}
-
+ ))}
-
+ ); } diff --git a/client/src/components/ui/GoBackButton.tsx b/client/src/components/ui/GoBackButton.tsx deleted file mode 100644 index 92a2b060..00000000 --- a/client/src/components/ui/GoBackButton.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { ArrowLeft } from "lucide-react"; -import Link from "next/link"; - -interface GoBackButtonProps { - url: string; - label: string; -} -const GoBackButton = ({ url, label }: GoBackButtonProps) => { - return ( - - - - ); -}; - -export default GoBackButton; diff --git a/client/src/components/ui/button.tsx b/client/src/components/ui/button.tsx index 2e263a18..74eb69d8 100644 --- a/client/src/components/ui/button.tsx +++ b/client/src/components/ui/button.tsx @@ -24,6 +24,7 @@ const buttonVariants = cva( sm: "h-9 rounded-md px-3", lg: "h-11 rounded-md px-8", icon: "h-10 w-10", + leftIcon: "h-10 ps-2 pe-4 py-2 flex gap-2", }, }, defaultVariants: { diff --git a/client/src/pages/artwork/[id].tsx b/client/src/pages/artwork/[id].tsx index dd1ab090..7086d6eb 100644 --- a/client/src/pages/artwork/[id].tsx +++ b/client/src/pages/artwork/[id].tsx @@ -1,9 +1,11 @@ +import { ArrowLeft } from "lucide-react"; import { GetServerSideProps } from "next"; import Image from "next/image"; +import Link from "next/link"; import { useRouter } from "next/router"; +import { Button } from "@/components/ui/button"; import ContributorsList from "@/components/ui/ContributorsList"; -import GoBackButton from "@/components/ui/GoBackButton"; import ErrorModal from "@/components/ui/modal/error-modal"; import api from "@/lib/api"; import { Art } from "@/types/art"; @@ -30,99 +32,33 @@ export default function ArtworkPage({ artwork, error }: ArtworkPageProps) { } return ( -
-
-
- +
+ + + +
+ Artwork image + +
+

+ {artwork.name} +

+

{artwork.description}

+ +
- -
-
- Artwork image -
- -
-
-
- {artwork.name} -
- -
-
- - {artwork.description} - -
-
- - -
-
-
- -
-
- {artwork.name} -
- -
-
- - {artwork.description} - -
-
- - -
- -
-
+
); } From c04847e8f5ba535878db89e11eeeaa0ba568ead0 Mon Sep 17 00:00:00 2001 From: laurenpudz Date: Sun, 1 Mar 2026 16:35:46 +0800 Subject: [PATCH 109/122] make individual page somewhat responsive --- client/src/components/ui/ContributorsList.tsx | 4 ++-- client/src/pages/artwork/[id].tsx | 21 ++++++++++--------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/client/src/components/ui/ContributorsList.tsx b/client/src/components/ui/ContributorsList.tsx index 9c08c57f..70be6fe9 100644 --- a/client/src/components/ui/ContributorsList.tsx +++ b/client/src/components/ui/ContributorsList.tsx @@ -16,9 +16,9 @@ export default function ContributorsList({ return ( <>
Contributors
-
+
{contributors.map((contributor) => ( -
+
Back to Showcase -
- Artwork image +
+
+ Artwork image +

From 67def40d3f3a82f4aeec7634288e2f772ea6e9de Mon Sep 17 00:00:00 2001 From: laurenpudz Date: Sun, 1 Mar 2026 16:45:17 +0800 Subject: [PATCH 110/122] start cleaning up showcase page --- client/src/pages/artwork/index.tsx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/client/src/pages/artwork/index.tsx b/client/src/pages/artwork/index.tsx index b7125dbe..50429a7f 100644 --- a/client/src/pages/artwork/index.tsx +++ b/client/src/pages/artwork/index.tsx @@ -101,10 +101,10 @@ function renderArtworkCard(artwork: Art) { e.stopPropagation()} > - VIEW FULL DETAILS + View full details

} @@ -123,11 +123,8 @@ export default function ArtworksPage({ artworks, error }: ArtworksPageProps) { return (
-

- FEATURED +

+ Featured Artwork

From f18ed12d8a4ce484b5cf5c226897454ab292d66a Mon Sep 17 00:00:00 2001 From: Games4Doritos Date: Sat, 28 Mar 2026 23:22:14 +0800 Subject: [PATCH 111/122] Refined Events docs --- client/documentation/admin-dashboard/events.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/client/documentation/admin-dashboard/events.md b/client/documentation/admin-dashboard/events.md index 0e87dca0..fe7348e3 100644 --- a/client/documentation/admin-dashboard/events.md +++ b/client/documentation/admin-dashboard/events.md @@ -1,15 +1,21 @@ # Events +Event instances that are displayed on the landing and events pages + ## Fields -**Name:** Required field for the event's name. A character field (includes letters, numbers and symbols) of maximum length 200 characters. +**Name:** Required character field for the event's name. **Date:** Required field for the event date and time. Must be a valid date and time. Uses either ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ) or American format (MM-DD-YYYY HH:MM) for date and time representation. -**Description:** Optional field for a description of the event. A character field of maximum length 256 characters. +**Description:** Optional character field for a description of the event. + +**Publication Date:** Required field for the date the event is published. Uses either ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ) or American format (MM-DD-YYYY HH:MM) for date and time representation. + +**Cover Image:** Optional field to upload a cover image for the event, which will otherwise display the event's name. -**Publication Date:** Required field for the date the event is published. Must be a valid date (DD/MM/YYYY). Uses either ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ) or American format (MM-DD-YYYY HH:MM) for date and time representation. +**Location:** Required field for the event location. -**Cover Image:** Optional field to upload a cover image for the event. Must be an image file. If no cover image is provided, a default club logo will be displayed instead. +## Other Notes -**Location:** Required field for the event location. A character field of maximum length 256 characters. +An Event object can referenced in a Game object's 'Event' field, which is also a foreign key. On the events page, events will either show up under past or upcoming depending on their date, and each Event object can be directly routed to at /events/{id} (integer uid). From 3d19d0dc53e911d14c516f561382fdef219cd42f Mon Sep 17 00:00:00 2001 From: Games4Doritos Date: Sun, 29 Mar 2026 20:44:35 +0800 Subject: [PATCH 112/122] Re-did contributors docs, no just for art and game --- .../admin-dashboard/contributors.md | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 client/documentation/admin-dashboard/contributors.md diff --git a/client/documentation/admin-dashboard/contributors.md b/client/documentation/admin-dashboard/contributors.md new file mode 100644 index 00000000..b01d8d11 --- /dev/null +++ b/client/documentation/admin-dashboard/contributors.md @@ -0,0 +1,36 @@ +## Contributors + +Models that associate a member object with another certain object as a contributor to it. There are two types: + +## Game Contributor + +A model that will associate a certain member with having contributed to a certain game. A Game Contributor object will be represented under the 'Contributors' section of a game's page as the name of the member who contributed. + +## Fields + +**Game:** Required field for the game that the contributor is for. It is an integer field that corresponds to the raw integer id of a Game object in the Game table, known as a Foreign Key. + +**Member:** Required field for the member that is the contributor to the specified game. It is an integer field that corresponds to the raw integer id of a Member object in the Member table, known as a Foreign Key. + +**Role:** Required character field for the description of the role that the person played in contributing to the game. Maximum length of 100 characters + +## Other Notes + +You will see a little magnifying glass when selecting a Game and Member, click on it to see their respective table and be able choose the integer id's using the desired details. Though none of the individual fields are a primary key, the actual primary key is the unique (Game, Member) pair that each object forms. + + +## Art Contributor + +A model that will associate a certain member with having contributed to a certain game. A Game Contributor object will be represented under the 'Contributors' section of a game's page as the name of the member who contributed. + +## Fields + +**Art:** Required field for the artwork that the contributor is for. It is an integer field that corresponds to the raw integer id of an Art object in the Art table, known as a Foreign Key. + +**Member:** Required field for the member that is the contributor to the specified artwork. It is an integer field that corresponds to the raw integer id of a Member object in the Member table, known as a Foreign Key. + +**Role:** Required character field for the description of the role that the person played in contributing to the artwork. Maximum length of 100 characters + +## Other Notes + +You will see a little magnifying glass when selecting an Art and a Member, click on it to see their respective table and be able choose the integer id's using the desired details. Though none of the individual fields are a primary key, the actual primary key is the unique (Art, Member) pair that each object forms. \ No newline at end of file From ee28504a2371985548a574a051f7c1b48cca4279 Mon Sep 17 00:00:00 2001 From: Games4Doritos Date: Sun, 29 Mar 2026 20:47:03 +0800 Subject: [PATCH 113/122] oops --- client/documentation/admin-dashboard.md | 83 ------------------------- 1 file changed, 83 deletions(-) delete mode 100644 client/documentation/admin-dashboard.md diff --git a/client/documentation/admin-dashboard.md b/client/documentation/admin-dashboard.md deleted file mode 100644 index f20f14d8..00000000 --- a/client/documentation/admin-dashboard.md +++ /dev/null @@ -1,83 +0,0 @@ -# Admin Dashboard Documentation - -for admin dashboard docco pages: - -- each page should contain a list of the fields that need to be filled out to create a new entry and a short explanation of each -- explain any noteworthy cases where filling out a field a certain way causes a major difference on the frontend. e.g. rendering an itch widget vs a non itch widget -- for fields that need information to be filled from itch.io, a description of where to get this information from should be included -- the process for adding, managing, and removing admin accounts who can access the admin site - -## Events - -The events section of the admin dashboard allows administrators to create and manage events that will be displayed on the frontend. -Existing events will be listed in a table, and administrators can click on an event to edit its details or delete it if it is no longer relevant. -To create a new event, administrators can click on the "Add Event" button in the top right, which will take them to a form where they can fill out the necessary information about the event. - -When creating a new event, there are several fields that need to be filled out. - -### Name - -**Required** - -The name of the event. This is what will be displayed on the frontend and should be descriptive enough to give users an idea of what the event is about. - -### Date and Time - -**Required** - -The date and time of the event. It has two fields, **Date** and **Time**. -The **Date** field should be given in DD/MM/YYYY format, while the **Time** field should be given in HH:MM:SS format. This information is crucial for users to know when the event is taking place. - -### Description - -A brief description of the event. This should provide users with more information about what to expect from the event and why they should attend, or alternatively what happened at the event if it is a past event. - -### Publication Date - -**Required** - -The date when the event was published on the frontend. This is important for users to know when the event was announced and can help them plan accordingly. - -### Cover Image - -**Required** - -An image that represents the event. This should be visually appealing and relevant to the event to attract users' attention. It will be displayed on the frontend alongside the event details. - -### Location - -**Required** - -The location of the event. This can be a physical location or an online platform where the event will take place. Providing this information is essential for users to know where they need to go to attend the event. - -## Members - -## Committees - -## Game Contributors - -### Game - -**Required** - -Search for a game that is already in the database and select it. If the game is not in the database, you will need to add it first before you can add a contributor to it. - -### Member - -**Required** - -Search for a member that is already in the database and select them. If the member is not in the database, you will need to add them first before you can add them as a contributor to a game. - -### Role - -**Required** - -The description of the role that the member played in the development of the game. This should be a brief description that gives users an idea of what the member contributed to the game. - -E.g. "Lead Developer", "Artist", "Composer", etc. - -## Games - -## Art - -## Game Showcases From f8395b4d69987fff120080e153a69ba84c71634c Mon Sep 17 00:00:00 2001 From: James Lee Date: Sat, 4 Apr 2026 12:52:35 +0800 Subject: [PATCH 114/122] Fixing test and flake8 errors --- .../migrations/0030_art_artcontributor_artshowcase.py | 8 +++++++- server/game_dev/tests.py | 7 +++++++ server/game_dev/urls.py | 4 ++-- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/server/game_dev/migrations/0030_art_artcontributor_artshowcase.py b/server/game_dev/migrations/0030_art_artcontributor_artshowcase.py index 83aa5053..f6407051 100644 --- a/server/game_dev/migrations/0030_art_artcontributor_artshowcase.py +++ b/server/game_dev/migrations/0030_art_artcontributor_artshowcase.py @@ -44,7 +44,13 @@ class Migration(migrations.Migration): ('art', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='showcase', to='game_dev.art')), ], options={ - 'constraints': [models.UniqueConstraint(fields=('art',), name='unique_artshowcase_per_art', violation_error_message='Each art piece can only have one showcase.')], + 'constraints': [ + models.UniqueConstraint( + fields=('art',), + name='unique_artshowcase_per_art', + violation_error_message='Each art piece can only have one showcase.' + ) + ], }, ), ] diff --git a/server/game_dev/tests.py b/server/game_dev/tests.py index aca28f84..06b6b219 100644 --- a/server/game_dev/tests.py +++ b/server/game_dev/tests.py @@ -265,6 +265,8 @@ def setUp(self): pronouns="She/Her" ) + self.game = Game.objects.create(name="Test Game") + # Create art image_file = SimpleUploadedFile( "test_art.jpg", @@ -275,6 +277,7 @@ def setUp(self): name="Collaborative Artwork", description="Art with multiple contributors", media=image_file, + source_game=self.game, ) # Create art contributor @@ -330,6 +333,8 @@ def test_art_contributor_role(self): class ArtShowcaseModelTest(TestCase): def setUp(self): + self.game = Game.objects.create(name="Test Game") + # Create art pieces image_file1 = SimpleUploadedFile( "test_art1.jpg", @@ -340,6 +345,7 @@ def setUp(self): name="Showcased Artwork", description="This art is showcased", media=image_file1, + source_game=self.game, ) image_file2 = SimpleUploadedFile( @@ -351,6 +357,7 @@ def setUp(self): name="Another Artwork", description="This art is also showcased", media=image_file2, + source_game=self.game, ) # Create showcase diff --git a/server/game_dev/urls.py b/server/game_dev/urls.py index 014ac218..df09be06 100644 --- a/server/game_dev/urls.py +++ b/server/game_dev/urls.py @@ -1,7 +1,7 @@ from django.urls import path from .views import (ContributorGamesListAPIView, EventListAPIView, EventDetailAPIView, GamesDetailAPIView, - GameshowcaseAPIView, MemberAPIView, CommitteeAPIView, - FeatureArtAPIView, ArtDetailAPIView) + GameshowcaseAPIView, MemberAPIView, CommitteeAPIView, + FeatureArtAPIView, ArtDetailAPIView) urlpatterns = [ From 80a53e3f6b7b06a9553479908b58f23c11f01806 Mon Sep 17 00:00:00 2001 From: James Lee Date: Sat, 4 Apr 2026 12:56:53 +0800 Subject: [PATCH 115/122] Merge migrations --- .../migrations/0031_merge_20260404_1256.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 server/game_dev/migrations/0031_merge_20260404_1256.py diff --git a/server/game_dev/migrations/0031_merge_20260404_1256.py b/server/game_dev/migrations/0031_merge_20260404_1256.py new file mode 100644 index 00000000..06840fc3 --- /dev/null +++ b/server/game_dev/migrations/0031_merge_20260404_1256.py @@ -0,0 +1,14 @@ +# Generated by Django 5.1.15 on 2026-04-04 04:56 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('game_dev', '0030_alter_game_itchgameheight_alter_game_itchgamewidth'), + ('game_dev', '0030_art_artcontributor_artshowcase'), + ] + + operations = [ + ] From 87aea7e946736f151f728b30b3237beee9848733 Mon Sep 17 00:00:00 2001 From: samjjacko Date: Wed, 15 Apr 2026 17:36:33 +0800 Subject: [PATCH 116/122] added url --- client/documentation/admin-dashboard/events.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/documentation/admin-dashboard/events.md b/client/documentation/admin-dashboard/events.md index fe7348e3..6911ffcf 100644 --- a/client/documentation/admin-dashboard/events.md +++ b/client/documentation/admin-dashboard/events.md @@ -1,4 +1,6 @@ -# Events +## Events + +URL: `/admin/game_dev/event/add` Event instances that are displayed on the landing and events pages From 22c27c3e0758d3f1f1128f119bfeb5256ed55e2c Mon Sep 17 00:00:00 2001 From: samjjacko Date: Wed, 15 Apr 2026 18:46:49 +0800 Subject: [PATCH 117/122] renamed and removed random script directory --- client/scripts/open-when-ready.js | 0 client/src/components/main/MemberProfile.tsx | 1 + client/src/hooks/{use-artwork-data.ts => useArtworkData.ts} | 0 client/src/pages/artwork/index.tsx | 2 +- 4 files changed, 2 insertions(+), 1 deletion(-) delete mode 100644 client/scripts/open-when-ready.js rename client/src/hooks/{use-artwork-data.ts => useArtworkData.ts} (100%) diff --git a/client/scripts/open-when-ready.js b/client/scripts/open-when-ready.js deleted file mode 100644 index e69de29b..00000000 diff --git a/client/src/components/main/MemberProfile.tsx b/client/src/components/main/MemberProfile.tsx index c66780b3..98289f45 100644 --- a/client/src/components/main/MemberProfile.tsx +++ b/client/src/components/main/MemberProfile.tsx @@ -20,6 +20,7 @@ export type MemberProfileData = { type MemberProfileProps = { member: MemberProfileData; + id: number; }; function initialsFromName(name: string) { diff --git a/client/src/hooks/use-artwork-data.ts b/client/src/hooks/useArtworkData.ts similarity index 100% rename from client/src/hooks/use-artwork-data.ts rename to client/src/hooks/useArtworkData.ts diff --git a/client/src/pages/artwork/index.tsx b/client/src/pages/artwork/index.tsx index 50429a7f..2c419fcf 100644 --- a/client/src/pages/artwork/index.tsx +++ b/client/src/pages/artwork/index.tsx @@ -5,7 +5,7 @@ import { useRouter } from "next/router"; import ImageCard from "@/components/ui/ImageCard"; import ErrorModal from "@/components/ui/modal/error-modal"; -import { generateMockArtworks } from "@/hooks/use-artwork-data"; +import { generateMockArtworks } from "@/hooks/useArtworkData"; import api from "@/lib/api"; import { Art } from "@/types/art"; From 06d08d65ab7139c9d22a95ecd599dc8cfc6818b5 Mon Sep 17 00:00:00 2001 From: samjjacko Date: Wed, 15 Apr 2026 19:25:41 +0800 Subject: [PATCH 118/122] merged issue-81 into this branch --- .../admin-dashboard/committee.md | 12 +- client/documentation/admin-dashboard/games.md | 15 +- .../documentation/admin-dashboard/members.md | 1 + .../documentation/admin-dashboard/showcase.md | 83 ++++++++ client/src/components/ui/GameArtCarousel.tsx | 124 +++++++++++ client/src/hooks/useGames.ts | 25 ++- client/src/hooks/useGameshowcase.ts | 14 +- client/src/pages/games/[id].tsx | 19 +- client/src/pages/games/index.tsx | 197 ++++++++++-------- package-lock.json | 6 - server/game_dev/admin.py | 6 + .../0031_art_artcontributor_artshowcase.py | 56 +++++ server/game_dev/models.py | 46 ++++ server/game_dev/serializers.py | 84 ++++++-- 14 files changed, 540 insertions(+), 148 deletions(-) create mode 100644 client/documentation/admin-dashboard/showcase.md create mode 100644 client/src/components/ui/GameArtCarousel.tsx delete mode 100644 package-lock.json create mode 100644 server/game_dev/migrations/0031_art_artcontributor_artshowcase.py diff --git a/client/documentation/admin-dashboard/committee.md b/client/documentation/admin-dashboard/committee.md index 5127371e..0dc62436 100644 --- a/client/documentation/admin-dashboard/committee.md +++ b/client/documentation/admin-dashboard/committee.md @@ -1,13 +1,15 @@ ## Committee Members -Profiles of the Committee Members of the club that are displayed on the about page. +URL: `/admin/game_dev/committee/add/` + +Profiles of the Committee Members of the club that are displayed on the about page `/about`. + +## Prerequisites + +As a committee member is also a member of the club, you must construct a member first via `/admin/game_dev/member/add/`. Back on the committee page, you can find a specific member (e.g. the one you just created) by clicking the magnifying glass icon next to the `Id` input box. ## Fields **Id:** Required and unique field for the club member that is on the committee. It is an integer field that corresponds to the raw integer id of a row in the Member table, as a Foreign Key if you know databases. **Role:** Required and unique field for the specific role in the committee that this member has. It is something known as an Enum (Enumeration), which has a discrete number of custom choices. You can choose from 'President', 'Vice President', 'Secretary', 'Treasurer', 'Marketing', 'Events OCM', 'Projects OCM', and 'Fresher Rep'. Since they must be unique, you can only have 8 objects in the Committee table at a time for now. This can definitely be changed in the future when the committee grows. - -## Other Notes - -Before making a Committee object in the Committee table, you must make a Member object for the person that's on the committee and then link it through the id (just to clarify) diff --git a/client/documentation/admin-dashboard/games.md b/client/documentation/admin-dashboard/games.md index 73caf794..a5ace97c 100644 --- a/client/documentation/admin-dashboard/games.md +++ b/client/documentation/admin-dashboard/games.md @@ -1,18 +1,20 @@ ## Games +URL: `/admin/game_dev/game/` Pages for games can be added and edited at the row 'Game' of the GAME_DEV section on the main admin page. ### Fields **Name:** Required field for the game's name. A character field (includes letters, numbers and symbols) of maximum length 200 characters. -**Descripiton:** Required field for the game's description. A text field. +**Description:** Required field for the game's description. A text field. **Completion:** Required field for the game's completion. A multichoice option field with four options including: - - "Work in progress (Unplayable)" - - "Playable - In Development" - - "Beta - Stable but not Final" - - "Completed" + +- "Work in progress (Unplayable)" +- "Playable - In Development" +- "Beta - Stable but not Final" +- "Completed" **Active:** Required field for if the game is continued to be being worked on. A boolean field. @@ -28,4 +30,5 @@ Pages for games can be added and edited at the row 'Game' of the GAME_DEV sectio **Itch Game Width:** This field is required if the playable field ID is non null. This value is gotten in a similar way to the Itch Game Embed, however for the developer it's the number after "width=" and in the page source is found after "data-width=". -**Itch Game Height:** This field is required if the playable field ID is non null. This value is gotten in a similar way to the Itch Game Embed, however for the developer it's the number after "height=" and in the page source is found after "data-height=". \ No newline at end of file +**Itch Game Height:** This field is required if the playable field ID is non null. This value is gotten in a similar way to the Itch Game Embed, however for the developer it's the number after "height=" and in the page source is found after "data-height=". + diff --git a/client/documentation/admin-dashboard/members.md b/client/documentation/admin-dashboard/members.md index f8fde6ad..4984ba98 100644 --- a/client/documentation/admin-dashboard/members.md +++ b/client/documentation/admin-dashboard/members.md @@ -1,5 +1,6 @@ ## Member Profiles +URL: `/admin/game_dev/member/add/` Profiles of club members can be added and edited at the row 'Member' of the GAME_DEV section on the main admin page. ### Fields diff --git a/client/documentation/admin-dashboard/showcase.md b/client/documentation/admin-dashboard/showcase.md new file mode 100644 index 00000000..796bad3f --- /dev/null +++ b/client/documentation/admin-dashboard/showcase.md @@ -0,0 +1,83 @@ +## Game Showcase +URL: `/admin/game_dev/gameshowcase/add/` +The Game Showcase page is managed by administrators to highlight outstanding individual games. It serves as the featured section of the game library (see `/games`). + +## Prerequisites + +1. **Add a Game** + + - Path: `/admin/game_dev/game/add/` + - Required fields: name, description, completion status, host URL, thumbnail, event, etc. + +2. **Add Game Contributors** + - Path: `/admin/game_dev/gamecontributor/add/` + - Link club members to individual games. Each contributor must first be created in the `/admin/game_dev/member/` member panel. + +## Add Game Showcase + +1. Navigate to: `/admin/game_dev/gameshowcase/add/` +2. Use the search function to find the desired game, then click the game ID to populate the value. +3. Enter the committee's recommendation in the "Description" field. +4. Save your changes and review the showcase page to ensure the content is correct. + +## Page Elements Maintenance + +- **Game Title** + + - Edit at: `/admin/game_dev/game/` + +- **Recommendation (Committee Advice)** + + - Edit at: `/admin/game_dev/gameshowcase/`, under the field "Description" + - This recommendation appears directly under the game title as advice from the committee. + +- **Game Description** + + - Edit at: `/admin/game_dev/game/` + - The description is shown below the cover image and contributor section, and matches the content on the individual game page. + +- **Cover Image** + - Uses the thumbnail uploaded when adding the game. + - Edit at: `/admin/game_dev/game/` + +## Art Showcase + +The Art Showcase page is managed by administrators to highlight outstanding individual artworks. It serves as the featured section of the art library. + +## Prerequisites + +1. **Add an Art Object** + + - Path: `/admin/game_dev/art/add/` + - Required fields: name, description, source_game, media, active + +2. **Add Art Contributors** + - Path: `/admin/game_dev/artcontributor/add/` + - Link club members to individual artworks. Each contributor must first be created in the `/admin/game_dev/member/` member panel. + +## Add Art Showcase + +1. Navigate to: `/admin/game_dev/artshowcase/add/` +2. Use the search function to find the desired art object, then click the art ID to populate the value. +3. Enter the committee's recommendation in the "Description" field. +4. Save your changes and review the showcase page to ensure the content is correct. + +## Page Elements Maintenance + +- **Art Name** + + - Edit at: `/admin/game_dev/art/` + +- **Recommendation (Committee Advice)** + + - Edit at: `/admin/game_dev/artshowcase/`, under the field "Description" + - This recommendation appears directly under the art name as advice from the committee. + +- **Art Description** + + - Edit at: `/admin/game_dev/art/` + +- **Media** + - Uses the media uploaded when adding the art. + - Edit at: `/admin/game_dev/art/` + diff --git a/client/src/components/ui/GameArtCarousel.tsx b/client/src/components/ui/GameArtCarousel.tsx new file mode 100644 index 00000000..0633accb --- /dev/null +++ b/client/src/components/ui/GameArtCarousel.tsx @@ -0,0 +1,124 @@ +// This carousel is for Artworks to be displayed in the Gameshowcase + +import { ChevronLeft, ChevronRight } from "lucide-react"; +import Image from "next/image"; +import Link from "next/link"; +import { useEffect, useRef, useState } from "react"; + +import type { UiArtwork } from "@/hooks/useGames"; + +// import { UiEvent as EventType } from "@/hooks/useEvents"; + +type GameArtCarouselProps = { + items: UiArtwork[]; +}; + +const GAP = 20; +const maxItemsPerPage = 4; + +export default function GameArtCarousel({ items }: GameArtCarouselProps) { + const firstItemRef = useRef(null); + + const [currentIndex, setCurrentIndex] = useState(0); + const [itemWidth, setItemWidth] = useState(0); + const [visibleCount, setVisibleCount] = useState(maxItemsPerPage); + + const maxIndex = Math.max(items.length - visibleCount, 0); + + const slideLeft = () => { + setCurrentIndex((prev) => Math.max(prev - 1, 0)); + }; + + const slideRight = () => { + setCurrentIndex((prev) => Math.min(prev + 1, maxIndex)); + }; + + const translateX = -(currentIndex * (itemWidth + GAP)); + + useEffect(() => { + if (!firstItemRef.current) return; + + const observer = new ResizeObserver(() => { + const width = firstItemRef.current?.clientWidth ?? 0; + setItemWidth(width); + }); + + observer.observe(firstItemRef.current); + + return () => observer.disconnect(); + }, []); + + useEffect(() => { + const updateVisibleCount = () => { + if (window.innerWidth < 768) { + setVisibleCount(1); + } else { + setVisibleCount(maxItemsPerPage); + } + }; + + updateVisibleCount(); + window.addEventListener("resize", updateVisibleCount); + + return () => window.removeEventListener("resize", updateVisibleCount); + }, []); + + return ( +
+
+ {/* LEFT ARROW */} + + + {/* VIEWPORT */} +
+
+ {items.map((art, index) => ( +
+ +
+ {art.name} +
+ + +

{art.name}

+
+ ))} +
+
+ + {/* RIGHT ARROW */} + +
+
+ ); +} diff --git a/client/src/hooks/useGames.ts b/client/src/hooks/useGames.ts index 758e9c76..33ff849f 100644 --- a/client/src/hooks/useGames.ts +++ b/client/src/hooks/useGames.ts @@ -14,6 +14,21 @@ type Contributor = { }>; }; +export type UiArtwork = { + id: number; + name: string; + image: string; + sourceGameId: number; +}; + +export type ApiArtworks = { + art_id: number; + name: string; + media: string; + active: boolean; + source_game_id: number; +}; + type ApiGame = { name: string; description: string; @@ -28,10 +43,12 @@ type ApiGame = { itchGameWidth: number; itchGameHeight: number; contributors: Contributor[]; + artworks: ApiArtworks[]; }; -type UiGame = Omit & { +type UiGame = Omit & { gameCover: string; + artworks: UiArtwork[]; }; /** @@ -49,6 +66,12 @@ function transformApiGameToUiGame(data: ApiGame): UiGame { return { ...data, gameCover: data.thumbnail ?? "/game_dev_club_logo.svg", + artworks: data.artworks.map((a) => ({ + id: a.art_id, + name: a.name, + image: a.media, + sourceGameId: a.source_game_id, + })), }; } diff --git a/client/src/hooks/useGameshowcase.ts b/client/src/hooks/useGameshowcase.ts index fc515a97..a879dcae 100644 --- a/client/src/hooks/useGameshowcase.ts +++ b/client/src/hooks/useGameshowcase.ts @@ -1,6 +1,7 @@ import { useQuery } from "@tanstack/react-query"; import { AxiosError } from "axios"; +import type { ApiArtworks, UiArtwork } from "@/hooks/useGames"; import api from "@/lib/api"; type Contributor = { @@ -20,16 +21,27 @@ type ApiShowcaseGame = { game_description: string; contributors: Contributor[]; game_cover_thumbnail?: string | null; + artworks: ApiArtworks[]; }; -type UiShowcaseGame = Omit & { +type UiShowcaseGame = Omit< + ApiShowcaseGame, + "game_cover_thumbnail" | "artworks" +> & { gameCover: string; + artworks: UiArtwork[]; }; function transformApiShowcaseGameToUi(data: ApiShowcaseGame): UiShowcaseGame { return { ...data, gameCover: data.game_cover_thumbnail ?? "/game_dev_club_logo.svg", + artworks: data.artworks.map((a) => ({ + id: a.art_id, + name: a.name, + image: a.media, + sourceGameId: a.source_game_id, + })), }; } diff --git a/client/src/pages/games/[id].tsx b/client/src/pages/games/[id].tsx index 69dd1c8d..818aa9d1 100644 --- a/client/src/pages/games/[id].tsx +++ b/client/src/pages/games/[id].tsx @@ -4,6 +4,7 @@ import { useRouter } from "next/router"; import React from "react"; import { SocialIcon } from "react-social-icons"; +import GameArtCarousel from "@/components/ui/GameArtCarousel"; import { GameEmbed } from "@/components/ui/GameEmbed"; import { ItchEmbed } from "@/components/ui/ItchEmbed"; import { useEvent } from "@/hooks/useEvent"; @@ -72,8 +73,6 @@ export default function IndividualGamePage() { const devStage = completionLabels[game.completion] ?? "Stage Unknown"; - // TODO ADD ARTIMAGES - const artImages: { src: string; alt: string }[] = []; // const artImages = [ // { // src: "https://upload.wikimedia.org/wikipedia/commons/thumb/a/aa/Minecraft_Zombie.png/120px-Minecraft_Zombie.png", @@ -212,21 +211,7 @@ export default function IndividualGamePage() {

ARTWORK

- {artImages.map((img) => ( -
- {img.alt} -
- ))} +
diff --git a/client/src/pages/games/index.tsx b/client/src/pages/games/index.tsx index 3ee3699c..bab4591b 100644 --- a/client/src/pages/games/index.tsx +++ b/client/src/pages/games/index.tsx @@ -3,6 +3,7 @@ import Link from "next/link"; import React from "react"; import { SocialIcon } from "react-social-icons"; +import GameArtCarousel from "@/components/ui/GameArtCarousel"; import { useGameshowcase } from "@/hooks/useGameshowcase"; export default function HomePage() { @@ -47,107 +48,125 @@ export default function HomePage() { Game Showcase
-
+
{!showcases || showcases.length === 0 ? (

No games available.

) : ( -
+
{showcases.map((showcase, idx) => ( -
- {/* Left: Cover Image */} -
- {showcase.gameCover ? ( - {showcase.game_name - ) : ( -
+
+ {/* Game CoverImage + Gameshowcase Detail */} +
+ {/* Left: Cover Image */} +
+ {showcase.gameCover ? ( Default game cover -
- )} -
- {/* Right: Details */} -
-
- {/* Title of the game */} -

- - - {showcase.game_name} - - -

- {/* Comments from committes */} -

- {/* double quotes from comments */} - - {showcase.description} - -

-

- Contributors -

-
    - {showcase.contributors.map((contributor, cidx) => ( -
  • + Default game cover +
+ )} +
+ {/* Right: Details */} +
+
+ {/* Title of the game */} +

+ - - {contributor.name} + + {showcase.game_name} - {/* Social icons from API */} - - {Array.isArray(contributor.social_media) && - contributor.social_media.map((sm) => ( - - ))} - - - - {contributor.role} - - - ))} - + +

+ {/* Comments from committes */} +

+ {/* double quotes from comments */} + + {showcase.description} + +

+
+
+
+

+ Contributors +

+
    + {showcase.contributors.map((contributor, cidx) => ( +
  • +
    + + {contributor.name} + + + {contributor.role} + +
    + {/* Social icons placeholder */} + {/* TODO: Add actual links */} + + {/* Social icons using react-social-icons */} + + + + +
  • + ))} +
+
-
-
- {showcase.game_description} + + {/* Game Art Carousel */} + + + {/* Description */} +
+ {showcase.game_description} +
))} diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 1aa4fe21..00000000 --- a/package-lock.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "game-dev", - "lockfileVersion": 3, - "requires": true, - "packages": {} -} diff --git a/server/game_dev/admin.py b/server/game_dev/admin.py index 03c3d8fb..4bb03ba3 100644 --- a/server/game_dev/admin.py +++ b/server/game_dev/admin.py @@ -37,6 +37,7 @@ class CommitteeAdmin(admin.ModelAdmin): raw_id_fields = ["id"] +# from issue-8-merge-40 temp need changes class ArtContributorInline(admin.TabularInline): model = ArtContributor extra = 1 @@ -54,3 +55,8 @@ class ArtAdmin(admin.ModelAdmin): admin.site.register(Art, ArtAdmin) admin.site.register(ArtShowcase) admin.site.register(Committee, CommitteeAdmin) + +# from issue-8-merge-40 temp need changes +admin.site.register(Art, ArtAdmin) +# admin.site.register(ArtContributor) +admin.site.register(ArtShowcase) diff --git a/server/game_dev/migrations/0031_art_artcontributor_artshowcase.py b/server/game_dev/migrations/0031_art_artcontributor_artshowcase.py new file mode 100644 index 00000000..2b9e96db --- /dev/null +++ b/server/game_dev/migrations/0031_art_artcontributor_artshowcase.py @@ -0,0 +1,56 @@ +# Generated by Django 5.1.15 on 2026-03-25 11:09 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('game_dev', '0030_alter_game_itchgameheight_alter_game_itchgamewidth'), + ] + + operations = [ + migrations.CreateModel( + name='Art', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200)), + ('description', models.CharField(max_length=200)), + ('media', models.ImageField(upload_to='art/')), + ('active', models.BooleanField(default=True)), + ('source_game', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='game_artwork', to='game_dev.game')), + ], + ), + migrations.CreateModel( + name='ArtContributor', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('role', models.CharField(max_length=100)), + ('art', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contributors', to='game_dev.art')), + ('member', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='art_contributions', to='game_dev.member')), + ], + options={ + 'verbose_name': 'Art Contributor', + 'verbose_name_plural': 'Art Contributors', + 'constraints': [models.UniqueConstraint(fields=('art', 'member'), name='unique_art_member')], + }, + ), + migrations.CreateModel( + name='ArtShowcase', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('description', models.CharField(max_length=200)), + ('art', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='showcase', to='game_dev.art')), + ], + options={ + 'constraints': [ + models.UniqueConstraint( + fields=('art',), + name='unique_artshowcase_per_art', + violation_error_message='Each art piece can only have one showcase.' + ) + ], + }, + ), + ] diff --git a/server/game_dev/models.py b/server/game_dev/models.py index 3e66973e..4c501ccb 100644 --- a/server/game_dev/models.py +++ b/server/game_dev/models.py @@ -173,3 +173,49 @@ def get_member(self): def __str__(self): return self.id.name + + +# Copied from issue-8-merge-40 therefore is just sample to work with +class Art(models.Model): + name = models.CharField(null=False, max_length=200) + description = models.CharField(max_length=200,) + + # Talk to the artwork team to change their model to meet the follow, remove the null and blank + source_game = models.ForeignKey('Game', on_delete=models.CASCADE, related_name='game_artwork') + media = models.ImageField(upload_to='art/', null=False) + active = models.BooleanField(default=True) + + def __str__(self): + return str(self.name) + + +class ArtContributor(models.Model): + art = models.ForeignKey('Art', on_delete=models.CASCADE, related_name='contributors') + member = models.ForeignKey('Member', on_delete=models.CASCADE, related_name='art_contributions') + role = models.CharField(max_length=100) + + class Meta: + constraints = [ + models.UniqueConstraint(fields=['art', 'member'], name='unique_art_member') + ] + verbose_name = 'Art Contributor' + verbose_name_plural = 'Art Contributors' + + def __str__(self): + return f"{self.member.name} - {self.art.name} ({self.role})" + + +class ArtShowcase(models.Model): + description = models.CharField(max_length=200) + art = models.ForeignKey(Art, on_delete=models.CASCADE, related_name='showcase') + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=['art'], + name='unique_artshowcase_per_art', + violation_error_message='Each art piece can only have one showcase.') + ] + + def __str__(self): + return f"ArtShowcase[Art={str(self.art.name)}, Description={self.description}]" diff --git a/server/game_dev/serializers.py b/server/game_dev/serializers.py index 2311efa0..9a1e39db 100644 --- a/server/game_dev/serializers.py +++ b/server/game_dev/serializers.py @@ -17,6 +17,43 @@ class Meta: ] +######################################## +# Copied from issue-8-merge-40 therefore is just sample to work with +class ArtContributorSerializer(serializers.ModelSerializer): + member_id = serializers.IntegerField(source='member.id', read_only=True) + member_name = serializers.CharField(source='member.name', read_only=True) + + class Meta: + model = ArtContributor + fields = ['id', 'member_id', 'member_name', 'role'] + + +class ArtSerializer(serializers.ModelSerializer): + art_id = serializers.IntegerField(source='id', read_only=True) + source_game_id = serializers.IntegerField(source='source_game.id', read_only=True) + source_game_name = serializers.CharField(source='source_game.name', read_only=True) + contributors = ArtContributorSerializer(many=True, read_only=True) + showcase_description = serializers.SerializerMethodField() + + class Meta: + model = Art + fields = ['art_id', 'name', 'description', 'media', 'active', 'source_game_id', 'source_game_name', 'contributors', 'showcase_description'] + + def get_showcase_description(self, obj): + showcase = obj.showcase.first() + return showcase.description if showcase else None + + +class ArtShowcaseSerializer(serializers.ModelSerializer): + art_name = serializers.CharField(source='art.name', read_only=True) + + class Meta: + model = ArtShowcase + fields = ['id', 'description', 'art', 'art_name'] + +######################################## + + # This is child serializer of GameSerializer class GameContributorSerializer(serializers.ModelSerializer): # to link contributors to their member/[id] page @@ -33,28 +70,31 @@ def get_social_media(self, obj): return SocialMediaSerializer(social_links, many=True).data +# Copied ArtSerializer at kept data only needed instead of all of it +class GameArtSerializer(serializers.ModelSerializer): + art_id = serializers.IntegerField(source='id', read_only=True) + source_game_id = serializers.IntegerField(source='source_game.id', read_only=True) + + class Meta: + model = Art + fields = ['art_id', 'name', 'media', 'active', 'source_game_id'] + + class GamesSerializer(serializers.ModelSerializer): contributors = GameContributorSerializer( many=True, source="game_contributors", read_only=True ) + artworks = GameArtSerializer( + many=True, + source="game_artwork", + read_only=True + ) class Meta: model = Game - fields = ( - "id", - "name", - "description", - "completion", - "active", - "hostURL", - "itchEmbedID", - "thumbnail", - "event", - "itchGamePlayableID", - "itchGameWidth", - "itchGameHeight", - "contributors", - ) + fields = ('id', 'name', 'description', 'completion', 'active', + 'hostURL', 'itchEmbedID', 'thumbnail', 'event', "contributors", "artworks", 'itchGameEmbedID', + 'itchGameWidth', 'itchGameHeight') # Contributor serializer for name and role @@ -83,23 +123,21 @@ class GameshowcaseSerializer(serializers.ModelSerializer): source="game.thumbnail", read_only=True ) contributors = serializers.SerializerMethodField() + artworks = serializers.SerializerMethodField() class Meta: model = GameShowcase - fields = ( - "game_id", - "game_name", - "game_description", - "description", - "contributors", - "game_cover_thumbnail", - ) + fields = ('game_id', 'game_name', 'game_description', + 'description', 'contributors', 'game_cover_thumbnail', 'artworks') def get_contributors(self, obj): # Always fetch contributors from GameContributor for the related game contributors = GameContributor.objects.filter(game=obj.game) return ShowcaseContributorSerializer(contributors, many=True).data + def get_artworks(self, obj): + return GameArtSerializer(obj.game.game_artwork.all(), many=True).data + class ContributorGameSerializer(serializers.ModelSerializer): game_id = serializers.IntegerField(source='game.id', read_only=True) From 2d68543f9f9716f135282f1bbc13e58c0bd46bd8 Mon Sep 17 00:00:00 2001 From: samjjacko Date: Thu, 16 Apr 2026 15:53:29 +0800 Subject: [PATCH 119/122] glued branches together, refactored a bit and wrote some logic to display artwork on individual member's pages properly --- client/src/components/main/MemberProfile.tsx | 118 ------------- .../components/ui/MemberProjectSection.tsx | 160 ++++++++++-------- client/src/hooks/useCommittee.ts | 1 - client/src/hooks/useContributor.ts | 66 ++++++-- client/src/hooks/useMember.ts | 7 +- client/src/hooks/useRedirectOn404.ts | 10 ++ .../generateMockArt.ts} | 0 client/src/pages/artwork/[id].tsx | 79 ++++----- client/src/pages/artwork/index.tsx | 2 +- client/src/pages/events/[id].tsx | 3 + client/src/pages/games/[id].tsx | 18 +- client/src/pages/members/[id].tsx | 128 +++++++++++++- server/game_dev/admin.py | 7 +- .../0031_art_artcontributor_artshowcase.py | 56 ------ server/game_dev/serializers.py | 25 +-- server/game_dev/urls.py | 4 +- server/game_dev/views.py | 11 +- 17 files changed, 346 insertions(+), 349 deletions(-) delete mode 100644 client/src/components/main/MemberProfile.tsx create mode 100644 client/src/hooks/useRedirectOn404.ts rename client/src/{hooks/useArtworkData.ts => lib/generateMockArt.ts} (100%) delete mode 100644 server/game_dev/migrations/0031_art_artcontributor_artshowcase.py diff --git a/client/src/components/main/MemberProfile.tsx b/client/src/components/main/MemberProfile.tsx deleted file mode 100644 index 98289f45..00000000 --- a/client/src/components/main/MemberProfile.tsx +++ /dev/null @@ -1,118 +0,0 @@ -"use client"; - -import { Palette, Sparkles } from "lucide-react"; -import Image from "next/image"; -import { SocialIcon } from "react-social-icons"; - -import MemberProjectSection from "../ui/MemberProjectSection"; - -export type MemberProfileData = { - name: string; - about: string; - pronouns?: string; - profile_picture?: string; - social_media?: { - link: string; - socialMediaUserName: string; - }[]; - pk: number; -}; - -type MemberProfileProps = { - member: MemberProfileData; - id: number; -}; - -function initialsFromName(name: string) { - return name - .trim() - .split(/\s+/) - .slice(0, 2) - .map((part) => part[0]?.toUpperCase()) - .join(""); -} - -export function MemberProfile({ member }: MemberProfileProps) { - const initials = initialsFromName(member.name); - - return ( - <> -
-
-
-
- {member.profile_picture ? ( - {`${member.name} - ) : ( -
-

{initials}

-
- )} -
- golden pixel art frame around profile picture -
-
-
-

{member.name}

-
-
-
- {member.social_media && member.social_media.length > 0 && ( -
-
- {member.social_media.map((sm) => ( - - - - {sm.socialMediaUserName} - - - ))} -
-
- )} -
-
-

{member.pronouns}

-
- -

{member.about}

-
-
-
-
-

- Games - -

- -

- Artwork - -

-
- - ); -} diff --git a/client/src/components/ui/MemberProjectSection.tsx b/client/src/components/ui/MemberProjectSection.tsx index 046ca556..723fba36 100644 --- a/client/src/components/ui/MemberProjectSection.tsx +++ b/client/src/components/ui/MemberProjectSection.tsx @@ -1,75 +1,103 @@ -import { ArrowUpRight } from "lucide-react"; +import { ArrowUpRight , Gamepad2,Palette } 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; -}; - -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 ( -
-

- {errorMessage} -

-
- ); - } +import type { ArtData,GameData } from "@/hooks/useContributor"; +export default function MemberProjectSection(props: { + games: GameData[]; + art: ArtData[]; +}) { + const { games, art } = props; return ( -
- {!games || games.length === 0 ? ( -

- No games available. -

- ) : ( -
- {games.map((game) => ( - -
-
- {`${game.game_data.name} - window.open(`/games/${game.game_id}`)} - > - Visit Game - + <> +

+ Games + +

+
+ {!games || games.length === 0 ? ( +

+ No games available. +

+ ) : ( +
+ {games.map((game) => ( + +
+
+ {`${game.game_data.name} + window.open(`/games/${game.game_id}`)} + > + Visit work + +
+

+ {game.game_data.name} +

+

+ {game.game_data.description} +

-

- {game.game_data.name} -

-

- {game.game_data.description} -

-
- - ))} -
- )} -
+ + ))} +
+ )} +
+

+ Artwork + +

+
+ {!art || art.length === 0 ? ( +

+ No games available. +

+ ) : ( +
+ {art.map((artwork) => ( + +
+
+ {`${artwork.artwork_data.name} + window.open(`/artwork/${artwork.art_id}`)} + > + Visit work + +
+

+ {artwork.artwork_data.name} +

+

+ {artwork.artwork_data.description} +

+
+
+ ))} +
+ )} +
+ ); } diff --git a/client/src/hooks/useCommittee.ts b/client/src/hooks/useCommittee.ts index 5759947d..74a545f2 100644 --- a/client/src/hooks/useCommittee.ts +++ b/client/src/hooks/useCommittee.ts @@ -20,7 +20,6 @@ export function useCommittee() { queryKey: ["role"], queryFn: async () => { const response = await api.get("/about/"); - console.log(response.data); return response.data; }, retry: (failureCount, error) => { diff --git a/client/src/hooks/useContributor.ts b/client/src/hooks/useContributor.ts index d29e7843..fe4a44b5 100644 --- a/client/src/hooks/useContributor.ts +++ b/client/src/hooks/useContributor.ts @@ -1,27 +1,57 @@ -import { useQuery } from "@tanstack/react-query"; +import { useQueries, UseQueryResult } from "@tanstack/react-query"; import { AxiosError } from "axios"; import api from "@/lib/api"; -type ApiContributorGameData = { - name: string; - thumbnail: string; - description: string; -}; - -type ApiContributorGamesList = { +// unused fields are ommitted because I'm lazy +export type GameData = { game_id: number; role: string; - game_data: ApiContributorGameData; + game_data: { + name: string; + description: string; + thumbnail: string | null; + }; +}; + +export type ArtData = { + art_id: number; + artwork_data: { + name: string; + description: string; + media: string | null; + }; +}; + +type UseContributorResult = { + gamesRes: UseQueryResult; + artRes: UseQueryResult; }; -export const useContributor = (member: string | string[] | undefined) => { - return useQuery({ - queryKey: ["contributor", member], - queryFn: async () => { - const response = await api.get(`/games/contributor/${member}/`); - return response.data; - }, - enabled: !!member, - }); +export const useContributor = ( + member: number | string[] | undefined, +): UseContributorResult => { + const [gamesRes, artRes] = useQueries({ + queries: [ + { + queryKey: ["gamecontributor", member], + queryFn: async () => { + const response = await api.get(`/games/contributor/${member}/`); + return response.data; + }, + enabled: !!member, + }, + { + queryKey: ["artcontributor", member], + queryFn: async () => { + const response = await api.get(`/arts/contributor/${member}/`); + return response.data; + }, + }, + ], + }) as [ + UseQueryResult, + UseQueryResult, + ]; + return { gamesRes, artRes }; }; diff --git a/client/src/hooks/useMember.ts b/client/src/hooks/useMember.ts index 0d1f0581..53a48f34 100644 --- a/client/src/hooks/useMember.ts +++ b/client/src/hooks/useMember.ts @@ -13,9 +13,8 @@ type ApiMember = { }[]; pk: number; }; - -// return api member, import id number from router, is not enabled if not a number type -export const useMember = (id?: number) => { +// should be called strictly with member's integer uuid. +export function useMember(id: number | undefined) { return useQuery({ queryKey: ["member", id], queryFn: async () => { @@ -24,4 +23,4 @@ export const useMember = (id?: number) => { }, enabled: Number.isFinite(id), }); -}; +} diff --git a/client/src/hooks/useRedirectOn404.ts b/client/src/hooks/useRedirectOn404.ts new file mode 100644 index 00000000..44a0abb3 --- /dev/null +++ b/client/src/hooks/useRedirectOn404.ts @@ -0,0 +1,10 @@ +import { AxiosError } from "axios"; +import { useRouter } from "next/router"; +import { useEffect } from "react"; + +export function useRedirectOn404(error?: AxiosError | null) { + const router = useRouter(); + useEffect(() => { + if (error?.response?.status == 404) router.push("/404"); + }, [error, router]); +} diff --git a/client/src/hooks/useArtworkData.ts b/client/src/lib/generateMockArt.ts similarity index 100% rename from client/src/hooks/useArtworkData.ts rename to client/src/lib/generateMockArt.ts diff --git a/client/src/pages/artwork/[id].tsx b/client/src/pages/artwork/[id].tsx index fb26967d..d92927c9 100644 --- a/client/src/pages/artwork/[id].tsx +++ b/client/src/pages/artwork/[id].tsx @@ -1,44 +1,58 @@ -import { ArrowLeft } from "lucide-react"; -import { GetServerSideProps } from "next"; +import { useQuery } from "@tanstack/react-query"; +import { AxiosError } from "axios"; +import { ArrowLeft, Loader2 } from "lucide-react"; import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/router"; import { Button } from "@/components/ui/button"; import ContributorsList from "@/components/ui/ContributorsList"; -import ErrorModal from "@/components/ui/modal/error-modal"; +import { useRedirectOn404 } from "@/hooks/useRedirectOn404"; import api from "@/lib/api"; import { Art } from "@/types/art"; -interface ArtworkPageProps { - artwork?: Art; - error?: string; -} - -export default function ArtworkPage({ artwork, error }: ArtworkPageProps) { +export default function ArtworkPage() { const router = useRouter(); + const { id } = router.query; + + const { + data: artwork, + error, + isLoading, + } = useQuery({ + queryKey: ["art", id], + queryFn: async () => { + const response = await api.get(`/arts/${id}/`); + return response.data; + }, + enabled: !!id, + retry: (failureCount, error) => { + if (error?.response?.status === 404) { + return false; + } + return failureCount < 3; + }, + }); - if (error) { - return router.back()} />; - } + useRedirectOn404(error); - if (!artwork) { + if (isLoading || !artwork) return ( - router.push("/artwork")} - /> +
+ + Fetching artwork..... If you get stuck here, please send us a message! + :) +
); - } return ( -
+
-
+
); } - -type FeaturedResponse = Art[] | { results: Art[] }; - -export const getServerSideProps: GetServerSideProps = async ( - context, -) => { - const { id } = context.params as { id: string }; - - try { - // We only have this endpoint, so reuse it and pick the item by art_id - const res = await api.get("arts/featured"); - const data = res.data; - - const list: Art[] = Array.isArray(data) ? data : (data?.results ?? []); - const artwork = list.find((a) => String(a.art_id) === String(id)); - - if (!artwork) return { notFound: true }; - - return { props: { artwork } }; - } catch (err: unknown) { - return { - props: { error: (err as Error).message || "Failed to load artwork." }, - }; - } -}; diff --git a/client/src/pages/artwork/index.tsx b/client/src/pages/artwork/index.tsx index 2c419fcf..65bfb7c9 100644 --- a/client/src/pages/artwork/index.tsx +++ b/client/src/pages/artwork/index.tsx @@ -5,8 +5,8 @@ import { useRouter } from "next/router"; import ImageCard from "@/components/ui/ImageCard"; import ErrorModal from "@/components/ui/modal/error-modal"; -import { generateMockArtworks } from "@/hooks/useArtworkData"; import api from "@/lib/api"; +import { generateMockArtworks } from "@/lib/generateMockArt"; import { Art } from "@/types/art"; export interface PageResult { diff --git a/client/src/pages/events/[id].tsx b/client/src/pages/events/[id].tsx index ec53f25a..80e8513b 100644 --- a/client/src/pages/events/[id].tsx +++ b/client/src/pages/events/[id].tsx @@ -3,6 +3,7 @@ import { useRouter } from "next/router"; import { EventDateDisplay } from "@/components/ui/EventDateDisplay"; import { useEvent } from "@/hooks/useEvent"; +import { useRedirectOn404 } from "@/hooks/useRedirectOn404"; export default function EventPage() { const router = useRouter(); @@ -15,6 +16,8 @@ export default function EventPage() { isError, } = useEvent(router.isReady ? id : undefined); + useRedirectOn404(error); + if (isPending) { return (
diff --git a/client/src/pages/games/[id].tsx b/client/src/pages/games/[id].tsx index 818aa9d1..91271a75 100644 --- a/client/src/pages/games/[id].tsx +++ b/client/src/pages/games/[id].tsx @@ -9,6 +9,7 @@ import { GameEmbed } from "@/components/ui/GameEmbed"; import { ItchEmbed } from "@/components/ui/ItchEmbed"; import { useEvent } from "@/hooks/useEvent"; import { useGame } from "@/hooks/useGames"; +import { useRedirectOn404 } from "@/hooks/useRedirectOn404"; export default function IndividualGamePage() { const router = useRouter(); @@ -23,7 +24,7 @@ export default function IndividualGamePage() { const { data: eventData } = useEvent( game?.event ? String(game.event) : undefined, ); - + useRedirectOn404(error); if (isPending) { return (
@@ -73,21 +74,6 @@ export default function IndividualGamePage() { const devStage = completionLabels[game.completion] ?? "Stage Unknown"; - // const artImages = [ - // { - // src: "https://upload.wikimedia.org/wikipedia/commons/thumb/a/aa/Minecraft_Zombie.png/120px-Minecraft_Zombie.png", - // alt: "Minecraft Zombie", - // }, - // { - // src: "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d8/Minecraft_Enderman.png/120px-Minecraft_Enderman.png", - // alt: "Minecraft Enderman", - // }, - // { - // src: "https://upload.wikimedia.org/wikipedia/en/thumb/1/17/Minecraft_explore_landscape.png/375px-Minecraft_explore_landscape.png", - // alt: "Minecraft Landscape", - // }, - // ]; - return (
diff --git a/client/src/pages/members/[id].tsx b/client/src/pages/members/[id].tsx index 7b17438f..afae1c3b 100644 --- a/client/src/pages/members/[id].tsx +++ b/client/src/pages/members/[id].tsx @@ -1,8 +1,12 @@ +"use client"; + +import Image from "next/image"; import { useRouter } from "next/router"; +import { SocialIcon } from "react-social-icons"; -import { MemberProfile } from "@/components/main/MemberProfile"; +import MemberProjectSection from "@/components/ui/MemberProjectSection"; +import { useContributor } from "@/hooks/useContributor"; import { useMember } from "@/hooks/useMember"; - // hook assumes correct input, page sanitises to correct type function normaliseId(id: string | string[] | number | undefined) { if (typeof id === "number" && Number.isFinite(id)) { @@ -17,6 +21,27 @@ function normaliseId(id: string | string[] | number | undefined) { return undefined; } +export type MemberProfileData = { + name: string; + about: string; + pronouns?: string; + profile_picture?: string; + social_media?: { + link: string; + socialMediaUserName: string; + }[]; + pk: number; +}; + +function initialsFromName(name: string) { + return name + .trim() + .split(/\s+/) + .slice(0, 2) + .map((part) => part[0]?.toUpperCase()) + .join(""); +} + export default function MemberPage() { const router = useRouter(); const id = normaliseId(router.query.id); @@ -26,6 +51,11 @@ export default function MemberPage() { isPending, isError, } = useMember(router.isReady ? id : undefined); + const { gamesRes, artRes } = useContributor(id); + + if (!router.isReady || id === undefined) { + return null; + } if (isPending) { return null; @@ -34,6 +64,98 @@ export default function MemberPage() { if (isError || !member) { return

Member not found

; } + console.log(gamesRes.data); + console.log(artRes.data); + const games = gamesRes.data; + const art = artRes.data; + if (gamesRes.isError || !games || artRes.isError || !art) { + const errorMessage = + gamesRes.error?.response?.status === 404 + ? "Games not found." + : "Failed to Load Games"; + return ( +
+

+ {errorMessage} +

+
+ ); + } + + const initials = initialsFromName(member.name); + return ( + <> +
+
+
+
+ {member.profile_picture ? ( + {`${member.name} + ) : ( +
+

{initials}

+
+ )} +
+ golden pixel art frame around profile picture +
+
+
+

{member.name}

+
+
+
+ {member.social_media && member.social_media.length > 0 && ( +
+
+ {member.social_media.map((sm) => ( + + + + {sm.socialMediaUserName} + + + ))} +
+
+ )} +
+
+

{member.pronouns}

+
- return ; +

{member.about}

+
+
+
+
+ +
+ + ); } diff --git a/server/game_dev/admin.py b/server/game_dev/admin.py index 4bb03ba3..6e8983a9 100644 --- a/server/game_dev/admin.py +++ b/server/game_dev/admin.py @@ -54,9 +54,4 @@ class ArtAdmin(admin.ModelAdmin): admin.site.register(GameShowcase, GameShowcaseAdmin) admin.site.register(Art, ArtAdmin) admin.site.register(ArtShowcase) -admin.site.register(Committee, CommitteeAdmin) - -# from issue-8-merge-40 temp need changes -admin.site.register(Art, ArtAdmin) -# admin.site.register(ArtContributor) -admin.site.register(ArtShowcase) +admin.site.register(Committee, CommitteeAdmin) \ No newline at end of file diff --git a/server/game_dev/migrations/0031_art_artcontributor_artshowcase.py b/server/game_dev/migrations/0031_art_artcontributor_artshowcase.py deleted file mode 100644 index 2b9e96db..00000000 --- a/server/game_dev/migrations/0031_art_artcontributor_artshowcase.py +++ /dev/null @@ -1,56 +0,0 @@ -# Generated by Django 5.1.15 on 2026-03-25 11:09 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('game_dev', '0030_alter_game_itchgameheight_alter_game_itchgamewidth'), - ] - - operations = [ - migrations.CreateModel( - name='Art', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=200)), - ('description', models.CharField(max_length=200)), - ('media', models.ImageField(upload_to='art/')), - ('active', models.BooleanField(default=True)), - ('source_game', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='game_artwork', to='game_dev.game')), - ], - ), - migrations.CreateModel( - name='ArtContributor', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('role', models.CharField(max_length=100)), - ('art', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contributors', to='game_dev.art')), - ('member', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='art_contributions', to='game_dev.member')), - ], - options={ - 'verbose_name': 'Art Contributor', - 'verbose_name_plural': 'Art Contributors', - 'constraints': [models.UniqueConstraint(fields=('art', 'member'), name='unique_art_member')], - }, - ), - migrations.CreateModel( - name='ArtShowcase', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('description', models.CharField(max_length=200)), - ('art', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='showcase', to='game_dev.art')), - ], - options={ - 'constraints': [ - models.UniqueConstraint( - fields=('art',), - name='unique_artshowcase_per_art', - violation_error_message='Each art piece can only have one showcase.' - ) - ], - }, - ), - ] diff --git a/server/game_dev/serializers.py b/server/game_dev/serializers.py index 9a1e39db..b0199df8 100644 --- a/server/game_dev/serializers.py +++ b/server/game_dev/serializers.py @@ -18,16 +18,25 @@ class Meta: ######################################## -# Copied from issue-8-merge-40 therefore is just sample to work with + class ArtContributorSerializer(serializers.ModelSerializer): + art_id = serializers.IntegerField(source='art.id', read_only=True) + artwork_data = serializers.SerializerMethodField() member_id = serializers.IntegerField(source='member.id', read_only=True) member_name = serializers.CharField(source='member.name', read_only=True) class Meta: model = ArtContributor - fields = ['id', 'member_id', 'member_name', 'role'] - - + fields = ['id', 'role', 'art_id', 'member_id', 'member_name','artwork_data'] + + def get_artwork_data(self, obj): + art = obj.art + request = self.context.get('request') + return { + 'name': art.name, + 'description': art.description, + 'media': request.build_absolute_uri(art.media.url) if art.media and request else None + } class ArtSerializer(serializers.ModelSerializer): art_id = serializers.IntegerField(source='id', read_only=True) source_game_id = serializers.IntegerField(source='source_game.id', read_only=True) @@ -93,7 +102,7 @@ class GamesSerializer(serializers.ModelSerializer): class Meta: model = Game fields = ('id', 'name', 'description', 'completion', 'active', - 'hostURL', 'itchEmbedID', 'thumbnail', 'event', "contributors", "artworks", 'itchGameEmbedID', + 'hostURL', 'itchEmbedID', 'thumbnail', 'event', "contributors", "artworks", 'itchGamePlayableID', 'itchGameWidth', 'itchGameHeight') @@ -184,13 +193,7 @@ class Meta: ] -class ArtContributorSerializer(serializers.ModelSerializer): - member_id = serializers.IntegerField(source='member.id', read_only=True) - member_name = serializers.CharField(source='member.name', read_only=True) - class Meta: - model = ArtContributor - fields = ['id', 'member_id', 'member_name', 'role'] class ArtSerializer(serializers.ModelSerializer): diff --git a/server/game_dev/urls.py b/server/game_dev/urls.py index df09be06..87299ad2 100644 --- a/server/game_dev/urls.py +++ b/server/game_dev/urls.py @@ -1,6 +1,6 @@ from django.urls import path from .views import (ContributorGamesListAPIView, EventListAPIView, EventDetailAPIView, GamesDetailAPIView, - GameshowcaseAPIView, MemberAPIView, CommitteeAPIView, + GameshowcaseAPIView, MemberAPIView, CommitteeAPIView, ContributorArtListAPIView, FeatureArtAPIView, ArtDetailAPIView) @@ -12,7 +12,7 @@ path("games//", GamesDetailAPIView.as_view()), path("games/contributor//", ContributorGamesListAPIView.as_view()), - # Updated line for GameShowcase endpoint + path("arts/contributor//", ContributorArtListAPIView.as_view()), path("gameshowcase/", GameshowcaseAPIView.as_view(), name="gameshowcase-api"), 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 703be36f..4d26a2f0 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 ContributorGameSerializer, GamesSerializer, GameshowcaseSerializer, EventSerializer, MemberSerializer, ArtSerializer -from .models import Game, GameContributor, GameShowcase, Event, Member, Committee, Art +from .serializers import ContributorGameSerializer, GamesSerializer, GameshowcaseSerializer, EventSerializer, MemberSerializer, ArtSerializer, ArtContributorSerializer +from .models import Game, GameContributor, GameShowcase, Event, Member, Committee, Art, ArtContributor from django.utils import timezone from rest_framework.views import APIView from rest_framework.response import Response @@ -89,6 +89,13 @@ def get_queryset(self): member_id = self.kwargs.get("member") return GameContributor.objects.filter(member=member_id) +class ContributorArtListAPIView(generics.ListAPIView): + serializer_class = ArtContributorSerializer + + def get_queryset(self): + member_id = self.kwargs.get("member") + return ArtContributor.objects.filter(member=member_id) + class MemberAPIView(generics.RetrieveAPIView): serializer_class = MemberSerializer From f8ee1302513ee0f52fa9a49f7ab2527f00879bc5 Mon Sep 17 00:00:00 2001 From: samjjacko Date: Thu, 16 Apr 2026 16:09:57 +0800 Subject: [PATCH 120/122] added events documentation --- client/documentation/admin-dashboard/committee.md | 2 +- .../documentation/admin-dashboard/contributors.md | 14 ++++---------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/client/documentation/admin-dashboard/committee.md b/client/documentation/admin-dashboard/committee.md index 0dc62436..ffd30ac8 100644 --- a/client/documentation/admin-dashboard/committee.md +++ b/client/documentation/admin-dashboard/committee.md @@ -1,6 +1,6 @@ ## Committee Members -URL: `/admin/game_dev/committee/add/` +URL: `/admin/game_dev/committee/` Profiles of the Committee Members of the club that are displayed on the about page `/about`. diff --git a/client/documentation/admin-dashboard/contributors.md b/client/documentation/admin-dashboard/contributors.md index b01d8d11..efd72473 100644 --- a/client/documentation/admin-dashboard/contributors.md +++ b/client/documentation/admin-dashboard/contributors.md @@ -3,7 +3,7 @@ Models that associate a member object with another certain object as a contributor to it. There are two types: ## Game Contributor - +URL: `/admin/game_dev/gamecontributor/` A model that will associate a certain member with having contributed to a certain game. A Game Contributor object will be represented under the 'Contributors' section of a game's page as the name of the member who contributed. ## Fields @@ -20,17 +20,11 @@ You will see a little magnifying glass when selecting a Game and Member, click o ## Art Contributor - -A model that will associate a certain member with having contributed to a certain game. A Game Contributor object will be represented under the 'Contributors' section of a game's page as the name of the member who contributed. +* This can be found inline inside of an art entry, e.g. `/admin/game_dev/art/1/change/`. +A model that will associate a certain member with having contributed to a certain artwork. ## Fields -**Art:** Required field for the artwork that the contributor is for. It is an integer field that corresponds to the raw integer id of an Art object in the Art table, known as a Foreign Key. - **Member:** Required field for the member that is the contributor to the specified artwork. It is an integer field that corresponds to the raw integer id of a Member object in the Member table, known as a Foreign Key. -**Role:** Required character field for the description of the role that the person played in contributing to the artwork. Maximum length of 100 characters - -## Other Notes - -You will see a little magnifying glass when selecting an Art and a Member, click on it to see their respective table and be able choose the integer id's using the desired details. Though none of the individual fields are a primary key, the actual primary key is the unique (Art, Member) pair that each object forms. \ No newline at end of file +**Role:** Required character field for the description of the role that the person played in contributing to the artwork. Maximum length of 100 characters \ No newline at end of file From 0fc6ef3ff3c1adaf8dd839dedbc4f4490d96174d Mon Sep 17 00:00:00 2001 From: samjjacko Date: Thu, 16 Apr 2026 16:28:41 +0800 Subject: [PATCH 121/122] genuinely have no idea how duplicates emerged --- server/game_dev/admin.py | 30 ++++++- server/game_dev/models.py | 115 ++++++++++--------------- server/game_dev/serializers.py | 152 ++++++++++++++++++--------------- server/game_dev/views.py | 50 +++++++++-- 4 files changed, 195 insertions(+), 152 deletions(-) diff --git a/server/game_dev/admin.py b/server/game_dev/admin.py index 6e8983a9..01ff4f4b 100644 --- a/server/game_dev/admin.py +++ b/server/game_dev/admin.py @@ -1,5 +1,16 @@ from django.contrib import admin -from .models import Art, ArtContributor, ArtShowcase, Member, Game, Event, GameContributor, GameShowcase, Committee, SocialMedia +from .models import ( + Art, + ArtContributor, + ArtShowcase, + Member, + Game, + Event, + GameContributor, + GameShowcase, + Committee, + SocialMedia, +) class SocialMediaInline(admin.TabularInline): @@ -27,8 +38,19 @@ class GameShowcaseAdmin(admin.ModelAdmin): class GamesAdmin(admin.ModelAdmin): - list_display = ("id", "name", "description", "completion", "active", "hostURL", "itchEmbedID", "itchGamePlayableID", "itchGameWidth", - "itchGameHeight", "thumbnail") + list_display = ( + "id", + "name", + "description", + "completion", + "active", + "hostURL", + "itchEmbedID", + "itchGamePlayableID", + "itchGameWidth", + "itchGameHeight", + "thumbnail", + ) search_fields = ["name", "description"] raw_id_fields = ["event"] @@ -54,4 +76,4 @@ class ArtAdmin(admin.ModelAdmin): admin.site.register(GameShowcase, GameShowcaseAdmin) admin.site.register(Art, ArtAdmin) admin.site.register(ArtShowcase) -admin.site.register(Committee, CommitteeAdmin) \ No newline at end of file +admin.site.register(Committee, CommitteeAdmin) diff --git a/server/game_dev/models.py b/server/game_dev/models.py index 4c501ccb..81a75ebd 100644 --- a/server/game_dev/models.py +++ b/server/game_dev/models.py @@ -28,12 +28,16 @@ def __str__(self): # GameContributor table: links Game, Member, and role (composite PK) class GameContributor(models.Model): - game = models.ForeignKey('Game', on_delete=models.CASCADE, related_name='game_contributors') - member = models.ForeignKey('Member', on_delete=models.CASCADE, related_name='member_games') + game = models.ForeignKey( + "Game", on_delete=models.CASCADE, related_name="game_contributors" + ) + member = models.ForeignKey( + "Member", on_delete=models.CASCADE, related_name="member_games" + ) role = models.CharField(max_length=100) class Meta: - unique_together = (('game', 'member'),) + unique_together = (("game", "member"),) def __str__(self): return f"{self.member.name} ({self.role}) for {self.game.name}" @@ -60,13 +64,13 @@ class CompletionStatus(models.IntegerChoices): default=None, null=True, blank=True, - help_text="If game is stored on itch.io, please enter the itchEmbedID, i.e., 1000200" + help_text="If game is stored on itch.io, please enter the itchEmbedID, i.e., 1000200", ) itchGamePlayableID = models.PositiveBigIntegerField( default=None, null=True, blank=True, - help_text="If a game is playable and has a web demo stored on itch.io, please enter the embed developer ID" + help_text="If a game is playable and has a web demo stored on itch.io, please enter the embed developer ID", ) itchGameWidth = models.PositiveBigIntegerField( default=None, @@ -89,13 +93,23 @@ def clean(self): super().clean() if self.itchGamePlayableID: if not self.itchGameWidth: - raise ValidationError({"itchGameWidth": "Game width is required if itchGamePlayableID is set."}) + raise ValidationError( + { + "itchGameWidth": "Game width is required if itchGamePlayableID is set." + } + ) if not self.itchGameHeight: - raise ValidationError({"itchGameHeight": "Game height is required if itchGamePlayableID is set."}) + raise ValidationError( + { + "itchGameHeight": "Game height is required if itchGamePlayableID is set." + } + ) class GameShowcase(models.Model): - game = models.OneToOneField('Game', on_delete=models.CASCADE, related_name='game_showcases') + game = models.OneToOneField( + "Game", on_delete=models.CASCADE, related_name="game_showcases" + ) description = models.TextField() def __str__(self): @@ -104,9 +118,13 @@ def __str__(self): class Art(models.Model): name = models.CharField(null=False, max_length=200) - description = models.CharField(max_length=200,) - source_game = models.ForeignKey('Game', on_delete=models.CASCADE, related_name='game_artwork') - media = models.ImageField(upload_to='art/', null=False) + description = models.CharField( + max_length=200, + ) + source_game = models.ForeignKey( + "Game", on_delete=models.CASCADE, related_name="game_artwork" + ) + media = models.ImageField(upload_to="art/", null=False) active = models.BooleanField(default=True) def __str__(self): @@ -114,16 +132,20 @@ def __str__(self): class ArtContributor(models.Model): - art = models.ForeignKey('Art', on_delete=models.CASCADE, related_name='contributors') - member = models.ForeignKey('Member', on_delete=models.CASCADE, related_name='art_contributions') + art = models.ForeignKey( + "Art", on_delete=models.CASCADE, related_name="contributors" + ) + member = models.ForeignKey( + "Member", on_delete=models.CASCADE, related_name="art_contributions" + ) role = models.CharField(max_length=100) class Meta: constraints = [ - models.UniqueConstraint(fields=['art', 'member'], name='unique_art_member') + models.UniqueConstraint(fields=["art", "member"], name="unique_art_member") ] - verbose_name = 'Art Contributor' - verbose_name_plural = 'Art Contributors' + verbose_name = "Art Contributor" + verbose_name_plural = "Art Contributors" def __str__(self): return f"{self.member.name} - {self.art.name} ({self.role})" @@ -131,14 +153,15 @@ def __str__(self): class ArtShowcase(models.Model): description = models.CharField(max_length=200) - art = models.ForeignKey(Art, on_delete=models.CASCADE, related_name='showcase') + art = models.ForeignKey(Art, on_delete=models.CASCADE, related_name="showcase") class Meta: constraints = [ models.UniqueConstraint( - fields=['art'], - name='unique_artshowcase_per_art', - violation_error_message='Each art piece can only have one showcase.') + fields=["art"], + name="unique_artshowcase_per_art", + violation_error_message="Each art piece can only have one showcase.", + ) ] def __str__(self): @@ -147,7 +170,9 @@ def __str__(self): class SocialMedia(models.Model): link = models.URLField(max_length=2083) - member = models.ForeignKey('Member', on_delete=models.CASCADE, related_name='social_media_links') + member = models.ForeignKey( + "Member", on_delete=models.CASCADE, related_name="social_media_links" + ) socialMediaUserName = models.CharField(max_length=200, blank=True) def __str__(self): @@ -164,7 +189,7 @@ class Committee(models.Model): "MARK": "Marketing", "EV": "Events OCM", "PRO": "Projects OCM", - "FRE": "Fresher Rep" + "FRE": "Fresher Rep", } role = models.CharField(max_length=9, choices=roles, default="FRE", unique=True) @@ -173,49 +198,3 @@ def get_member(self): def __str__(self): return self.id.name - - -# Copied from issue-8-merge-40 therefore is just sample to work with -class Art(models.Model): - name = models.CharField(null=False, max_length=200) - description = models.CharField(max_length=200,) - - # Talk to the artwork team to change their model to meet the follow, remove the null and blank - source_game = models.ForeignKey('Game', on_delete=models.CASCADE, related_name='game_artwork') - media = models.ImageField(upload_to='art/', null=False) - active = models.BooleanField(default=True) - - def __str__(self): - return str(self.name) - - -class ArtContributor(models.Model): - art = models.ForeignKey('Art', on_delete=models.CASCADE, related_name='contributors') - member = models.ForeignKey('Member', on_delete=models.CASCADE, related_name='art_contributions') - role = models.CharField(max_length=100) - - class Meta: - constraints = [ - models.UniqueConstraint(fields=['art', 'member'], name='unique_art_member') - ] - verbose_name = 'Art Contributor' - verbose_name_plural = 'Art Contributors' - - def __str__(self): - return f"{self.member.name} - {self.art.name} ({self.role})" - - -class ArtShowcase(models.Model): - description = models.CharField(max_length=200) - art = models.ForeignKey(Art, on_delete=models.CASCADE, related_name='showcase') - - class Meta: - constraints = [ - models.UniqueConstraint( - fields=['art'], - name='unique_artshowcase_per_art', - violation_error_message='Each art piece can only have one showcase.') - ] - - def __str__(self): - return f"ArtShowcase[Art={str(self.art.name)}, Description={self.description}]" diff --git a/server/game_dev/serializers.py b/server/game_dev/serializers.py index b0199df8..e28ffe1d 100644 --- a/server/game_dev/serializers.py +++ b/server/game_dev/serializers.py @@ -1,5 +1,15 @@ from rest_framework import serializers -from .models import ArtShowcase, Event, Game, Art, ArtContributor, Member, GameShowcase, GameContributor, SocialMedia +from .models import ( + ArtShowcase, + Event, + Game, + Art, + ArtContributor, + Member, + GameShowcase, + GameContributor, + SocialMedia, +) class EventSerializer(serializers.ModelSerializer): @@ -19,47 +29,55 @@ class Meta: ######################################## + class ArtContributorSerializer(serializers.ModelSerializer): - art_id = serializers.IntegerField(source='art.id', read_only=True) - artwork_data = serializers.SerializerMethodField() - member_id = serializers.IntegerField(source='member.id', read_only=True) - member_name = serializers.CharField(source='member.name', read_only=True) + art_id = serializers.IntegerField(source="art.id", read_only=True) + artwork_data = serializers.SerializerMethodField() + member_id = serializers.IntegerField(source="member.id", read_only=True) + member_name = serializers.CharField(source="member.name", read_only=True) class Meta: model = ArtContributor - fields = ['id', 'role', 'art_id', 'member_id', 'member_name','artwork_data'] - + fields = ["id", "role", "art_id", "member_id", "member_name", "artwork_data"] + def get_artwork_data(self, obj): art = obj.art - request = self.context.get('request') + request = self.context.get("request") return { - 'name': art.name, - 'description': art.description, - 'media': request.build_absolute_uri(art.media.url) if art.media and request else None - } + "name": art.name, + "description": art.description, + "media": request.build_absolute_uri(art.media.url) + if art.media and request + else None, + } + + class ArtSerializer(serializers.ModelSerializer): - art_id = serializers.IntegerField(source='id', read_only=True) - source_game_id = serializers.IntegerField(source='source_game.id', read_only=True) - source_game_name = serializers.CharField(source='source_game.name', read_only=True) + art_id = serializers.IntegerField(source="id", read_only=True) + source_game_id = serializers.IntegerField(source="source_game.id", read_only=True) + source_game_name = serializers.CharField(source="source_game.name", read_only=True) contributors = ArtContributorSerializer(many=True, read_only=True) showcase_description = serializers.SerializerMethodField() class Meta: model = Art - fields = ['art_id', 'name', 'description', 'media', 'active', 'source_game_id', 'source_game_name', 'contributors', 'showcase_description'] + fields = [ + "art_id", + "name", + "description", + "media", + "active", + "source_game_id", + "source_game_name", + "contributors", + "showcase_description", + ] def get_showcase_description(self, obj): showcase = obj.showcase.first() return showcase.description if showcase else None -class ArtShowcaseSerializer(serializers.ModelSerializer): - art_name = serializers.CharField(source='art.name', read_only=True) - - class Meta: - model = ArtShowcase - fields = ['id', 'description', 'art', 'art_name'] - ######################################## @@ -81,29 +99,38 @@ def get_social_media(self, obj): # Copied ArtSerializer at kept data only needed instead of all of it class GameArtSerializer(serializers.ModelSerializer): - art_id = serializers.IntegerField(source='id', read_only=True) - source_game_id = serializers.IntegerField(source='source_game.id', read_only=True) + art_id = serializers.IntegerField(source="id", read_only=True) + source_game_id = serializers.IntegerField(source="source_game.id", read_only=True) class Meta: model = Art - fields = ['art_id', 'name', 'media', 'active', 'source_game_id'] + fields = ["art_id", "name", "media", "active", "source_game_id"] class GamesSerializer(serializers.ModelSerializer): contributors = GameContributorSerializer( many=True, source="game_contributors", read_only=True ) - artworks = GameArtSerializer( - many=True, - source="game_artwork", - read_only=True - ) + artworks = GameArtSerializer(many=True, source="game_artwork", read_only=True) class Meta: model = Game - fields = ('id', 'name', 'description', 'completion', 'active', - 'hostURL', 'itchEmbedID', 'thumbnail', 'event', "contributors", "artworks", 'itchGamePlayableID', - 'itchGameWidth', 'itchGameHeight') + fields = ( + "id", + "name", + "description", + "completion", + "active", + "hostURL", + "itchEmbedID", + "thumbnail", + "event", + "contributors", + "artworks", + "itchGamePlayableID", + "itchGameWidth", + "itchGameHeight", + ) # Contributor serializer for name and role @@ -136,8 +163,15 @@ class GameshowcaseSerializer(serializers.ModelSerializer): class Meta: model = GameShowcase - fields = ('game_id', 'game_name', 'game_description', - 'description', 'contributors', 'game_cover_thumbnail', 'artworks') + fields = ( + "game_id", + "game_name", + "game_description", + "description", + "contributors", + "game_cover_thumbnail", + "artworks", + ) def get_contributors(self, obj): # Always fetch contributors from GameContributor for the related game @@ -149,21 +183,23 @@ def get_artworks(self, obj): class ContributorGameSerializer(serializers.ModelSerializer): - game_id = serializers.IntegerField(source='game.id', read_only=True) + 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'] + fields = ["game_id", "role", "game_data"] def get_game_data(self, obj): game = obj.game - request = self.context.get('request') + request = self.context.get("request") return { - 'name': game.name, - 'description': game.description, - 'thumbnail': request.build_absolute_uri(game.thumbnail.url) if game.thumbnail and request else None + "name": game.name, + "description": game.description, + "thumbnail": request.build_absolute_uri(game.thumbnail.url) + if game.thumbnail and request + else None, } @@ -183,38 +219,12 @@ class MemberSerializer(serializers.ModelSerializer): class Meta: model = Member - fields = [ - "name", - "profile_picture", - "about", - "pronouns", - "social_media", - "pk" - ] - - - - - -class ArtSerializer(serializers.ModelSerializer): - art_id = serializers.IntegerField(source='id', read_only=True) - source_game_id = serializers.IntegerField(source='source_game.id', read_only=True) - source_game_name = serializers.CharField(source='source_game.name', read_only=True) - contributors = ArtContributorSerializer(many=True, read_only=True) - showcase_description = serializers.SerializerMethodField() - - class Meta: - model = Art - fields = ['art_id', 'name', 'description', 'media', 'active', 'source_game_id', 'source_game_name', 'contributors', 'showcase_description'] - - def get_showcase_description(self, obj): - showcase = obj.showcase.first() - return showcase.description if showcase else None + fields = ["name", "profile_picture", "about", "pronouns", "social_media", "pk"] class ArtShowcaseSerializer(serializers.ModelSerializer): - art_name = serializers.CharField(source='art.name', read_only=True) + art_name = serializers.CharField(source="art.name", read_only=True) class Meta: model = ArtShowcase - fields = ['id', 'description', 'art', 'art_name'] + fields = ["id", "description", "art", "art_name"] diff --git a/server/game_dev/views.py b/server/game_dev/views.py index 4d26a2f0..eaba7718 100644 --- a/server/game_dev/views.py +++ b/server/game_dev/views.py @@ -1,6 +1,23 @@ from rest_framework import generics -from .serializers import ContributorGameSerializer, GamesSerializer, GameshowcaseSerializer, EventSerializer, MemberSerializer, ArtSerializer, ArtContributorSerializer -from .models import Game, GameContributor, GameShowcase, Event, Member, Committee, Art, ArtContributor +from .serializers import ( + ContributorGameSerializer, + GamesSerializer, + GameshowcaseSerializer, + EventSerializer, + MemberSerializer, + ArtSerializer, + ArtContributorSerializer, +) +from .models import ( + Game, + GameContributor, + GameShowcase, + Event, + Member, + Committee, + Art, + ArtContributor, +) from django.utils import timezone from rest_framework.views import APIView from rest_framework.response import Response @@ -12,6 +29,7 @@ class GamesDetailAPIView(generics.RetrieveAPIView): """ GET /api/games// """ + serializer_class = GamesSerializer lookup_url_kwarg = "id" @@ -30,6 +48,7 @@ class EventListAPIView(generics.ListAPIView): GET /api/events/ Returns a paginated list of events (optionally filtered by time) """ + serializer_class = EventSerializer pagination_class = EventPagination @@ -51,14 +70,14 @@ def get_queryset(self): if type_param == "upcoming": return qs.filter(date__gte=now).order_by("date") - raise ValidationError( - {"type": "Invalid value. Use 'past' or 'upcoming'."}) + raise ValidationError({"type": "Invalid value. Use 'past' or 'upcoming'."}) class EventDetailAPIView(generics.RetrieveAPIView): """ GET /api/events// """ + serializer_class = EventSerializer lookup_url_kwarg = "id" @@ -72,13 +91,18 @@ def get_object(self): return queryset.get() except Event.DoesNotExist: from rest_framework.exceptions import NotFound - raise NotFound(detail="The event is not yet published by admin or does not exist.") + + raise NotFound( + detail="The event is not yet published by admin or does not exist." + ) class GameshowcaseAPIView(APIView): def get(self, request): showcases = GameShowcase.objects.all() - serializer = GameshowcaseSerializer(showcases, many=True, context={'request': request}) + serializer = GameshowcaseSerializer( + showcases, many=True, context={"request": request} + ) return Response(serializer.data) @@ -89,8 +113,9 @@ def get_queryset(self): member_id = self.kwargs.get("member") return GameContributor.objects.filter(member=member_id) + class ContributorArtListAPIView(generics.ListAPIView): - serializer_class = ArtContributorSerializer + serializer_class = ArtContributorSerializer def get_queryset(self): member_id = self.kwargs.get("member") @@ -111,8 +136,13 @@ class CommitteeAPIView(generics.ListAPIView): 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": "", "pk": 0} + placeholderMember = { + "name": "Position not filled", + "profile_picture": "url('/landing_placeholder.png')", + "about": "", + "pronouns": "", + "pk": 0, + } for i in roleOrder: try: cur = Committee.objects.get(role=i).id @@ -129,6 +159,7 @@ class ArtDetailAPIView(generics.RetrieveAPIView): """ GET /api/artworks// """ + serializer_class = ArtSerializer lookup_url_kwarg = "id" @@ -140,6 +171,7 @@ class FeatureArtAPIView(generics.ListAPIView): """ GET /api/arts/featured/ """ + serializer_class = ArtSerializer def get_queryset(self): From 82086652c329c3f70cd2dcfbd75eb867fb0b082b Mon Sep 17 00:00:00 2001 From: samjjacko Date: Thu, 16 Apr 2026 16:31:42 +0800 Subject: [PATCH 122/122] prettier --- client/src/components/ui/MemberProjectSection.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/components/ui/MemberProjectSection.tsx b/client/src/components/ui/MemberProjectSection.tsx index 723fba36..75aa4382 100644 --- a/client/src/components/ui/MemberProjectSection.tsx +++ b/client/src/components/ui/MemberProjectSection.tsx @@ -1,9 +1,9 @@ -import { ArrowUpRight , Gamepad2,Palette } from "lucide-react"; +import { ArrowUpRight, Gamepad2, Palette } from "lucide-react"; import Image from "next/image"; import Link from "next/link"; import React from "react"; -import type { ArtData,GameData } from "@/hooks/useContributor"; +import type { ArtData, GameData } from "@/hooks/useContributor"; export default function MemberProjectSection(props: { games: GameData[];