mirror of
https://github.com/BradNut/boredgame
synced 2025-09-08 17:40:22 +00:00
Moving search to do a GET with q search param.
This commit is contained in:
parent
f7c3939765
commit
2e6c38dc44
19 changed files with 1112 additions and 829 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -8,4 +8,5 @@ node_modules
|
|||
!.env.example
|
||||
.vercel
|
||||
.output
|
||||
.idea
|
||||
.idea
|
||||
.fleet
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"useTabs": true,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
|
|
|
|||
43
package.json
43
package.json
|
|
@ -13,37 +13,38 @@
|
|||
"format": "prettier --write --plugin-search-dir=. ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.27.1",
|
||||
"@playwright/test": "^1.29.1",
|
||||
"@rgossiaux/svelte-headlessui": "1.0.2",
|
||||
"@rgossiaux/svelte-heroicons": "^0.1.2",
|
||||
"@sveltejs/adapter-auto": "next",
|
||||
"@sveltejs/kit": "next",
|
||||
"@sveltejs/adapter-auto": "^1.0.0",
|
||||
"@sveltejs/kit": "^1.0.1",
|
||||
"@types/cookie": "^0.5.1",
|
||||
"@types/node": "^18.11.9",
|
||||
"@typescript-eslint/eslint-plugin": "^5.42.1",
|
||||
"@typescript-eslint/parser": "^5.42.1",
|
||||
"@types/node": "^18.11.17",
|
||||
"@typescript-eslint/eslint-plugin": "^5.47.0",
|
||||
"@typescript-eslint/parser": "^5.47.0",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"eslint": "^8.27.0",
|
||||
"eslint-config-prettier": "^8.1.0",
|
||||
"eslint": "^8.30.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-svelte3": "^4.0.0",
|
||||
"just-debounce-it": "^3.1.1",
|
||||
"postcss": "^8.4.19",
|
||||
"just-debounce-it": "^3.2.0",
|
||||
"postcss": "^8.4.20",
|
||||
"postcss-color-functional-notation": "^4.2.4",
|
||||
"postcss-custom-media": "^9.0.1",
|
||||
"postcss-env-function": "^4.0.6",
|
||||
"postcss-import": "^15.0.0",
|
||||
"postcss-import": "^15.1.0",
|
||||
"postcss-load-config": "^4.0.1",
|
||||
"postcss-media-minmax": "^5.0.0",
|
||||
"postcss-nested": "^6.0.0",
|
||||
"prettier": "^2.7.1",
|
||||
"prettier-plugin-svelte": "^2.8.0",
|
||||
"sass": "^1.56.1",
|
||||
"svelte": "^3.53.1",
|
||||
"svelte-check": "^2.9.2",
|
||||
"prettier": "^2.8.1",
|
||||
"prettier-plugin-svelte": "^2.9.0",
|
||||
"sass": "^1.57.1",
|
||||
"svelte": "^3.55.0",
|
||||
"svelte-check": "^2.10.3",
|
||||
"svelte-preprocess": "^4.10.7",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^4.8.4",
|
||||
"vite": "^3.2.3"
|
||||
"typescript": "^4.9.4",
|
||||
"vite": "^4.0.3",
|
||||
"vitest": "^0.25.3"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
|
@ -51,11 +52,11 @@
|
|||
"@leveluptuts/svelte-side-menu": "^1.0.5",
|
||||
"@leveluptuts/svelte-toy": "^2.0.3",
|
||||
"@lukeed/uuid": "^2.0.0",
|
||||
"@types/feather-icons": "^4.7.0",
|
||||
"@types/feather-icons": "^4.29.1",
|
||||
"cookie": "^0.5.0",
|
||||
"feather-icons": "^4.29.0",
|
||||
"svelte-lazy-loader": "^1.0.0",
|
||||
"zod": "^3.19.1",
|
||||
"zod-to-json-schema": "^3.19.3"
|
||||
"zod": "^3.20.2",
|
||||
"zod-to-json-schema": "^3.20.1"
|
||||
}
|
||||
}
|
||||
1029
pnpm-lock.yaml
1029
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
|
@ -1,16 +1,17 @@
|
|||
<script lang="ts">
|
||||
import type { ActionData } from './$types';
|
||||
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 minAge = +form?.data?.minAge || 1;
|
||||
let minPlayers = +form?.data?.minPlayers || 1;
|
||||
let maxPlayers = +form?.data?.maxPlayers || 1;
|
||||
let exactMinPlayers = Boolean(form?.data?.exactMinPlayers) || false;
|
||||
let exactMaxPlayers = Boolean(form?.data?.exactMaxPlayers) || false;
|
||||
let minAge = +data?.minAge || 1;
|
||||
let minPlayers = +data?.minPlayers || 1;
|
||||
let maxPlayers = +data?.maxPlayers || 1;
|
||||
let exactMinPlayers = Boolean(data?.exactMinPlayers) || false;
|
||||
let exactMaxPlayers = Boolean(data?.exactMaxPlayers) || false;
|
||||
</script>
|
||||
|
||||
<fieldset class="advanced-search" aria-busy={submitting} disabled={submitting}>
|
||||
|
|
@ -19,10 +20,10 @@
|
|||
Min Age
|
||||
<input id="minAge" name="minAge" bind:value={minAge} type="number" min={1} max={120} />
|
||||
</label>
|
||||
{#if form?.errors?.minAge}
|
||||
{#if data?.errors?.minAge}
|
||||
<div id="minPlayers-error" class="error">
|
||||
<p aria-label={`Error: ${form?.errors?.minAge}`} class="center">
|
||||
{form?.errors?.minAge}
|
||||
<p aria-label={`Error: ${data?.errors?.minAge}`} class="center">
|
||||
{data?.errors?.minAge}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -49,10 +50,10 @@
|
|||
bind:value={exactMinPlayers}
|
||||
/>
|
||||
</label>
|
||||
{#if form?.errors?.minPlayers}
|
||||
{#if data?.errors?.minPlayers}
|
||||
<div id="minPlayers-error" class="error">
|
||||
<p aria-label={`Error: ${form?.errors?.minPlayers}`} class="center">
|
||||
{form?.errors?.minPlayers}
|
||||
<p aria-label={`Error: ${data?.errors?.minPlayers}`} class="center">
|
||||
{data?.errors?.minPlayers}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -79,10 +80,10 @@
|
|||
bind:value={exactMaxPlayers}
|
||||
/>
|
||||
</label>
|
||||
{#if form?.error?.id === 'maxPlayers'}
|
||||
{#if data?.error?.id === 'maxPlayers'}
|
||||
<div id="maxPlayers-error" class="error">
|
||||
<p aria-label={`Error: ${form.error.message}`} class="center">
|
||||
Error: {form.error.message}
|
||||
<p aria-label={`Error: ${data.error.message}`} class="center">
|
||||
Error: {data.error.message}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
// console.log('search page data', data);
|
||||
export let form: ActionData;
|
||||
// console.log('search page form', form);
|
||||
const errors = form?.errors;
|
||||
const errors = data?.errors;
|
||||
|
||||
export let showButton: boolean = false;
|
||||
export let advancedSearch: boolean = false;
|
||||
|
|
@ -34,26 +34,22 @@
|
|||
let numberOfGameSkeleton = 1;
|
||||
let submitButton: HTMLElement;
|
||||
let pageSize = 10;
|
||||
let page = +form?.data?.page || 1;
|
||||
let totalItems = form?.totalCount || data?.totalCount || 0;
|
||||
let page = +data?.page || 1;
|
||||
let totalItems = data?.totalCount || 0;
|
||||
let submitting = $boredState?.loading;
|
||||
let name = form?.name || '';
|
||||
let name = data?.name || '';
|
||||
let disclosureOpen = errors || false;
|
||||
|
||||
$: skip = (page - 1) * pageSize;
|
||||
$: showPagination = $gameStore?.length > 1;
|
||||
|
||||
if ($xl) {
|
||||
console.log('Was xl');
|
||||
numberOfGameSkeleton = 8;
|
||||
} else if ($md) {
|
||||
console.log('Was md');
|
||||
numberOfGameSkeleton = 3;
|
||||
} else if ($sm) {
|
||||
console.log('Was sm');
|
||||
numberOfGameSkeleton = 2;
|
||||
} else {
|
||||
console.log('Was none');
|
||||
numberOfGameSkeleton = 1;
|
||||
}
|
||||
|
||||
|
|
@ -141,22 +137,22 @@
|
|||
// TODO: Add cache for certain number of pages so back and forth doesn't request data again
|
||||
</script>
|
||||
|
||||
<form id="search-form" action="/search" method="post" use:enhance={submitSearch}>
|
||||
<input id="skip" type="hidden" name="skip" bind:value={skip} />
|
||||
<input id="limit" type="hidden" name="limit" bind:value={pageSize} />
|
||||
<form id="search-form" action="/search" method="get">
|
||||
<div class="search">
|
||||
<fieldset class="text-search" aria-busy={submitting} disabled={submitting}>
|
||||
<label for="name">
|
||||
<label for="q">
|
||||
Search
|
||||
<input
|
||||
id="name"
|
||||
name="name"
|
||||
id="q"
|
||||
name="q"
|
||||
bind:value={name}
|
||||
type="text"
|
||||
aria-label="Search boardgame"
|
||||
placeholder="Search boardgame"
|
||||
/>
|
||||
</label>
|
||||
<input id="skip" type="hidden" name="skip" bind:value={skip} />
|
||||
<input id="limit" type="hidden" name="limit" bind:value={pageSize} />
|
||||
</fieldset>
|
||||
{#if advancedSearch}
|
||||
<Disclosure>
|
||||
|
|
@ -178,7 +174,7 @@
|
|||
<!-- Using `static`, `DisclosurePanel` is always rendered,
|
||||
and ignores the `open` state -->
|
||||
<DisclosurePanel static>
|
||||
<AdvancedSearch {form} />
|
||||
<AdvancedSearch {data} />
|
||||
</DisclosurePanel>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -198,19 +194,34 @@
|
|||
{/if}
|
||||
</form>
|
||||
|
||||
{#if $gameStore?.length > 0}
|
||||
{#if $boredState.loading}
|
||||
<div class="games">
|
||||
<h1>Games Found:</h1>
|
||||
<div class="games-list">
|
||||
{#each $gameStore as game (game.id)}
|
||||
<Game
|
||||
on:handleRemoveWishlist={handleRemoveWishlist}
|
||||
on:handleRemoveCollection={handleRemoveCollection}
|
||||
{game}
|
||||
{#each placeholderList as game, i}
|
||||
<SkeletonPlaceholder
|
||||
style="width: 100%; height: 500px; border-radius: var(--borderRadius);"
|
||||
/>
|
||||
{/each}
|
||||
</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
|
||||
{pageSize}
|
||||
{page}
|
||||
|
|
@ -224,17 +235,6 @@
|
|||
/>
|
||||
{/if}
|
||||
</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}
|
||||
|
||||
<style lang="postcss">
|
||||
|
|
|
|||
|
|
@ -2,154 +2,156 @@ import { z, ZodNumber, ZodOptional } from 'zod';
|
|||
// import zodToJsonSchema from 'zod-to-json-schema';
|
||||
|
||||
export const BoardGameSearch = z.object({
|
||||
minAge: z.number(),
|
||||
maxAge: z.number(),
|
||||
minPlayers: z.number(),
|
||||
maxPlayers: z.number()
|
||||
minAge: z.number(),
|
||||
maxAge: z.number(),
|
||||
minPlayers: z.number(),
|
||||
maxPlayers: z.number()
|
||||
});
|
||||
|
||||
export const saved_game_schema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
thumb_url: z.string(),
|
||||
players: z.string(),
|
||||
playtime: IntegerString(z.number()),
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
thumb_url: z.string(),
|
||||
players: z.string(),
|
||||
playtime: IntegerString(z.number())
|
||||
});
|
||||
|
||||
// https://github.com/colinhacks/zod/discussions/330
|
||||
function IntegerString
|
||||
<schema extends (ZodNumber | ZodOptional<ZodNumber>)>
|
||||
(schema: schema)
|
||||
{
|
||||
return (
|
||||
z.preprocess((value) => (
|
||||
( (typeof value === "string") ? parseInt(value, 10)
|
||||
: (typeof value === "number") ? value
|
||||
: undefined
|
||||
)), schema)
|
||||
)
|
||||
function IntegerString<schema extends ZodNumber | ZodOptional<ZodNumber>>(schema: schema) {
|
||||
return z.preprocess(
|
||||
(value) =>
|
||||
typeof value === 'string'
|
||||
? parseInt(value, 10)
|
||||
: typeof value === 'number'
|
||||
? value
|
||||
: undefined,
|
||||
schema
|
||||
);
|
||||
}
|
||||
|
||||
export const search_schema = z.object({
|
||||
name: z.string().trim().optional(),
|
||||
minAge: IntegerString(z.number().min(1).max(120).optional()),
|
||||
minPlayers: IntegerString(z.number().min(1).max(50).optional()),
|
||||
maxPlayers: IntegerString(z.number().min(1).max(50).optional()),
|
||||
exactMinAge: 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 });
|
||||
if (minPlayers && maxPlayers && minPlayers > maxPlayers) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
message: 'Min Players must be smaller than Max Players',
|
||||
path: ['minPlayers'],
|
||||
});
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
message: 'Min Players must be smaller than Max Players',
|
||||
path: ['maxPlayers'],
|
||||
});
|
||||
}
|
||||
export const search_schema = z
|
||||
.object({
|
||||
name: z.string().trim().optional(),
|
||||
minAge: IntegerString(z.number().min(1).max(120).optional()),
|
||||
minPlayers: IntegerString(z.number().min(1).max(50).optional()),
|
||||
maxPlayers: IntegerString(z.number().min(1).max(50).optional()),
|
||||
exactMinAge: 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 });
|
||||
if (minPlayers && maxPlayers && minPlayers > maxPlayers) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
message: 'Min Players must be smaller than Max Players',
|
||||
path: ['minPlayers']
|
||||
});
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
message: 'Min Players must be smaller than Max Players',
|
||||
path: ['maxPlayers']
|
||||
});
|
||||
}
|
||||
|
||||
if (exactMinAge && !minAge) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
message: 'Min Age required when searching for exact min age',
|
||||
path: ['minAge'],
|
||||
});
|
||||
}
|
||||
if (exactMinAge && !minAge) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
message: 'Min Age required when searching for exact min age',
|
||||
path: ['minAge']
|
||||
});
|
||||
}
|
||||
|
||||
if (exactMinPlayers && !minPlayers) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
message: 'Min Players required when searching for exact min players',
|
||||
path: ['minPlayers'],
|
||||
});
|
||||
}
|
||||
if (exactMinPlayers && !minPlayers) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
message: 'Min Players required when searching for exact min players',
|
||||
path: ['minPlayers']
|
||||
});
|
||||
}
|
||||
|
||||
if (exactMaxPlayers && !maxPlayers) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
message: 'Max Players required when searching for exact max players',
|
||||
path: ['maxPlayers'],
|
||||
});
|
||||
}
|
||||
});
|
||||
if (exactMaxPlayers && !maxPlayers) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
message: 'Max Players required when searching for exact max players',
|
||||
path: ['maxPlayers']
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const search_result_schema = z.object({
|
||||
client_id: z.string(),
|
||||
limit: z.number(),
|
||||
skip: z.number(),
|
||||
ids: z.string().array(),
|
||||
list_id: z.string(),
|
||||
kickstarter: z.boolean(),
|
||||
random: z.boolean(),
|
||||
name: z.string(),
|
||||
exact: z.boolean(),
|
||||
fuzzy_match: z.boolean(),
|
||||
designer: z.string(),
|
||||
publisher: z.string(),
|
||||
artist: z.string(),
|
||||
mechanics: z.string(),
|
||||
categories: z.string(),
|
||||
order_by: z.string(),
|
||||
ascending: z.boolean(),
|
||||
min_players: z.number(),
|
||||
max_players: z.number(),
|
||||
min_playtime: z.number(),
|
||||
max_playtime: z.number(),
|
||||
min_age: z.number(),
|
||||
year_published: z.number(),
|
||||
gt_min_players: z.number(),
|
||||
gt_max_players: z.number(),
|
||||
gt_min_playtime: z.number(),
|
||||
gt_max_playtime: z.number(),
|
||||
gt_min_age: z.number(),
|
||||
gt_year_published:z.number(),
|
||||
gt_price: z.bigint(),
|
||||
gt_msrp: z.bigint(),
|
||||
gt_discount: z.bigint(),
|
||||
gt_reddit_count: z.number(),
|
||||
gt_reddit_week_count: z.number(),
|
||||
gt_reddit_day_count: z.number(),
|
||||
lt_min_players: z.number(),
|
||||
lt_max_players: z.number(),
|
||||
lt_min_playtime: z.number(),
|
||||
lt_max_playtime: z.number(),
|
||||
lt_min_age: z.number(),
|
||||
lt_year_published: z.number(),
|
||||
lt_price: z.bigint(),
|
||||
lt_msrp: z.bigint(),
|
||||
lt_discount: z.bigint(),
|
||||
lt_reddit_count: z.number(),
|
||||
lt_reddit_week_count: z.number(),
|
||||
lt_reddit_day_count: z.number(),
|
||||
fields: z.string(),
|
||||
client_id: z.string(),
|
||||
limit: z.number(),
|
||||
skip: z.number(),
|
||||
ids: z.string().array(),
|
||||
list_id: z.string(),
|
||||
kickstarter: z.boolean(),
|
||||
random: z.boolean(),
|
||||
name: z.string(),
|
||||
exact: z.boolean(),
|
||||
fuzzy_match: z.boolean(),
|
||||
designer: z.string(),
|
||||
publisher: z.string(),
|
||||
artist: z.string(),
|
||||
mechanics: z.string(),
|
||||
categories: z.string(),
|
||||
order_by: z.string(),
|
||||
ascending: z.boolean(),
|
||||
min_players: z.number(),
|
||||
max_players: z.number(),
|
||||
min_playtime: z.number(),
|
||||
max_playtime: z.number(),
|
||||
min_age: z.number(),
|
||||
year_published: z.number(),
|
||||
gt_min_players: z.number(),
|
||||
gt_max_players: z.number(),
|
||||
gt_min_playtime: z.number(),
|
||||
gt_max_playtime: z.number(),
|
||||
gt_min_age: z.number(),
|
||||
gt_year_published: z.number(),
|
||||
gt_price: z.bigint(),
|
||||
gt_msrp: z.bigint(),
|
||||
gt_discount: z.bigint(),
|
||||
gt_reddit_count: z.number(),
|
||||
gt_reddit_week_count: z.number(),
|
||||
gt_reddit_day_count: z.number(),
|
||||
lt_min_players: z.number(),
|
||||
lt_max_players: z.number(),
|
||||
lt_min_playtime: z.number(),
|
||||
lt_max_playtime: z.number(),
|
||||
lt_min_age: z.number(),
|
||||
lt_year_published: z.number(),
|
||||
lt_price: z.bigint(),
|
||||
lt_msrp: z.bigint(),
|
||||
lt_discount: z.bigint(),
|
||||
lt_reddit_count: z.number(),
|
||||
lt_reddit_week_count: z.number(),
|
||||
lt_reddit_day_count: z.number(),
|
||||
fields: z.string()
|
||||
});
|
||||
|
||||
export const game_schema = z.object({
|
||||
id: z.string(),
|
||||
handle: z.string(),
|
||||
name: z.string(),
|
||||
url: z.string(),
|
||||
edit_url: z.string(),
|
||||
price: z.number(),
|
||||
price_ca: z.number(),
|
||||
price_uk: z.number(),
|
||||
price_au: z.number(),
|
||||
msrp: z.number(),
|
||||
year_published: z.number(),
|
||||
min_players: z.number(),
|
||||
max_players: z.number(),
|
||||
min_playtime: z.number(),
|
||||
max_playtime: z.number(),
|
||||
min_age: z.number(),
|
||||
description: z.string(),
|
||||
players: z.string(),
|
||||
playtime: z.string()
|
||||
id: z.string(),
|
||||
handle: z.string(),
|
||||
name: z.string(),
|
||||
url: z.string(),
|
||||
edit_url: z.string(),
|
||||
price: z.number(),
|
||||
price_ca: z.number(),
|
||||
price_uk: z.number(),
|
||||
price_au: z.number(),
|
||||
msrp: z.number(),
|
||||
year_published: z.number(),
|
||||
min_players: z.number(),
|
||||
max_players: z.number(),
|
||||
min_playtime: z.number(),
|
||||
max_playtime: z.number(),
|
||||
min_age: z.number(),
|
||||
description: z.string(),
|
||||
players: z.string(),
|
||||
playtime: z.string()
|
||||
});
|
||||
|
||||
// export const game_raw_schema_json = zodToJsonSchema(game_schema, {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
import { gameStore } from '$lib/stores/gameSearchStore';
|
||||
import { toast } from '$lib/components/toast/toast';
|
||||
import Toast from '$lib/components/toast/Toast.svelte';
|
||||
import '$root/styles/styles.postcss';
|
||||
import '$root/styles/styles.pcss';
|
||||
|
||||
$: {
|
||||
if ($navigating) {
|
||||
|
|
|
|||
|
|
@ -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 { URLSearchParams } from 'url';
|
||||
|
||||
const base = 'https://api.boardgameatlas.com/api';
|
||||
|
||||
export function boardGameApi(
|
||||
method: string,
|
||||
resource: string,
|
||||
queryParams: Record<string, string>,
|
||||
data?: Record<string, unknown>
|
||||
method: string,
|
||||
resource: string,
|
||||
queryParams: Record<string, string>,
|
||||
data?: Record<string, unknown>
|
||||
) {
|
||||
// console.log('queryParams', queryParams);
|
||||
queryParams.client_id = BOARD_GAME_ATLAS_CLIENT_ID;
|
||||
const urlQueryParams = new URLSearchParams(queryParams);
|
||||
const url = `${base}/${resource}${urlQueryParams ? `?${urlQueryParams}` : ''}`;
|
||||
return fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
'content-type': 'application/json'
|
||||
},
|
||||
body: data && JSON.stringify(data)
|
||||
});
|
||||
// console.log('queryParams', queryParams);
|
||||
queryParams.client_id = BOARD_GAME_ATLAS_CLIENT_ID;
|
||||
const urlQueryParams = new URLSearchParams(queryParams);
|
||||
const url = `${base}/${resource}${urlQueryParams ? `?${urlQueryParams}` : ''}`;
|
||||
return fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: data && JSON.stringify(data)
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,31 @@
|
|||
import { error } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types'
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { boardGameApi } from '../../api';
|
||||
|
||||
type GamePageParams = {
|
||||
params: {
|
||||
id: string;
|
||||
}
|
||||
}
|
||||
|
||||
export const load: PageServerLoad = async ({ params }: GamePageParams) => {
|
||||
const queryParams = {
|
||||
ids: `${params?.id}`
|
||||
};
|
||||
|
||||
const response = await boardGameApi('get', `search`, queryParams);
|
||||
|
||||
if (response.status === 200) {
|
||||
const gameResponse = await response.json();
|
||||
return {
|
||||
game: gameResponse?.games[0]
|
||||
};
|
||||
}
|
||||
|
||||
throw error(response.status, 'not found');
|
||||
params: {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const load: PageServerLoad = async ({ params, setHeaders }: GamePageParams) => {
|
||||
const queryParams = {
|
||||
ids: `${params?.id}`
|
||||
};
|
||||
|
||||
const response = await boardGameApi('get', `search`, queryParams);
|
||||
|
||||
if (response.status === 200) {
|
||||
const gameResponse = await response.json();
|
||||
|
||||
setHeaders({
|
||||
'Cache-Control': 'max-age=3600'
|
||||
});
|
||||
|
||||
return {
|
||||
game: gameResponse?.games[0]
|
||||
};
|
||||
}
|
||||
|
||||
throw error(response.status, 'not found');
|
||||
};
|
||||
|
|
|
|||
|
|
@ -25,7 +25,9 @@
|
|||
$: existsInWishlist = $wishlistStore.find((item: SavedGameType) => item.id === game.id);
|
||||
|
||||
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 firstParagraphEnd = 0;
|
||||
if (game?.description?.indexOf('</p>') > 0) {
|
||||
|
|
|
|||
|
|
@ -1,149 +1,279 @@
|
|||
import type { Actions, PageServerLoad, RequestEvent } from '../$types';
|
||||
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 { mapAPIGameToBoredGame } from '$root/lib/util/gameMapper';
|
||||
import { search_schema } from '$root/lib/zodValidation';
|
||||
import { ZodError } from 'zod';
|
||||
|
||||
export const load: PageServerLoad = () => {
|
||||
return {
|
||||
games: [],
|
||||
totalCount: 0,
|
||||
}
|
||||
}
|
||||
export const load: PageServerLoad = async ({ fetch, url }) => {
|
||||
const formData = Object.fromEntries(url?.searchParams);
|
||||
formData.name = formData?.q;
|
||||
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 = {
|
||||
default: async ({ request }: RequestEvent): Promise<any> => {
|
||||
console.log("In search action specific")
|
||||
// Do things in here
|
||||
const formData = Object.fromEntries(await request.formData()) as Search;
|
||||
console.log('formData', formData);
|
||||
console.log('passed in limit:', formData?.limit)
|
||||
console.log('passed in skip:', formData?.skip)
|
||||
const limit = formData?.limit || 10;
|
||||
const skip = formData?.skip || 0;
|
||||
default: async ({ request }: RequestEvent): Promise<any> => {
|
||||
console.log('In search action specific');
|
||||
// Do things in here
|
||||
const formData = Object.fromEntries(await request.formData()) as Search;
|
||||
console.log('formData', formData);
|
||||
console.log('passed in limit:', formData?.limit);
|
||||
console.log('passed in skip:', formData?.skip);
|
||||
const limit = formData?.limit || 10;
|
||||
const skip = formData?.skip || 0;
|
||||
|
||||
const queryParams: SearchQuery = {
|
||||
order_by: 'rank',
|
||||
ascending: false,
|
||||
limit: +limit,
|
||||
skip: +skip,
|
||||
client_id: BOARD_GAME_ATLAS_CLIENT_ID,
|
||||
fuzzy_match: true,
|
||||
name: ''
|
||||
};
|
||||
const queryParams: SearchQuery = {
|
||||
order_by: 'rank',
|
||||
ascending: false,
|
||||
limit: +limit,
|
||||
skip: +skip,
|
||||
client_id: BOARD_GAME_ATLAS_CLIENT_ID,
|
||||
fuzzy_match: true,
|
||||
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) {
|
||||
console.log('Random');
|
||||
queryParams.random = random;
|
||||
} else {
|
||||
try {
|
||||
const {
|
||||
name,
|
||||
minAge,
|
||||
minPlayers,
|
||||
maxPlayers,
|
||||
exactMinAge,
|
||||
exactMinPlayers,
|
||||
exactMaxPlayers
|
||||
} = search_schema.parse(formData);
|
||||
if (random) {
|
||||
console.log('Random');
|
||||
queryParams.random = random;
|
||||
} else {
|
||||
try {
|
||||
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 (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 (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 (error: unknown) {
|
||||
if (error instanceof ZodError) {
|
||||
console.log(error);
|
||||
|
||||
const { fieldErrors: errors } = error.flatten();
|
||||
return invalid(400, { data: formData, errors });
|
||||
}
|
||||
}
|
||||
}
|
||||
if (name) {
|
||||
queryParams.name = name;
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof ZodError) {
|
||||
console.log(error);
|
||||
|
||||
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 { fieldErrors: errors } = error.flatten();
|
||||
return fail(400, { data: formData, errors });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const urlQueryParams = new URLSearchParams(newQueryParams);
|
||||
console.log('urlQueryParams', urlQueryParams);
|
||||
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]}`;
|
||||
}
|
||||
|
||||
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);
|
||||
const urlQueryParams = new URLSearchParams(newQueryParams);
|
||||
console.log('urlQueryParams', urlQueryParams);
|
||||
|
||||
if (!response.ok) {
|
||||
console.log('Status not 200', response.status);
|
||||
throw error(response.status);
|
||||
}
|
||||
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.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));
|
||||
});
|
||||
if (!response.ok) {
|
||||
console.log('Status not 200', response.status);
|
||||
throw error(response.status);
|
||||
}
|
||||
|
||||
// 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 {
|
||||
games,
|
||||
totalCount,
|
||||
limit: parseInt(limit),
|
||||
skip: parseInt(skip),
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`Error searching board games ${e}`);
|
||||
}
|
||||
return {
|
||||
games: [],
|
||||
totalCount: 0,
|
||||
limit,
|
||||
skip
|
||||
};
|
||||
}
|
||||
}
|
||||
// console.log('returning from search', games)
|
||||
|
||||
return {
|
||||
games,
|
||||
totalCount,
|
||||
limit: parseInt(limit),
|
||||
skip: parseInt(skip)
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`Error searching board games ${e}`);
|
||||
}
|
||||
return {
|
||||
games: [],
|
||||
totalCount: 0,
|
||||
limit,
|
||||
skip
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
|||
3
src/styles/styles.pcss
Normal file
3
src/styles/styles.pcss
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
@import 'reset.pcss';
|
||||
@import 'global.pcss';
|
||||
@import '$root/styles/theme.pcss';
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
@import 'reset.postcss';
|
||||
@import 'global.postcss';
|
||||
@import 'theme.postcss';
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
import adapter from '@sveltejs/adapter-auto';
|
||||
import preprocess from 'svelte-preprocess';
|
||||
import { vitePreprocess } from '@sveltejs/kit/vite';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
// Consult https://github.com/sveltejs/svelte-preprocess
|
||||
// for more information about preprocessors
|
||||
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
|
||||
// for more information about preprocessors
|
||||
preprocess: [
|
||||
preprocess({
|
||||
postcss: true
|
||||
})
|
||||
vitePreprocess({
|
||||
postcss: true,
|
||||
}),
|
||||
],
|
||||
kit: {
|
||||
adapter: adapter(),
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@ import { sveltekit } from '@sveltejs/kit/vite';
|
|||
import type { UserConfig } from 'vite';
|
||||
|
||||
const config: UserConfig = {
|
||||
plugins: [sveltekit()]
|
||||
plugins: [sveltekit()],
|
||||
test: {
|
||||
include: ['src/**/*.{test,spec}.{js,ts}']
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
|
|
|||
Loading…
Reference in a new issue