This is a mirror from the GitHub repo. github.com/STBoyden/game-site
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

wip: work on fetching multiple games on search (it's broken)

stboyden.com 844151a7 14309f6b

verified
+125 -98
+1 -6
bun.lock
··· 1 1 { 2 2 "lockfileVersion": 1, 3 + "configVersion": 0, 3 4 "workspaces": { 4 5 "": { 5 6 "name": "game-site", ··· 9 10 "convex-svelte": "^0.0.12", 10 11 "mode-watcher": "^1.1.0", 11 12 "runed": "^0.37.0", 12 - "svelte-placeholder": "^0.0.5", 13 13 "type-fest": "^5.3.1", 14 14 }, 15 15 "devDependencies": { ··· 47 47 "tw-animate-css": "^1.4.0", 48 48 "typescript": "^5.9.3", 49 49 "typescript-eslint": "^8.47.0", 50 - "valibot": "^1.2.0", 51 50 "vite": "^7.2.2", 52 51 "vite-plugin-devtools-json": "^1.0.0", 53 52 }, ··· 662 661 663 662 "svelte-eslint-parser": ["svelte-eslint-parser@1.4.0", "", { "dependencies": { "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.0.0", "espree": "^10.0.0", "postcss": "^8.4.49", "postcss-scss": "^4.0.9", "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-fjPzOfipR5S7gQ/JvI9r2H8y9gMGXO3JtmrylHLLyahEMquXI0lrebcjT+9/hNgDej0H7abTyox5HpHmW1PSWA=="], 664 663 665 - "svelte-placeholder": ["svelte-placeholder@0.0.5", "", {}, "sha512-rikHoXeg/FdgOoiyHg6VJF9oW6RWJylVvX/yeQWoitBzADlBxVJaDNMXcKdkatoE9yi0ttxpmXvbbpYvgJfHLQ=="], 666 - 667 664 "svelte-toolbelt": ["svelte-toolbelt@0.7.1", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.23.2", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ=="], 668 665 669 666 "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], ··· 705 702 "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], 706 703 707 704 "uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], 708 - 709 - "valibot": ["valibot@1.2.0", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg=="], 710 705 711 706 "vite": ["vite@7.2.4", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w=="], 712 707
-2
package.json
··· 48 48 "tw-animate-css": "^1.4.0", 49 49 "typescript": "^5.9.3", 50 50 "typescript-eslint": "^8.47.0", 51 - "valibot": "^1.2.0", 52 51 "vite": "^7.2.2", 53 52 "vite-plugin-devtools-json": "^1.0.0" 54 53 }, ··· 58 57 "convex-svelte": "^0.0.12", 59 58 "mode-watcher": "^1.1.0", 60 59 "runed": "^0.37.0", 61 - "svelte-placeholder": "^0.0.5", 62 60 "type-fest": "^5.3.1" 63 61 } 64 62 }
+37 -6
src/convex/games.ts
··· 1 1 import { query, internalMutation, internalQuery } from "./functions"; 2 - 3 2 import type { Doc } from "./_generated/dataModel"; 4 3 import { api, internal } from "./_generated/api"; 5 4 import type { Merge, Simplify } from "type-fest"; ··· 7 6 import { Effect, Fiber, pipe } from "effect"; 8 7 import { type SGDBGame } from "steamgriddb"; 9 8 import { v } from "convex/values"; 9 + import { steamGridDB } from "."; 10 10 11 11 export const getAll = query({ 12 12 args: {}, ··· 17 17 icon: await ctx.storage.getUrl(game.iconId), 18 18 grid: await ctx.storage.getUrl(game.gridId) 19 19 })) 20 + }); 21 + 22 + export const getAllByIDs = query({ 23 + args: { ids: v.array(v.id("games")) }, 24 + handler: (ctx, args) => 25 + Effect.runPromise( 26 + pipe( 27 + Effect.promise(() => ctx.table("games").getMany(args.ids)), 28 + Effect.andThen((results) => 29 + Effect.promise(() => 30 + Promise.all( 31 + results 32 + .filter((x) => x !== null) 33 + .map(async (game) => ({ 34 + ...game, 35 + hero: await ctx.storage.getUrl(game.heroId), 36 + icon: await ctx.storage.getUrl(game.iconId), 37 + grid: await ctx.storage.getUrl(game.gridId) 38 + })) 39 + ) 40 + ) 41 + ) 42 + ) 43 + ) 20 44 }); 21 45 22 46 export const get = query({ ··· 102 126 onTrue: () => 103 127 pipe( 104 128 Effect.promise(() => 105 - ctx.runAction(api.games_node.addGame, { name: args.query }) 129 + steamGridDB 130 + .searchGame(args.query) 131 + .then((results) => results.map((game) => game.name)) 132 + ), 133 + Effect.andThen((names) => 134 + Effect.promise(() => ctx.runAction(api.games_node.addGames, { names })) 106 135 ), 107 - Effect.andThen((gameID) => 108 - Effect.if(gameID !== null, { 136 + Effect.andThen((gameIDs) => 137 + Effect.if(gameIDs.length !== 0, { 109 138 onTrue: () => 110 139 pipe( 111 - Effect.promise(() => ctx.runQuery(api.games.get, { id: gameID! })), 112 - Effect.map((game) => [game!]) 140 + Effect.promise(() => 141 + ctx.runQuery(api.games.getAllByIDs, { ids: gameIDs }) 142 + ), 143 + Effect.andThen((games) => Effect.succeed(games)) 113 144 ), 114 145 onFalse: () => Effect.succeed(null) 115 146 })
+13
src/convex/games_node.ts
··· 204 204 // ) 205 205 // ) 206 206 }); 207 + 208 + export const addGames = action({ 209 + args: v.object({ names: v.array(v.string()) }), 210 + handler: (ctx, args): Promise<Id<"games">[]> => 211 + Effect.runPromise( 212 + pipe( 213 + Effect.forEach(args.names, (name) => 214 + Effect.promise(() => ctx.runAction(api.games_node.addGame, { name })) 215 + ), 216 + Effect.andThen((games) => Effect.succeed(games.filter((game) => game !== null))) 217 + ) 218 + ) 219 + });
+41 -41
src/hooks.server.ts
··· 1 - import { STEAM_API_KEY } from "$env/static/private"; 2 - import { steamAPI } from "$lib/server/steam-api"; 3 - import type { ServerInit } from "@sveltejs/kit"; 4 - import { api } from "./convex/_generated/api"; 5 - import { convexClient } from "$lib/server"; 6 - import { Data, Effect } from "effect"; 1 + // import { STEAM_API_KEY } from "$env/static/private"; 2 + // import { steamAPI } from "$lib/server/steam-api"; 3 + // import type { ServerInit } from "@sveltejs/kit"; 4 + // import { api } from "./convex/_generated/api"; 5 + // import { convexClient } from "$lib/server"; 6 + // import { Data, Effect } from "effect"; 7 7 8 - export class SteamAPIError extends Data.TaggedError("SteamAPIError")<{ 9 - reason: unknown; 10 - }> {} 8 + // export class SteamAPIError extends Data.TaggedError("SteamAPIError")<{ 9 + // reason: unknown; 10 + // }> {} 11 11 12 - export class AddGameError extends Data.TaggedError("AddGameError")<{ 13 - reason: unknown; 14 - }> {} 12 + // export class AddGameError extends Data.TaggedError("AddGameError")<{ 13 + // reason: unknown; 14 + // }> {} 15 15 16 - export const init: ServerInit = () => 17 - Effect.runPromise( 18 - Effect.gen(function* () { 19 - yield* Effect.logInfo("Server started."); 16 + // // export const init: ServerInit = () => 17 + // Effect.runPromise( 18 + // Effect.gen(function* () { 19 + // yield* Effect.logInfo("Server started."); 20 20 21 - const games = yield* Effect.tryPromise({ 22 - try: () => steamAPI("IStoreService").getAppList({ key: STEAM_API_KEY, maxResults: 100 }), 23 - catch: (reason) => new SteamAPIError({ reason }) 24 - }); 21 + // const games = yield* Effect.tryPromise({ 22 + // try: () => steamAPI("IStoreService").getAppList({ key: STEAM_API_KEY, maxResults: 100 }), 23 + // catch: (reason) => new SteamAPIError({ reason }) 24 + // }); 25 25 26 - for (const game of games) { 27 - const newGameID = yield* Effect.orElseSucceed( 28 - Effect.tryPromise(() => convexClient.action(api.games_node.addGame, { name: game.name })), 29 - () => null 30 - ); 26 + // for (const game of games) { 27 + // const newGameID = yield* Effect.orElseSucceed( 28 + // Effect.tryPromise(() => convexClient.action(api.games_node.addGame, { name: game.name })), 29 + // () => null 30 + // ); 31 31 32 - if (!newGameID) { 33 - continue; 34 - } 32 + // if (!newGameID) { 33 + // continue; 34 + // } 35 35 36 - const newGame = yield* Effect.promise(() => 37 - convexClient.query(api.games.get, { id: newGameID }) 38 - ); 36 + // const newGame = yield* Effect.promise(() => 37 + // convexClient.query(api.games.get, { id: newGameID }) 38 + // ); 39 39 40 - yield* Effect.logDebug(`Added ${newGameID}: ${newGame?.name}`); 41 - } 42 - }).pipe( 43 - Effect.catchAllCause((cause) => 44 - Effect.logWarning( 45 - `Encountered an error, ignoring and continuing server start... reason: ${cause.toString()}` 46 - ) 47 - ) 48 - ) 49 - ); 40 + // yield* Effect.logDebug(`Added ${newGameID}: ${newGame?.name}`); 41 + // } 42 + // }).pipe( 43 + // Effect.catchAllCause((cause) => 44 + // Effect.logWarning( 45 + // `Encountered an error, ignoring and continuing server start... reason: ${cause.toString()}` 46 + // ) 47 + // ) 48 + // ) 49 + // );
+18 -25
src/lib/components/Navbar.svelte
··· 1 1 <script lang="ts"> 2 2 import { resolve } from "$app/paths"; 3 + import ModeSwitcher from "./ModeSwitcher.svelte"; 4 + import { Debounced, onClickOutside } from "runed"; 3 5 import { useConvexClient } from "convex-svelte"; 4 - import ModeSwitcher from "./ModeSwitcher.svelte"; 5 6 import { api } from "../../convex/_generated/api"; 6 - import { onClickOutside, useThrottle } from "runed"; 7 7 8 8 let searchElement = $state<HTMLElement>(); 9 9 let searchQuery = $state(""); 10 - let throttledSearchQuery = $state(""); 11 - let throttleDurationMS = $state(1000); 10 + let debouncedSearchQuery = new Debounced(() => searchQuery, 500); 12 11 13 12 const convexClient = useConvexClient(); 14 13 15 14 const searchResults = $derived.by(() => { 16 - if (throttledSearchQuery === "") { 15 + if (debouncedSearchQuery.pending) { 17 16 return null; 18 17 } 19 18 20 - console.log(`Searching for ${throttledSearchQuery}...`); 19 + console.log(`Searching for ${debouncedSearchQuery.current}...`); 21 20 22 - return convexClient.action(api.games.search, { query: throttledSearchQuery, limit: null }); 21 + return convexClient.action(api.games.search, { 22 + query: debouncedSearchQuery.current, 23 + limit: null 24 + }); 23 25 }); 24 26 25 - const throttledUpdate = useThrottle( 26 - () => (throttledSearchQuery = searchQuery), 27 - () => throttleDurationMS 28 - ); 29 - 30 27 onClickOutside( 31 28 () => searchElement, 32 - () => (searchQuery = "") 29 + () => { 30 + searchQuery = ""; 31 + debouncedSearchQuery.updateImmediately(); 32 + } 33 33 ); 34 34 </script> 35 35 ··· 41 41 <details class="dropdown w-full" open={searchQuery !== ""} bind:this={searchElement}> 42 42 <summary class="input input-md w-full"> 43 43 <span class="label">Search</span> 44 - <input 45 - type="text" 46 - placeholder="Search for a game..." 47 - bind:value={ 48 - () => searchQuery, 49 - (v) => { 50 - searchQuery = v; 51 - throttledUpdate(); 52 - } 53 - } 54 - /> 44 + <input type="text" placeholder="Search for a game..." bind:value={searchQuery} /> 55 45 <button 56 46 class="label hover:text-base-content cursor-pointer" 57 - onclick={() => (searchQuery = "")}>Clear</button 47 + onclick={() => { 48 + searchQuery = ""; 49 + debouncedSearchQuery.updateImmediately(); 50 + }}>Clear</button 58 51 > 59 52 </summary> 60 53
+14 -17
src/routes/+page.svelte
··· 1 1 <script lang="ts"> 2 2 import { resolve } from "$app/paths"; 3 3 import { getGameStoreContext } from "$lib/client/GameStore.svelte"; 4 - import { ArrowLeft, ArrowRight } from "@lucide/svelte"; 4 + // import { ArrowLeft, ArrowRight } from "@lucide/svelte"; 5 5 import { ScrollState } from "runed"; 6 - import Image from "svelte-placeholder"; 7 6 8 7 type Game = Awaited<typeof gamesStore.games>[number]; 9 8 ··· 11 10 const gamesStore = getGameStoreContext(); 12 11 const carouselScroll = new ScrollState({ element: () => carouselElement }); 13 12 14 - const scroll = (direction: "left" | "right") => { 15 - if (direction === "left") { 16 - carouselScroll.x -= 320; 17 - } else { 18 - carouselScroll.x += 320; 19 - } 20 - }; 13 + // const scroll = (direction: "left" | "right") => { 14 + // if (direction === "left") { 15 + // carouselScroll.x -= 320; 16 + // } else { 17 + // carouselScroll.x += 320; 18 + // } 19 + // }; 21 20 </script> 22 21 23 22 {#snippet gameCard(game: Game)} 24 23 <div class="align-middle justify-center items-center flex flex-col gap-4 h-fit"> 25 - <div class="hover-3d w-fit aspect-2/3"> 26 - <span class="rounded-box"> 27 - <Image src={game.grid!} alt={game.name} placeholder="skeleton" /> 24 + <div class="hover-3d w-fit aspect-2/3 hover:z-10"> 25 + <span class="rounded-box align-middle items-center justify-center flex bg-black"> 26 + <img src={game.grid!} alt={game.name} placeholder="skeleton" /> 28 27 </span> 29 28 30 29 <div></div> ··· 37 36 <div></div> 38 37 </div> 39 38 40 - <p class="text-lg font-semibold">{game.name}</p> 39 + <p class="text-lg text-center font-semibold">{game.name}</p> 41 40 </div> 42 41 {/snippet} 43 42 ··· 80 79 {/each} 81 80 {/if} 82 81 83 - <div 84 - class="absolute px-5 left-5 right-5 top-1/2 flex -translate-y-1/2 transform justify-between" 85 - > 82 + <!-- <div class="z-1 px-5 left-5 right-5 top-1/2 flex -translate-y-1/2 transform justify-between"> 86 83 <button class="btn btn-circle bg-base-300/90" onclick={() => scroll("left")}> 87 84 <ArrowLeft class="w-[1em] h-[1em]" /> 88 85 </button> 89 86 <button class="btn btn-circle bg-base-300/90" onclick={() => scroll("right")}> 90 87 <ArrowRight class="w-[1em] h-[1em]" /> 91 88 </button> 92 - </div> 89 + </div> --> 93 90 </div> 94 91 </div>
+1 -1
src/routes/game-overview/[sortName]/+page.svelte
··· 1 1 <script lang="ts"> 2 - const { params, data } = $props(); 2 + const { data } = $props(); 3 3 const game = data.game; 4 4 </script> 5 5