diff --git a/.env.example b/.env.example index d9a3f0e..63754b7 100644 --- a/.env.example +++ b/.env.example @@ -3,6 +3,7 @@ VITE_API_URL= # Server PORT=3001 -ALLOWED_ORIGIN=https://serve.uber.space +ALLOWED_ORIGIN=http://localhost:5173 TWITCH_CLIENT_ID= TWITCH_CLIENT_SECRET= +DATABASE_URL=postgresql://localhost:5432/whattoplay diff --git a/.gitignore b/.gitignore index 842f8ad..67832f8 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ node_modules .claude # Override global gitignore exclusions -!/server/ !/src/ !**/lib/ @@ -14,9 +13,7 @@ node_modules !.env.example # IGDB cache (generated at runtime) -server/data/igdb-cache.json -server/data/igdb-metadata.json -server/data/igdb-images/ +src/server/data/ # Build outputs dist @@ -24,9 +21,13 @@ build .vite coverage +# Database +drizzle/ + # Logs *.log npm-debug.log* # bun bun.lock +.mise.local.toml diff --git a/.mise.toml b/.mise.toml index 567d5e8..fa27325 100644 --- a/.mise.toml +++ b/.mise.toml @@ -1,3 +1,2 @@ [tools] -node = "22" -bun = "latest" +bun = "1.3.0" diff --git a/.vscode/tasks.json b/.vscode/tasks.json index f73b093..56d68c5 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -5,13 +5,10 @@ "label": "vite: dev server", "type": "shell", "command": "npm", - "args": [ - "run", - "dev" - ], + "args": ["run", "dev"], "isBackground": true, "problemMatcher": [], "group": "build" } ] -} \ No newline at end of file +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..3a279e8 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,55 @@ +# whattoplay — Game Discovery App + +Game recommendation and collection management tool with Steam/GOG integration and IGDB metadata. + +## Stack + +- **Frontend:** React 19, Vite, Tailwind CSS 4, TanStack Router (file-based), Zustand, PGlite +- **Backend:** Hono (Bun), Drizzle ORM, PostgreSQL, Twitch/IGDB API +- **Linting:** Biome (tabs, 80 chars, double quotes) + +## Project Structure + +``` +src/ +├── client/ ← React PWA (features/, routes/, shared/) +├── server/ ← Hono API (features/, shared/) +└── shared/ ← isomorphic code +``` + +## Local Development + +```bash +bun install +bun run dev # frontend (Vite) +bun run dev:server # backend (Bun --watch) +bun run dev:all # both +``` + +## Deployment + +```bash +./deploy.sh +``` + +Deploys to Uberspace (`serve.uber.space`): +- Frontend → `/var/www/virtual/serve/html/whattoplay/` +- Backend → `~/services/whattoplay/` (systemd: `whattoplay.service`, port 3001) +- Route: `/whattoplay/api/*` → port 3001 (prefix removed) + +## Environment Variables + +See `.env.example`: +- `DATABASE_URL` — PostgreSQL connection string +- `PORT` — server port (default 3001) +- `ALLOWED_ORIGIN` — CORS origin +- `TWITCH_CLIENT_ID`, `TWITCH_CLIENT_SECRET` — Twitch API credentials (for IGDB) + +## Database + +PostgreSQL via Drizzle ORM. Migrations in `drizzle/`. + +```bash +bun run db:generate +bun run db:migrate +``` diff --git a/biome.json b/biome.json index 549bd2a..79f8df2 100644 --- a/biome.json +++ b/biome.json @@ -1,7 +1,7 @@ { "$schema": "https://biomejs.dev/schemas/1.9.0/schema.json", "files": { - "ignore": ["src/routeTree.gen.ts"] + "ignore": ["src/client/routeTree.gen.ts", "drizzle/"] }, "organizeImports": { "enabled": true @@ -9,7 +9,7 @@ "formatter": { "enabled": true, "indentStyle": "tab", - "lineWidth": 100 + "lineWidth": 80 }, "linter": { "enabled": true, diff --git a/deploy.sh b/deploy.sh index 17261bd..5ae7ac9 100755 --- a/deploy.sh +++ b/deploy.sh @@ -16,16 +16,20 @@ echo "==> syncing frontend to $REMOTE_HTML_DIR/" ssh "$UBERSPACE_HOST" "mkdir -p $REMOTE_HTML_DIR" rsync -avz --delete dist/ "$UBERSPACE_HOST:$REMOTE_HTML_DIR/" -echo "==> syncing server to $REMOTE_SERVICE_DIR/" -ssh "$UBERSPACE_HOST" "mkdir -p $REMOTE_SERVICE_DIR/src $REMOTE_SERVICE_DIR/drizzle $REMOTE_SERVICE_DIR/data/steam-icons $REMOTE_SERVICE_DIR/data/igdb-images/thumb $REMOTE_SERVICE_DIR/data/igdb-images/cover_big $REMOTE_SERVICE_DIR/data/igdb-images/screenshot_med" +echo "==> syncing project to $REMOTE_SERVICE_DIR/" +ssh "$UBERSPACE_HOST" "mkdir -p $REMOTE_SERVICE_DIR" rsync -avz --delete \ - server/src/ "$UBERSPACE_HOST:$REMOTE_SERVICE_DIR/src/" -rsync -avz --delete \ - server/drizzle/ "$UBERSPACE_HOST:$REMOTE_SERVICE_DIR/drizzle/" -rsync -avz \ - server/package.json server/drizzle.config.ts "$UBERSPACE_HOST:$REMOTE_SERVICE_DIR/" + --exclude='.git/' \ + --exclude='.env' \ + --exclude='dist/' \ + --exclude='node_modules/' \ + --exclude='.DS_Store' \ + ./ "$UBERSPACE_HOST:$REMOTE_SERVICE_DIR/" -echo "==> installing server dependencies..." +echo "==> ensuring data directories exist..." +ssh "$UBERSPACE_HOST" "mkdir -p $REMOTE_SERVICE_DIR/src/server/data/steam-icons $REMOTE_SERVICE_DIR/src/server/data/igdb-images/thumb $REMOTE_SERVICE_DIR/src/server/data/igdb-images/cover_big $REMOTE_SERVICE_DIR/src/server/data/igdb-images/screenshot_med" + +echo "==> installing dependencies..." ssh "$UBERSPACE_HOST" "cd $REMOTE_SERVICE_DIR && bun install" echo "==> creating .env if missing..." @@ -48,17 +52,18 @@ ssh "$UBERSPACE_HOST" "uberspace web backend add /whattoplay/api port $PORT --re echo "==> setting up systemd service..." ssh "$UBERSPACE_HOST" "mkdir -p ~/.config/systemd/user" -ssh "$UBERSPACE_HOST" "cat > ~/.config/systemd/user/$SERVICE_NAME.service" <<'UNIT' +ssh "$UBERSPACE_HOST" "cat > ~/.config/systemd/user/$SERVICE_NAME.service" <
- + diff --git a/package.json b/package.json index bba85b6..5e8e0d0 100644 --- a/package.json +++ b/package.json @@ -5,24 +5,29 @@ "type": "module", "scripts": { "dev": "vite", - "dev:server": "cd server && bun run dev", + "dev:server": "bun --watch src/server/index.ts", "dev:all": "bun run dev & bun run dev:server", "build": "tsc --noEmit && vite build", "preview": "vite preview", + "start": "bun run src/server/index.ts", "lint": "biome check .", "lint:fix": "biome check --write .", "test": "vitest run", + "db:generate": "bunx drizzle-kit generate", + "db:migrate": "bunx drizzle-kit migrate", "prepare": "simple-git-hooks" }, "dependencies": { - "@biomejs/cli-darwin-arm64": "^2.4.5", "@electric-sql/pglite": "^0.2.17", + "@hono/zod-validator": "^0.5.0", "@tanstack/react-form": "^1.0.0", "@tanstack/react-router": "^1.114.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "drizzle-orm": "^0.45.1", "hono": "^4.7.0", "lucide-react": "^0.474.0", + "postgres": "^3.4.8", "radix-ui": "^1.4.3", "react": "^19.0.0", "react-dom": "^19.0.0", @@ -35,10 +40,12 @@ "@tailwindcss/vite": "^4.0.0", "@tanstack/router-plugin": "^1.114.0", "@testing-library/react": "^16.0.0", + "@types/bun": "^1.3.10", "@types/node": "^25.3.3", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@vitejs/plugin-react": "^4.4.0", + "drizzle-kit": "^0.31.9", "lint-staged": "^15.0.0", "simple-git-hooks": "^2.11.0", "tailwindcss": "^4.0.0", diff --git a/server/.env.example b/server/.env.example deleted file mode 100644 index 5956ef2..0000000 --- a/server/.env.example +++ /dev/null @@ -1,5 +0,0 @@ -PORT=3001 -ALLOWED_ORIGIN=http://localhost:5173 -TWITCH_CLIENT_ID= -TWITCH_CLIENT_SECRET= -DATABASE_URL=postgresql://localhost:5432/whattoplay diff --git a/server/drizzle/0000_heavy_lila_cheney.sql b/server/drizzle/0000_heavy_lila_cheney.sql deleted file mode 100644 index f1d811c..0000000 --- a/server/drizzle/0000_heavy_lila_cheney.sql +++ /dev/null @@ -1,20 +0,0 @@ -CREATE TABLE "igdb_metadata" ( - "canonical_id" text PRIMARY KEY NOT NULL, - "summary" text, - "cover_image_id" text, - "screenshots" jsonb, - "video_ids" jsonb, - "genres" jsonb, - "aggregated_rating" real, - "release_date" text, - "developers" jsonb, - "fetched_at" timestamp with time zone DEFAULT now() -); ---> statement-breakpoint -CREATE TABLE "igdb_resolutions" ( - "cache_key" text PRIMARY KEY NOT NULL, - "source" text NOT NULL, - "source_id" text NOT NULL, - "igdb_id" integer, - "resolved_at" timestamp with time zone DEFAULT now() -); diff --git a/server/drizzle/meta/0000_snapshot.json b/server/drizzle/meta/0000_snapshot.json deleted file mode 100644 index a776191..0000000 --- a/server/drizzle/meta/0000_snapshot.json +++ /dev/null @@ -1,137 +0,0 @@ -{ - "id": "cb778c12-a1c7-4483-ae9a-cfa44e7cab62", - "prevId": "00000000-0000-0000-0000-000000000000", - "version": "7", - "dialect": "postgresql", - "tables": { - "public.igdb_metadata": { - "name": "igdb_metadata", - "schema": "", - "columns": { - "canonical_id": { - "name": "canonical_id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "summary": { - "name": "summary", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "cover_image_id": { - "name": "cover_image_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "screenshots": { - "name": "screenshots", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "video_ids": { - "name": "video_ids", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "genres": { - "name": "genres", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "aggregated_rating": { - "name": "aggregated_rating", - "type": "real", - "primaryKey": false, - "notNull": false - }, - "release_date": { - "name": "release_date", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "developers": { - "name": "developers", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "fetched_at": { - "name": "fetched_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.igdb_resolutions": { - "name": "igdb_resolutions", - "schema": "", - "columns": { - "cache_key": { - "name": "cache_key", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "source": { - "name": "source", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "source_id": { - "name": "source_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "igdb_id": { - "name": "igdb_id", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "resolved_at": { - "name": "resolved_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - } - }, - "enums": {}, - "schemas": {}, - "sequences": {}, - "roles": {}, - "policies": {}, - "views": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} diff --git a/server/drizzle/meta/_journal.json b/server/drizzle/meta/_journal.json deleted file mode 100644 index 4d50f59..0000000 --- a/server/drizzle/meta/_journal.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "version": "7", - "dialect": "postgresql", - "entries": [ - { - "idx": 0, - "version": "7", - "when": 1772543801794, - "tag": "0000_heavy_lila_cheney", - "breakpoints": true - } - ] -} diff --git a/server/package.json b/server/package.json deleted file mode 100644 index ab192d2..0000000 --- a/server/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "whattoplay-server", - "private": true, - "version": "2026.03.02", - "type": "module", - "scripts": { - "dev": "bun --watch src/index.ts", - "start": "bun src/index.ts", - "db:generate": "bunx drizzle-kit generate", - "db:migrate": "bunx drizzle-kit migrate" - }, - "dependencies": { - "@hono/zod-validator": "^0.5.0", - "drizzle-orm": "^0.45.1", - "hono": "^4.7.0", - "postgres": "^3.4.8", - "zod": "^3.24.0" - }, - "devDependencies": { - "@types/bun": "^1.3.10", - "drizzle-kit": "^0.31.9" - } -} diff --git a/server/scripts/migrate-json-to-pg.ts b/server/scripts/migrate-json-to-pg.ts deleted file mode 100644 index a84b516..0000000 --- a/server/scripts/migrate-json-to-pg.ts +++ /dev/null @@ -1,106 +0,0 @@ -/** - * One-time migration: import existing igdb-cache.json and igdb-metadata.json - * into PostgreSQL. Run with: DATABASE_URL=... bun server/scripts/migrate-json-to-pg.ts - */ -import { readFileSync } from "node:fs" -import { dirname, join } from "node:path" -import { fileURLToPath } from "node:url" -import postgres from "postgres" - -const __dirname = dirname(fileURLToPath(import.meta.url)) -const DATA_DIR = join(__dirname, "../src/data") - -const DATABASE_URL = process.env.DATABASE_URL -if (!DATABASE_URL) { - console.error("DATABASE_URL is required") - process.exit(1) -} - -const sql = postgres(DATABASE_URL) - -const BATCH_SIZE = 500 - -async function migrateResolutions() { - const file = join(DATA_DIR, "igdb-cache.json") - let data: Record - try { - data = JSON.parse(readFileSync(file, "utf-8")) - } catch { - console.log("[migrate] No igdb-cache.json found, skipping resolutions") - return - } - - const entries = Object.entries(data) - console.log(`[migrate] Importing ${entries.length} resolution entries...`) - - for (let i = 0; i < entries.length; i += BATCH_SIZE) { - const batch = entries.slice(i, i + BATCH_SIZE) - const values = batch.map(([key, val]) => { - const [source, ...rest] = key.split(":") - const sourceId = rest.join(":") - return { cache_key: key, source, source_id: sourceId, igdb_id: val.igdbId } - }) - - await sql` - INSERT INTO igdb_resolutions ${sql(values, "cache_key", "source", "source_id", "igdb_id")} - ON CONFLICT (cache_key) DO NOTHING - ` - - console.log( - `[migrate] Resolutions: ${Math.min(i + BATCH_SIZE, entries.length)}/${entries.length}`, - ) - } -} - -async function migrateMetadata() { - const file = join(DATA_DIR, "igdb-metadata.json") - let data: Record< - string, - { - summary: string | null - coverImageId: string | null - screenshots: string[] - videoIds: string[] - genres: string[] - aggregatedRating: number | null - releaseDate: string | null - developers: string[] - } - > - try { - data = JSON.parse(readFileSync(file, "utf-8")) - } catch { - console.log("[migrate] No igdb-metadata.json found, skipping metadata") - return - } - - const entries = Object.entries(data) - console.log(`[migrate] Importing ${entries.length} metadata entries...`) - - for (let i = 0; i < entries.length; i += BATCH_SIZE) { - const batch = entries.slice(i, i + BATCH_SIZE) - const values = batch.map(([canonicalId, meta]) => ({ - canonical_id: canonicalId, - summary: meta.summary, - cover_image_id: meta.coverImageId, - screenshots: JSON.stringify(meta.screenshots), - video_ids: JSON.stringify(meta.videoIds), - genres: JSON.stringify(meta.genres), - aggregated_rating: meta.aggregatedRating, - release_date: meta.releaseDate, - developers: JSON.stringify(meta.developers), - })) - - await sql` - INSERT INTO igdb_metadata ${sql(values, "canonical_id", "summary", "cover_image_id", "screenshots", "video_ids", "genres", "aggregated_rating", "release_date", "developers")} - ON CONFLICT (canonical_id) DO NOTHING - ` - - console.log(`[migrate] Metadata: ${Math.min(i + BATCH_SIZE, entries.length)}/${entries.length}`) - } -} - -await migrateResolutions() -await migrateMetadata() -console.log("[migrate] Done!") -await sql.end() diff --git a/server/tsconfig.json b/server/tsconfig.json deleted file mode 100644 index d049ba0..0000000 --- a/server/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "moduleResolution": "bundler", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "allowImportingTsExtensions": true, - "types": ["bun-types"] - }, - "include": ["src"] -} diff --git a/src/app.css b/src/client/app.css similarity index 100% rename from src/app.css rename to src/client/app.css diff --git a/src/features/discover/components/card-stack.tsx b/src/client/features/discover/components/card-stack.tsx similarity index 91% rename from src/features/discover/components/card-stack.tsx rename to src/client/features/discover/components/card-stack.tsx index 20ae905..a4eb874 100644 --- a/src/features/discover/components/card-stack.tsx +++ b/src/client/features/discover/components/card-stack.tsx @@ -11,7 +11,8 @@ function getSteamHeaderImage(sourceId: string): string { } function getPreloadUrl(game: Game): string | null { - if (game.cover_image_id) return `${apiBase}/api/igdb/image/${game.cover_image_id}/cover_big` + if (game.cover_image_id) + return `${apiBase}/api/igdb/image/${game.cover_image_id}/cover_big` if (game.source === "steam") return getSteamHeaderImage(game.source_id) return null } @@ -22,7 +23,11 @@ interface CardStackProps { onSwipeRight: () => void } -export function CardStack({ games, onSwipeLeft, onSwipeRight }: CardStackProps) { +export function CardStack({ + games, + onSwipeLeft, + onSwipeRight, +}: CardStackProps) { const navigate = useNavigate() const topGame = games[0] diff --git a/src/features/discover/components/discover-done.tsx b/src/client/features/discover/components/discover-done.tsx similarity index 100% rename from src/features/discover/components/discover-done.tsx rename to src/client/features/discover/components/discover-done.tsx diff --git a/src/features/discover/components/discover-progress.tsx b/src/client/features/discover/components/discover-progress.tsx similarity index 84% rename from src/features/discover/components/discover-progress.tsx rename to src/client/features/discover/components/discover-progress.tsx index 53ed69a..7d3a2f4 100644 --- a/src/features/discover/components/discover-progress.tsx +++ b/src/client/features/discover/components/discover-progress.tsx @@ -6,7 +6,11 @@ interface DiscoverProgressProps { totalCount: number } -export function DiscoverProgress({ progress, seenCount, totalCount }: DiscoverProgressProps) { +export function DiscoverProgress({ + progress, + seenCount, + totalCount, +}: DiscoverProgressProps) { return (
diff --git a/src/features/discover/components/game-discover-card.tsx b/src/client/features/discover/components/game-discover-card.tsx similarity index 76% rename from src/features/discover/components/game-discover-card.tsx rename to src/client/features/discover/components/game-discover-card.tsx index bcb853d..bdc587b 100644 --- a/src/features/discover/components/game-discover-card.tsx +++ b/src/client/features/discover/components/game-discover-card.tsx @@ -20,15 +20,22 @@ function parseJsonArray(text: string | null): string[] { } export function GameDiscoverCard({ game }: GameDiscoverCardProps) { - const imageUrl = game.source === "steam" ? getSteamHeaderImage(game.source_id) : null - const dotColor = game.game_state !== "not_set" ? gameStateColors[game.game_state] : null + const imageUrl = + game.source === "steam" ? getSteamHeaderImage(game.source_id) : null + const dotColor = + game.game_state !== "not_set" ? gameStateColors[game.game_state] : null const genres = parseJsonArray(game.genres).slice(0, 3) - const rating = game.aggregated_rating != null ? Math.round(game.aggregated_rating) : null + const rating = + game.aggregated_rating != null ? Math.round(game.aggregated_rating) : null return (
{imageUrl && ( - {game.title} + {game.title} )}
@@ -37,7 +44,9 @@ export function GameDiscoverCard({ game }: GameDiscoverCardProps) { {game.source} {dotColor && ( - + )} {rating != null && ( @@ -55,7 +64,9 @@ export function GameDiscoverCard({ game }: GameDiscoverCardProps) {
)} {game.summary && ( -

{game.summary}

+

+ {game.summary} +

)} {!game.summary && game.playtime_hours > 0 && (

diff --git a/src/features/discover/components/swipe-buttons.tsx b/src/client/features/discover/components/swipe-buttons.tsx similarity index 100% rename from src/features/discover/components/swipe-buttons.tsx rename to src/client/features/discover/components/swipe-buttons.tsx diff --git a/src/features/discover/hooks/use-discover.ts b/src/client/features/discover/hooks/use-discover.ts similarity index 86% rename from src/features/discover/hooks/use-discover.ts rename to src/client/features/discover/hooks/use-discover.ts index d1cad43..3f88644 100644 --- a/src/features/discover/hooks/use-discover.ts +++ b/src/client/features/discover/hooks/use-discover.ts @@ -5,10 +5,13 @@ import { useDiscoverStore } from "../store" export function useDiscover() { const { games: allGames } = useGames() - const { games: wantToPlayGames, reload: reloadWtp } = usePlaylist("want-to-play") - const { games: notIntGames, reload: reloadNi } = usePlaylist("not-interesting") + const { games: wantToPlayGames, reload: reloadWtp } = + usePlaylist("want-to-play") + const { games: notIntGames, reload: reloadNi } = + usePlaylist("not-interesting") const { addGame } = usePlaylistMutations() - const { currentIndex, setCurrentIndex, updateShuffledGames, reset } = useDiscoverStore() + const { currentIndex, setCurrentIndex, updateShuffledGames, reset } = + useDiscoverStore() const [ready, setReady] = useState(false) const [localSeenIds, setLocalSeenIds] = useState>(new Set()) @@ -32,7 +35,9 @@ export function useDiscover() { const currentGame: Game | null = unseenGames[currentIndex] ?? null const isDone = ready && unseenGames.length === 0 const progress = - allGames.length > 0 ? ((allGames.length - unseenGames.length) / allGames.length) * 100 : 0 + allGames.length > 0 + ? ((allGames.length - unseenGames.length) / allGames.length) * 100 + : 0 const swipeRight = useCallback(() => { if (!currentGame) return diff --git a/src/features/discover/store.ts b/src/client/features/discover/store.ts similarity index 96% rename from src/features/discover/store.ts rename to src/client/features/discover/store.ts index b11f076..f46a4df 100644 --- a/src/features/discover/store.ts +++ b/src/client/features/discover/store.ts @@ -16,7 +16,11 @@ interface DiscoverState { reset: () => void } -function buildShuffleKey(games: Game[], seenIds: Set, seed: number): string { +function buildShuffleKey( + games: Game[], + seenIds: Set, + seed: number, +): string { return `${games.length}:${seenIds.size}:${seed}` } diff --git a/src/features/games/components/favorite-button.tsx b/src/client/features/games/components/favorite-button.tsx similarity index 91% rename from src/features/games/components/favorite-button.tsx rename to src/client/features/games/components/favorite-button.tsx index a47b574..3adbed2 100644 --- a/src/features/games/components/favorite-button.tsx +++ b/src/client/features/games/components/favorite-button.tsx @@ -8,7 +8,11 @@ interface FavoriteButtonProps { onChange?: () => void } -export function FavoriteButton({ gameId, isFavorite, onChange }: FavoriteButtonProps) { +export function FavoriteButton({ + gameId, + isFavorite, + onChange, +}: FavoriteButtonProps) { const updateGame = useUpdateGame() const { addGame, removeGame } = usePlaylistMutations() diff --git a/src/features/games/components/game-card.tsx b/src/client/features/games/components/game-card.tsx similarity index 72% rename from src/features/games/components/game-card.tsx rename to src/client/features/games/components/game-card.tsx index 3595c8b..8039acb 100644 --- a/src/features/games/components/game-card.tsx +++ b/src/client/features/games/components/game-card.tsx @@ -24,15 +24,29 @@ export function GameCard({ game, onUpdate }: GameCardProps) {

- {game.playtime_hours > 0 && {formatPlaytime(game.playtime_hours)}} + {game.playtime_hours > 0 && ( + {formatPlaytime(game.playtime_hours)} + )} {game.last_played && Last: {game.last_played}}
- +
- - + +
diff --git a/src/features/games/components/game-detail.tsx b/src/client/features/games/components/game-detail.tsx similarity index 81% rename from src/features/games/components/game-detail.tsx rename to src/client/features/games/components/game-detail.tsx index ce774c7..9fadfd4 100644 --- a/src/features/games/components/game-detail.tsx +++ b/src/client/features/games/components/game-detail.tsx @@ -33,19 +33,28 @@ export function GameDetail({ gameId }: GameDetailProps) { if (loading) return null if (!game) { - return

{t("game.notFound")}

+ return ( +

+ {t("game.notFound")} +

+ ) } return } -function GameDetailContent({ game, onUpdate }: { game: Game; onUpdate: () => void }) { - const imageUrl = game.source === "steam" ? getSteamHeaderImage(game.source_id) : null +function GameDetailContent({ + game, + onUpdate, +}: { game: Game; onUpdate: () => void }) { + const imageUrl = + game.source === "steam" ? getSteamHeaderImage(game.source_id) : null const genres = parseJsonArray(game.genres) const developers = parseJsonArray(game.developers) const screenshots = parseJsonArray(game.screenshots) const videoIds = parseJsonArray(game.video_ids) - const rating = game.aggregated_rating != null ? Math.round(game.aggregated_rating) : null + const rating = + game.aggregated_rating != null ? Math.round(game.aggregated_rating) : null return (
@@ -66,7 +75,9 @@ function GameDetailContent({ game, onUpdate }: { game: Game; onUpdate: () => voi
- {game.playtime_hours > 0 && {formatPlaytime(game.playtime_hours)}} + {game.playtime_hours > 0 && ( + {formatPlaytime(game.playtime_hours)} + )} {game.last_played && ( {t("game.lastPlayed")}: {game.last_played} @@ -74,7 +85,11 @@ function GameDetailContent({ game, onUpdate }: { game: Game; onUpdate: () => voi )}
- + {genres.length > 0 && ( @@ -89,10 +104,18 @@ function GameDetailContent({ game, onUpdate }: { game: Game; onUpdate: () => voi
- + {rating != null && {rating}%}
- +
{(developers.length > 0 || game.release_date) && ( @@ -124,7 +147,9 @@ function GameDetailContent({ game, onUpdate }: { game: Game; onUpdate: () => voi {screenshots.length > 0 && (
-

{t("game.screenshots")}

+

+ {t("game.screenshots")} +

{screenshots.map((id) => ( 0 ? formatPlaytime(game.playtime_hours) : undefined} + subtitle={ + game.playtime_hours > 0 + ? formatPlaytime(game.playtime_hours) + : undefined + } media={ coverUrl ? ( @@ -42,13 +46,17 @@ export function GameListItem({ game, onClick }: GameListItemProps) { } function GameListItemAfter({ game }: { game: Game }) { - const ratingText = game.rating >= 0 ? `★ ${Math.round(game.rating / 2)}/5` : null - const dotColor = game.game_state !== "not_set" ? gameStateColors[game.game_state] : null + const ratingText = + game.rating >= 0 ? `★ ${Math.round(game.rating / 2)}/5` : null + const dotColor = + game.game_state !== "not_set" ? gameStateColors[game.game_state] : null return ( {ratingText && {ratingText}} - {dotColor && } + {dotColor && ( + + )} {game.is_favorite && } ) diff --git a/src/features/games/components/game-state-select.tsx b/src/client/features/games/components/game-state-select.tsx similarity index 91% rename from src/features/games/components/game-state-select.tsx rename to src/client/features/games/components/game-state-select.tsx index 6b69c8a..3dd85a2 100644 --- a/src/features/games/components/game-state-select.tsx +++ b/src/client/features/games/components/game-state-select.tsx @@ -10,7 +10,11 @@ interface GameStateSelectProps { const states = Object.keys(gameStateLabels) as GameState[] -export function GameStateSelect({ gameId, state, onChange }: GameStateSelectProps) { +export function GameStateSelect({ + gameId, + state, + onChange, +}: GameStateSelectProps) { const updateGame = useUpdateGame() const handleChange = async (e: React.ChangeEvent) => { diff --git a/src/features/games/components/star-rating.tsx b/src/client/features/games/components/star-rating.tsx similarity index 94% rename from src/features/games/components/star-rating.tsx rename to src/client/features/games/components/star-rating.tsx index 52f3b18..a0c8e96 100644 --- a/src/features/games/components/star-rating.tsx +++ b/src/client/features/games/components/star-rating.tsx @@ -52,7 +52,10 @@ export function StarRating({ gameId, rating, onChange }: StarRatingProps) { ) : halfFilled ? (
-
+
diff --git a/src/features/games/schema.ts b/src/client/features/games/schema.ts similarity index 100% rename from src/features/games/schema.ts rename to src/client/features/games/schema.ts diff --git a/src/features/library/components/library-header.tsx b/src/client/features/library/components/library-header.tsx similarity index 86% rename from src/features/library/components/library-header.tsx rename to src/client/features/library/components/library-header.tsx index 8bfa6f5..0fedecb 100644 --- a/src/features/library/components/library-header.tsx +++ b/src/client/features/library/components/library-header.tsx @@ -6,7 +6,10 @@ interface LibraryHeaderProps { totalPlaytime: number } -export function LibraryHeader({ totalCount, totalPlaytime }: LibraryHeaderProps) { +export function LibraryHeader({ + totalCount, + totalPlaytime, +}: LibraryHeaderProps) { return (

{t("library.title")}

diff --git a/src/features/library/components/library-list.tsx b/src/client/features/library/components/library-list.tsx similarity index 84% rename from src/features/library/components/library-list.tsx rename to src/client/features/library/components/library-list.tsx index 29605aa..5402c11 100644 --- a/src/features/library/components/library-list.tsx +++ b/src/client/features/library/components/library-list.tsx @@ -30,7 +30,11 @@ export function LibraryList({ games, hasMore, loadMore }: LibraryListProps) { }, [hasMore, loadMore]) if (games.length === 0) { - return

{t("library.empty")}

+ return ( +

+ {t("library.empty")} +

+ ) } return ( @@ -40,7 +44,9 @@ export function LibraryList({ games, hasMore, loadMore }: LibraryListProps) { navigate({ to: "/games/$gameId", params: { gameId: game.id } })} + onClick={() => + navigate({ to: "/games/$gameId", params: { gameId: game.id } }) + } /> ))}
diff --git a/src/features/library/components/library-search.tsx b/src/client/features/library/components/library-search.tsx similarity index 83% rename from src/features/library/components/library-search.tsx rename to src/client/features/library/components/library-search.tsx index 452c752..e76db55 100644 --- a/src/features/library/components/library-search.tsx +++ b/src/client/features/library/components/library-search.tsx @@ -5,8 +5,14 @@ import { ArrowDownAZ, ArrowUpAZ } from "lucide-react" import { startTransition } from "react" export function LibrarySearch() { - const { searchText, setSearchText, sortBy, setSortBy, sortDirection, toggleSortDirection } = - useUiStore() + const { + searchText, + setSearchText, + sortBy, + setSortBy, + sortDirection, + toggleSortDirection, + } = useUiStore() return (
@@ -19,7 +25,9 @@ export function LibrarySearch() { +
{t("settings.data.clear")} - {t("settings.data.clearConfirm")} + + {t("settings.data.clearConfirm")} + -
diff --git a/src/features/settings/components/settings-list.tsx b/src/client/features/settings/components/settings-list.tsx similarity index 78% rename from src/features/settings/components/settings-list.tsx rename to src/client/features/settings/components/settings-list.tsx index 2cb6c31..5f7d716 100644 --- a/src/features/settings/components/settings-list.tsx +++ b/src/client/features/settings/components/settings-list.tsx @@ -19,7 +19,9 @@ export function SettingsList() { const gogConfig = useConfig<{ accessToken: string }>("gog") const navigate = useNavigate() - const [testState, setTestState] = useState<"idle" | "testing" | "ok" | "failed">("idle") + const [testState, setTestState] = useState< + "idle" | "testing" | "ok" | "failed" + >("idle") const { needRefresh: [needRefresh], @@ -53,19 +55,27 @@ export function SettingsList() {
updateServiceWorker()}> {t("settings.updateApp")} ) : ( - {t("settings.upToDate")} + + {t("settings.upToDate")} + ) } />
-

v{__APP_VERSION__}

+

+ v{__APP_VERSION__} +

{t("settings.server")} @@ -75,7 +85,9 @@ export function SettingsList() { title={t("settings.connection")} after={
- {testState === "testing" && } + {testState === "testing" && ( + + )} {testState === "ok" && ( {t("settings.connectionOk")} @@ -111,13 +123,20 @@ export function SettingsList() { after={ {isConnected(p.id) ? "Connected" : "Not configured"} } - onClick={() => navigate({ to: "/settings/$provider", params: { provider: p.id } })} + onClick={() => + navigate({ + to: "/settings/$provider", + params: { provider: p.id }, + }) + } /> ))}
@@ -129,7 +148,12 @@ export function SettingsList() { navigate({ to: "/settings/$provider", params: { provider: "data" } })} + onClick={() => + navigate({ + to: "/settings/$provider", + params: { provider: "data" }, + }) + } />

diff --git a/src/features/settings/components/steam-settings.tsx b/src/client/features/settings/components/steam-settings.tsx similarity index 86% rename from src/features/settings/components/steam-settings.tsx rename to src/client/features/settings/components/steam-settings.tsx index be7a151..7a77f94 100644 --- a/src/features/settings/components/steam-settings.tsx +++ b/src/client/features/settings/components/steam-settings.tsx @@ -30,7 +30,9 @@ export function SteamSettings() { return ( <> )} diff --git a/src/features/settings/hooks/use-data-management.ts b/src/client/features/settings/hooks/use-data-management.ts similarity index 96% rename from src/features/settings/hooks/use-data-management.ts rename to src/client/features/settings/hooks/use-data-management.ts index a0c53b3..662ca87 100644 --- a/src/features/settings/hooks/use-data-management.ts +++ b/src/client/features/settings/hooks/use-data-management.ts @@ -18,7 +18,9 @@ export function useDataManagement() { config: config.rows, } - const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" }) + const blob = new Blob([JSON.stringify(data, null, 2)], { + type: "application/json", + }) const url = URL.createObjectURL(blob) const a = document.createElement("a") a.href = url diff --git a/src/features/settings/schema.ts b/src/client/features/settings/schema.ts similarity index 100% rename from src/features/settings/schema.ts rename to src/client/features/settings/schema.ts diff --git a/src/main.tsx b/src/client/main.tsx similarity index 100% rename from src/main.tsx rename to src/client/main.tsx diff --git a/src/routeTree.gen.ts b/src/client/routeTree.gen.ts similarity index 100% rename from src/routeTree.gen.ts rename to src/client/routeTree.gen.ts diff --git a/src/routes/__root.tsx b/src/client/routes/__root.tsx similarity index 93% rename from src/routes/__root.tsx rename to src/client/routes/__root.tsx index 673882f..b960f8f 100644 --- a/src/routes/__root.tsx +++ b/src/client/routes/__root.tsx @@ -1,5 +1,10 @@ import { t } from "@/shared/i18n" -import { Outlet, createRootRoute, useNavigate, useRouterState } from "@tanstack/react-router" +import { + Outlet, + createRootRoute, + useNavigate, + useRouterState, +} from "@tanstack/react-router" import { Gamepad2, Library, ListMusic, Settings } from "lucide-react" export const Route = createRootRoute({ diff --git a/src/routes/discover/index.tsx b/src/client/routes/discover/index.tsx similarity index 69% rename from src/routes/discover/index.tsx rename to src/client/routes/discover/index.tsx index d22acbd..05967a8 100644 --- a/src/routes/discover/index.tsx +++ b/src/client/routes/discover/index.tsx @@ -11,21 +11,39 @@ export const Route = createFileRoute("/discover/")({ }) function DiscoverPage() { - const { unseenGames, isDone, progress, totalCount, seenCount, swipeRight, swipeLeft, reset } = - useDiscover() + const { + unseenGames, + isDone, + progress, + totalCount, + seenCount, + swipeRight, + swipeLeft, + reset, + } = useDiscover() return (
- + {isDone ? ( ) : unseenGames.length === 0 ? ( -

{t("discover.empty")}

+

+ {t("discover.empty")} +

) : ( <>
- +
{t("general.loading")}
+ return ( +
+ {t("general.loading")} +
+ ) } return (
- +
) } diff --git a/src/routes/playlists/$playlistId.tsx b/src/client/routes/playlists/$playlistId.tsx similarity index 82% rename from src/routes/playlists/$playlistId.tsx rename to src/client/routes/playlists/$playlistId.tsx index 496ceca..7e1e811 100644 --- a/src/routes/playlists/$playlistId.tsx +++ b/src/client/routes/playlists/$playlistId.tsx @@ -11,7 +11,10 @@ function PlaylistDetailPage() { return (
- + Back diff --git a/src/routes/playlists/index.tsx b/src/client/routes/playlists/index.tsx similarity index 100% rename from src/routes/playlists/index.tsx rename to src/client/routes/playlists/index.tsx diff --git a/src/routes/settings/$provider.tsx b/src/client/routes/settings/$provider.tsx similarity index 91% rename from src/routes/settings/$provider.tsx rename to src/client/routes/settings/$provider.tsx index 19d72df..debba84 100644 --- a/src/routes/settings/$provider.tsx +++ b/src/client/routes/settings/$provider.tsx @@ -22,7 +22,11 @@ function ProviderSettingsPage() { return (
-

{titles[provider] ?? provider}

diff --git a/src/routes/settings/index.tsx b/src/client/routes/settings/index.tsx similarity index 100% rename from src/routes/settings/index.tsx rename to src/client/routes/settings/index.tsx diff --git a/src/shared/components/sync-progress.tsx b/src/client/shared/components/sync-progress.tsx similarity index 100% rename from src/shared/components/sync-progress.tsx rename to src/client/shared/components/sync-progress.tsx diff --git a/src/shared/components/ui/badge.tsx b/src/client/shared/components/ui/badge.tsx similarity index 89% rename from src/shared/components/ui/badge.tsx rename to src/client/shared/components/ui/badge.tsx index 1fdb58a..25e0d96 100644 --- a/src/shared/components/ui/badge.tsx +++ b/src/client/shared/components/ui/badge.tsx @@ -10,7 +10,8 @@ const badgeVariants = cva( variants: { variant: { default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90", - secondary: "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + secondary: + "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", destructive: "bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", outline: @@ -30,7 +31,8 @@ function Badge({ variant = "default", asChild = false, ...props -}: React.ComponentProps<"span"> & VariantProps & { asChild?: boolean }) { +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { const Comp = asChild ? Slot.Root : "span" return ( diff --git a/src/shared/components/ui/button.tsx b/src/client/shared/components/ui/button.tsx similarity index 92% rename from src/shared/components/ui/button.tsx rename to src/client/shared/components/ui/button.tsx index 1e15413..3e64c86 100644 --- a/src/shared/components/ui/button.tsx +++ b/src/client/shared/components/ui/button.tsx @@ -14,8 +14,10 @@ const buttonVariants = cva( "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", outline: "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", - secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", - ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", link: "text-primary underline-offset-4 hover:underline", }, size: { diff --git a/src/shared/components/ui/card.tsx b/src/client/shared/components/ui/card.tsx similarity index 83% rename from src/shared/components/ui/card.tsx rename to src/client/shared/components/ui/card.tsx index 760c10b..9e59abb 100644 --- a/src/shared/components/ui/card.tsx +++ b/src/client/shared/components/ui/card.tsx @@ -52,14 +52,23 @@ function CardAction({ className, ...props }: React.ComponentProps<"div">) { return (
) } function CardContent({ className, ...props }: React.ComponentProps<"div">) { - return
+ return ( +
+ ) } function CardFooter({ className, ...props }: React.ComponentProps<"div">) { @@ -72,4 +81,12 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) { ) } -export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent } +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/src/shared/components/ui/dialog.tsx b/src/client/shared/components/ui/dialog.tsx similarity index 86% rename from src/shared/components/ui/dialog.tsx rename to src/client/shared/components/ui/dialog.tsx index 9a53029..e4b1797 100644 --- a/src/shared/components/ui/dialog.tsx +++ b/src/client/shared/components/ui/dialog.tsx @@ -5,19 +5,27 @@ import type * as React from "react" import { Button } from "@/shared/components/ui/button" import { cn } from "@/shared/lib/utils" -function Dialog({ ...props }: React.ComponentProps) { +function Dialog({ + ...props +}: React.ComponentProps) { return } -function DialogTrigger({ ...props }: React.ComponentProps) { +function DialogTrigger({ + ...props +}: React.ComponentProps) { return } -function DialogPortal({ ...props }: React.ComponentProps) { +function DialogPortal({ + ...props +}: React.ComponentProps) { return } -function DialogClose({ ...props }: React.ComponentProps) { +function DialogClose({ + ...props +}: React.ComponentProps) { return } @@ -92,7 +100,10 @@ function DialogFooter({ return (
{children} @@ -105,7 +116,10 @@ function DialogFooter({ ) } -function DialogTitle({ className, ...props }: React.ComponentProps) { +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { return ( ) { +function DropdownMenu({ + ...props +}: React.ComponentProps) { return } function DropdownMenuPortal({ ...props }: React.ComponentProps) { - return + return ( + + ) } function DropdownMenuTrigger({ ...props }: React.ComponentProps) { - return + return ( + + ) } function DropdownMenuContent({ @@ -42,8 +51,12 @@ function DropdownMenuContent({ ) } -function DropdownMenuGroup({ ...props }: React.ComponentProps) { - return +function DropdownMenuGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) } function DropdownMenuItem({ @@ -98,7 +111,12 @@ function DropdownMenuCheckboxItem({ function DropdownMenuRadioGroup({ ...props }: React.ComponentProps) { - return + return ( + + ) } function DropdownMenuRadioItem({ @@ -136,7 +154,10 @@ function DropdownMenuLabel({ ) @@ -155,17 +176,25 @@ function DropdownMenuSeparator({ ) } -function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<"span">) { +function DropdownMenuShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { return ( ) } -function DropdownMenuSub({ ...props }: React.ComponentProps) { +function DropdownMenuSub({ + ...props +}: React.ComponentProps) { return } diff --git a/src/shared/components/ui/input.tsx b/src/client/shared/components/ui/input.tsx similarity index 100% rename from src/shared/components/ui/input.tsx rename to src/client/shared/components/ui/input.tsx diff --git a/src/shared/components/ui/list-item.tsx b/src/client/shared/components/ui/list-item.tsx similarity index 81% rename from src/shared/components/ui/list-item.tsx rename to src/client/shared/components/ui/list-item.tsx index 4cf16d9..b0670ca 100644 --- a/src/shared/components/ui/list-item.tsx +++ b/src/client/shared/components/ui/list-item.tsx @@ -36,10 +36,16 @@ export function ListItem({ {media &&
{media}
}
{title}
- {subtitle &&
{subtitle}
} + {subtitle && ( +
+ {subtitle} +
+ )}
{after &&
{after}
} - {link && } + {link && ( + + )} ) } diff --git a/src/shared/components/ui/select.tsx b/src/client/shared/components/ui/select.tsx similarity index 90% rename from src/shared/components/ui/select.tsx rename to src/client/shared/components/ui/select.tsx index b09478f..06a7e4e 100644 --- a/src/shared/components/ui/select.tsx +++ b/src/client/shared/components/ui/select.tsx @@ -6,15 +6,21 @@ import type * as React from "react" import { cn } from "@/shared/lib/utils" -function Select({ ...props }: React.ComponentProps) { +function Select({ + ...props +}: React.ComponentProps) { return } -function SelectGroup({ ...props }: React.ComponentProps) { +function SelectGroup({ + ...props +}: React.ComponentProps) { return } -function SelectValue({ ...props }: React.ComponentProps) { +function SelectValue({ + ...props +}: React.ComponentProps) { return } @@ -81,7 +87,10 @@ function SelectContent({ ) } -function SelectLabel({ className, ...props }: React.ComponentProps) { +function SelectLabel({ + className, + ...props +}: React.ComponentProps) { return ( @@ -153,7 +165,10 @@ function SelectScrollDownButton({ return ( diff --git a/src/shared/components/ui/separator.tsx b/src/client/shared/components/ui/separator.tsx similarity index 100% rename from src/shared/components/ui/separator.tsx rename to src/client/shared/components/ui/separator.tsx diff --git a/src/shared/components/ui/sheet.tsx b/src/client/shared/components/ui/sheet.tsx similarity index 90% rename from src/shared/components/ui/sheet.tsx rename to src/client/shared/components/ui/sheet.tsx index 1d9ec32..598e2bd 100644 --- a/src/shared/components/ui/sheet.tsx +++ b/src/client/shared/components/ui/sheet.tsx @@ -10,15 +10,21 @@ function Sheet({ ...props }: React.ComponentProps) { return } -function SheetTrigger({ ...props }: React.ComponentProps) { +function SheetTrigger({ + ...props +}: React.ComponentProps) { return } -function SheetClose({ ...props }: React.ComponentProps) { +function SheetClose({ + ...props +}: React.ComponentProps) { return } -function SheetPortal({ ...props }: React.ComponentProps) { +function SheetPortal({ + ...props +}: React.ComponentProps) { return } @@ -99,7 +105,10 @@ function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { ) } -function SheetTitle({ className, ...props }: React.ComponentProps) { +function SheetTitle({ + className, + ...props +}: React.ComponentProps) { return ( ) @@ -39,7 +42,8 @@ function TabsList({ className, variant = "default", ...props -}: React.ComponentProps & VariantProps) { +}: React.ComponentProps & + VariantProps) { return ( ) { +function TabsTrigger({ + className, + ...props +}: React.ComponentProps) { return ( ) { +function TabsContent({ + className, + ...props +}: React.ComponentProps) { return ( { const db = await getDb() - const result = await db.query("SELECT * FROM games WHERE id = $1", [id]) + const result = await db.query("SELECT * FROM games WHERE id = $1", [ + id, + ]) setGame(result.rows[0] ?? null) setLoading(false) }, [id]) @@ -39,7 +41,9 @@ export function useGame(id: string) { } export function usePlaylists() { - const [playlists, setPlaylists] = useState<(Playlist & { game_count: number })[]>([]) + const [playlists, setPlaylists] = useState< + (Playlist & { game_count: number })[] + >([]) const [loading, setLoading] = useState(true) const reload = useCallback(async () => { @@ -72,7 +76,10 @@ export function usePlaylist(id: string) { const reload = useCallback(async () => { const db = await getDb() - const pResult = await db.query("SELECT * FROM playlists WHERE id = $1", [id]) + const pResult = await db.query( + "SELECT * FROM playlists WHERE id = $1", + [id], + ) setPlaylist(pResult.rows[0] ?? null) const gResult = await db.query( @@ -101,7 +108,10 @@ export function useConfig(key: string) { useEffect(() => { async function load() { const db = await getDb() - const result = await db.query<{ value: T }>("SELECT value FROM config WHERE key = $1", [key]) + const result = await db.query<{ value: T }>( + "SELECT value FROM config WHERE key = $1", + [key], + ) setValue(result.rows[0]?.value ?? null) } load() @@ -123,7 +133,10 @@ export function useSaveConfig() { export function useSaveGamesBySource() { return useCallback( - async (_source: string, games: Omit[]) => { + async ( + _source: string, + games: Omit[], + ) => { const db = await getDb() for (const game of games) { await db.query( @@ -168,7 +181,10 @@ export function useUpdateGame() { fields.push("updated_at = NOW()") values.push(id) - await db.query(`UPDATE games SET ${fields.join(", ")} WHERE id = $${idx}`, values) + await db.query( + `UPDATE games SET ${fields.join(", ")} WHERE id = $${idx}`, + values, + ) }, []) } @@ -184,27 +200,36 @@ export function usePlaylistMutations() { const removeGame = useCallback(async (playlistId: string, gameId: string) => { const db = await getDb() - await db.query("DELETE FROM playlist_games WHERE playlist_id = $1 AND game_id = $2", [ - playlistId, - gameId, - ]) + await db.query( + "DELETE FROM playlist_games WHERE playlist_id = $1 AND game_id = $2", + [playlistId, gameId], + ) }, []) const createPlaylist = useCallback(async (name: string): Promise => { const db = await getDb() const id = `custom-${Date.now()}` - await db.query("INSERT INTO playlists (id, name, is_static) VALUES ($1, $2, FALSE)", [id, name]) + await db.query( + "INSERT INTO playlists (id, name, is_static) VALUES ($1, $2, FALSE)", + [id, name], + ) return id }, []) const renamePlaylist = useCallback(async (id: string, name: string) => { const db = await getDb() - await db.query("UPDATE playlists SET name = $1 WHERE id = $2 AND is_static = FALSE", [name, id]) + await db.query( + "UPDATE playlists SET name = $1 WHERE id = $2 AND is_static = FALSE", + [name, id], + ) }, []) const deletePlaylist = useCallback(async (id: string) => { const db = await getDb() - await db.query("DELETE FROM playlists WHERE id = $1 AND is_static = FALSE", [id]) + await db.query( + "DELETE FROM playlists WHERE id = $1 AND is_static = FALSE", + [id], + ) }, []) return { addGame, removeGame, createPlaylist, renamePlaylist, deletePlaylist } diff --git a/src/shared/db/migrations/001-initial.sql b/src/client/shared/db/migrations/001-initial.sql similarity index 100% rename from src/shared/db/migrations/001-initial.sql rename to src/client/shared/db/migrations/001-initial.sql diff --git a/src/shared/db/migrations/002-metadata.sql b/src/client/shared/db/migrations/002-metadata.sql similarity index 100% rename from src/shared/db/migrations/002-metadata.sql rename to src/client/shared/db/migrations/002-metadata.sql diff --git a/src/shared/db/schema.ts b/src/client/shared/db/schema.ts similarity index 100% rename from src/shared/db/schema.ts rename to src/client/shared/db/schema.ts diff --git a/src/shared/hooks/use-swipe-gesture.ts b/src/client/shared/hooks/use-swipe-gesture.ts similarity index 100% rename from src/shared/hooks/use-swipe-gesture.ts rename to src/client/shared/hooks/use-swipe-gesture.ts diff --git a/src/shared/i18n/index.ts b/src/client/shared/i18n/index.ts similarity index 86% rename from src/shared/i18n/index.ts rename to src/client/shared/i18n/index.ts index a429ca2..56fa8df 100644 --- a/src/shared/i18n/index.ts +++ b/src/client/shared/i18n/index.ts @@ -18,7 +18,10 @@ export function setLocale(locale: Locale) { currentLocale = locale } -export function t(key: TranslationKey, params?: Record): string { +export function t( + key: TranslationKey, + params?: Record, +): string { const value = translations[currentLocale][key] ?? translations.en[key] ?? key if (!params) return value return Object.entries(params).reduce( diff --git a/src/shared/i18n/locales/de.ts b/src/client/shared/i18n/locales/de.ts similarity index 94% rename from src/shared/i18n/locales/de.ts rename to src/client/shared/i18n/locales/de.ts index 566864d..be5eb7a 100644 --- a/src/shared/i18n/locales/de.ts +++ b/src/client/shared/i18n/locales/de.ts @@ -8,7 +8,8 @@ export const de: Record = { "library.title": "Bibliothek", "library.search": "Spiele suchen...", - "library.empty": "Noch keine Spiele. Füge Anbieter in den Einstellungen hinzu.", + "library.empty": + "Noch keine Spiele. Füge Anbieter in den Einstellungen hinzu.", "library.games": "Spiele", "library.hours": "Stunden gespielt", "library.sort.title": "Titel", @@ -44,7 +45,8 @@ export const de: Record = { "settings.data.export": "Daten exportieren", "settings.data.import": "Daten importieren", "settings.data.clear": "Alle Daten löschen", - "settings.data.clearConfirm": "Bist du sicher? Alle Spiele und Playlisten werden gelöscht.", + "settings.data.clearConfirm": + "Bist du sicher? Alle Spiele und Playlisten werden gelöscht.", "settings.app": "App", "settings.updateAvailable": "Update verfügbar", "settings.appUpToDate": "App ist aktuell", diff --git a/src/shared/i18n/locales/en.ts b/src/client/shared/i18n/locales/en.ts similarity index 97% rename from src/shared/i18n/locales/en.ts rename to src/client/shared/i18n/locales/en.ts index 55c1e0a..b40255b 100644 --- a/src/shared/i18n/locales/en.ts +++ b/src/client/shared/i18n/locales/en.ts @@ -47,7 +47,8 @@ export const en = { "settings.data.export": "Export Data", "settings.data.import": "Import Data", "settings.data.clear": "Clear All Data", - "settings.data.clearConfirm": "Are you sure? This will delete all games and playlists.", + "settings.data.clearConfirm": + "Are you sure? This will delete all games and playlists.", "settings.app": "App", "settings.updateAvailable": "Update available", "settings.appUpToDate": "App is up to date", diff --git a/src/shared/i18n/locales/es.ts b/src/client/shared/i18n/locales/es.ts similarity index 100% rename from src/shared/i18n/locales/es.ts rename to src/client/shared/i18n/locales/es.ts diff --git a/src/shared/i18n/locales/fr.ts b/src/client/shared/i18n/locales/fr.ts similarity index 100% rename from src/shared/i18n/locales/fr.ts rename to src/client/shared/i18n/locales/fr.ts diff --git a/src/client/shared/lib/api.ts b/src/client/shared/lib/api.ts new file mode 100644 index 0000000..d72da8f --- /dev/null +++ b/src/client/shared/lib/api.ts @@ -0,0 +1,8 @@ +import { hc } from "hono/client" +import type { AppType } from "../../../server/app" + +const baseUrl = + import.meta.env.VITE_API_URL || + `${import.meta.env.BASE_URL.replace(/\/$/, "")}/api` + +export const api = hc(baseUrl) diff --git a/src/shared/lib/utils.ts b/src/client/shared/lib/utils.ts similarity index 100% rename from src/shared/lib/utils.ts rename to src/client/shared/lib/utils.ts diff --git a/src/shared/stores/sync-store.ts b/src/client/shared/stores/sync-store.ts similarity index 78% rename from src/shared/stores/sync-store.ts rename to src/client/shared/stores/sync-store.ts index 384d51f..2a3bf11 100644 --- a/src/shared/stores/sync-store.ts +++ b/src/client/shared/stores/sync-store.ts @@ -14,14 +14,21 @@ interface SyncStore { steam: SourceState gog: SourceState syncSteam: (config: { apiKey: string; steamId: string }) => Promise - connectGog: ( - code: string, - ) => Promise<{ access_token: string; refresh_token: string; user_id: string } | null> + connectGog: (code: string) => Promise<{ + access_token: string + refresh_token: string + user_id: string + } | null> syncGogGames: (accessToken: string, refreshToken: string) => Promise clearError: (source: "steam" | "gog") => void } -const initial: SourceState = { syncing: false, error: null, lastCount: null, progress: null } +const initial: SourceState = { + syncing: false, + error: null, + lastCount: null, + progress: null, +} async function enrichAfterSync( source: "steam" | "gog", @@ -45,7 +52,9 @@ async function enrichAfterSync( })) // Single round trip: resolve canonical IDs + fetch metadata - const res = await api.igdb.resolve.$post({ json: { games: gamesToResolve } }) + const res = await api.igdb.resolve.$post({ + json: { games: gamesToResolve }, + }) if (!res.ok) return const data = await res.json() @@ -53,11 +62,10 @@ async function enrichAfterSync( // Apply canonical IDs for (const g of data.games) { if (g.canonicalId) { - await db.query("UPDATE games SET canonical_id = $1 WHERE source = $2 AND source_id = $3", [ - g.canonicalId, - g.source, - g.sourceId, - ]) + await db.query( + "UPDATE games SET canonical_id = $1 WHERE source = $2 AND source_id = $3", + [g.canonicalId, g.source, g.sourceId], + ) } } @@ -83,7 +91,10 @@ async function enrichAfterSync( ) } } catch (err) { - console.error("[sync] enrichment failed (non-fatal):", (err as Error).message) + console.error( + "[sync] enrichment failed (non-fatal):", + (err as Error).message, + ) } } @@ -147,9 +158,19 @@ export const useSyncStore = create((set, get) => ({ syncSteam: async (config) => { if (get().steam.syncing) return - set({ steam: { syncing: true, error: null, lastCount: null, progress: "fetching" } }) + set({ + steam: { + syncing: true, + error: null, + lastCount: null, + progress: "fetching", + }, + }) try { - await saveConfig("steam", { apiKey: config.apiKey, steamId: config.steamId }) + await saveConfig("steam", { + apiKey: config.apiKey, + steamId: config.steamId, + }) const res = await api.steam.games.$post({ json: { apiKey: config.apiKey, steamId: config.steamId }, @@ -180,21 +201,42 @@ export const useSyncStore = create((set, get) => ({ })) await saveGamesBySource("steam", dbGames, (current, total) => { - set({ steam: { ...get().steam, progress: `saving:${current}:${total}` } }) + set({ + steam: { ...get().steam, progress: `saving:${current}:${total}` }, + }) }) await saveConfig("steam_last_sync", new Date().toISOString()) await enrichAfterSync("steam", set, get) - set({ steam: { syncing: false, error: null, lastCount: data.count, progress: null } }) + set({ + steam: { + syncing: false, + error: null, + lastCount: data.count, + progress: null, + }, + }) } catch (err) { set({ - steam: { syncing: false, error: (err as Error).message, lastCount: null, progress: null }, + steam: { + syncing: false, + error: (err as Error).message, + lastCount: null, + progress: null, + }, }) } }, connectGog: async (code) => { if (get().gog.syncing) return null - set({ gog: { syncing: true, error: null, lastCount: null, progress: "fetching" } }) + set({ + gog: { + syncing: true, + error: null, + lastCount: null, + progress: "fetching", + }, + }) try { const res = await api.gog.auth.$post({ json: { code } }) if (!res.ok) { @@ -216,11 +258,18 @@ export const useSyncStore = create((set, get) => ({ userId: tokens.user_id, }) - set({ gog: { syncing: false, error: null, lastCount: null, progress: null } }) + set({ + gog: { syncing: false, error: null, lastCount: null, progress: null }, + }) return tokens } catch (err) { set({ - gog: { syncing: false, error: (err as Error).message, lastCount: null, progress: null }, + gog: { + syncing: false, + error: (err as Error).message, + lastCount: null, + progress: null, + }, }) return null } @@ -228,7 +277,14 @@ export const useSyncStore = create((set, get) => ({ syncGogGames: async (accessToken, refreshToken) => { if (get().gog.syncing) return - set({ gog: { syncing: true, error: null, lastCount: null, progress: "fetching" } }) + set({ + gog: { + syncing: true, + error: null, + lastCount: null, + progress: "fetching", + }, + }) try { const res = await api.gog.games.$post({ json: { accessToken, refreshToken }, @@ -270,10 +326,22 @@ export const useSyncStore = create((set, get) => ({ }) await saveConfig("gog_last_sync", new Date().toISOString()) await enrichAfterSync("gog", set, get) - set({ gog: { syncing: false, error: null, lastCount: data.count, progress: null } }) + set({ + gog: { + syncing: false, + error: null, + lastCount: data.count, + progress: null, + }, + }) } catch (err) { set({ - gog: { syncing: false, error: (err as Error).message, lastCount: null, progress: null }, + gog: { + syncing: false, + error: (err as Error).message, + lastCount: null, + progress: null, + }, }) } }, diff --git a/src/shared/stores/ui-store.ts b/src/client/shared/stores/ui-store.ts similarity index 100% rename from src/shared/stores/ui-store.ts rename to src/client/shared/stores/ui-store.ts diff --git a/src/vite-env.d.ts b/src/client/vite-env.d.ts similarity index 100% rename from src/vite-env.d.ts rename to src/client/vite-env.d.ts diff --git a/server/src/app.ts b/src/server/app.ts similarity index 100% rename from server/src/app.ts rename to src/server/app.ts diff --git a/server/src/features/gog/router.ts b/src/server/features/gog/router.ts similarity index 100% rename from server/src/features/gog/router.ts rename to src/server/features/gog/router.ts diff --git a/server/src/features/gog/schema.ts b/src/server/features/gog/schema.ts similarity index 100% rename from server/src/features/gog/schema.ts rename to src/server/features/gog/schema.ts diff --git a/server/src/features/gog/service.ts b/src/server/features/gog/service.ts similarity index 93% rename from server/src/features/gog/service.ts rename to src/server/features/gog/service.ts index 6ea4a29..9f9c8ff 100644 --- a/server/src/features/gog/service.ts +++ b/src/server/features/gog/service.ts @@ -1,5 +1,6 @@ const CLIENT_ID = "46899977096215655" -const CLIENT_SECRET = "9d85c43b1482497dbbce61f6e4aa173a433796eeae2ca8c5f6129f2dc4de46d9" +const CLIENT_SECRET = + "9d85c43b1482497dbbce61f6e4aa173a433796eeae2ca8c5f6129f2dc4de46d9" const REDIRECT_URI = "https://embed.gog.com/on_login_success?origin=client" export async function exchangeGogCode(code: string) { @@ -76,7 +77,9 @@ export async function fetchGogGames(accessToken: string, refreshToken: string) { } if (!response.ok) { - throw new Error(`GOG API Error: ${response.status} ${response.statusText}`) + throw new Error( + `GOG API Error: ${response.status} ${response.statusText}`, + ) } const data = await response.json() @@ -86,7 +89,9 @@ export async function fetchGogGames(accessToken: string, refreshToken: string) { } const games = allProducts - .filter((product): product is GogProduct & { title: string } => Boolean(product.title)) + .filter((product): product is GogProduct & { title: string } => + Boolean(product.title), + ) .map((product) => ({ id: `gog-${product.id}`, title: product.title, diff --git a/server/src/features/igdb/cache.ts b/src/server/features/igdb/cache.ts similarity index 72% rename from server/src/features/igdb/cache.ts rename to src/server/features/igdb/cache.ts index 905ada2..4ac09af 100644 --- a/server/src/features/igdb/cache.ts +++ b/src/server/features/igdb/cache.ts @@ -6,10 +6,15 @@ interface CacheEntry { igdbId: number | null } -export async function getCacheEntries(keys: string[]): Promise> { +export async function getCacheEntries( + keys: string[], +): Promise> { if (keys.length === 0) return new Map() const rows = await db - .select({ cacheKey: igdbResolutions.cacheKey, igdbId: igdbResolutions.igdbId }) + .select({ + cacheKey: igdbResolutions.cacheKey, + igdbId: igdbResolutions.igdbId, + }) .from(igdbResolutions) .where(inArray(igdbResolutions.cacheKey, keys)) const result = new Map() @@ -20,7 +25,12 @@ export async function getCacheEntries(keys: string[]): Promise, + entries: Array<{ + cacheKey: string + source: string + sourceId: string + igdbId: number | null + }>, ) { if (entries.length === 0) return await db diff --git a/server/src/features/igdb/image-cache.ts b/src/server/features/igdb/image-cache.ts similarity index 100% rename from server/src/features/igdb/image-cache.ts rename to src/server/features/igdb/image-cache.ts diff --git a/server/src/features/igdb/metadata-cache.ts b/src/server/features/igdb/metadata-cache.ts similarity index 94% rename from server/src/features/igdb/metadata-cache.ts rename to src/server/features/igdb/metadata-cache.ts index bf11aad..8710317 100644 --- a/server/src/features/igdb/metadata-cache.ts +++ b/src/server/features/igdb/metadata-cache.ts @@ -13,7 +13,9 @@ export interface IgdbMetadata { developers: string[] } -export async function getMetadataBatch(canonicalIds: string[]): Promise> { +export async function getMetadataBatch( + canonicalIds: string[], +): Promise> { if (canonicalIds.length === 0) return new Map() const rows = await db .select() diff --git a/server/src/features/igdb/router.ts b/src/server/features/igdb/router.ts similarity index 90% rename from server/src/features/igdb/router.ts rename to src/server/features/igdb/router.ts index 68c6b5a..5df8f85 100644 --- a/server/src/features/igdb/router.ts +++ b/src/server/features/igdb/router.ts @@ -1,8 +1,17 @@ import { zValidator } from "@hono/zod-validator" import { Hono } from "hono" import { z } from "zod" -import { fetchAndCacheImage, hasImage, isValidSize, readImage } from "./image-cache.ts" -import { enrichAndFetchMetadata, enrichGamesWithIgdb, fetchMetadataForGames } from "./service.ts" +import { + fetchAndCacheImage, + hasImage, + isValidSize, + readImage, +} from "./image-cache.ts" +import { + enrichAndFetchMetadata, + enrichGamesWithIgdb, + fetchMetadataForGames, +} from "./service.ts" const enrichInput = z.object({ games: z.array( diff --git a/server/src/features/igdb/service.ts b/src/server/features/igdb/service.ts similarity index 91% rename from server/src/features/igdb/service.ts rename to src/server/features/igdb/service.ts index f60e3cb..be46c63 100644 --- a/server/src/features/igdb/service.ts +++ b/src/server/features/igdb/service.ts @@ -1,6 +1,10 @@ import { env } from "../../shared/lib/env.ts" import { getCacheEntries, setCacheEntries } from "./cache.ts" -import { type IgdbMetadata, getMetadataBatch, setMetadataBatch } from "./metadata-cache.ts" +import { + type IgdbMetadata, + getMetadataBatch, + setMetadataBatch, +} from "./metadata-cache.ts" const SOURCE_URL_PREFIX: Record = { steam: "https://store.steampowered.com/app/", @@ -26,7 +30,9 @@ async function getIgdbToken(): Promise { const response = await fetch(url, { method: "POST" }) if (!response.ok) { - console.error(`[IGDB] Twitch auth failed: ${response.status} ${response.statusText}`) + console.error( + `[IGDB] Twitch auth failed: ${response.status} ${response.statusText}`, + ) return null } @@ -37,7 +43,10 @@ async function getIgdbToken(): Promise { return twitchToken } -async function igdbRequest(endpoint: string, query: string): Promise { +async function igdbRequest( + endpoint: string, + query: string, +): Promise { const token = await getIgdbToken() if (!token) return [] @@ -62,7 +71,10 @@ async function igdbRequest(endpoint: string, query: string): Promise const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) -async function batchResolve(source: string, sourceIds: string[]): Promise> { +async function batchResolve( + source: string, + sourceIds: string[], +): Promise> { const results = new Map() const urlPrefix = SOURCE_URL_PREFIX[source] if (!urlPrefix) return results @@ -138,14 +150,24 @@ export async function enrichGamesWithIgdb( const resolved = await batchResolve(source, sourceIds) for (const [uid, igdbId] of resolved) { - const entry = { cacheKey: `${source}:${uid}`, source, sourceId: uid, igdbId } + const entry = { + cacheKey: `${source}:${uid}`, + source, + sourceId: uid, + igdbId, + } newEntries.push(entry) cached.set(entry.cacheKey, { igdbId }) } for (const uid of sourceIds) { if (!resolved.has(uid)) { - const entry = { cacheKey: `${source}:${uid}`, source, sourceId: uid, igdbId: null } + const entry = { + cacheKey: `${source}:${uid}`, + source, + sourceId: uid, + igdbId: null, + } newEntries.push(entry) cached.set(entry.cacheKey, { igdbId: null }) } @@ -157,7 +179,10 @@ export async function enrichGamesWithIgdb( console.log(`[IGDB] Resolved ${newEntries.length} new games`) } } catch (err) { - console.error("[IGDB] Enrichment failed (non-fatal):", (err as Error).message) + console.error( + "[IGDB] Enrichment failed (non-fatal):", + (err as Error).message, + ) } return games.map((game) => { diff --git a/server/src/features/steam/icon-cache.ts b/src/server/features/steam/icon-cache.ts similarity index 100% rename from server/src/features/steam/icon-cache.ts rename to src/server/features/steam/icon-cache.ts diff --git a/server/src/features/steam/router.ts b/src/server/features/steam/router.ts similarity index 92% rename from server/src/features/steam/router.ts rename to src/server/features/steam/router.ts index fb203e7..7710bc3 100644 --- a/server/src/features/steam/router.ts +++ b/src/server/features/steam/router.ts @@ -21,7 +21,9 @@ export const steamRouter = new Hono() return c.text("Invalid app ID", 400) } try { - const bytes = hasIcon(appId) ? readIcon(appId) : await fetchAndCacheIcon(appId) + const bytes = hasIcon(appId) + ? readIcon(appId) + : await fetchAndCacheIcon(appId) return new Response(bytes.buffer as ArrayBuffer, { headers: { "Content-Type": "image/jpeg", diff --git a/server/src/features/steam/schema.ts b/src/server/features/steam/schema.ts similarity index 100% rename from server/src/features/steam/schema.ts rename to src/server/features/steam/schema.ts diff --git a/server/src/features/steam/service.ts b/src/server/features/steam/service.ts similarity index 82% rename from server/src/features/steam/service.ts rename to src/server/features/steam/service.ts index 58ec558..52a9a74 100644 --- a/server/src/features/steam/service.ts +++ b/src/server/features/steam/service.ts @@ -5,8 +5,13 @@ interface SteamGame { rtime_last_played?: number } -async function resolveVanityUrl(apiKey: string, vanityName: string): Promise { - const url = new URL("https://api.steampowered.com/ISteamUser/ResolveVanityURL/v1/") +async function resolveVanityUrl( + apiKey: string, + vanityName: string, +): Promise { + const url = new URL( + "https://api.steampowered.com/ISteamUser/ResolveVanityURL/v1/", + ) url.searchParams.set("key", apiKey) url.searchParams.set("vanityurl", vanityName) @@ -43,7 +48,9 @@ export async function fetchSteamGames(apiKey: string, rawSteamId: string) { steamId = await resolveVanityUrl(apiKey, steamId) } - const url = new URL("https://api.steampowered.com/IPlayerService/GetOwnedGames/v1/") + const url = new URL( + "https://api.steampowered.com/IPlayerService/GetOwnedGames/v1/", + ) url.searchParams.set("key", apiKey) url.searchParams.set("steamid", steamId) url.searchParams.set("include_appinfo", "true") @@ -57,7 +64,9 @@ export async function fetchSteamGames(apiKey: string, rawSteamId: string) { throw new Error("Unauthorized. Check your API key and Steam ID.") } if (!response.ok) { - throw new Error(`Steam API error: ${response.status} ${response.statusText}`) + throw new Error( + `Steam API error: ${response.status} ${response.statusText}`, + ) } const text = await response.text() @@ -65,7 +74,9 @@ export async function fetchSteamGames(apiKey: string, rawSteamId: string) { try { data = JSON.parse(text) } catch { - throw new Error("Steam API returned an unexpected response. Check your credentials.") + throw new Error( + "Steam API returned an unexpected response. Check your credentials.", + ) } const rawGames: SteamGame[] = data.response?.games ?? [] diff --git a/server/src/index.ts b/src/server/index.ts similarity index 100% rename from server/src/index.ts rename to src/server/index.ts diff --git a/server/src/shared/db/client.ts b/src/server/shared/db/client.ts similarity index 100% rename from server/src/shared/db/client.ts rename to src/server/shared/db/client.ts diff --git a/server/src/shared/db/schema/igdb.ts b/src/server/shared/db/schema/igdb.ts similarity index 89% rename from server/src/shared/db/schema/igdb.ts rename to src/server/shared/db/schema/igdb.ts index 8df4b35..a02c822 100644 --- a/server/src/shared/db/schema/igdb.ts +++ b/src/server/shared/db/schema/igdb.ts @@ -1,4 +1,11 @@ -import { integer, jsonb, pgTable, real, text, timestamp } from "drizzle-orm/pg-core" +import { + integer, + jsonb, + pgTable, + real, + text, + timestamp, +} from "drizzle-orm/pg-core" export const igdbResolutions = pgTable("igdb_resolutions", { cacheKey: text("cache_key").primaryKey(), diff --git a/server/src/shared/lib/env.ts b/src/server/shared/lib/env.ts similarity index 100% rename from server/src/shared/lib/env.ts rename to src/server/shared/lib/env.ts diff --git a/src/shared/.gitkeep b/src/shared/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/shared/lib/api.ts b/src/shared/lib/api.ts deleted file mode 100644 index dd8db58..0000000 --- a/src/shared/lib/api.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { hc } from "hono/client" -import type { AppType } from "../../../server/src/app" - -const baseUrl = import.meta.env.VITE_API_URL || `${import.meta.env.BASE_URL.replace(/\/$/, "")}/api` - -export const api = hc(baseUrl) diff --git a/tsconfig.json b/tsconfig.json index e09c8b2..94be41b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,9 +15,9 @@ "allowImportingTsExtensions": true, "types": ["vite-plugin-pwa/react", "node"], "paths": { - "@/*": ["./src/*"] + "@/*": ["./src/client/*"] } }, "include": ["src"], - "exclude": ["server"] + "exclude": ["node_modules", "dist"] } diff --git a/vite.config.ts b/vite.config.ts index e558302..56053da 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -7,7 +7,9 @@ import { defineConfig } from "vite" import { VitePWA } from "vite-plugin-pwa" const pkg = JSON.parse(readFileSync("./package.json", "utf-8")) -const gitCount = execSync("git rev-list --count HEAD", { encoding: "utf-8" }).trim() +const gitCount = execSync("git rev-list --count HEAD", { + encoding: "utf-8", +}).trim() export default defineConfig({ base: "/whattoplay/", @@ -15,7 +17,10 @@ export default defineConfig({ __APP_VERSION__: JSON.stringify(`${pkg.version}+${gitCount}`), }, plugins: [ - TanStackRouterVite(), + TanStackRouterVite({ + routesDirectory: "src/client/routes", + generatedRouteTree: "src/client/routeTree.gen.ts", + }), react(), tailwindcss(), VitePWA({ @@ -40,7 +45,7 @@ export default defineConfig({ ], resolve: { alias: { - "@": "/src", + "@": "/src/client", }, }, server: {