Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
84 changes: 84 additions & 0 deletions src/lib/games.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { eq } from 'drizzle-orm';
import { createTestDatabase } from '../../db/test-helpers';
import { categories, publishers, games } from '../../db/schema';
import type { Database } from './db';
import {
getAllGames,
getAllGameIds,
getGameById,
getRelatedGames,
} from './games';

async function seedGames(db: Database, count: number): Promise<void> {
Expand Down Expand Up @@ -63,4 +65,86 @@ describe('games data-access helpers', () => {
await seedGames(db, 2);
expect(await getGameById(db, 99999)).toBeNull();
});

it('returns related games from the same category excluding the source game', async () => {
const [strategyCategory] = await db
.insert(categories)
.values({ name: 'Strategy', description: 'strategy category' })
.returning({ id: categories.id });
const [adventureCategory] = await db
.insert(categories)
.values({ name: 'Adventure', description: 'adventure category' })
.returning({ id: categories.id });
const [publisher] = await db
.insert(publishers)
.values({ name: 'Pub One', description: 'pub' })
.returning({ id: publishers.id });

await db.insert(games).values([
{
title: 'Core Strategy',
description: 'source',
starRating: 4.5,
categoryId: strategyCategory.id,
publisherId: publisher.id,
},
{
title: 'Alpha Tactics',
description: 'related 1',
starRating: 4.2,
categoryId: strategyCategory.id,
publisherId: publisher.id,
},
{
title: 'Zenith Plans',
description: 'related 2',
starRating: 4.4,
categoryId: strategyCategory.id,
publisherId: publisher.id,
},
{
title: 'Forest Quest',
description: 'different category',
starRating: 4.0,
categoryId: adventureCategory.id,
publisherId: publisher.id,
},
]);

const source = await db
.select({ id: games.id })
.from(games)
.where(eq(games.title, 'Core Strategy'))
.get();

expect(source).not.toBeUndefined();

const related = await getRelatedGames(db, source!.id);
expect(related.map((game) => game.title)).toEqual(['Alpha Tactics', 'Zenith Plans']);
});

it('returns an empty list when no related games exist', async () => {
const [category] = await db
.insert(categories)
.values({ name: 'Puzzle', description: 'puzzle category' })
.returning({ id: categories.id });
const [publisher] = await db
.insert(publishers)
.values({ name: 'Solo Pub', description: 'pub' })
.returning({ id: publishers.id });

const [insertedGame] = await db
.insert(games)
.values({
title: 'Only Puzzle Game',
description: 'single game in category',
starRating: 3.8,
categoryId: category.id,
publisherId: publisher.id,
})
.returning({ id: games.id });

const related = await getRelatedGames(db, insertedGame.id);
expect(related).toEqual([]);
});
});
27 changes: 26 additions & 1 deletion src/lib/games.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { eq, asc } from 'drizzle-orm';
/**
* Data-access helpers for retrieving games and related metadata from SQLite.
*/

import { and, asc, eq, ne } from 'drizzle-orm';
import type { Database } from './db';
import { games, categories, publishers } from '../../db/schema';
import type { Game } from '../types/game';
Expand Down Expand Up @@ -67,3 +71,24 @@ export async function getGameById(db: Database, id: number): Promise<Game | null
const row = await baseGamesQuery(db).where(eq(games.id, id)).get();
return row ? mapGame(row) : null;
}

/**
* Other games in the same category as the given game, excluding that game.
*/
export async function getRelatedGames(db: Database, id: number): Promise<Game[]> {
const gameRow = await db
.select({ categoryId: games.categoryId })
.from(games)
.where(eq(games.id, id))
.get();

if (!gameRow || gameRow.categoryId === null) {
return [];
}

const rows = await baseGamesQuery(db)
.where(and(eq(games.categoryId, gameRow.categoryId), ne(games.id, id)))
.orderBy(asc(games.title));

return rows.map(mapGame);
}
33 changes: 31 additions & 2 deletions src/pages/game/[id].astro
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
---
/**
* Static game details page with category-based related-game recommendations.
*/

import type { GetStaticPaths } from 'astro';
import Layout from '../../layouts/Layout.astro';
import StarRating from '../../components/StarRating.astro';
import Tag from '../../components/Tag.astro';
import Card from '../../components/Card.astro';
import Button from '../../components/Button.astro';
import EmptyState from '../../components/EmptyState.astro';
import ArrowLeft from '../../assets/icons/arrow-left.svg';
import Heart from '../../assets/icons/heart.svg';
import { getDatabase } from '../../lib/db';
import { getAllGameIds, getGameById } from '../../lib/games';
import { getAllGameIds, getGameById, getRelatedGames } from '../../lib/games';

export const prerender = true;

Expand All @@ -18,11 +23,14 @@ export const getStaticPaths = (async () => {
}) satisfies GetStaticPaths;

const { id } = Astro.params;
const game = await getGameById(getDatabase(), Number(id));
const database = getDatabase();
const game = await getGameById(database, Number(id));

if (!game) {
return Astro.redirect('/404');
}

const relatedGames = await getRelatedGames(database, game.id);
---

<Layout title="Game Details - Tailspin Toys">
Expand Down Expand Up @@ -55,6 +63,27 @@ if (!game) {
</div>
</div>

<div class="space-y-4 mt-8">
<h2 class="text-lg font-semibold text-slate-200 mb-2">Related games</h2>
{relatedGames.length === 0 ? (
<EmptyState text="No related games found yet." />
) : (
<ul class="grid grid-cols-1 sm:grid-cols-2 gap-3">
{relatedGames.map((relatedGame) => (
<li>
<a
href={`/game/${relatedGame.id}`}
class="block rounded-lg border border-slate-700 bg-slate-900/60 px-4 py-3 text-slate-200 transition-colors duration-200 hover:bg-slate-800 hover:text-slate-100 focus:ring-2 focus:ring-blue-500 focus:outline-none"
data-testid={`related-game-link-${relatedGame.id}`}
>
{relatedGame.title}
</a>
</li>
))}
</ul>
)}
</div>

<div class="mt-8">
<Button variant="gradient" fullWidth data-testid="back-game-button">
<Heart class="h-5 w-5 mr-2" aria-hidden="true" />
Expand Down
Loading