diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index 4fc2812..8132532 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -89,12 +89,12 @@ This is a crowdfunding platform for games with a developer theme. The applicatio
The application lives at the repository root:
- `db/`: Drizzle schema, migrations, transforms, seed, and `games.csv`
-- `src/lib/`: Node SQLite client (`db.ts`) and data-access helpers (`games.ts`)
-- `src/components/`: reusable `.astro` components
+- `src/lib/`: Node SQLite client (`db.ts`) and data-access helpers (`games.ts` with category/publisher filtering, `categories.ts`, `publishers.ts`)
+- `src/components/`: reusable `.astro` components (including `GameFilters.astro` for category/publisher filtering)
- `src/layouts/`: Astro layout templates
-- `src/pages/`: Astro page routes (`index.astro` listing, `game/[id].astro`, `404.astro`, `about.astro`)
+- `src/pages/`: Astro page routes (`index.astro` listing with filters, `game/[id].astro`, `404.astro`, `about.astro`)
- `src/styles/`: CSS and Tailwind configuration
- `src/types/`: TypeScript interfaces (Game, Publisher, Category)
-- `e2e-tests/`: Playwright E2E tests (home, games, accessibility)
+- `e2e-tests/`: Playwright E2E tests (home, games, accessibility, filters)
- `drizzle.config.ts`, `vitest.config.ts`, `astro.config.mjs`, `playwright.config.ts`: tooling config
- `README.md`: Project documentation
diff --git a/DOCUMENT.S.md b/DOCUMENT.S.md
new file mode 100644
index 0000000..02ef7b7
--- /dev/null
+++ b/DOCUMENT.S.md
@@ -0,0 +1,7 @@
+# Useful Docs
+
+- Tokens optimization: https://olivomarco.github.io/github-copilot-token-optimization/#comparisons
+- Agentic DevOps Workshop: https://copilot-dev-days.github.io/agentic-workflows-workshop/step.html?step=readme
+- Spec Kit: https://github.com/github/spec-kit
+- HVE Core: https://github.com/microsoft/hve-core
+- Awesome Copilot: https://awesome-copilot.github.com/
diff --git a/README.md b/README.md
index 219b666..9e1c39d 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,12 @@
+Basic workshop url: https://github-samples.github.io/copilot-workshops/vscode/0-prerequisites/
+Agentic devops workshop: https://copilot-dev-days.github.io/agentic-workflows-workshop/step.html?step=readme
+
+Free hack:
+
+
+
+
+
# Tailspin Toys
Tailspin Toys is a crowdfunding platform for games with a developer theme. The project is a website for a fictional game crowd-funding company, built as a single [Astro](https://astro.build/) site (fully prerendered/static output) styled with [Tailwind CSS](https://tailwindcss.com/). Its data lives in a local SQLite database accessed through [Drizzle ORM](https://orm.drizzle.team/) and Node.js's built-in SQLite driver; pages query the database directly in frontmatter at build time, so there is no separate backend service.
@@ -12,6 +21,11 @@ Tailspin Toys is a crowdfunding platform for games with a developer theme. The p
The database is migrated and seeded automatically before `dev`/`build` (via the `predev`/`prebuild` npm scripts) and is written to the gitignored `tailspin.db` file.
+## Features
+
+- **Game catalog** (`/`) — lists all games as cards, each linking to a details page.
+- **Category & publisher filtering** — the homepage includes filter controls (`src/components/GameFilters.astro`) so visitors can narrow the catalog by one or more categories and/or a publisher; filters combine (category AND publisher, with OR between multiple selections in the same group). Since the site is fully prerendered, filtering runs client-side over the already-rendered game grid — no rebuild or server round-trip is needed. The underlying data-access helpers (`getAllGames` in `src/lib/games.ts`, plus `getAllCategories`/`getAllPublishers` in `src/lib/categories.ts`/`src/lib/publishers.ts`) also support filtering directly for reuse and testing.
+
## Using this template
This repository is a GitHub template. When you create a new repository from it, a one-time **Bootstrap template issues** workflow (`.github/workflows/bootstrap-issues.yml`) runs automatically on the first push to `main` and opens a set of starter issues describing suggested first features. Each issue is defined by a Markdown file in `.github/bootstrap-issues/` — the first heading becomes the issue title and the remaining content becomes the body — so you can edit, add, or remove files there to control which issues are created.
diff --git a/e2e-tests/filters.spec.ts b/e2e-tests/filters.spec.ts
new file mode 100644
index 0000000..69195c2
--- /dev/null
+++ b/e2e-tests/filters.spec.ts
@@ -0,0 +1,117 @@
+import { test, expect } from '@playwright/test';
+
+test.describe('Game Filtering', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/');
+ await expect(page.getByTestId('games-grid')).toBeVisible();
+ });
+
+ test('should display category and publisher filter controls', async ({ page }) => {
+ await test.step('Verify the filter panel and groups are visible', async () => {
+ await expect(page.getByTestId('game-filters')).toBeVisible();
+ await expect(page.getByRole('group', { name: 'Category' })).toBeVisible();
+ await expect(page.getByRole('group', { name: 'Publisher' })).toBeVisible();
+ });
+
+ await test.step('Verify filter options are unchecked by default', async () => {
+ const strategyCheckbox = page.getByRole('checkbox', { name: 'Strategy' });
+ await expect(strategyCheckbox).not.toBeChecked();
+ });
+ });
+
+ test('should filter games by a single category', async ({ page }) => {
+ const totalCount = await page.getByTestId('game-card').count();
+
+ await test.step('Select the Strategy category filter', async () => {
+ await page.getByRole('checkbox', { name: 'Strategy' }).check();
+ });
+
+ await test.step('Verify only Strategy games remain visible', async () => {
+ const visibleCards = page.locator('[data-testid="game-card"]:visible');
+ const categories = await visibleCards.locator('[data-testid="game-category"]').allTextContents();
+ expect(categories.length).toBeGreaterThan(0);
+ expect(categories.every((category) => category === 'Strategy')).toBeTruthy();
+ });
+
+ await test.step('Verify the results count is announced', async () => {
+ const visibleCount = await page.locator('[data-testid="game-card"]:visible').count();
+ await expect(page.getByTestId('filter-results-count')).toHaveText(`Showing ${visibleCount} of ${totalCount} games`);
+ });
+ });
+
+ test('should combine multiple categories with OR semantics', async ({ page }) => {
+ await test.step('Select two category filters', async () => {
+ await page.getByRole('checkbox', { name: 'Strategy' }).check();
+ await page.getByRole('checkbox', { name: 'Puzzle' }).check();
+ });
+
+ await test.step('Verify only games from either category remain visible', async () => {
+ const visibleCards = page.locator('[data-testid="game-card"]:visible');
+ const categories = await visibleCards.locator('[data-testid="game-category"]').allTextContents();
+ expect(categories.length).toBeGreaterThan(0);
+ expect(categories.every((category) => category === 'Strategy' || category === 'Puzzle')).toBeTruthy();
+ });
+ });
+
+ test('should filter games by publisher', async ({ page }) => {
+ await test.step('Select a publisher filter', async () => {
+ const publisherCheckbox = page.getByTestId('publisher-filter').getByRole('checkbox').first();
+ const publisherName = await publisherCheckbox.getAttribute('aria-label');
+ await publisherCheckbox.check();
+
+ await test.step('Verify only games from that publisher remain visible', async () => {
+ const visibleCards = page.locator('[data-testid="game-card"]:visible');
+ const publisherNames = await visibleCards.locator('[data-testid="game-publisher"]').allTextContents();
+ expect(publisherNames.length).toBeGreaterThan(0);
+ expect(publisherNames.every((name) => name === publisherName)).toBeTruthy();
+ });
+ });
+ });
+
+ test('should combine category and publisher filters with AND semantics', async ({ page }) => {
+ await test.step('Select a category and a publisher filter', async () => {
+ await page.getByRole('checkbox', { name: 'Strategy' }).check();
+ await page.getByRole('checkbox', { name: 'GitHub Games' }).check();
+ });
+
+ await test.step('Verify only games matching both filters remain visible', async () => {
+ const visibleCards = page.locator('[data-testid="game-card"]:visible');
+ await expect(visibleCards).toHaveCount(1);
+ await expect(visibleCards.getByTestId('game-category')).toHaveText('Strategy');
+ await expect(visibleCards.getByTestId('game-publisher')).toHaveText('GitHub Games');
+ });
+ });
+
+ test('should clear all filters and restore the full game list', async ({ page }) => {
+ const totalCount = await page.getByTestId('game-card').count();
+
+ await test.step('Apply filters', async () => {
+ await page.getByRole('checkbox', { name: 'Strategy' }).check();
+ await page.getByRole('checkbox', { name: 'GitHub Games' }).check();
+ await expect(page.locator('[data-testid="game-card"]:visible')).toHaveCount(1);
+ });
+
+ await test.step('Clear filters and verify all games are visible again', async () => {
+ await page.getByTestId('clear-filters-button').click();
+ await expect(page.locator('[data-testid="game-card"]:visible')).toHaveCount(totalCount);
+ await expect(page.getByRole('checkbox', { name: 'Strategy' })).not.toBeChecked();
+ await expect(page.getByRole('checkbox', { name: 'GitHub Games' })).not.toBeChecked();
+ });
+ });
+
+ test('should support keyboard interaction with filter checkboxes', async ({ page }) => {
+ const strategyCheckbox = page.getByRole('checkbox', { name: 'Strategy' });
+
+ await test.step('Focus and toggle the checkbox with the keyboard', async () => {
+ await strategyCheckbox.focus();
+ await expect(strategyCheckbox).toBeFocused();
+ await page.keyboard.press('Space');
+ await expect(strategyCheckbox).toBeChecked();
+ });
+
+ await test.step('Verify the grid updates in response', async () => {
+ const categories = await page.locator('[data-testid="game-card"]:visible [data-testid="game-category"]').allTextContents();
+ expect(categories.every((category) => category === 'Strategy')).toBeTruthy();
+ });
+ });
+});
diff --git a/src/components/GameCard.astro b/src/components/GameCard.astro
index da71b54..77d8532 100644
--- a/src/components/GameCard.astro
+++ b/src/components/GameCard.astro
@@ -16,6 +16,8 @@ const { game } = Astro.props;
data-testid="game-card"
data-game-id={game.id}
data-game-title={game.title}
+ data-category-id={game.category?.id}
+ data-publisher-id={game.publisher?.id}
>