fix gitignore

This commit is contained in:
2026-05-17 18:28:50 -04:00
parent 5a96eb385d
commit 6233bbf8d1
3 changed files with 163 additions and 1 deletions

2
.gitignore vendored
View File

@@ -5,4 +5,4 @@ data/
.env
.env.*
!.env.example
games/
/games/

View File

@@ -0,0 +1,91 @@
import { error, fail } from '@sveltejs/kit';
import type { Actions, PageServerLoad } from './$types';
import { getDb } from '$lib/server/db';
import { getSession } from '$lib/server/auth';
import type { Game, GameFile, Screenshot, Tag } from '$lib/types';
export const load: PageServerLoad = async ({ params, parent, cookies }) => {
const { loggedIn } = await parent();
const db = getDb();
const total = (db.prepare('SELECT COUNT(*) as n FROM games').get() as { n: number }).n;
console.log(`[games/${params.slug}] page server: slug="${params.slug}" total_games_in_db=${total}`);
const game = db.prepare('SELECT * FROM games WHERE slug = ?').get(params.slug) as
| Game
| undefined;
if (!game) {
const inSeries = db.prepare('SELECT slug, title FROM series WHERE slug = ?').get(params.slug) as { slug: string; title: string } | undefined;
console.log(
`[games/${params.slug}] 404 — not in games table. Total games in DB: ${total}. In series table: ${inSeries ? `yes ("${inSeries.title}")` : 'no'}`
);
error(404, 'Game not found');
}
if (game.library === 'private' && !loggedIn) error(403, 'Login required');
const files = db
.prepare('SELECT * FROM game_files WHERE game_id = ? ORDER BY platform, filename')
.all(game.id) as GameFile[];
const screenshots = db
.prepare('SELECT * FROM screenshots WHERE game_id = ? ORDER BY sort_order')
.all(game.id) as Screenshot[];
const tags = db
.prepare(
`SELECT t.id, t.name FROM tags t
JOIN game_tags gt ON gt.tag_id = t.id
WHERE gt.game_id = ?
ORDER BY t.name`
)
.all(game.id) as Tag[];
return { game, files, screenshots, tags };
};
export const actions: Actions = {
updateMeta: async ({ request, params, cookies }) => {
if (!getSession(cookies)) error(403, 'Login required');
const db = getDb();
const data = await request.formData();
const description = (data.get('description') as string) ?? '';
const genre = (data.get('genre') as string) ?? '';
db.prepare('UPDATE games SET description = ?, genre = ? WHERE slug = ?').run(
description,
genre,
params.slug
);
return { success: true };
},
addTag: async ({ request, params, cookies }) => {
if (!getSession(cookies)) error(403, 'Login required');
const db = getDb();
const data = await request.formData();
const name = ((data.get('tag') as string) ?? '').trim();
if (!name) return fail(400, { tagError: 'Tag name required' });
db.prepare('INSERT OR IGNORE INTO tags (name) VALUES (?)').run(name);
const tag = db.prepare('SELECT id FROM tags WHERE name = ?').get(name) as { id: number };
const game = db.prepare('SELECT id FROM games WHERE slug = ?').get(params.slug) as {
id: number;
};
db.prepare('INSERT OR IGNORE INTO game_tags (game_id, tag_id) VALUES (?, ?)').run(
game.id,
tag.id
);
return { success: true };
},
removeTag: async ({ request, params, cookies }) => {
if (!getSession(cookies)) error(403, 'Login required');
const db = getDb();
const data = await request.formData();
const tagId = Number(data.get('tagId'));
const game = db.prepare('SELECT id FROM games WHERE slug = ?').get(params.slug) as {
id: number;
};
db.prepare('DELETE FROM game_tags WHERE game_id = ? AND tag_id = ?').run(game.id, tagId);
return { success: true };
}
};

View File

@@ -0,0 +1,71 @@
<script lang="ts">
import type { PageData } from './$types';
import type { Game, GameFile, Tag } from '$lib/types';
import DownloadButton from '$lib/components/DownloadButton.svelte';
import TagEditor from '$lib/components/TagEditor.svelte';
import MetaEditor from '$lib/components/MetaEditor.svelte';
import ScreenshotGallery from '$lib/components/ScreenshotGallery.svelte';
let { data }: { data: PageData } = $props();
const game = $derived(data.game as Game);
const files = $derived(data.files as GameFile[]);
const tags = $derived(data.tags as Tag[]);
</script>
<svelte:head>
<title>{game.title} — Game Grid</title>
</svelte:head>
<div class="max-w-4xl mx-auto px-4 py-8">
<!-- Hero image -->
{#if game.has_wide || game.has_cover}
<div class="mb-8 rounded-xl overflow-hidden bg-gray-800 aspect-video max-h-80">
<img
src="/api/cover/{game.slug}?wide={(game.has_wide ? '1' : '0')}"
alt={game.title}
class="w-full h-full object-cover"
/>
</div>
{/if}
<div class="flex flex-col md:flex-row gap-8">
<!-- Cover art (portrait) -->
{#if game.has_cover}
<div class="flex-none w-40 md:w-48">
<div class="aspect-[2/3] rounded-lg overflow-hidden bg-gray-800 shadow-xl">
<img src="/api/cover/{game.slug}" alt={game.title} class="w-full h-full object-cover" />
</div>
</div>
{/if}
<div class="flex-1 min-w-0">
<h1 class="text-3xl font-bold text-white mb-2">{game.title}</h1>
{#if game.genre}
<p class="text-purple-400 text-sm font-medium mb-4">{game.genre}</p>
{/if}
<TagEditor tags={tags} gameSlug={game.slug} loggedIn={data.loggedIn} />
<div class="mt-6">
<DownloadButton {files} />
</div>
</div>
</div>
<div class="mt-8">
<MetaEditor {game} loggedIn={data.loggedIn} />
</div>
{#if data.screenshots.length > 0}
<div class="mt-8">
<h2 class="text-lg font-semibold text-gray-200 mb-3">Screenshots</h2>
<ScreenshotGallery screenshots={data.screenshots} />
</div>
{/if}
<div class="mt-8">
<a href="/" class="text-sm text-gray-500 hover:text-gray-300 transition-colors">← Back to library</a>
</div>
</div>