diff --git a/client/next.config.mjs b/client/next.config.mjs
index 8b6bf03..0fcd33e 100644
--- a/client/next.config.mjs
+++ b/client/next.config.mjs
@@ -13,7 +13,11 @@ const config = {
},
outputFileTracingRoot: import.meta.dirname,
images: {
- domains: ["localhost"],
+ remotePatterns: [
+ { protocol: 'http', hostname: '127.0.0.1' },
+ { protocol: 'http', hostname: 'localhost' },
+ { protocol: 'https', hostname: 'upload.wikimedia.org' },
+ ],
},
// Turns on file change polling for the Windows Dev Container
// Doesn't work currently for turbopack, so file changes will not automatically update the client.
diff --git a/client/src/components/ui/ItchEmbed.tsx b/client/src/components/ui/ItchEmbed.tsx
new file mode 100644
index 0000000..eccff19
--- /dev/null
+++ b/client/src/components/ui/ItchEmbed.tsx
@@ -0,0 +1,17 @@
+type ItchEmbedProps = {
+ embedID: string;
+ name: string;
+};
+
+export function ItchEmbed({ embedID, name }: ItchEmbedProps) {
+ return (
+
+
+
+ );
+}
diff --git a/client/src/hooks/useGames.ts b/client/src/hooks/useGames.ts
new file mode 100644
index 0000000..ea3bd4d
--- /dev/null
+++ b/client/src/hooks/useGames.ts
@@ -0,0 +1,81 @@
+import { useQuery } from "@tanstack/react-query";
+import { AxiosError } from "axios";
+
+import api from "@/lib/api";
+
+type Contributor = {
+ member_id: number;
+ name: string;
+ role: string;
+};
+
+type ApiGame = {
+ name: string;
+ description: string;
+ completion: number;
+ active: boolean;
+ hostURL: string;
+ isItch: boolean;
+ // TO DO: Add support for no itchEmbedID for non-itch games
+ itchEmbedID: string;
+ pathToThumbnail: string | null;
+ event: number | null;
+ contributors: Contributor[];
+};
+
+type UiGame = Omit & {
+ gameCover: string;
+};
+
+/**
+ * Normalizes Next.js router query parameter to a single string ID.
+ * Handles both string and array formats from dynamic routes.
+ */
+function normalizeGameId(
+ gameId: string | string[] | undefined,
+): string | undefined {
+ if (!gameId) return undefined;
+ return typeof gameId === "string" ? gameId : gameId[0];
+}
+
+function transformApiGameToUiGame(data: ApiGame): UiGame {
+ return {
+ ...data,
+ gameCover: data.pathToThumbnail ?? "/game_dev_club_logo.svg",
+ };
+}
+
+/**
+ * Custom hook to fetch a single game by ID.
+ *
+ * @param gameId - game ID from Next.js router query (can be string, string[], or undefined)
+ * @returns React Query result with transformed UI game data
+ *
+ * @example
+ * ```tsx
+ * const { id } = router.query;
+ * const { data: game, isPending, error } = useGame(id);
+ * ```
+ */
+export function useGame(gameId: string | string[] | undefined) {
+ const id = normalizeGameId(gameId);
+
+ return useQuery({
+ queryKey: ["games", id],
+ queryFn: async () => {
+ if (!id) {
+ throw new Error("Game ID is required");
+ }
+ const response = await api.get(`/games/${id}/`);
+ return response.data;
+ },
+ enabled: !!id,
+ select: transformApiGameToUiGame,
+ retry: (failureCount, error) => {
+ if (error!.response?.status === 404) {
+ return false;
+ }
+ return failureCount < 3;
+ },
+ });
+}
diff --git a/client/src/pages/games/[id].tsx b/client/src/pages/games/[id].tsx
new file mode 100644
index 0000000..e9e5d5d
--- /dev/null
+++ b/client/src/pages/games/[id].tsx
@@ -0,0 +1,187 @@
+import Image from "next/image";
+import { useRouter } from "next/router";
+import React from "react";
+
+import { ItchEmbed } from "@/components/ui/ItchEmbed";
+import { useGame } from "@/hooks/useGames";
+
+export default function IndividualGamePage() {
+ const router = useRouter();
+ const { id } = router.query;
+
+ const {
+ data: game,
+ isPending,
+ error,
+ isError,
+ } = useGame(router.isReady ? id : undefined);
+
+ if (isPending) {
+ return (
+
+ Loading Game...
+
+ );
+ }
+
+ if (isError) {
+ const errorMessage =
+ error?.response?.status === 404
+ ? "Game not found."
+ : "Failed to Load Game";
+
+ return (
+
+
+ {errorMessage}
+
+
+ );
+ }
+
+ if (!game) {
+ return (
+
+ No Game data available.
+
+ );
+ }
+
+ const gameTitle = game.name;
+ const gameCover = game.gameCover;
+ const gameDescription = game.description.split("\n");
+
+ const completionLabels: Record = {
+ 1: "WIP",
+ 2: "Playable Dev",
+ 3: "Beta",
+ 4: "Completed",
+ };
+
+ const devStage = completionLabels[game.completion] ?? "Stage Unknown";
+
+ // TODO ADD EVENT
+ const event = "Game Jam November 2025";
+ // TODO ADD ARTIMAGES
+ 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 (
+
+
+
+
+
+
+ {gameTitle}
+
+
+
+
+
+ |
+ Contributors
+ |
+
+
+ {game.contributors.map((c) => (
+
+
+ {c.name}
+
+ {c.role}
+
+ ))}
+
+ |
+
+
+ |
+ Development Stage
+ |
+ {devStage} |
+
+
+ |
+ Host Site
+ |
+
+
+ {game.hostURL}
+
+ |
+
+
+ |
+ Event
+ |
+ {event} |
+
+
+
+
+
+ {gameDescription.map((desc, i) => (
+ - {desc}
+ ))}
+
+
+
+
+ {game.isItch && (
+
+ )}
+ ARTWORK
+
+
+ {artImages.map((img) => (
+
+
+
+ ))}
+
+
+
+ {/*
*/}
+
+ );
+}
diff --git a/client/src/pages/games/index.tsx b/client/src/pages/games/index.tsx
new file mode 100644
index 0000000..e74c44b
--- /dev/null
+++ b/client/src/pages/games/index.tsx
@@ -0,0 +1,182 @@
+import Image from "next/image";
+import Link from "next/link";
+import React, { useEffect, useState } from "react";
+
+type Contributor = {
+ name: string;
+ role: string;
+};
+
+type ShowcaseGame = {
+ game_id: number;
+ game_name: string;
+ description: string;
+ game_description: string;
+ contributors: Contributor[];
+ game_cover_thumbnail?: string;
+};
+
+export default function HomePage() {
+ const [showcases, setShowcases] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ fetch("http://localhost:8000/api/gameshowcase/")
+ .then((res) => res.json())
+ .then((data: ShowcaseGame[]) => {
+ setShowcases(data);
+ setLoading(false);
+ })
+ .catch(() => setLoading(false));
+ }, []);
+
+ if (loading) {
+ return (
+
+ Loading games...
+
+ );
+ }
+
+ return (
+ <>
+
+
+ Game Showcase
+
+
+
+ {showcases.length === 0 ? (
+ No games available.
+ ) : (
+
+ {showcases.map((showcase, idx) => (
+
+
+ {/* Left: Cover Image */}
+
+ {showcase.game_cover_thumbnail ? (
+
+ ) : (
+
+ )}
+
+ {/* Right: Details */}
+
+
+ {/* Title of the game */}
+
+
+
+ {showcase.game_name}
+
+
+
+ {/* Comments from committes */}
+
+ {showcase.description}
+
+
+ Contributors
+
+
+ {showcase.contributors.map((contributor, cidx) => (
+ -
+
+ {contributor.name}
+
+
+ - {contributor.role}
+
+ {/* Social icons placeholder */}
+ {/* TODO: Add actual links */}
+
+ {/* Facebook icon */}
+
+ {/* Instagram icon */}
+
+ {/* Github icon */}
+
+
+
+ ))}
+
+
+
+
+
+ {showcase.game_description}
+
+
+ ))}
+
+ )}
+
+ >
+ );
+}
diff --git a/server/api/urls.py b/server/api/urls.py
index 5d54527..944a906 100644
--- a/server/api/urls.py
+++ b/server/api/urls.py
@@ -20,11 +20,11 @@
from django.conf import settings
from django.conf.urls.static import static
+
urlpatterns = [
path("admin/", admin.site.urls),
path("api/", include("game_dev.urls")),
]
if settings.DEBUG:
- urlpatterns += static(settings.MEDIA_URL,
- document_root=settings.MEDIA_ROOT)
+ urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
diff --git a/server/game_dev/admin.py b/server/game_dev/admin.py
index 46d358c..28441b5 100644
--- a/server/game_dev/admin.py
+++ b/server/game_dev/admin.py
@@ -1,14 +1,30 @@
from django.contrib import admin
-from .models import Member, Event
+from .models import Member, Game, Event, GameContributor, GameShowcase
class MemberAdmin(admin.ModelAdmin):
pass
-class EventAdmin(admin.ModelAdmin):
- list_display = ("name", "date", "location", "publicationDate")
+# Sample EventsAdmin Class made
+class EventsAdmin(admin.ModelAdmin):
+ pass
+
+
+class GameContributorAdmin(admin.ModelAdmin):
+ pass
+
+
+class GameShowcaseAdmin(admin.ModelAdmin):
+ pass
+
+
+class GamesAdmin(admin.ModelAdmin):
+ list_display = ("name", "description", "completion", "active", "hostURL", "isItch", "itchEmbedID", "pathToThumbnail", "event")
admin.site.register(Member, MemberAdmin)
-admin.site.register(Event, EventAdmin)
+admin.site.register(Event, EventsAdmin)
+admin.site.register(Game, GamesAdmin)
+admin.site.register(GameContributor, GameContributorAdmin)
+admin.site.register(GameShowcase, GameShowcaseAdmin)
diff --git a/server/game_dev/migrations/0003_event_date.py b/server/game_dev/migrations/0003_event_date.py
deleted file mode 100644
index fe8c64d..0000000
--- a/server/game_dev/migrations/0003_event_date.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 5.1.14 on 2025-12-06 07:23
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ("game_dev", "0002_event"),
- ]
-
- operations = [
- migrations.AddField(
- model_name="event",
- name="date",
- field=models.DateTimeField(blank=True, null=True),
- ),
- ]
diff --git a/server/game_dev/migrations/0003_event_date_game.py b/server/game_dev/migrations/0003_event_date_game.py
new file mode 100644
index 0000000..2cd3495
--- /dev/null
+++ b/server/game_dev/migrations/0003_event_date_game.py
@@ -0,0 +1,69 @@
+# Generated by Django 5.1.14 on 2026-01-10 03:27
+
+import django.db.models.deletion
+import django.db.models.functions.datetime
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("game_dev", "0002_event"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="event",
+ name="date",
+ field=models.DateTimeField(
+ db_default=django.db.models.functions.datetime.Now()
+ ),
+ ),
+ migrations.CreateModel(
+ name="Game",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("name", models.CharField(max_length=200)),
+ ("description", models.TextField()),
+ (
+ "completion",
+ models.IntegerField(
+ choices=[
+ (1, "Work in Progress (Unplayable)"),
+ (2, "Playable - In Development"),
+ (3, "Beta - Stable but not Final"),
+ (4, "Completed"),
+ ],
+ default=1,
+ ),
+ ),
+ ("active", models.BooleanField(default=True)),
+ (
+ "hostURL",
+ models.CharField(
+ help_text="If game is stored on itch.io, please enter the 7 digit long game id as its hostURL, i.e., 1000200",
+ max_length=2083,
+ ),
+ ),
+ ("isItch", models.BooleanField(default=True)),
+ ("pathToThumbnail", models.ImageField(null=True, upload_to="games/")),
+ (
+ "event",
+ models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ to="game_dev.event",
+ ),
+ ),
+ ],
+ ),
+ ]
diff --git a/server/game_dev/migrations/0004_alter_event_date.py b/server/game_dev/migrations/0004_alter_event_date.py
deleted file mode 100644
index edca9b7..0000000
--- a/server/game_dev/migrations/0004_alter_event_date.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 5.1.14 on 2025-12-06 07:46
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ("game_dev", "0003_event_date"),
- ]
-
- operations = [
- migrations.AlterField(
- model_name="event",
- name="date",
- field=models.DateTimeField(),
- ),
- ]
diff --git a/server/game_dev/migrations/0004_gamecontributors.py b/server/game_dev/migrations/0004_gamecontributors.py
new file mode 100644
index 0000000..78f166f
--- /dev/null
+++ b/server/game_dev/migrations/0004_gamecontributors.py
@@ -0,0 +1,48 @@
+# Generated by Django 5.1.14 on 2026-01-10 04:59
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("game_dev", "0003_event_date_game"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="GameContributors",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("role", models.CharField(max_length=100)),
+ (
+ "game",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="game_contributors",
+ to="game_dev.game",
+ ),
+ ),
+ (
+ "member",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="member_games",
+ to="game_dev.member",
+ ),
+ ),
+ ],
+ options={
+ "unique_together": {("game", "member")},
+ },
+ ),
+ ]
diff --git a/server/game_dev/migrations/0005_gameshowcase.py b/server/game_dev/migrations/0005_gameshowcase.py
new file mode 100644
index 0000000..8695d95
--- /dev/null
+++ b/server/game_dev/migrations/0005_gameshowcase.py
@@ -0,0 +1,44 @@
+# Generated by Django 5.1.14 on 2026-01-17
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("game_dev", "0004_gamecontributors"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="GameShowcase",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("description", models.TextField()),
+ (
+ "game",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="game_showcases",
+ to="game_dev.game",
+ ),
+ ),
+ (
+ "member",
+ models.ManyToManyField(
+ related_name="showcased_games",
+ to="game_dev.member",
+ ),
+ ),
+ ],
+ ),
+ ]
diff --git a/server/game_dev/migrations/0005_rename_gamecontributors_gamecontributor.py b/server/game_dev/migrations/0005_rename_gamecontributors_gamecontributor.py
new file mode 100644
index 0000000..5fd7927
--- /dev/null
+++ b/server/game_dev/migrations/0005_rename_gamecontributors_gamecontributor.py
@@ -0,0 +1,17 @@
+# Generated by Django 5.1.14 on 2026-01-17 07:49
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("game_dev", "0004_gamecontributors"),
+ ]
+
+ operations = [
+ migrations.RenameModel(
+ old_name="GameContributors",
+ new_name="GameContributor",
+ ),
+ ]
diff --git a/server/game_dev/migrations/0006_merge_20260121_1416.py b/server/game_dev/migrations/0006_merge_20260121_1416.py
new file mode 100644
index 0000000..ef9cd6c
--- /dev/null
+++ b/server/game_dev/migrations/0006_merge_20260121_1416.py
@@ -0,0 +1,13 @@
+# Generated by Django 5.1.14 on 2026-01-21 06:16
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("game_dev", "0005_gameshowcase"),
+ ("game_dev", "0005_rename_gamecontributors_gamecontributor"),
+ ]
+
+ operations = []
diff --git a/server/game_dev/migrations/0007_remove_gameshowcase_member_game_itchembedid_and_more.py b/server/game_dev/migrations/0007_remove_gameshowcase_member_game_itchembedid_and_more.py
new file mode 100644
index 0000000..e8669f6
--- /dev/null
+++ b/server/game_dev/migrations/0007_remove_gameshowcase_member_game_itchembedid_and_more.py
@@ -0,0 +1,32 @@
+# Generated by Django 5.1.14 on 2026-01-22 02:58
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("game_dev", "0006_merge_20260121_1416"),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name="gameshowcase",
+ name="member",
+ ),
+ migrations.AddField(
+ model_name="game",
+ name="itchEmbedID",
+ field=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",
+ null=True,
+ ),
+ ),
+ migrations.AlterField(
+ model_name="game",
+ name="hostURL",
+ field=models.CharField(max_length=2083),
+ ),
+ ]
diff --git a/server/game_dev/models.py b/server/game_dev/models.py
index 6398070..fbb8019 100644
--- a/server/game_dev/models.py
+++ b/server/game_dev/models.py
@@ -1,4 +1,5 @@
from django.db import models
+from django.db.models.functions import Now
class Member(models.Model):
@@ -14,7 +15,7 @@ def __str__(self):
class Event(models.Model):
name = models.CharField(max_length=200)
- date = models.DateTimeField()
+ date = models.DateTimeField(db_default=Now())
description = models.CharField(max_length=256, blank=True)
publicationDate = models.DateField()
cover_image = models.ImageField(upload_to="events/", null=True)
@@ -22,3 +23,56 @@ class Event(models.Model):
def __str__(self):
return self.name
+
+
+# GameContributor table: links Game, Member, and role (composite PK)
+class GameContributor(models.Model):
+ game = models.ForeignKey('Game', on_delete=models.CASCADE, related_name='game_contributors')
+ member = models.ForeignKey('Member', on_delete=models.CASCADE, related_name='member_games')
+ role = models.CharField(max_length=100)
+
+ class Meta:
+ unique_together = (('game', 'member'),)
+
+ def __str__(self):
+ return f"{self.member.name} ({self.role}) for {self.game.name}"
+
+
+class Game(models.Model):
+ # Enum choices
+ class CompletionStatus(models.IntegerChoices):
+ WIP = 1, "Work in Progress (Unplayable)"
+ PLAYABLE_DEV = 2, "Playable - In Development"
+ BETA = 3, "Beta - Stable but not Final"
+ COMPLETED = 4, "Completed"
+
+ name = models.CharField(max_length=200, null=False)
+ description = models.TextField()
+ completion = models.IntegerField(
+ choices=CompletionStatus.choices,
+ default=CompletionStatus.WIP,
+ null=False,
+ )
+ active = models.BooleanField(default=True, null=False)
+ hostURL = models.CharField(max_length=2083)
+ isItch = models.BooleanField(default=True, null=False)
+ itchEmbedID = models.PositiveIntegerField(
+ default=None,
+ null=True,
+ blank=True,
+ help_text="If game is stored on itch.io, please enter the 7 digit long game id as its itchEmbedID, i.e., 1000200"
+ )
+
+ pathToThumbnail = models.ImageField(upload_to="games/", null=True)
+ event = models.ForeignKey(Event, on_delete=models.SET_NULL, null=True, blank=True)
+
+ def __str__(self):
+ return str(self.name)
+
+
+class GameShowcase(models.Model):
+ game = models.ForeignKey('Game', on_delete=models.CASCADE, related_name='game_showcases')
+ description = models.TextField()
+
+ def __str__(self):
+ return f"{self.game.name}"
diff --git a/server/game_dev/serializers.py b/server/game_dev/serializers.py
index b50638d..6a1318c 100644
--- a/server/game_dev/serializers.py
+++ b/server/game_dev/serializers.py
@@ -1,5 +1,5 @@
from rest_framework import serializers
-from .models import Event, Member
+from .models import Event, Game, Member, GameShowcase, GameContributor
class EventSerializer(serializers.ModelSerializer):
@@ -16,6 +16,58 @@ class Meta:
]
+# This is child serializer of GameSerializer
+class GameContributorSerializer(serializers.ModelSerializer):
+ member_id = serializers.IntegerField(source="member.id") # to link contributors to their member/[id] page
+ name = serializers.CharField(source="member.name")
+
+ class Meta:
+ model = GameContributor
+ fields = ("member_id", "name", "role")
+
+
+class GamesSerializer(serializers.ModelSerializer):
+ contributors = GameContributorSerializer(
+ many=True,
+ source="game_contributors",
+ read_only=True
+ )
+
+ class Meta:
+ model = Game
+ fields = ('id', 'name', 'description', 'completion', 'active', 'hostURL', 'isItch', 'itchEmbedID', 'pathToThumbnail', 'event', "contributors")
+
+
+# Contributor serializer for name and role
+class ShowcaseContributorSerializer(serializers.ModelSerializer):
+ name = serializers.CharField(source='member.name', read_only=True)
+ role = serializers.CharField(read_only=True)
+ # social_links = serializers.CharField(source='member.social_links', read_only=True)
+ # socialmedia_name = serializers.CharField(source='member.socialmedia_name', read_only=True)
+
+ class Meta:
+ model = GameContributor
+ fields = ("name", "role")
+
+
+# Serializer for GameShowcase
+class GameshowcaseSerializer(serializers.ModelSerializer):
+ game_id = serializers.IntegerField(source='game.id', read_only=True)
+ game_name = serializers.CharField(source='game.name', read_only=True)
+ game_description = serializers.CharField(source='game.description', read_only=True)
+ game_cover_thumbnail = serializers.ImageField(source='game.pathToThumbnail', read_only=True)
+ contributors = serializers.SerializerMethodField()
+
+ class Meta:
+ model = GameShowcase
+ fields = ('game_id', 'game_name', 'game_description', 'description', 'contributors', 'game_cover_thumbnail')
+
+ 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
+
+
class MemberSerializer(serializers.ModelSerializer):
class Meta:
model = Member
diff --git a/server/game_dev/urls.py b/server/game_dev/urls.py
index d42d6a8..ae402cb 100644
--- a/server/game_dev/urls.py
+++ b/server/game_dev/urls.py
@@ -1,7 +1,10 @@
from django.urls import path
-from .views import EventListAPIView, EventDetailAPIView
+from .views import EventListAPIView, EventDetailAPIView, GamesDetailAPIView, itch_embed_proxy, GameshowcaseAPIView
urlpatterns = [
path("events/", EventListAPIView.as_view(), name="events-list"),
path("events//", EventDetailAPIView.as_view()),
+ path("games//", GamesDetailAPIView.as_view()),
+ path("itch-embed//", itch_embed_proxy),
+ path("gameshowcase/", GameshowcaseAPIView.as_view(), name="gameshowcase-api"), # Updated line for GameShowcase endpoint
]
diff --git a/server/game_dev/views.py b/server/game_dev/views.py
index 89ce3a9..f06c66a 100644
--- a/server/game_dev/views.py
+++ b/server/game_dev/views.py
@@ -1,14 +1,39 @@
-# from django.shortcuts import render
-
-# Create your views here.
from rest_framework import generics
-from .models import Event
-from .serializers import EventSerializer
+from .serializers import GamesSerializer, GameshowcaseSerializer, EventSerializer
+from .models import Game, GameShowcase
+import urllib.request
+from django.http import JsonResponse
+from django.views.decorators.csrf import csrf_exempt
from django.utils import timezone
+from .models import Event
+from rest_framework.views import APIView
+from rest_framework.response import Response
from rest_framework.exceptions import ValidationError
from rest_framework.pagination import PageNumberPagination
+class GamesDetailAPIView(generics.RetrieveAPIView):
+ """
+ GET /api/games//
+ """
+ serializer_class = GamesSerializer
+ lookup_url_kwarg = "id"
+
+ def get_queryset(self):
+ return Game.objects.filter(id=self.kwargs["id"])
+
+
+@csrf_exempt
+def itch_embed_proxy(request, embed_id):
+ url = f"https://itch.io/embed/{embed_id}"
+ try:
+ with urllib.request.urlopen(url, timeout=10) as response:
+ html = response.read().decode("utf-8")
+ return JsonResponse({"html": html})
+ except Exception as e:
+ return JsonResponse({"error": str(e)}, status=500)
+
+
class EventPagination(PageNumberPagination):
page_size = 20
page_size_query_param = "page_size"
@@ -51,3 +76,10 @@ class EventDetailAPIView(generics.RetrieveAPIView):
def get_queryset(self):
return Event.objects.filter(id=self.kwargs["id"])
+
+
+class GameshowcaseAPIView(APIView):
+ def get(self, request):
+ showcases = GameShowcase.objects.all()
+ serializer = GameshowcaseSerializer(showcases, many=True)
+ return Response(serializer.data)