Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
8c942c0
Tried pulling from main branch
RadinMan Feb 13, 2026
f577316
Used eventCarousel.tsx to build a carousel for artworks inside of Gam…
RadinMan Feb 13, 2026
c35cfca
Added: Mock data for artwork standins
RadinMan Feb 13, 2026
e79e278
Added: Mock artwork, filter function to filter artwork by Game, and t…
RadinMan Feb 13, 2026
d74372d
Added a third game with sample data (2 artworks) to see how the arrow…
RadinMan Feb 13, 2026
5addb72
Modified layout to match figma, Chevron arrows are larger and outside…
RadinMan Feb 13, 2026
9386aa3
Modified spacing between each showcased game to 32 gap, inside each g…
RadinMan Feb 13, 2026
1b2b15f
Added GameArtCarousel to the individual games pages and filter by the…
RadinMan Feb 13, 2026
5a2dbdc
Merge branch 'issue-81-Incorporate_art_into_gameShowcase_page_2' into…
RadinMan Feb 14, 2026
242ec0c
Added Art, Art Contributors, and ArtShowcase models from issue-8-merg…
RadinMan Feb 14, 2026
d789780
Added GamesArtSerializer (only necessary fields from ArtSerializer by…
RadinMan Feb 14, 2026
79a089b
Added: New Migration file to match database structure by issue-8-merg…
RadinMan Feb 14, 2026
ed94249
Added: ArtContributorInline and ArtAdmin to Djnago admin
RadinMan Feb 14, 2026
097cdc0
Removed mockArtwork, instead it uses UiArtwork from the useGame Hooks…
RadinMan Feb 14, 2026
e558363
Added: ApiArtworks which is the structure of backend GameArtSerialize…
RadinMan Feb 14, 2026
335f904
Removed old artwork filter and just uses game.artworks for the GameAr…
RadinMan Feb 14, 2026
649683c
Added Link to each image that directs to /artworks/[art_id]
RadinMan Feb 14, 2026
b1a3ff6
Removed: blank and null paramaters from the Art model source_game fie…
RadinMan Feb 15, 2026
229ef1f
Added artworks field to the GameshowcaseSerializer, uses get_artwork …
RadinMan Feb 15, 2026
1298419
Made type ApiArtworks export and usable for useGameshowcase.ts, allow…
RadinMan Feb 15, 2026
ce54591
Added artworks field to ApiShowcaseGame struct, map backend artworks …
RadinMan Feb 15, 2026
865bd0e
Added artworks field to ApiShowcaseGame struct, map backend artworks …
RadinMan Feb 15, 2026
755fa9d
Removed art mockdata and frontend art group filter, using backend ser…
RadinMan Feb 15, 2026
075d6da
Merge remote-tracking branch 'origin/main' into issue-81-Art_in_Games…
RadinMan Feb 15, 2026
b8c9514
Fixed: after merge conflict the get_artworks was in wrong line, now f…
RadinMan Feb 21, 2026
29e41fe
Added new migration
RadinMan Feb 21, 2026
e0d84ce
Adjusted the layout on showcase page to match Figma more, with a 4:3 …
nicostellar Feb 21, 2026
e5fd5fc
Showcase page is now responsive with some adjustments to gaps and lay…
nicostellar Feb 21, 2026
4b2513c
Merge remote-tracking branch 'origin/main' into issue-81-Art_in_Games…
nicostellar Feb 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions client/src/components/ui/GameArtCarousel.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>(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 (
<div className="container mx-auto py-10">
<div className="relative">
{/* LEFT ARROW */}
<button
onClick={slideLeft}
disabled={currentIndex === 0}
className="absolute left-[-50px] top-1/2 z-10 flex h-10 w-10 -translate-y-1/2 items-center justify-center rounded-full bg-dark_3 text-white shadow-md hover:bg-accent disabled:opacity-30"
>
<ChevronLeft size={30} />
</button>

{/* VIEWPORT */}
<div className="overflow-hidden">
<div
className="flex transition-transform duration-300 ease-out"
style={{
gap: GAP,
transform: `translateX(${translateX}px)`,
}}
>
{items.map((art, index) => (
<div
key={art.id}
ref={index === 0 ? firstItemRef : undefined}
className="flex-shrink-0"
style={{
width: `calc((100% - ${(visibleCount - 1) * GAP}px) / ${visibleCount})`,
}}
>
<Link href={`/artwork/${art.id}`}>
<div className="relative aspect-[4/3] w-full overflow-hidden rounded-lg">
<Image
src={art.image}
alt={art.name}
fill
className="object-cover"
/>
</div>
</Link>

<h3 className="mt-4 text-lg text-white">{art.name}</h3>
</div>
))}
</div>
</div>

{/* RIGHT ARROW */}
<button
onClick={slideRight}
disabled={currentIndex === maxIndex}
className="absolute right-[-50px] top-1/2 z-10 flex h-10 w-10 -translate-y-1/2 items-center justify-center rounded-full bg-dark_3 text-white shadow-md hover:bg-accent disabled:opacity-30"
>
<ChevronRight size={30} />
</button>
</div>
</div>
);
}
25 changes: 24 additions & 1 deletion client/src/hooks/useGames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -28,10 +43,12 @@ type ApiGame = {
itchGameWidth: number;
itchGameHeight: number;
contributors: Contributor[];
artworks: ApiArtworks[];
};

type UiGame = Omit<ApiGame, "thumbnail"> & {
type UiGame = Omit<ApiGame, "thumbnail" | "artworks"> & {
gameCover: string;
artworks: UiArtwork[];
};

/**
Expand All @@ -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,
})),
};
}

Expand Down
20 changes: 15 additions & 5 deletions client/src/hooks/useGameshowcase.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -20,15 +21,18 @@ type ApiShowcaseGame = {
game_description: string;
contributors: Contributor[];
game_cover_thumbnail?: string | null;
artworks: ApiArtworks[];
};

type UiShowcaseGame = Omit<ApiShowcaseGame, "game_cover_thumbnail"> & {
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
Expand All @@ -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,
})),
};
}

Expand Down
2 changes: 1 addition & 1 deletion client/src/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
33 changes: 2 additions & 31 deletions client/src/pages/games/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 (
<div className="min-h-screen bg-background font-sans text-foreground">
Expand Down Expand Up @@ -191,21 +176,7 @@ export default function IndividualGamePage() {
<h2 className="font-jersey10 text-5xl text-primary">ARTWORK</h2>

<div className="mx-auto mb-6 flex h-auto w-full max-w-4xl flex-col items-center gap-4 px-4 sm:flex-row sm:justify-center sm:gap-6 sm:px-6 md:h-60">
{artImages.map((img) => (
<div
key={img.src}
className="h-48 w-full overflow-hidden rounded-lg bg-popover shadow-md sm:h-60 sm:w-1/3"
>
<Image
key={img.alt}
src={img.src}
alt={img.alt}
width={240}
height={240}
className="h-full w-full object-cover"
/>
</div>
))}
<GameArtCarousel items={game.artworks || []} />
</div>
</section>
</main>
Expand Down
Loading
Loading