Files
pgames-manager/frontend/src/pages/Library.tsx
T
mrleo1nidandClaude Opus 5.5 364b312064 Initial MVP: local visual novel library with Ren'Py → PGVN converter
- FastAPI backend: admin bootstrap with forced credential change, admin-created
  users with one-time generated passwords, roles as permission sets
- Games and versions with content-addressed blob storage; forks copy only the
  manifest, so mods share assets with the original
- Worker: archive/RPA extraction, unrpyc for .rpyc, own .rpy parser, Python
  subset translated via ast, media converted to WebP/Opus/WebM
- React panel (library search, game page, upload, users, roles) and a
  framework-free web player with saves, history, skip/auto, transitions
- docker compose: postgres, backend, worker, nginx on a single HTTP port

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 02:41:32 +03:00

58 lines
2.0 KiB
TypeScript

import { useEffect, useState } from "react";
import { Link, useSearchParams } from "react-router-dom";
import { api, type Game } from "../api";
export function Library() {
const [params, setParams] = useSearchParams();
const q = params.get("q") ?? "";
const [query, setQuery] = useState(q);
const [games, setGames] = useState<Game[] | null>(null);
const [error, setError] = useState("");
useEffect(() => {
const t = setTimeout(() => setParams(query ? { q: query } : {}, { replace: true }), 250);
return () => clearTimeout(t);
}, [query, setParams]);
useEffect(() => {
let cancelled = false;
api.get<Game[]>(`/api/games?q=${encodeURIComponent(q)}`)
.then((g) => !cancelled && setGames(g))
.catch((e) => !cancelled && setError(e.message));
return () => {
cancelled = true;
};
}, [q]);
return (
<div className="page">
<div className="search">
<input placeholder="Поиск по названию, описанию или тегу…" value={query}
onChange={(e) => setQuery(e.target.value)} autoFocus />
</div>
{error && <div className="error">{error}</div>}
{games === null ? (
<p className="muted">Загрузка…</p>
) : games.length === 0 ? (
<p className="muted">{q ? "Ничего не найдено." : "В библиотеке пока нет игр."}</p>
) : (
<div className="grid">
{games.map((g) => (
<Link key={g.id} to={`/games/${g.id}`} className="game-card">
<div className="cover">
{g.cover_hash ? <img src={`/blobs/${g.cover_hash}`} alt="" loading="lazy" /> : <span>{g.title[0]}</span>}
</div>
<div className="game-card-body">
<b>{g.title}</b>
<div className="tags">
{g.tags.slice(0, 4).map((t) => <span key={t} className="tag">{t}</span>)}
</div>
</div>
</Link>
))}
</div>
)}
</div>
);
}