Moving search to do a GET with q search param.

This commit is contained in:
Bradley Shellnut 2022-12-25 10:52:36 -08:00
parent f7c3939765
commit 2e6c38dc44
19 changed files with 1112 additions and 829 deletions

3
.gitignore vendored
View file

@ -8,4 +8,5 @@ node_modules
!.env.example !.env.example
.vercel .vercel
.output .output
.idea .idea
.fleet

View file

@ -1,5 +1,6 @@
{ {
"useTabs": true, "useTabs": true,
"tabWidth": 2,
"singleQuote": true, "singleQuote": true,
"trailingComma": "none", "trailingComma": "none",
"printWidth": 100, "printWidth": 100,

View file

@ -13,37 +13,38 @@
"format": "prettier --write --plugin-search-dir=. ." "format": "prettier --write --plugin-search-dir=. ."
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.27.1", "@playwright/test": "^1.29.1",
"@rgossiaux/svelte-headlessui": "1.0.2", "@rgossiaux/svelte-headlessui": "1.0.2",
"@rgossiaux/svelte-heroicons": "^0.1.2", "@rgossiaux/svelte-heroicons": "^0.1.2",
"@sveltejs/adapter-auto": "next", "@sveltejs/adapter-auto": "^1.0.0",
"@sveltejs/kit": "next", "@sveltejs/kit": "^1.0.1",
"@types/cookie": "^0.5.1", "@types/cookie": "^0.5.1",
"@types/node": "^18.11.9", "@types/node": "^18.11.17",
"@typescript-eslint/eslint-plugin": "^5.42.1", "@typescript-eslint/eslint-plugin": "^5.47.0",
"@typescript-eslint/parser": "^5.42.1", "@typescript-eslint/parser": "^5.47.0",
"autoprefixer": "^10.4.13", "autoprefixer": "^10.4.13",
"eslint": "^8.27.0", "eslint": "^8.30.0",
"eslint-config-prettier": "^8.1.0", "eslint-config-prettier": "^8.5.0",
"eslint-plugin-svelte3": "^4.0.0", "eslint-plugin-svelte3": "^4.0.0",
"just-debounce-it": "^3.1.1", "just-debounce-it": "^3.2.0",
"postcss": "^8.4.19", "postcss": "^8.4.20",
"postcss-color-functional-notation": "^4.2.4", "postcss-color-functional-notation": "^4.2.4",
"postcss-custom-media": "^9.0.1", "postcss-custom-media": "^9.0.1",
"postcss-env-function": "^4.0.6", "postcss-env-function": "^4.0.6",
"postcss-import": "^15.0.0", "postcss-import": "^15.1.0",
"postcss-load-config": "^4.0.1", "postcss-load-config": "^4.0.1",
"postcss-media-minmax": "^5.0.0", "postcss-media-minmax": "^5.0.0",
"postcss-nested": "^6.0.0", "postcss-nested": "^6.0.0",
"prettier": "^2.7.1", "prettier": "^2.8.1",
"prettier-plugin-svelte": "^2.8.0", "prettier-plugin-svelte": "^2.9.0",
"sass": "^1.56.1", "sass": "^1.57.1",
"svelte": "^3.53.1", "svelte": "^3.55.0",
"svelte-check": "^2.9.2", "svelte-check": "^2.10.3",
"svelte-preprocess": "^4.10.7", "svelte-preprocess": "^4.10.7",
"tslib": "^2.4.1", "tslib": "^2.4.1",
"typescript": "^4.8.4", "typescript": "^4.9.4",
"vite": "^3.2.3" "vite": "^4.0.3",
"vitest": "^0.25.3"
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
@ -51,11 +52,11 @@
"@leveluptuts/svelte-side-menu": "^1.0.5", "@leveluptuts/svelte-side-menu": "^1.0.5",
"@leveluptuts/svelte-toy": "^2.0.3", "@leveluptuts/svelte-toy": "^2.0.3",
"@lukeed/uuid": "^2.0.0", "@lukeed/uuid": "^2.0.0",
"@types/feather-icons": "^4.7.0", "@types/feather-icons": "^4.29.1",
"cookie": "^0.5.0", "cookie": "^0.5.0",
"feather-icons": "^4.29.0", "feather-icons": "^4.29.0",
"svelte-lazy-loader": "^1.0.0", "svelte-lazy-loader": "^1.0.0",
"zod": "^3.19.1", "zod": "^3.20.2",
"zod-to-json-schema": "^3.19.3" "zod-to-json-schema": "^3.20.1"
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -1,16 +1,17 @@
<script lang="ts"> <script lang="ts">
import type { ActionData } from './$types'; import type { ActionData } from './$types';
import { boredState } from '$lib/stores/boredState'; import { boredState } from '$lib/stores/boredState';
import type { PageData } from '.svelte-kit/types/src/routes/$types';
export let data: PageData;
console.log('advanced search data', data);
export let form: ActionData;
console.log('form data', form?.data);
const errors = form?.errors;
let submitting = $boredState?.loading; let submitting = $boredState?.loading;
let minAge = +form?.data?.minAge || 1; let minAge = +data?.minAge || 1;
let minPlayers = +form?.data?.minPlayers || 1; let minPlayers = +data?.minPlayers || 1;
let maxPlayers = +form?.data?.maxPlayers || 1; let maxPlayers = +data?.maxPlayers || 1;
let exactMinPlayers = Boolean(form?.data?.exactMinPlayers) || false; let exactMinPlayers = Boolean(data?.exactMinPlayers) || false;
let exactMaxPlayers = Boolean(form?.data?.exactMaxPlayers) || false; let exactMaxPlayers = Boolean(data?.exactMaxPlayers) || false;
</script> </script>
<fieldset class="advanced-search" aria-busy={submitting} disabled={submitting}> <fieldset class="advanced-search" aria-busy={submitting} disabled={submitting}>
@ -19,10 +20,10 @@
Min Age Min Age
<input id="minAge" name="minAge" bind:value={minAge} type="number" min={1} max={120} /> <input id="minAge" name="minAge" bind:value={minAge} type="number" min={1} max={120} />
</label> </label>
{#if form?.errors?.minAge} {#if data?.errors?.minAge}
<div id="minPlayers-error" class="error"> <div id="minPlayers-error" class="error">
<p aria-label={`Error: ${form?.errors?.minAge}`} class="center"> <p aria-label={`Error: ${data?.errors?.minAge}`} class="center">
{form?.errors?.minAge} {data?.errors?.minAge}
</p> </p>
</div> </div>
{/if} {/if}
@ -49,10 +50,10 @@
bind:value={exactMinPlayers} bind:value={exactMinPlayers}
/> />
</label> </label>
{#if form?.errors?.minPlayers} {#if data?.errors?.minPlayers}
<div id="minPlayers-error" class="error"> <div id="minPlayers-error" class="error">
<p aria-label={`Error: ${form?.errors?.minPlayers}`} class="center"> <p aria-label={`Error: ${data?.errors?.minPlayers}`} class="center">
{form?.errors?.minPlayers} {data?.errors?.minPlayers}
</p> </p>
</div> </div>
{/if} {/if}
@ -79,10 +80,10 @@
bind:value={exactMaxPlayers} bind:value={exactMaxPlayers}
/> />
</label> </label>
{#if form?.error?.id === 'maxPlayers'} {#if data?.error?.id === 'maxPlayers'}
<div id="maxPlayers-error" class="error"> <div id="maxPlayers-error" class="error">
<p aria-label={`Error: ${form.error.message}`} class="center"> <p aria-label={`Error: ${data.error.message}`} class="center">
Error: {form.error.message} Error: {data.error.message}
</p> </p>
</div> </div>
{/if} {/if}

View file

@ -25,7 +25,7 @@
// console.log('search page data', data); // console.log('search page data', data);
export let form: ActionData; export let form: ActionData;
// console.log('search page form', form); // console.log('search page form', form);
const errors = form?.errors; const errors = data?.errors;
export let showButton: boolean = false; export let showButton: boolean = false;
export let advancedSearch: boolean = false; export let advancedSearch: boolean = false;
@ -34,26 +34,22 @@
let numberOfGameSkeleton = 1; let numberOfGameSkeleton = 1;
let submitButton: HTMLElement; let submitButton: HTMLElement;
let pageSize = 10; let pageSize = 10;
let page = +form?.data?.page || 1; let page = +data?.page || 1;
let totalItems = form?.totalCount || data?.totalCount || 0; let totalItems = data?.totalCount || 0;
let submitting = $boredState?.loading; let submitting = $boredState?.loading;
let name = form?.name || ''; let name = data?.name || '';
let disclosureOpen = errors || false; let disclosureOpen = errors || false;
$: skip = (page - 1) * pageSize; $: skip = (page - 1) * pageSize;
$: showPagination = $gameStore?.length > 1; $: showPagination = $gameStore?.length > 1;
if ($xl) { if ($xl) {
console.log('Was xl');
numberOfGameSkeleton = 8; numberOfGameSkeleton = 8;
} else if ($md) { } else if ($md) {
console.log('Was md');
numberOfGameSkeleton = 3; numberOfGameSkeleton = 3;
} else if ($sm) { } else if ($sm) {
console.log('Was sm');
numberOfGameSkeleton = 2; numberOfGameSkeleton = 2;
} else { } else {
console.log('Was none');
numberOfGameSkeleton = 1; numberOfGameSkeleton = 1;
} }
@ -141,22 +137,22 @@
// TODO: Add cache for certain number of pages so back and forth doesn't request data again // TODO: Add cache for certain number of pages so back and forth doesn't request data again
</script> </script>
<form id="search-form" action="/search" method="post" use:enhance={submitSearch}> <form id="search-form" action="/search" method="get">
<input id="skip" type="hidden" name="skip" bind:value={skip} />
<input id="limit" type="hidden" name="limit" bind:value={pageSize} />
<div class="search"> <div class="search">
<fieldset class="text-search" aria-busy={submitting} disabled={submitting}> <fieldset class="text-search" aria-busy={submitting} disabled={submitting}>
<label for="name"> <label for="q">
Search Search
<input <input
id="name" id="q"
name="name" name="q"
bind:value={name} bind:value={name}
type="text" type="text"
aria-label="Search boardgame" aria-label="Search boardgame"
placeholder="Search boardgame" placeholder="Search boardgame"
/> />
</label> </label>
<input id="skip" type="hidden" name="skip" bind:value={skip} />
<input id="limit" type="hidden" name="limit" bind:value={pageSize} />
</fieldset> </fieldset>
{#if advancedSearch} {#if advancedSearch}
<Disclosure> <Disclosure>
@ -178,7 +174,7 @@
<!-- Using `static`, `DisclosurePanel` is always rendered, <!-- Using `static`, `DisclosurePanel` is always rendered,
and ignores the `open` state --> and ignores the `open` state -->
<DisclosurePanel static> <DisclosurePanel static>
<AdvancedSearch {form} /> <AdvancedSearch {data} />
</DisclosurePanel> </DisclosurePanel>
</div> </div>
{/if} {/if}
@ -198,19 +194,34 @@
{/if} {/if}
</form> </form>
{#if $gameStore?.length > 0} {#if $boredState.loading}
<div class="games"> <div class="games">
<h1>Games Found:</h1> <h1>Games Found:</h1>
<div class="games-list"> <div class="games-list">
{#each $gameStore as game (game.id)} {#each placeholderList as game, i}
<Game <SkeletonPlaceholder
on:handleRemoveWishlist={handleRemoveWishlist} style="width: 100%; height: 500px; border-radius: var(--borderRadius);"
on:handleRemoveCollection={handleRemoveCollection}
{game}
/> />
{/each} {/each}
</div> </div>
{#if showPagination} </div>
{:else}
<div class="games">
<h1>Games Found:</h1>
<div class="games-list">
{#if $gameStore?.length > 0}
{#each $gameStore as game (game.id)}
<Game
on:handleRemoveWishlist={handleRemoveWishlist}
on:handleRemoveCollection={handleRemoveCollection}
{game}
/>
{/each}
{:else}
<h2>Sorry no games found!</h2>
{/if}
</div>
{#if showPagination && $gameStore?.length > 0}
<Pagination <Pagination
{pageSize} {pageSize}
{page} {page}
@ -224,17 +235,6 @@
/> />
{/if} {/if}
</div> </div>
{:else if $boredState.loading}
<div class="games">
<h1>Games Found:</h1>
<div class="games-list">
{#each placeholderList as game, i}
<SkeletonPlaceholder
style="width: 100%; height: 500px; border-radius: var(--borderRadius);"
/>
{/each}
</div>
</div>
{/if} {/if}
<style lang="postcss"> <style lang="postcss">

View file

@ -2,154 +2,156 @@ import { z, ZodNumber, ZodOptional } from 'zod';
// import zodToJsonSchema from 'zod-to-json-schema'; // import zodToJsonSchema from 'zod-to-json-schema';
export const BoardGameSearch = z.object({ export const BoardGameSearch = z.object({
minAge: z.number(), minAge: z.number(),
maxAge: z.number(), maxAge: z.number(),
minPlayers: z.number(), minPlayers: z.number(),
maxPlayers: z.number() maxPlayers: z.number()
}); });
export const saved_game_schema = z.object({ export const saved_game_schema = z.object({
id: z.string(), id: z.string(),
name: z.string(), name: z.string(),
thumb_url: z.string(), thumb_url: z.string(),
players: z.string(), players: z.string(),
playtime: IntegerString(z.number()), playtime: IntegerString(z.number())
}); });
// https://github.com/colinhacks/zod/discussions/330 // https://github.com/colinhacks/zod/discussions/330
function IntegerString function IntegerString<schema extends ZodNumber | ZodOptional<ZodNumber>>(schema: schema) {
<schema extends (ZodNumber | ZodOptional<ZodNumber>)> return z.preprocess(
(schema: schema) (value) =>
{ typeof value === 'string'
return ( ? parseInt(value, 10)
z.preprocess((value) => ( : typeof value === 'number'
( (typeof value === "string") ? parseInt(value, 10) ? value
: (typeof value === "number") ? value : undefined,
: undefined schema
)), schema) );
)
} }
export const search_schema = z.object({ export const search_schema = z
name: z.string().trim().optional(), .object({
minAge: IntegerString(z.number().min(1).max(120).optional()), name: z.string().trim().optional(),
minPlayers: IntegerString(z.number().min(1).max(50).optional()), minAge: IntegerString(z.number().min(1).max(120).optional()),
maxPlayers: IntegerString(z.number().min(1).max(50).optional()), minPlayers: IntegerString(z.number().min(1).max(50).optional()),
exactMinAge: z.preprocess((a) => Boolean(a), z.boolean().optional()), maxPlayers: IntegerString(z.number().min(1).max(50).optional()),
exactMinPlayers: z.preprocess((a) => Boolean(a), z.boolean().optional()), exactMinAge: z.preprocess((a) => Boolean(a), z.boolean().optional()),
exactMaxPlayers: z.preprocess((a) => Boolean(a), z.boolean().optional()) exactMinPlayers: z.preprocess((a) => Boolean(a), z.boolean().optional()),
}) exactMaxPlayers: z.preprocess((a) => Boolean(a), z.boolean().optional())
.superRefine(({ minPlayers, maxPlayers, minAge, exactMinAge, exactMinPlayers, exactMaxPlayers }, ctx) => { })
console.log({ minPlayers, maxPlayers }); .superRefine(
if (minPlayers && maxPlayers && minPlayers > maxPlayers) { ({ minPlayers, maxPlayers, minAge, exactMinAge, exactMinPlayers, exactMaxPlayers }, ctx) => {
ctx.addIssue({ console.log({ minPlayers, maxPlayers });
code: 'custom', if (minPlayers && maxPlayers && minPlayers > maxPlayers) {
message: 'Min Players must be smaller than Max Players', ctx.addIssue({
path: ['minPlayers'], code: 'custom',
}); message: 'Min Players must be smaller than Max Players',
ctx.addIssue({ path: ['minPlayers']
code: 'custom', });
message: 'Min Players must be smaller than Max Players', ctx.addIssue({
path: ['maxPlayers'], code: 'custom',
}); message: 'Min Players must be smaller than Max Players',
} path: ['maxPlayers']
});
}
if (exactMinAge && !minAge) { if (exactMinAge && !minAge) {
ctx.addIssue({ ctx.addIssue({
code: 'custom', code: 'custom',
message: 'Min Age required when searching for exact min age', message: 'Min Age required when searching for exact min age',
path: ['minAge'], path: ['minAge']
}); });
} }
if (exactMinPlayers && !minPlayers) { if (exactMinPlayers && !minPlayers) {
ctx.addIssue({ ctx.addIssue({
code: 'custom', code: 'custom',
message: 'Min Players required when searching for exact min players', message: 'Min Players required when searching for exact min players',
path: ['minPlayers'], path: ['minPlayers']
}); });
} }
if (exactMaxPlayers && !maxPlayers) { if (exactMaxPlayers && !maxPlayers) {
ctx.addIssue({ ctx.addIssue({
code: 'custom', code: 'custom',
message: 'Max Players required when searching for exact max players', message: 'Max Players required when searching for exact max players',
path: ['maxPlayers'], path: ['maxPlayers']
}); });
} }
}); }
);
export const search_result_schema = z.object({ export const search_result_schema = z.object({
client_id: z.string(), client_id: z.string(),
limit: z.number(), limit: z.number(),
skip: z.number(), skip: z.number(),
ids: z.string().array(), ids: z.string().array(),
list_id: z.string(), list_id: z.string(),
kickstarter: z.boolean(), kickstarter: z.boolean(),
random: z.boolean(), random: z.boolean(),
name: z.string(), name: z.string(),
exact: z.boolean(), exact: z.boolean(),
fuzzy_match: z.boolean(), fuzzy_match: z.boolean(),
designer: z.string(), designer: z.string(),
publisher: z.string(), publisher: z.string(),
artist: z.string(), artist: z.string(),
mechanics: z.string(), mechanics: z.string(),
categories: z.string(), categories: z.string(),
order_by: z.string(), order_by: z.string(),
ascending: z.boolean(), ascending: z.boolean(),
min_players: z.number(), min_players: z.number(),
max_players: z.number(), max_players: z.number(),
min_playtime: z.number(), min_playtime: z.number(),
max_playtime: z.number(), max_playtime: z.number(),
min_age: z.number(), min_age: z.number(),
year_published: z.number(), year_published: z.number(),
gt_min_players: z.number(), gt_min_players: z.number(),
gt_max_players: z.number(), gt_max_players: z.number(),
gt_min_playtime: z.number(), gt_min_playtime: z.number(),
gt_max_playtime: z.number(), gt_max_playtime: z.number(),
gt_min_age: z.number(), gt_min_age: z.number(),
gt_year_published:z.number(), gt_year_published: z.number(),
gt_price: z.bigint(), gt_price: z.bigint(),
gt_msrp: z.bigint(), gt_msrp: z.bigint(),
gt_discount: z.bigint(), gt_discount: z.bigint(),
gt_reddit_count: z.number(), gt_reddit_count: z.number(),
gt_reddit_week_count: z.number(), gt_reddit_week_count: z.number(),
gt_reddit_day_count: z.number(), gt_reddit_day_count: z.number(),
lt_min_players: z.number(), lt_min_players: z.number(),
lt_max_players: z.number(), lt_max_players: z.number(),
lt_min_playtime: z.number(), lt_min_playtime: z.number(),
lt_max_playtime: z.number(), lt_max_playtime: z.number(),
lt_min_age: z.number(), lt_min_age: z.number(),
lt_year_published: z.number(), lt_year_published: z.number(),
lt_price: z.bigint(), lt_price: z.bigint(),
lt_msrp: z.bigint(), lt_msrp: z.bigint(),
lt_discount: z.bigint(), lt_discount: z.bigint(),
lt_reddit_count: z.number(), lt_reddit_count: z.number(),
lt_reddit_week_count: z.number(), lt_reddit_week_count: z.number(),
lt_reddit_day_count: z.number(), lt_reddit_day_count: z.number(),
fields: z.string(), fields: z.string()
}); });
export const game_schema = z.object({ export const game_schema = z.object({
id: z.string(), id: z.string(),
handle: z.string(), handle: z.string(),
name: z.string(), name: z.string(),
url: z.string(), url: z.string(),
edit_url: z.string(), edit_url: z.string(),
price: z.number(), price: z.number(),
price_ca: z.number(), price_ca: z.number(),
price_uk: z.number(), price_uk: z.number(),
price_au: z.number(), price_au: z.number(),
msrp: z.number(), msrp: z.number(),
year_published: z.number(), year_published: z.number(),
min_players: z.number(), min_players: z.number(),
max_players: z.number(), max_players: z.number(),
min_playtime: z.number(), min_playtime: z.number(),
max_playtime: z.number(), max_playtime: z.number(),
min_age: z.number(), min_age: z.number(),
description: z.string(), description: z.string(),
players: z.string(), players: z.string(),
playtime: z.string() playtime: z.string()
}); });
// export const game_raw_schema_json = zodToJsonSchema(game_schema, { // export const game_raw_schema_json = zodToJsonSchema(game_schema, {

View file

@ -15,7 +15,7 @@
import { gameStore } from '$lib/stores/gameSearchStore'; import { gameStore } from '$lib/stores/gameSearchStore';
import { toast } from '$lib/components/toast/toast'; import { toast } from '$lib/components/toast/toast';
import Toast from '$lib/components/toast/Toast.svelte'; import Toast from '$lib/components/toast/Toast.svelte';
import '$root/styles/styles.postcss'; import '$root/styles/styles.pcss';
$: { $: {
if ($navigating) { if ($navigating) {

View file

@ -1,33 +1,23 @@
/*
This module is used by the /todos endpoint to
make calls to api.svelte.dev, which stores todos
for each user. The leading underscore indicates that this is
a private module, _not_ an endpoint visiting /todos/_api
will net you a 404 response.
(The data on the todo app will expire periodically; no
guarantees are made. Don't use it to organize your life.)
*/
import { BOARD_GAME_ATLAS_CLIENT_ID } from '$env/static/private'; import { BOARD_GAME_ATLAS_CLIENT_ID } from '$env/static/private';
import { URLSearchParams } from 'url'; import { URLSearchParams } from 'url';
const base = 'https://api.boardgameatlas.com/api'; const base = 'https://api.boardgameatlas.com/api';
export function boardGameApi( export function boardGameApi(
method: string, method: string,
resource: string, resource: string,
queryParams: Record<string, string>, queryParams: Record<string, string>,
data?: Record<string, unknown> data?: Record<string, unknown>
) { ) {
// console.log('queryParams', queryParams); // console.log('queryParams', queryParams);
queryParams.client_id = BOARD_GAME_ATLAS_CLIENT_ID; queryParams.client_id = BOARD_GAME_ATLAS_CLIENT_ID;
const urlQueryParams = new URLSearchParams(queryParams); const urlQueryParams = new URLSearchParams(queryParams);
const url = `${base}/${resource}${urlQueryParams ? `?${urlQueryParams}` : ''}`; const url = `${base}/${resource}${urlQueryParams ? `?${urlQueryParams}` : ''}`;
return fetch(url, { return fetch(url, {
method, method,
headers: { headers: {
'content-type': 'application/json' 'Content-Type': 'application/json'
}, },
body: data && JSON.stringify(data) body: data && JSON.stringify(data)
}); });
} }

View file

@ -1,26 +1,31 @@
import { error } from '@sveltejs/kit'; import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types' import type { PageServerLoad } from './$types';
import { boardGameApi } from '../../api'; import { boardGameApi } from '../../api';
type GamePageParams = { type GamePageParams = {
params: { params: {
id: string; id: string;
} };
} };
export const load: PageServerLoad = async ({ params }: GamePageParams) => { export const load: PageServerLoad = async ({ params, setHeaders }: GamePageParams) => {
const queryParams = { const queryParams = {
ids: `${params?.id}` ids: `${params?.id}`
}; };
const response = await boardGameApi('get', `search`, queryParams); const response = await boardGameApi('get', `search`, queryParams);
if (response.status === 200) { if (response.status === 200) {
const gameResponse = await response.json(); const gameResponse = await response.json();
return {
game: gameResponse?.games[0] setHeaders({
}; 'Cache-Control': 'max-age=3600'
} });
throw error(response.status, 'not found'); return {
game: gameResponse?.games[0]
};
}
throw error(response.status, 'not found');
}; };

View file

@ -25,7 +25,9 @@
$: existsInWishlist = $wishlistStore.find((item: SavedGameType) => item.id === game.id); $: existsInWishlist = $wishlistStore.find((item: SavedGameType) => item.id === game.id);
export let data: PageData; export let data: PageData;
export let game: GameType = data?.game; let game: GameType;
$: ({ game } = data);
// export let game: GameType = data?.game;
let seeMore: boolean = false; let seeMore: boolean = false;
let firstParagraphEnd = 0; let firstParagraphEnd = 0;
if (game?.description?.indexOf('</p>') > 0) { if (game?.description?.indexOf('</p>') > 0) {

View file

@ -1,149 +1,279 @@
import type { Actions, PageServerLoad, RequestEvent } from '../$types'; import type { Actions, PageServerLoad, RequestEvent } from '../$types';
import { BOARD_GAME_ATLAS_CLIENT_ID } from '$env/static/private'; import { BOARD_GAME_ATLAS_CLIENT_ID } from '$env/static/private';
import { error, invalid, type ServerLoadEvent } from '@sveltejs/kit'; import { error, fail, type ServerLoadEvent } from '@sveltejs/kit';
import type { GameType, Search, SearchQuery } from '$root/lib/types'; import type { GameType, Search, SearchQuery } from '$root/lib/types';
import { mapAPIGameToBoredGame } from '$root/lib/util/gameMapper'; import { mapAPIGameToBoredGame } from '$root/lib/util/gameMapper';
import { search_schema } from '$root/lib/zodValidation'; import { search_schema } from '$root/lib/zodValidation';
import { ZodError } from 'zod'; import { ZodError } from 'zod';
export const load: PageServerLoad = () => { export const load: PageServerLoad = async ({ fetch, url }) => {
return { const formData = Object.fromEntries(url?.searchParams);
games: [], formData.name = formData?.q;
totalCount: 0, const limit = parseInt(formData?.limit) || 10;
} const skip = parseInt(formData?.skip) || 0;
}
const queryParams: SearchQuery = {
order_by: 'rank',
ascending: false,
limit,
skip,
client_id: BOARD_GAME_ATLAS_CLIENT_ID,
fuzzy_match: true,
name: ''
};
try {
console.log('Parsing Search Schema');
const { name, minAge, minPlayers, maxPlayers, exactMinAge, exactMinPlayers, exactMaxPlayers } =
search_schema.parse(formData);
if (minAge) {
if (exactMinAge) {
queryParams.min_age = minAge;
} else {
queryParams.gt_min_age = minAge === 1 ? 0 : minAge - 1;
}
}
if (minPlayers) {
if (exactMinPlayers) {
queryParams.min_players = minPlayers;
} else {
queryParams.gt_min_players = minPlayers === 1 ? 0 : minPlayers - 1;
}
}
if (maxPlayers) {
if (exactMaxPlayers) {
queryParams.max_players = maxPlayers;
} else {
queryParams.lt_max_players = maxPlayers + 1;
}
}
if (name) {
queryParams.name = name;
}
} catch (parsingError: unknown) {
let errors;
if (parsingError instanceof ZodError) {
// console.log('Parse error');
// console.log(parsingError);
const { fieldErrors } = parsingError.flatten();
console.log(`Errors with user input ${fieldErrors}}`);
errors = fieldErrors;
//throw error(400, { message: 'There was an error searching for games!' }); // fail(400, { data: formData, errors });
}
return {
errors,
name: formData.name,
minAge: formData.minAge,
minPlayers: formData.minPlayers,
maxPlayers: formData.maxPlayers,
exactMinPlayers: formData.exactMinPlayers,
exactMaxPlayers: formData.exactMaxPlayers,
games: [],
totalCount: 0,
limit,
skip
};
}
const newQueryParams: Record<string, string> = {};
for (const key in queryParams) {
// console.log('key', key);
// console.log('queryParams[key]', queryParams[key as keyof SearchQuery]);
newQueryParams[key] = `${queryParams[key as keyof SearchQuery]}`;
}
const urlQueryParams = new URLSearchParams(newQueryParams);
console.log('urlQueryParams', urlQueryParams);
try {
const url = `https://api.boardgameatlas.com/api/search${
urlQueryParams ? `?${urlQueryParams}` : ''
}`;
const response = await fetch(url, {
method: 'get',
headers: {
'content-type': 'application/json'
}
});
// console.log('board game response', response);
if (!response.ok) {
console.log('Status not 200', response.status);
throw error(response.status);
}
if (response.status === 200) {
const gameResponse = await response.json();
// console.log('gameResponse', gameResponse);
const gameList = gameResponse?.games;
const totalCount = gameResponse?.count;
console.log('totalCount', totalCount);
const games: GameType[] = [];
gameList.forEach((game) => {
games.push(mapAPIGameToBoredGame(game));
});
// console.log('returning from search', games)
return {
name: formData.name,
minAge: formData.minAge,
minPlayers: formData.minPlayers,
maxPlayers: formData.maxPlayers,
exactMinPlayers: formData.exactMinPlayers,
exactMaxPlayers: formData.exactMaxPlayers,
games,
totalCount,
limit,
skip
};
}
} catch (e) {
console.log(`Error searching board games ${e}`);
}
return {
games: [],
totalCount: 0,
limit,
skip
};
};
export const actions: Actions = { export const actions: Actions = {
default: async ({ request }: RequestEvent): Promise<any> => { default: async ({ request }: RequestEvent): Promise<any> => {
console.log("In search action specific") console.log('In search action specific');
// Do things in here // Do things in here
const formData = Object.fromEntries(await request.formData()) as Search; const formData = Object.fromEntries(await request.formData()) as Search;
console.log('formData', formData); console.log('formData', formData);
console.log('passed in limit:', formData?.limit) console.log('passed in limit:', formData?.limit);
console.log('passed in skip:', formData?.skip) console.log('passed in skip:', formData?.skip);
const limit = formData?.limit || 10; const limit = formData?.limit || 10;
const skip = formData?.skip || 0; const skip = formData?.skip || 0;
const queryParams: SearchQuery = { const queryParams: SearchQuery = {
order_by: 'rank', order_by: 'rank',
ascending: false, ascending: false,
limit: +limit, limit: +limit,
skip: +skip, skip: +skip,
client_id: BOARD_GAME_ATLAS_CLIENT_ID, client_id: BOARD_GAME_ATLAS_CLIENT_ID,
fuzzy_match: true, fuzzy_match: true,
name: '' name: ''
}; };
// TODO: Check name length and not search if not advanced search // TODO: Check name length and not search if not advanced search
const random = formData?.random === 'on'; const random = formData?.random === 'on';
if (random) { if (random) {
console.log('Random'); console.log('Random');
queryParams.random = random; queryParams.random = random;
} else { } else {
try { try {
const { const {
name, name,
minAge, minAge,
minPlayers, minPlayers,
maxPlayers, maxPlayers,
exactMinAge, exactMinAge,
exactMinPlayers, exactMinPlayers,
exactMaxPlayers exactMaxPlayers
} = search_schema.parse(formData); } = search_schema.parse(formData);
if (minAge) {
if (minAge) { if (exactMinAge) {
if (exactMinAge) { queryParams.min_age = minAge;
queryParams.min_age = minAge; } else {
} else { queryParams.gt_min_age = minAge === 1 ? 0 : minAge - 1;
queryParams.gt_min_age = minAge === 1 ? 0 : minAge - 1; }
} }
}
if (minPlayers) { if (minPlayers) {
if (exactMinPlayers) { if (exactMinPlayers) {
queryParams.min_players = minPlayers; queryParams.min_players = minPlayers;
} else { } else {
queryParams.gt_min_players = minPlayers === 1 ? 0 : minPlayers - 1; queryParams.gt_min_players = minPlayers === 1 ? 0 : minPlayers - 1;
} }
} }
if (maxPlayers) { if (maxPlayers) {
if (exactMaxPlayers) { if (exactMaxPlayers) {
queryParams.max_players = maxPlayers; queryParams.max_players = maxPlayers;
} else { } else {
queryParams.lt_max_players = maxPlayers + 1; queryParams.lt_max_players = maxPlayers + 1;
} }
} }
if (name) { if (name) {
queryParams.name = name; queryParams.name = name;
} }
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof ZodError) { if (error instanceof ZodError) {
console.log(error); console.log(error);
const { fieldErrors: errors } = error.flatten();
return invalid(400, { data: formData, errors });
}
}
}
const newQueryParams: Record<string, string> = {}; const { fieldErrors: errors } = error.flatten();
for (const key in queryParams) { return fail(400, { data: formData, errors });
console.log('key', key); }
console.log('queryParams[key]', queryParams[key as keyof SearchQuery]); }
newQueryParams[key] = `${queryParams[key as keyof SearchQuery]}`; }
}
const urlQueryParams = new URLSearchParams(newQueryParams); const newQueryParams: Record<string, string> = {};
console.log('urlQueryParams', urlQueryParams); for (const key in queryParams) {
console.log('key', key);
console.log('queryParams[key]', queryParams[key as keyof SearchQuery]);
newQueryParams[key] = `${queryParams[key as keyof SearchQuery]}`;
}
try { const urlQueryParams = new URLSearchParams(newQueryParams);
const url = `https://api.boardgameatlas.com/api/search${urlQueryParams ? `?${urlQueryParams}` : '' console.log('urlQueryParams', urlQueryParams);
}`;
const response = await fetch(url, {
method: 'get',
headers: {
'content-type': 'application/json'
}
});
// console.log('board game response', response);
if (!response.ok) { try {
console.log('Status not 200', response.status); const url = `https://api.boardgameatlas.com/api/search${
throw error(response.status); urlQueryParams ? `?${urlQueryParams}` : ''
} }`;
const response = await fetch(url, {
method: 'get',
headers: {
'content-type': 'application/json'
}
});
// console.log('board game response', response);
if (response.status === 200) { if (!response.ok) {
const gameResponse = await response.json(); console.log('Status not 200', response.status);
// console.log('gameResponse', gameResponse); throw error(response.status);
const gameList = gameResponse?.games; }
const totalCount = gameResponse?.count;
console.log('totalCount', totalCount);
const games: GameType[] = [];
gameList.forEach((game) => {
games.push(mapAPIGameToBoredGame(game));
});
// console.log('returning from search', games) if (response.status === 200) {
const gameResponse = await response.json();
// console.log('gameResponse', gameResponse);
const gameList = gameResponse?.games;
const totalCount = gameResponse?.count;
console.log('totalCount', totalCount);
const games: GameType[] = [];
gameList.forEach((game) => {
games.push(mapAPIGameToBoredGame(game));
});
return { // console.log('returning from search', games)
games,
totalCount, return {
limit: parseInt(limit), games,
skip: parseInt(skip), totalCount,
}; limit: parseInt(limit),
} skip: parseInt(skip)
} catch (e) { };
console.log(`Error searching board games ${e}`); }
} } catch (e) {
return { console.log(`Error searching board games ${e}`);
games: [], }
totalCount: 0, return {
limit, games: [],
skip totalCount: 0,
}; limit,
} skip
} };
}
};

3
src/styles/styles.pcss Normal file
View file

@ -0,0 +1,3 @@
@import 'reset.pcss';
@import 'global.pcss';
@import '$root/styles/theme.pcss';

View file

@ -1,3 +0,0 @@
@import 'reset.postcss';
@import 'global.postcss';
@import 'theme.postcss';

View file

@ -1,14 +1,14 @@
import adapter from '@sveltejs/adapter-auto'; import adapter from '@sveltejs/adapter-auto';
import preprocess from 'svelte-preprocess'; import { vitePreprocess } from '@sveltejs/kit/vite';
/** @type {import('@sveltejs/kit').Config} */ /** @type {import('@sveltejs/kit').Config} */
const config = { const config = {
// Consult https://github.com/sveltejs/svelte-preprocess // Consult https://kit.svelte.dev/docs/integrations#preprocessors
// for more information about preprocessors // for more information about preprocessors
preprocess: [ preprocess: [
preprocess({ vitePreprocess({
postcss: true postcss: true,
}) }),
], ],
kit: { kit: {
adapter: adapter(), adapter: adapter(),

View file

@ -2,7 +2,10 @@ import { sveltekit } from '@sveltejs/kit/vite';
import type { UserConfig } from 'vite'; import type { UserConfig } from 'vite';
const config: UserConfig = { const config: UserConfig = {
plugins: [sveltekit()] plugins: [sveltekit()],
test: {
include: ['src/**/*.{test,spec}.{js,ts}']
}
}; };
export default config; export default config;