From 66f1c0d931452f5e0bcbc1d4970191157073cdc8 Mon Sep 17 00:00:00 2001 From: Karl_Sue Date: Tue, 25 Nov 2025 23:30:43 +0000 Subject: [PATCH 01/34] 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 9c26a56..54e9f29 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 02/34] 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 4185d36..69865a6 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 54e9f29..6fa4bb1 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 03/34] 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 1347bb7..c6a1618 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 69865a6..2cfeab2 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 0000000..138685d --- /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 6fa4bb1..ea132d6 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 0000000..768a4bf --- /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 0000000..d179949 --- /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 fd0e044..264448f 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 04/34] 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 0000000..897ccea --- /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 0000000..b3e6d92 --- /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 616c2b1..9079808 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 05/34] 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 0000000..34f928c --- /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 0000000..732f645 --- /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 0000000..b3e6d92 --- /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 616c2b1..9079808 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 06/34] 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 2cfeab2..0e8d557 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 07/34] 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 ea132d6..0735d3f 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 08/34] 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 c5b60fe..1402c10 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 732f645..ce89b26 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 b3e6d92..d0ca4af 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 9079808..53eb51b 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 0000000..9ccc389 --- /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 0000000..3587cbe --- /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 0000000..9e3b687 --- /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 768a4bf..5c21cbc 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 d179949..b6f94c2 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 264448f..bf68916 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 09/34] 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 ce89b26..a8f6170 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 10/34] 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 5c21cbc..54c5c7a 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 11/34] 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 a8f6170..00527cd 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 12/34] 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 00527cd..0aa8f7c 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 3587cbe..5a5397c 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 424f34e..f460957 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 54c5c7a..4890199 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 bf68916..86d0f0a 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 13/34] 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 d0ca4af..8a3586f 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 0000000..e5fa692 --- /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 f460957..4664e69 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 14/34] 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 0aa8f7c..b5c464d 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 15/34] 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 b5c464d..7cb0cea 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 16/34] 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 7cb0cea..95adacb 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 17/34] 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 0000000..4273aad --- /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 0000000..6d66335 --- /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 732f645..0afcd2a 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 18/34] 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 0000000..1008d07 --- /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 f5f7d8c..e34fbdd 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 19/34] 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 1008d07..eca1cee 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 d37ad25..15d1681 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 20/34] 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 eca1cee..44d0136 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 15d1681..ac8ecc3 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 21/34] 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 4664e69..4690275 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 c6a1618..073e0e7 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 22/34] 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 53eb51b..43a7421 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 23/34] 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 0e8d557..b148cac 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 24/34] 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 4273aad..22109a4 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 6d66335..f3ca51b 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 0000000..b7e25e5 --- /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 ac8ecc3..8e3b89b 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 25/34] 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 0000000..d5ff49d --- /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 8e3b89b..29c6a36 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 ab5542d..6388ccd 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 43a7421..8128755 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 26/34] 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 44d0136..0000000 --- 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 27/34] 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 b25a62b..67a0dbe 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 6388ccd..a29a78d 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 58e968d..c2d8a5e 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 28/34] 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 659373c..d917397 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 70e5bfb..313093f 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 29/34] 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 cf0f0ca..f61b3ed 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 a29a78d..4211949 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 0836bc2..8cf2f38 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 30/34] 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 dd0e67e..fb713c8 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 31/34] 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 282ace1..aab552a 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 32/34] 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 aab552a..202b5e9 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 33/34] 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 34/34] 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 138685d..f3d0c90 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 = [