Skip to content

Commit

Permalink
separate gallery game into its own component (#2738)
Browse files Browse the repository at this point in the history
* separate gallery game into its own component

fixes an issue where game image gets mixed up during search

* add key to game component for react and remove dependency on filename in GalleryGame component
  • Loading branch information
JosiasAurel authored Dec 21, 2024
1 parent 4d1161b commit 3014a78
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 87 deletions.
60 changes: 60 additions & 0 deletions src/components/GalleryGame.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useEffect, useState, useRef } from "preact/hooks";
import { loadThumbnailUrl } from "../lib/thumbnail";

type GalleryGameProps = {
filename: string
title: string
author: string
tags: string[]
isNew: boolean
show: boolean
filter: any,
setFilter: Function
}

export default function GalleryGame({ setFilter, filter, show, filename, title, author, tags, isNew }: GalleryGameProps) {
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(null);
const gameRef = useRef<HTMLDivElement>(null);

useEffect(() => {
const observer = new IntersectionObserver(entries => {
entries.forEach(async entry => {
if (entry.isIntersecting) {
const thumbnail = await loadThumbnailUrl(filename)
setThumbnailUrl(thumbnail);
}
})
}, { threshold: 0.1 });

observer.observe(gameRef.current!);
return () => {
observer.unobserve(gameRef.current!);
}
}, []);

if (!show) return null;
return (
<div
ref={gameRef}
className="game"
onClick={() => window.open(`/gallery/${filename}`, '_blank')}
>
{tags.includes("tutorial") ? (
<span class="badge tutorial">Tutorial</span>
) : isNew ? (
<span class="badge new">New</span>
) : null}

{thumbnailUrl && <img src={thumbnailUrl} alt="thumbnail" data-loaded='true' /> }

<h3>{title}</h3>
<p class="author">by @{author}</p>
<p class="tags" onClick={(e) => e.stopPropagation()}>

{tags.map((tag) =>
<span class="game-tag" onClick={() => setFilter((_filter: any) => ({ ..._filter, tags: [...filter.tags, tag] }))}>#{tag} </span>
)}
</p>
</div>
)
}
100 changes: 13 additions & 87 deletions src/pages/gallery/gallery.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useState, useEffect } from "preact/hooks";
import { loadThumbnailUrl } from "../../lib/thumbnail";
import { GameMetadata } from "../../lib/game-saving/gallery";
import Button from "../../components/design-system/button";
import Input from "../../components/design-system/input";
import { IoCaretDown, IoSearch } from "react-icons/io5";
import GalleryGame from "../../components/GalleryGame";
import "./gallery.css";

enum SortOrder {
Expand Down Expand Up @@ -54,6 +54,7 @@ export default function Gallery({ games, tags }: { games: GameMetadata[], tags:
}

countTags(_games)
sortGames(gamesState, SortOrder.TUTORIALS_AND_CHRONOLOGICAL);
}, [filter]);

function sortGames(games: GameMetadata[], order: SortOrder): GameMetadata[] {
Expand Down Expand Up @@ -93,59 +94,6 @@ export default function Gallery({ games, tags }: { games: GameMetadata[], tags:
setTagCount(tags)
}

useEffect(() => {
sortGames(gamesState, SortOrder.TUTORIALS_AND_CHRONOLOGICAL);

interface GameCard {
element: HTMLLIElement;
filename: string;
title: string;
author: string;
tags: string[];
isNew: boolean;
}
for (const element of document.querySelectorAll("#games > .game")) {
element.querySelector("img")?.setAttribute("data-loaded", "false");
}

const loadImage = async (gameCard: GameCard): Promise<void> => {
const img = gameCard.element.querySelector(
"img"
) as HTMLImageElement;
if (["loading", "true"].includes(img.dataset.loaded!)) return;
img.dataset.loaded = "loading";
const thumbnail = await loadThumbnailUrl(gameCard.filename);
img.src = thumbnail;
img.dataset.loaded = "true";
};

const gameCards: GameCard[] = [];
for (const _element of document.querySelectorAll(
"#games > .game"
)) {
const element = _element as HTMLLIElement;
const gameCard = {
element,
filename: element.dataset.filename!,
title: element.dataset.title!,
author: element.dataset.author!,
tags: element.dataset.tags!.split(","),
isNew: element.dataset.isNew === "true",
};

gameCards.push(gameCard);
new IntersectionObserver((_update) => {
const update = (_update[0] ||
_update) as IntersectionObserverEntry;
if (update.isIntersecting) loadImage(gameCard);
}).observe(element);
}

setTimeout(() => {
for (const gameCard of gameCards) loadImage(gameCard);
}, 500);
}, [gamesState]);

return (
<div>
<div class="info-split">
Expand Down Expand Up @@ -236,39 +184,17 @@ export default function Gallery({ games, tags }: { games: GameMetadata[], tags:
</div>

<div id="games">
{
gamesState.map((game: GalleryGameMetadata) => (
<div
style={`display:${game.show ? "block" : "none"}`}
class="game"
href={`/gallery/${game.filename}`}
onClick={() => window.open(`/gallery/${game.filename}`, '_blank')}
data-filename={game.filename}
data-title={game.title}
data-author={game.author}
data-tags={game.tags.join(",")}
data-is-new={String(game.isNew)}
>
{game.tags.includes("tutorial") ? (
<span class="badge tutorial">Tutorial</span>
) : game.isNew ? (
<span class="badge new">New</span>
) : null}

<img
alt={`preview of ${game.filename}.js`}
/>
<h3>{game.title}</h3>
<p class="author">by @{game.author}</p>
<p class="tags" onClick={(e) => e.stopPropagation()}>

{game.tags.map((tag) =>
<span class="game-tag" onClick={() => setFilter(_filter => ({ ..._filter, tags: [...filter.tags, tag] }))}>#{tag} </span>
)}
</p>
</div>
))
}
{gamesState.map((game) => (
<GalleryGame
key={game.filename}
show={game.show}
filename={game.filename}
title={game.title}
author={game.author}
tags={game.tags}
isNew={game.isNew!}
setFilter={setFilter} filter={filter} />
))}
</div>
</div>
)
Expand Down

0 comments on commit 3014a78

Please sign in to comment.