Adding separate endpoint for fetching games.

This commit is contained in:
Bradley Shellnut 2022-04-20 19:41:26 -07:00
parent ea533499f2
commit 2a5eafdc1a
10 changed files with 339 additions and 138 deletions

6
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,6 @@
{
"cSpell.words": [
"kickstarter",
"msrp"
]
}

View file

@ -36,6 +36,7 @@
"dependencies": { "dependencies": {
"@fontsource/fira-mono": "^4.5.7", "@fontsource/fira-mono": "^4.5.7",
"@lukeed/uuid": "^2.0.0", "@lukeed/uuid": "^2.0.0",
"cookie": "^0.5.0" "cookie": "^0.5.0",
"zod": "^3.14.4"
} }
} }

View file

@ -23,11 +23,13 @@ specifiers:
svelte-preprocess: ^4.10.6 svelte-preprocess: ^4.10.6
tslib: ^2.3.1 tslib: ^2.3.1
typescript: ^4.6.3 typescript: ^4.6.3
zod: ^3.14.4
dependencies: dependencies:
'@fontsource/fira-mono': 4.5.7 '@fontsource/fira-mono': 4.5.7
'@lukeed/uuid': 2.0.0 '@lukeed/uuid': 2.0.0
cookie: 0.5.0 cookie: 0.5.0
zod: 3.14.4
devDependencies: devDependencies:
'@playwright/test': 1.21.1 '@playwright/test': 1.21.1
@ -2823,3 +2825,7 @@ packages:
dependencies: dependencies:
buffer-crc32: 0.2.13 buffer-crc32: 0.2.13
dev: true dev: true
/zod/3.14.4:
resolution: {integrity: sha512-U9BFLb2GO34Sfo9IUYp0w3wJLlmcyGoMd75qU9yf+DrdGA4kEx6e+l9KOkAlyUO0PSQzZCa3TR4qVlcmwqSDuw==}
dev: false

View file

View file

@ -59,7 +59,7 @@ export function enhance(
const url = new URL(form.action); const url = new URL(form.action);
url.search = url.hash = ''; url.search = url.hash = '';
// invalidate(url.href); invalidate(url.href);
} else if (error) { } else if (error) {
error({ data, form, error: null, response }); error({ data, form, error: null, response });
} else { } else {

72
src/lib/types.ts Normal file
View file

@ -0,0 +1,72 @@
export type Game = {
id: string;
handle: string;
name: string;
url: string;
edit_url: string;
price: number;
price_ca: number;
price_uk: number;
price_au: number;
msrp: number;
year_published: number;
min_players: number;
max_players: number;
min_playtime: number;
max_playtime: number;
min_age: number;
description: string;
players: string;
playtime: string;
}
export type SearchQuery = {
client_id: string;
limit?: number;
skip?: number;
ids?: string[];
list_id?: string;
kickstarter?: boolean;
random?: boolean;
name?: string;
exact?: boolean;
fuzzy_match?: boolean;
designer?: string;
publisher?: string;
artist?: string;
mechanics?: string;
categories?: string;
order_by?: string;
ascending?: boolean;
min_players?: number;
max_players?: number;
min_playtime?: number;
max_playtime?: number;
min_age?: number;
year_published?: number;
gt_min_players?: number;
gt_max_players?: number;
gt_min_playtime?: number;
gt_max_playtime?: number;
gt_min_age?: number;
gt_year_published?: number;
gt_price?: bigint;
gt_msrp?: bigint;
gt_discount?: bigint;
gt_reddit_count?: number;
gt_reddit_week_count?: number;
gt_reddit_day_count?: number;
lt_min_players?: number;
lt_max_players?: number;
lt_min_playtime?: number;
lt_max_playtime?: number;
lt_min_age?: number;
lt_year_published?: number;
lt_price?: bigint;
lt_msrp?: bigint;
lt_discount?: bigint;
lt_reddit_count?: number;
lt_reddit_week_count?: number;
lt_reddit_day_count?: number;
fields?: string;
}

30
src/lib/zodValidation.ts Normal file
View file

@ -0,0 +1,30 @@
import { z } from "zod";
export const BoardGameSearch = z.object({
minAge: z.number(),
maxAge: z.number(),
minPlayers: z.number(),
maxPlayers: z.number(),
});
export const Game = 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(),
});

View file

@ -0,0 +1,62 @@
import type { SearchQuery } from "$lib/types";
import type { RequestHandler } from "@sveltejs/kit";
export const post: RequestHandler = async ({ request }) => {
const form = await request.formData();
const queryParams : SearchQuery = {
order_by: 'rank',
ascending: false,
limit: 20,
client_id: import.meta.env.VITE_PUBLIC_CLIENT_ID,
}
const minAge = form.get('minAge');
const minPlayers = form.get('minPlayers');
const maxPlayers = form.get('maxPlayers');
const random = form.get('random') === 'on' || false;
if (minPlayers) queryParams.gt_min_players = (+minPlayers === 1 ? 0 : (+minPlayers - 1));
if (maxPlayers) queryParams.lt_max_players = +maxPlayers + 1;
if (minAge) queryParams.gt_min_age = +minAge === 1 ? 0 : +minAge - 1;
queryParams.random = random;
const newQueryParams = {};
for (const key in queryParams) {
newQueryParams[key] = new String(queryParams[key]);
}
const urlQueryParams = new URLSearchParams(newQueryParams);
console.log('urlQueryParams', JSON.stringify(urlQueryParams, null, 2));
const url = `https://api.boardgameatlas.com/api/search${urlQueryParams ? `?${urlQueryParams}` : ''}`
const response = await fetch(url, {
method: 'get',
headers: {
'content-type': 'application/json'
},
});
console.log('response', response);
if (response.status === 404) {
// user hasn't created a todo list.
// start with an empty array
return {
body: {
games: []
}
};
}
if (response.status === 200) {
const gameResponse = await response.json();
const games = gameResponse?.games;
console.log('games', games);
return {
body: {
games: gameResponse?.games,
}
};
}
return {
status: response.status
};
}

View file

@ -1,34 +1,27 @@
<script lang="ts"> <script lang="ts">
import { enhance } from "$lib/form"; import type { Game } from "$lib/types";
import { Checkbox, NumberInput, Tooltip } from "carbon-components-svelte";
// import { enhance } from "$lib/form";
let games: Game[] = [];
let submitting = false;
async function handleSubmit(event: SubmitEvent) {
submitting = true;
const form = event.target as HTMLFormElement;
const response = await fetch('/api/games', {
method: 'post',
headers: { accept: 'application/json' },
body: new FormData(form)
});
const responseData = await response.json();
submitting = false;
games = responseData?.games;
}
let minAge = 0; let minAge = 0;
let maxAge = 0; let minPlayers = 1;
let minPlayers = 0; let maxPlayers = 1;
let maxPlayers = 0;
let loading = false;
type Game = {
id: string;
handle: string;
name: string;
url: string;
edit_url: string;
price: number;
price_ca: number;
price_uk: number;
price_au: number;
msrp: number;
year_published: number;
min_players: number;
max_players: number;
min_playtime: number;
max_playtime: number;
min_age: number;
description: string;
players: string;
playtime: string;
}
export let games: Game[];
</script> </script>
@ -37,44 +30,62 @@
</svelte:head> </svelte:head>
<div class="game-form"> <div class="game-form">
<form <form on:submit|preventDefault={handleSubmit} method="post">
action="/games" <fieldset aria-busy={submitting} disabled={submitting}>
method="post" <div>
use:enhance={{ <NumberInput
pending: () => { name="minAge"
loading = true; min={0}
}, max={120}
result: async ({ data, form, response }) => { bind:value={minAge}
loading = false; invalidText="Number must be between 0 and 120"
console.log(JSON.stringify(data)) label="Min Age"
} />
}} <Checkbox name="exactMinAge" labelText="Exact?" />
> </div>
<fieldset aria-busy={loading} disabled={loading}> <div>
<label for="minAge">Min Age: <NumberInput
<input id="minAge" name="minAge" type="range" min="0" max="120" step="1" bind:value={minAge} /> name="minPlayers"
{minAge} min={1}
</label> max={50}
<label for="maxAge">Max Age: bind:value={minPlayers}
<input id="maxAge" name="maxAge" type="range" min="0" max="120" step="1" bind:value={maxAge} /> invalidText="Number must be between 1 and 50"
{maxAge} label="Min Players"
</label> />
<label for="minPlayers">Min Players: <div>
<input id="minPlayers" name="minPlayers" type="range" min="1" max="50" step="1" bind:value={minPlayers} /> <Checkbox name="exactMinPlayers" labelText="Exact?" />
{minPlayers} <Tooltip>
</label> <p id="tooltip-body">
<label for="maxPlayers">Max Players: Resources are provisioned based on your account's organization.
<input id="maxPlayers" name="maxPlayers" type="range" min="1" max="50" step="1" bind:value={maxPlayers} /> </p>
{maxPlayers} </Tooltip>
</label> </div>
</div>
<div>
<NumberInput
name="maxPlayers"
min={1}
max={50}
bind:value={maxPlayers}
invalidText="Number must be between 1 and 50"
label="Max Players"
/>
<Checkbox name="exactMaxPlayers" labelText="Exact?" />
</div>
</fieldset>
<button type="submit" disabled={submitting}>Submit</button>
</form>
<form on:submit|preventDefault={handleSubmit} method="post">
<fieldset aria-busy={submitting} disabled={submitting}>
<input type="checkbox" id="random" name="random" hidden checked />
<button type="submit" disabled={submitting}>🎲</button>
</fieldset> </fieldset>
<button type="submit" disabled={loading}>Submit</button>
</form> </form>
</div> </div>
<div class="games"> <div class="games">
<h1>Games</h1> <h1>Games</h1>
{#each games as game (game.id)} {#each games as game}
<section> <section>
<div> <div>
<h2>{game.name}</h2> <h2>{game.name}</h2>
@ -100,6 +111,13 @@
font-weight: 600; font-weight: 600;
} }
button {
border-radius: 4px;
margin: 0;
padding: 0.2rem;
background-color: palegreen;
}
.games { .games {
display: grid; display: grid;
gap: 2rem; gap: 2rem;
@ -110,7 +128,13 @@
} }
.game-form { .game-form {
display: flex;
place-items: center;
}
.game-form fieldset {
display: grid; display: grid;
grid-template-columns: 1fr; gap: 1rem;
grid-template-columns: repeat(3, minmax(200px, 1fr));
} }
</style> </style>

View file

@ -1,84 +1,84 @@
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import { boardGameApi } from '../_api'; import { boardGameApi } from '../_api';
export const get: RequestHandler = async ({ params }) => { // export const get: RequestHandler = async ({ params }) => {
const queryParams = { // const queryParams = {
order_by: 'rank', // order_by: 'rank',
ascending: 'false', // ascending: 'false',
limit: '10', // limit: '10',
} // }
const response = await boardGameApi('get', `search`, queryParams); // const response = await boardGameApi('get', `search`, queryParams);
console.log('response', response); // console.log('response', response);
if (response.status === 404) { // if (response.status === 404) {
// user hasn't created a todo list. // // user hasn't created a todo list.
// start with an empty array // // start with an empty array
return { // return {
body: { // body: {
games: [] // games: []
} // }
}; // };
} // }
if (response.status === 200) { // if (response.status === 200) {
const gameResponse = await response.json(); // const gameResponse = await response.json();
const games = gameResponse?.games; // const games = gameResponse?.games;
console.log('games', games); // console.log('games', games);
return { // return {
body: { // body: {
games: gameResponse?.games, // games: gameResponse?.games,
} // }
}; // };
} // }
return { // return {
status: response.status // status: response.status
}; // };
} // }
export const post: RequestHandler = async ({ request }) => { // export const post: RequestHandler = async ({ request }) => {
const form = await request.formData(); // const form = await request.formData();
const minAge = form.get('minAge') || 0; // const minAge = form.get('minAge') || 0;
console.log('minAge', minAge); // console.log('minAge', minAge);
const maxAge = form.get('maxAge') || 0; // const maxAge = form.get('maxAge') || 0;
console.log('maxAge', maxAge); // console.log('maxAge', maxAge);
const minPlayers = form.get('minPlayers') || 1; // const minPlayers = form.get('minPlayers') || 1;
console.log('minPlayers', minPlayers); // console.log('minPlayers', minPlayers);
const maxPlayers = form.get('maxPlayers') || 1; // const maxPlayers = form.get('maxPlayers') || 1;
console.log('maxPlayers', maxPlayers); // console.log('maxPlayers', maxPlayers);
const queryParams = { // const queryParams = {
order_by: 'rank', // order_by: 'rank',
ascending: 'false', // ascending: 'false',
limit: '2', // limit: '1',
gt_min_players: String(+minPlayers === 1 ? 0 : +minPlayers - 1), // gt_min_players: String(+minPlayers === 1 ? 0 : +minPlayers - 1),
lt_max_players: String(+maxPlayers + 1), // lt_max_players: String(+maxPlayers + 1),
gt_min_age: String(+minAge === 1 ? 0 : +minAge - 1), // gt_min_age: String(+minAge === 1 ? 0 : +minAge - 1),
lt_max_age: String(+maxAge + 1), // lt_max_age: String(+maxAge + 1),
} // }
const response = await boardGameApi('get', `search`, queryParams); // const response = await boardGameApi('get', `search`, queryParams);
console.log('response', response); // console.log('response', response);
if (response.status === 404) { // if (response.status === 404) {
// user hasn't created a todo list. // // user hasn't created a todo list.
// start with an empty array // // start with an empty array
return { // return {
body: { // body: {
games: [] // games: []
} // }
}; // };
} // }
if (response.status === 200) { // if (response.status === 200) {
const gameResponse = await response.json(); // const gameResponse = await response.json();
const games = gameResponse?.games; // const games = gameResponse?.games;
console.log('games', games); // console.log('games', games);
return { // return {
body: { // body: {
games: gameResponse?.games, // games,
} // }
}; // };
} // }
return { // return {
status: response.status // status: response.status
}; // };
} // }