From 66f1c0d931452f5e0bcbc1d4970191157073cdc8 Mon Sep 17 00:00:00 2001 From: Karl_Sue Date: Tue, 25 Nov 2025 23:30:43 +0000 Subject: [PATCH 001/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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/102] 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 7e445c9eaa5f86ef35dcc406716e2e018618bea2 Mon Sep 17 00:00:00 2001 From: Tuan Khanh Hoang Date: Sat, 14 Feb 2026 08:01:41 +0000 Subject: [PATCH 083/102] 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 084/102] 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 085/102] 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 086/102] 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 087/102] 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 088/102] 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 089/102] 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 090/102] 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 091/102] 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 092/102] 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 093/102] 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 094/102] 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 095/102] 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 096/102] 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 097/102] 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 98880aa3bf4be1c4a576aa4859d489de46552dd2 Mon Sep 17 00:00:00 2001 From: Tuan Khanh Hoang Date: Sat, 21 Feb 2026 07:29:32 +0000 Subject: [PATCH 099/102] 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 100/102] 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 (