diff --git a/client/src/components/ui/GameArtCarousel.tsx b/client/src/components/ui/GameArtCarousel.tsx new file mode 100644 index 00000000..0633accb --- /dev/null +++ b/client/src/components/ui/GameArtCarousel.tsx @@ -0,0 +1,124 @@ +// This carousel is for Artworks to be displayed in the Gameshowcase + +import { ChevronLeft, ChevronRight } from "lucide-react"; +import Image from "next/image"; +import Link from "next/link"; +import { useEffect, useRef, useState } from "react"; + +import type { UiArtwork } from "@/hooks/useGames"; + +// import { UiEvent as EventType } from "@/hooks/useEvents"; + +type GameArtCarouselProps = { + items: UiArtwork[]; +}; + +const GAP = 20; +const maxItemsPerPage = 4; + +export default function GameArtCarousel({ items }: GameArtCarouselProps) { + const firstItemRef = useRef(null); + + const [currentIndex, setCurrentIndex] = useState(0); + const [itemWidth, setItemWidth] = useState(0); + const [visibleCount, setVisibleCount] = useState(maxItemsPerPage); + + const maxIndex = Math.max(items.length - visibleCount, 0); + + const slideLeft = () => { + setCurrentIndex((prev) => Math.max(prev - 1, 0)); + }; + + const slideRight = () => { + setCurrentIndex((prev) => Math.min(prev + 1, maxIndex)); + }; + + const translateX = -(currentIndex * (itemWidth + GAP)); + + useEffect(() => { + if (!firstItemRef.current) return; + + const observer = new ResizeObserver(() => { + const width = firstItemRef.current?.clientWidth ?? 0; + setItemWidth(width); + }); + + observer.observe(firstItemRef.current); + + return () => observer.disconnect(); + }, []); + + useEffect(() => { + const updateVisibleCount = () => { + if (window.innerWidth < 768) { + setVisibleCount(1); + } else { + setVisibleCount(maxItemsPerPage); + } + }; + + updateVisibleCount(); + window.addEventListener("resize", updateVisibleCount); + + return () => window.removeEventListener("resize", updateVisibleCount); + }, []); + + return ( +
+
+ {/* LEFT ARROW */} + + + {/* VIEWPORT */} +
+
+ {items.map((art, index) => ( +
+ +
+ {art.name} +
+ + +

{art.name}

+
+ ))} +
+
+ + {/* RIGHT ARROW */} + +
+
+ ); +} diff --git a/client/src/hooks/useGames.ts b/client/src/hooks/useGames.ts index ddc4d839..ff5587d0 100644 --- a/client/src/hooks/useGames.ts +++ b/client/src/hooks/useGames.ts @@ -14,6 +14,21 @@ type Contributor = { }>; }; +export type UiArtwork = { + id: number; + name: string; + image: string; + sourceGameId: number; +}; + +export type ApiArtworks = { + art_id: number; + name: string; + media: string; + active: boolean; + source_game_id: number; +}; + type ApiGame = { name: string; description: string; @@ -28,10 +43,12 @@ type ApiGame = { itchGameWidth: number; itchGameHeight: number; contributors: Contributor[]; + artworks: ApiArtworks[]; }; -type UiGame = Omit & { +type UiGame = Omit & { gameCover: string; + artworks: UiArtwork[]; }; /** @@ -49,6 +66,12 @@ function transformApiGameToUiGame(data: ApiGame): UiGame { return { ...data, gameCover: data.thumbnail ?? "/game_dev_club_logo.svg", + artworks: data.artworks.map((a) => ({ + id: a.art_id, + name: a.name, + image: a.media, + sourceGameId: a.source_game_id, + })), }; } diff --git a/client/src/hooks/useGameshowcase.ts b/client/src/hooks/useGameshowcase.ts index ca0a96b6..c3e2d17e 100644 --- a/client/src/hooks/useGameshowcase.ts +++ b/client/src/hooks/useGameshowcase.ts @@ -1,6 +1,7 @@ import { useQuery } from "@tanstack/react-query"; import { AxiosError } from "axios"; +import type { ApiArtworks, UiArtwork } from "@/hooks/useGames"; import api from "@/lib/api"; type Contributor = { @@ -20,15 +21,18 @@ type ApiShowcaseGame = { game_description: string; contributors: Contributor[]; game_cover_thumbnail?: string | null; + artworks: ApiArtworks[]; }; -type UiShowcaseGame = Omit & { +type UiShowcaseGame = Omit< + ApiShowcaseGame, + "game_cover_thumbnail" | "artworks" +> & { gameCover: string; + artworks: UiArtwork[]; }; -function getGameCoverUrl( - game_cover_thumbnail: string | null | undefined, -): string { +function getMediaUrl(game_cover_thumbnail: string | null | undefined): string { if (!game_cover_thumbnail) return "/game_dev_club_logo.svg"; if (game_cover_thumbnail.startsWith("http")) return game_cover_thumbnail; // Use environment variable for Django backend base URL @@ -40,7 +44,13 @@ function getGameCoverUrl( function transformApiShowcaseGameToUi(data: ApiShowcaseGame): UiShowcaseGame { return { ...data, - gameCover: getGameCoverUrl(data.game_cover_thumbnail), + gameCover: getMediaUrl(data.game_cover_thumbnail), + artworks: data.artworks.map((a) => ({ + id: a.art_id, + name: a.name, + image: getMediaUrl(a.media), + sourceGameId: a.source_game_id, + })), }; } diff --git a/client/src/pages/_app.tsx b/client/src/pages/_app.tsx index 748e87cc..aa09d857 100644 --- a/client/src/pages/_app.tsx +++ b/client/src/pages/_app.tsx @@ -5,8 +5,8 @@ import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import type { AppProps } from "next/app"; import { Fira_Code, Inter as FontSans, Jersey_10 } from "next/font/google"; -import Navbar from "@/components/main/Navbar"; import Footer from "@/components/main/Footer"; +import Navbar from "@/components/main/Navbar"; import { ExplosionProvider } from "@/contexts/ExplosionContext"; const fontSans = FontSans({ diff --git a/client/src/pages/games/[id].tsx b/client/src/pages/games/[id].tsx index 5c110783..66d9a243 100644 --- a/client/src/pages/games/[id].tsx +++ b/client/src/pages/games/[id].tsx @@ -3,6 +3,7 @@ import { useRouter } from "next/router"; import React from "react"; import { SocialIcon } from "react-social-icons"; +import GameArtCarousel from "@/components/ui/GameArtCarousel"; import { GameEmbed } from "@/components/ui/GameEmbed"; import { ItchEmbed } from "@/components/ui/ItchEmbed"; import { useGame } from "@/hooks/useGames"; @@ -67,22 +68,6 @@ export default function IndividualGamePage() { // TODO ADD EVENT const event = "Game Jam November 2025"; - // TODO ADD ARTIMAGES - const artImages: { src: string; alt: string }[] = []; - // const artImages = [ - // { - // src: "https://upload.wikimedia.org/wikipedia/commons/thumb/a/aa/Minecraft_Zombie.png/120px-Minecraft_Zombie.png", - // alt: "Minecraft Zombie", - // }, - // { - // src: "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d8/Minecraft_Enderman.png/120px-Minecraft_Enderman.png", - // alt: "Minecraft Enderman", - // }, - // { - // src: "https://upload.wikimedia.org/wikipedia/en/thumb/1/17/Minecraft_explore_landscape.png/375px-Minecraft_explore_landscape.png", - // alt: "Minecraft Landscape", - // }, - // ]; return (
@@ -191,21 +176,7 @@ export default function IndividualGamePage() {

ARTWORK

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

No games available.

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

- - - {showcase.game_name} - - -

- {/* Comments from committes */} -

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

-

- Contributors -

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

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

+ {/* Comments from committes */} +

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

+
+
+
+

+ Contributors +

+
    + {showcase.contributors.map((contributor, cidx) => ( +
  • +
    + + {contributor.name} + + + {contributor.role} + +
    + {/* Social icons placeholder */} + {/* TODO: Add actual links */} + + {/* Social icons using react-social-icons */} + + + + +
  • + ))} +
+
-
-
- {showcase.game_description} + + {/* Game Art Carousel */} + + + {/* Description */} +
+ {showcase.game_description} +
))} diff --git a/server/api/settings.py b/server/api/settings.py index 1f4df893..5c93d765 100644 --- a/server/api/settings.py +++ b/server/api/settings.py @@ -30,6 +30,7 @@ SECRET_KEY = os.environ.get("API_SECRET_KEY") # SECURITY WARNING: don't run with debug turned on in production! +# export APP_ENV=DEVELOPMENT DEBUG = os.environ.get("APP_ENV") == "DEVELOPMENT" ALLOWED_HOSTS = ( @@ -65,6 +66,8 @@ "corsheaders.middleware.CorsMiddleware", ] + +# export FRONTEND_URL=http://localhost:3000 CORS_ALLOWED_ORIGINS = [ "http://localhost:3000", "http://127.0.0.1:3000", diff --git a/server/game_dev/admin.py b/server/game_dev/admin.py index a50a46a4..8f12723a 100644 --- a/server/game_dev/admin.py +++ b/server/game_dev/admin.py @@ -6,6 +6,9 @@ class SocialMediaInline(admin.TabularInline): model = SocialMedia extra = 1 +# from issue-8-merge-40 temp need changes +from .models import Art, ArtContributor, ArtShowcase + class MemberAdmin(admin.ModelAdmin): list_display = ("id", "name", "active", "profile_picture", "about", "pronouns") @@ -36,9 +39,24 @@ class CommitteeAdmin(admin.ModelAdmin): raw_id_fields = ["id"] +# from issue-8-merge-40 temp need changes +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(Committee, CommitteeAdmin) + +# from issue-8-merge-40 temp need changes +admin.site.register(Art, ArtAdmin) +# admin.site.register(ArtContributor) +admin.site.register(ArtShowcase) diff --git a/server/game_dev/migrations/0005_alter_member_profile_picture.py b/server/game_dev/migrations/0005_alter_member_profile_picture.py deleted file mode 100644 index 76031d25..00000000 --- a/server/game_dev/migrations/0005_alter_member_profile_picture.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.1.15 on 2026-01-18 15:01 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("game_dev", "0004_alter_event_date"), - ] - - operations = [ - migrations.AlterField( - model_name="member", - name="profile_picture", - field=models.ImageField(blank=True, null=True, upload_to="profiles/"), - ), - ] diff --git a/server/game_dev/migrations/0005_committee.py b/server/game_dev/migrations/0005_committee.py deleted file mode 100644 index 271dee18..00000000 --- a/server/game_dev/migrations/0005_committee.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 5.1.15 on 2026-01-09 09:46 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('game_dev', '0004_alter_event_date'), - ] - - operations = [ - migrations.CreateModel( - name='Committee', - fields=[ - ('id', models.OneToOneField(on_delete=django.db.models.deletion.DO_NOTHING, primary_key=True, serialize=False, to='game_dev.member')), - ], - ), - ] diff --git a/server/game_dev/migrations/0005_alter_event_date_game_gameshowcase_gamecontributor.py b/server/game_dev/migrations/0005_committee_alter_member_profile_picture_game_and_more.py similarity index 70% rename from server/game_dev/migrations/0005_alter_event_date_game_gameshowcase_gamecontributor.py rename to server/game_dev/migrations/0005_committee_alter_member_profile_picture_game_and_more.py index 89ea924d..de6f2910 100644 --- a/server/game_dev/migrations/0005_alter_event_date_game_gameshowcase_gamecontributor.py +++ b/server/game_dev/migrations/0005_committee_alter_member_profile_picture_game_and_more.py @@ -1,7 +1,6 @@ -# Generated by Django 5.1.14 on 2026-01-24 06:27 +# Generated by Django 5.1.14 on 2026-02-07 07:47 import django.db.models.deletion -import django.db.models.functions.datetime from django.db import migrations, models @@ -12,12 +11,42 @@ class Migration(migrations.Migration): ] operations = [ + migrations.CreateModel( + name="Committee", + fields=[ + ( + "id", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + primary_key=True, + serialize=False, + to="game_dev.member", + ), + ), + ( + "role", + models.CharField( + choices=[ + ("P", "President"), + ("VP", "Vice-President"), + ("SEC", "Secretary"), + ("TRE", "Treasurer"), + ("MARK", "Marketing"), + ("EV", "Events OCM"), + ("PRO", "Projects OCM"), + ("FRE", "Fresher Rep"), + ], + default="FRE", + max_length=9, + unique=True, + ), + ), + ], + ), migrations.AlterField( - model_name="event", - name="date", - field=models.DateTimeField( - db_default=django.db.models.functions.datetime.Now() - ), + model_name="member", + name="profile_picture", + field=models.ImageField(blank=True, null=True, upload_to="profiles/"), ), migrations.CreateModel( name="Game", @@ -46,18 +75,17 @@ class Migration(migrations.Migration): ), ), ("active", models.BooleanField(default=True)), - ("hostURL", models.CharField(max_length=2083)), - ("isItch", models.BooleanField(default=True)), + ("hostURL", models.URLField(max_length=2083)), ( "itchEmbedID", models.PositiveIntegerField( blank=True, default=None, - help_text="If game is stored on itch.io, please enter the 7 digit long game id as its itchEmbedID, i.e., 1000200", + help_text="If game is stored on itch.io, please enter the itchEmbedID, i.e., 1000200", null=True, ), ), - ("pathToThumbnail", models.ImageField(null=True, upload_to="games/")), + ("thumbnail", models.ImageField(null=True, upload_to="games/")), ( "event", models.ForeignKey( @@ -88,6 +116,7 @@ class Migration(migrations.Migration): on_delete=django.db.models.deletion.CASCADE, related_name="game_showcases", to="game_dev.game", + unique=True, ), ), ], diff --git a/server/game_dev/migrations/0006_alter_gameshowcase_game.py b/server/game_dev/migrations/0006_alter_gameshowcase_game.py new file mode 100644 index 00000000..c4abf9ba --- /dev/null +++ b/server/game_dev/migrations/0006_alter_gameshowcase_game.py @@ -0,0 +1,23 @@ +# Generated by Django 5.1.14 on 2026-02-14 05:05 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("game_dev", "0005_committee_alter_member_profile_picture_game_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="gameshowcase", + name="game", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="game_showcases", + to="game_dev.game", + ), + ), + ] diff --git a/server/game_dev/migrations/0006_committee_role.py b/server/game_dev/migrations/0006_committee_role.py deleted file mode 100644 index 457afa83..00000000 --- a/server/game_dev/migrations/0006_committee_role.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 5.1.15 on 2026-01-09 09:59 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('game_dev', '0005_committee'), - ] - - operations = [ - migrations.AddField( - model_name='committee', - name='role', - field=models.CharField(choices=[('P', 'President'), ('VP', 'Vice-President'), ('SEC', 'Secretary'), - ('TRE', 'Treasurer'), ('MARK', 'Marketing'), ('EV', 'Events OCM'), - ('PRO', 'Projects OCM'), ('FRE', 'Fresher Rep')], default='FRE', max_length=9), - ), - ] diff --git a/server/game_dev/migrations/0006_remove_game_isitch_alter_game_itchembedid.py b/server/game_dev/migrations/0006_remove_game_isitch_alter_game_itchembedid.py deleted file mode 100644 index f6c45bc2..00000000 --- a/server/game_dev/migrations/0006_remove_game_isitch_alter_game_itchembedid.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 5.1.14 on 2026-01-24 07:10 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("game_dev", "0005_alter_event_date_game_gameshowcase_gamecontributor"), - ] - - operations = [ - migrations.RemoveField( - model_name="game", - name="isItch", - ), - migrations.AlterField( - model_name="game", - name="itchEmbedID", - field=models.PositiveIntegerField( - blank=True, - default=None, - help_text="If game is stored on itch.io, please enter the itchEmbedID, i.e., 1000200", - null=True, - ), - ), - ] diff --git a/server/game_dev/migrations/0007_alter_committee_id.py b/server/game_dev/migrations/0007_alter_committee_id.py deleted file mode 100644 index fa9a4841..00000000 --- a/server/game_dev/migrations/0007_alter_committee_id.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 5.1.15 on 2026-01-21 07:59 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('game_dev', '0006_committee_role'), - ] - - operations = [ - migrations.AlterField( - model_name='committee', - name='id', - field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='game_dev.member'), - ), - ] diff --git a/server/game_dev/migrations/0007_art_artcontributor_artshowcase.py b/server/game_dev/migrations/0007_art_artcontributor_artshowcase.py new file mode 100644 index 00000000..69f5b56c --- /dev/null +++ b/server/game_dev/migrations/0007_art_artcontributor_artshowcase.py @@ -0,0 +1,114 @@ +# Generated by Django 5.1.14 on 2026-02-14 06:13 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("game_dev", "0006_alter_gameshowcase_game"), + ] + + operations = [ + migrations.CreateModel( + name="Art", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=200)), + ("description", models.CharField(max_length=200)), + ("media", models.ImageField(upload_to="art/")), + ("active", models.BooleanField(default=True)), + ( + "source_game", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="game_artwork", + to="game_dev.game", + ), + ), + ], + ), + migrations.CreateModel( + name="ArtContributor", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("role", models.CharField(max_length=100)), + ( + "art", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="contributors", + to="game_dev.art", + ), + ), + ( + "member", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="art_contributions", + to="game_dev.member", + ), + ), + ], + options={ + "verbose_name": "Art Contributor", + "verbose_name_plural": "Art Contributors", + "constraints": [ + models.UniqueConstraint( + fields=("art", "member"), name="unique_art_member" + ) + ], + }, + ), + migrations.CreateModel( + name="ArtShowcase", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("description", models.CharField(max_length=200)), + ( + "art", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="showcase", + to="game_dev.art", + ), + ), + ], + options={ + "constraints": [ + models.UniqueConstraint( + fields=("art",), + name="unique_artshowcase_per_art", + violation_error_message="Each art piece can only have one showcase.", + ) + ], + }, + ), + ] diff --git a/server/game_dev/migrations/0007_rename_pathtothumbnail_game_thumbnail_and_more.py b/server/game_dev/migrations/0007_rename_pathtothumbnail_game_thumbnail_and_more.py deleted file mode 100644 index d2b7f58d..00000000 --- a/server/game_dev/migrations/0007_rename_pathtothumbnail_game_thumbnail_and_more.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.1.14 on 2026-01-24 07:17 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("game_dev", "0006_remove_game_isitch_alter_game_itchembedid"), - ] - - operations = [ - migrations.RenameField( - model_name="game", - old_name="pathToThumbnail", - new_name="thumbnail", - ), - migrations.AlterField( - model_name="game", - name="hostURL", - field=models.URLField(max_length=2083), - ), - ] diff --git a/server/game_dev/migrations/0010_socialmedia.py b/server/game_dev/migrations/0008_alter_art_source_game_socialmedia.py similarity index 59% rename from server/game_dev/migrations/0010_socialmedia.py rename to server/game_dev/migrations/0008_alter_art_source_game_socialmedia.py index f266b880..9343b231 100644 --- a/server/game_dev/migrations/0010_socialmedia.py +++ b/server/game_dev/migrations/0008_alter_art_source_game_socialmedia.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.14 on 2026-01-31 06:18 +# Generated by Django 5.1.14 on 2026-02-21 03:15 import django.db.models.deletion from django.db import migrations, models @@ -7,10 +7,19 @@ class Migration(migrations.Migration): dependencies = [ - ("game_dev", "0009_merge_20260131_1044"), + ("game_dev", "0007_art_artcontributor_artshowcase"), ] operations = [ + migrations.AlterField( + model_name="art", + name="source_game", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="game_artwork", + to="game_dev.game", + ), + ), migrations.CreateModel( name="SocialMedia", fields=[ @@ -23,21 +32,6 @@ class Migration(migrations.Migration): verbose_name="ID", ), ), - ( - "socialMediaName", - models.CharField( - choices=[ - ("TWITTER", "Twitter"), - ("FACEBOOK", "Facebook"), - ("INSTAGRAM", "Instagram"), - ("LINKEDIN", "LinkedIn"), - ("GITHUB", "GitHub"), - ("OTHER", "Other"), - ], - default="OTHER", - max_length=20, - ), - ), ("link", models.URLField(max_length=2083)), ("socialMediaUserName", models.CharField(blank=True, max_length=200)), ( diff --git a/server/game_dev/migrations/0008_alter_committee_role.py b/server/game_dev/migrations/0008_alter_committee_role.py deleted file mode 100644 index ecd84c0c..00000000 --- a/server/game_dev/migrations/0008_alter_committee_role.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 5.1.15 on 2026-01-21 08:11 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('game_dev', '0007_alter_committee_id'), - ] - - operations = [ - migrations.AlterField( - model_name='committee', - name='role', - field=models.CharField(choices=[('P', 'President'), ('VP', 'Vice-President'), ('SEC', 'Secretary'), - ('TRE', 'Treasurer'), ('MARK', 'Marketing'), ('EV', 'Events OCM'), - ('PRO', 'Projects OCM'), ('FRE', 'Fresher Rep')], default='FRE', max_length=9, unique=True), - ), - ] diff --git a/server/game_dev/migrations/0008_alter_event_date.py b/server/game_dev/migrations/0008_alter_event_date.py deleted file mode 100644 index 13c0a175..00000000 --- a/server/game_dev/migrations/0008_alter_event_date.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.1.14 on 2026-01-24 07:28 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("game_dev", "0007_rename_pathtothumbnail_game_thumbnail_and_more"), - ] - - operations = [ - migrations.AlterField( - model_name="event", - name="date", - field=models.DateTimeField(), - ), - ] diff --git a/server/game_dev/migrations/0009_merge_20260129_2104.py b/server/game_dev/migrations/0009_merge_20260129_2104.py deleted file mode 100644 index c7d68a0d..00000000 --- a/server/game_dev/migrations/0009_merge_20260129_2104.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 5.1.15 on 2026-01-29 13:04 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('game_dev', '0005_alter_member_profile_picture'), - ('game_dev', '0008_alter_committee_role'), - ] - - operations = [ - ] diff --git a/server/game_dev/migrations/0009_merge_20260131_1044.py b/server/game_dev/migrations/0009_merge_20260131_1044.py deleted file mode 100644 index 1abbcd99..00000000 --- a/server/game_dev/migrations/0009_merge_20260131_1044.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 5.1.15 on 2026-01-31 02:44 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('game_dev', '0005_alter_member_profile_picture'), - ('game_dev', '0008_alter_event_date'), - ] - - operations = [ - ] diff --git a/server/game_dev/migrations/0010_merge_20260131_1118.py b/server/game_dev/migrations/0010_merge_20260131_1118.py deleted file mode 100644 index 35027726..00000000 --- a/server/game_dev/migrations/0010_merge_20260131_1118.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 5.1.15 on 2026-01-31 03:18 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('game_dev', '0009_merge_20260129_2104'), - ('game_dev', '0009_merge_20260131_1044'), - ] - - operations = [ - ] diff --git a/server/game_dev/migrations/0011_alter_gameshowcase_game.py b/server/game_dev/migrations/0011_alter_gameshowcase_game.py deleted file mode 100644 index 120447ce..00000000 --- a/server/game_dev/migrations/0011_alter_gameshowcase_game.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 5.1.15 on 2026-02-11 06:58 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('game_dev', '0010_merge_20260131_1118'), - ] - - operations = [ - migrations.AlterField( - model_name='gameshowcase', - name='game', - field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='game_showcases', to='game_dev.game'), - ), - ] diff --git a/server/game_dev/migrations/0011_merge_0010_merge_20260131_1118_0010_socialmedia.py b/server/game_dev/migrations/0011_merge_0010_merge_20260131_1118_0010_socialmedia.py deleted file mode 100644 index 2034020c..00000000 --- a/server/game_dev/migrations/0011_merge_0010_merge_20260131_1118_0010_socialmedia.py +++ /dev/null @@ -1,13 +0,0 @@ -# Generated by Django 5.1.14 on 2026-02-03 03:27 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ("game_dev", "0010_merge_20260131_1118"), - ("game_dev", "0010_socialmedia"), - ] - - operations = [] diff --git a/server/game_dev/migrations/0012_remove_socialmedia_socialmedianame.py b/server/game_dev/migrations/0012_remove_socialmedia_socialmedianame.py deleted file mode 100644 index e51428a5..00000000 --- a/server/game_dev/migrations/0012_remove_socialmedia_socialmedianame.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 5.1.14 on 2026-02-07 03:31 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ("game_dev", "0011_merge_0010_merge_20260131_1118_0010_socialmedia"), - ] - - operations = [ - migrations.RemoveField( - model_name="socialmedia", - name="socialMediaName", - ), - ] diff --git a/server/game_dev/migrations/0013_merge_20260214_1347.py b/server/game_dev/migrations/0013_merge_20260214_1347.py deleted file mode 100644 index 9a1c3592..00000000 --- a/server/game_dev/migrations/0013_merge_20260214_1347.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 5.1.15 on 2026-02-14 05:47 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('game_dev', '0011_alter_gameshowcase_game'), - ('game_dev', '0012_remove_socialmedia_socialmedianame'), - ] - - operations = [ - ] diff --git a/server/game_dev/models.py b/server/game_dev/models.py index d626275d..c3785ded 100644 --- a/server/game_dev/models.py +++ b/server/game_dev/models.py @@ -118,3 +118,49 @@ def get_member(self): def __str__(self): return self.id.name + + +# Copied from issue-8-merge-40 therefore is just sample to work with +class Art(models.Model): + name = models.CharField(null=False, max_length=200) + description = models.CharField(max_length=200,) + + # Talk to the artwork team to change their model to meet the follow, remove the null and blank + source_game = models.ForeignKey('Game', on_delete=models.CASCADE, related_name='game_artwork') + media = models.ImageField(upload_to='art/', null=False) + active = models.BooleanField(default=True) + + def __str__(self): + return str(self.name) + + +class ArtContributor(models.Model): + art = models.ForeignKey('Art', on_delete=models.CASCADE, related_name='contributors') + member = models.ForeignKey('Member', on_delete=models.CASCADE, related_name='art_contributions') + role = models.CharField(max_length=100) + + class Meta: + constraints = [ + models.UniqueConstraint(fields=['art', 'member'], name='unique_art_member') + ] + verbose_name = 'Art Contributor' + verbose_name_plural = 'Art Contributors' + + def __str__(self): + return f"{self.member.name} - {self.art.name} ({self.role})" + + +class ArtShowcase(models.Model): + description = models.CharField(max_length=200) + art = models.ForeignKey(Art, on_delete=models.CASCADE, related_name='showcase') + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=['art'], + name='unique_artshowcase_per_art', + violation_error_message='Each art piece can only have one showcase.') + ] + + def __str__(self): + return f"ArtShowcase[Art={str(self.art.name)}, Description={self.description}]" diff --git a/server/game_dev/serializers.py b/server/game_dev/serializers.py index c576aaa3..d47f9bb5 100644 --- a/server/game_dev/serializers.py +++ b/server/game_dev/serializers.py @@ -1,5 +1,7 @@ from rest_framework import serializers from .models import Event, Game, Member, GameShowcase, GameContributor, SocialMedia +# from issue-8-merge-40 temp need changes +from .models import Art, ArtContributor, ArtShowcase class EventSerializer(serializers.ModelSerializer): @@ -16,6 +18,43 @@ class Meta: ] +######################################## +# Copied from issue-8-merge-40 therefore is just sample to work with +class ArtContributorSerializer(serializers.ModelSerializer): + member_id = serializers.IntegerField(source='member.id', read_only=True) + member_name = serializers.CharField(source='member.name', read_only=True) + + class Meta: + model = ArtContributor + fields = ['id', 'member_id', 'member_name', 'role'] + + +class ArtSerializer(serializers.ModelSerializer): + art_id = serializers.IntegerField(source='id', read_only=True) + source_game_id = serializers.IntegerField(source='source_game.id', read_only=True) + source_game_name = serializers.CharField(source='source_game.name', read_only=True) + contributors = ArtContributorSerializer(many=True, read_only=True) + showcase_description = serializers.SerializerMethodField() + + class Meta: + model = Art + fields = ['art_id', 'name', 'description', 'media', 'active', 'source_game_id', 'source_game_name', 'contributors', 'showcase_description'] + + def get_showcase_description(self, obj): + showcase = obj.showcase.first() + return showcase.description if showcase else None + + +class ArtShowcaseSerializer(serializers.ModelSerializer): + art_name = serializers.CharField(source='art.name', read_only=True) + + class Meta: + model = ArtShowcase + fields = ['id', 'description', 'art', 'art_name'] + +######################################## + + # This is child serializer of GameSerializer class GameContributorSerializer(serializers.ModelSerializer): # to link contributors to their member/[id] page @@ -32,17 +71,33 @@ def get_social_media(self, obj): return SocialMediaSerializer(social_links, many=True).data +# Copied ArtSerializer at kept data only needed instead of all of it +class GameArtSerializer(serializers.ModelSerializer): + art_id = serializers.IntegerField(source='id', read_only=True) + source_game_id = serializers.IntegerField(source='source_game.id', read_only=True) + + class Meta: + model = Art + fields = ['art_id', 'name', 'media', 'active', 'source_game_id'] + + class GamesSerializer(serializers.ModelSerializer): contributors = GameContributorSerializer( many=True, source="game_contributors", read_only=True ) + artworks = GameArtSerializer( + many=True, + source="game_artwork", + read_only=True + ) class Meta: model = Game - fields = ('id', 'name', 'description', 'completion', 'active', 'hostURL', 'itchEmbedID', 'thumbnail', 'event', 'itchGameEmbedID', - 'itchGameWidth', 'itchGameHeight', "contributors") + fields = ('id', 'name', 'description', 'completion', 'active', + 'hostURL', 'itchEmbedID', 'thumbnail', 'event', "contributors", "artworks", 'itchGameEmbedID', + 'itchGameWidth', 'itchGameHeight') # Contributor serializer for name and role @@ -70,17 +125,21 @@ class GameshowcaseSerializer(serializers.ModelSerializer): game_cover_thumbnail = serializers.ImageField( source='game.thumbnail', read_only=True) contributors = serializers.SerializerMethodField() + artworks = serializers.SerializerMethodField() class Meta: model = GameShowcase fields = ('game_id', 'game_name', 'game_description', - 'description', 'contributors', 'game_cover_thumbnail') + 'description', 'contributors', 'game_cover_thumbnail', 'artworks') def get_contributors(self, obj): # Always fetch contributors from GameContributor for the related game contributors = GameContributor.objects.filter(game=obj.game) return ShowcaseContributorSerializer(contributors, many=True).data + def get_artworks(self, obj): + return GameArtSerializer(obj.game.game_artwork.all(), many=True).data + class SocialMediaSerializer(serializers.ModelSerializer): class Meta: