mirror of
https://github.com/BradNut/boredgame
synced 2025-09-08 17:40:22 +00:00
commit
458693b073
26 changed files with 1479 additions and 1218 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -8,4 +8,5 @@ node_modules
|
||||||
!.env.example
|
!.env.example
|
||||||
.vercel
|
.vercel
|
||||||
.output
|
.output
|
||||||
.idea
|
.idea
|
||||||
|
.fleet
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
{
|
{
|
||||||
"useTabs": true,
|
"useTabs": true,
|
||||||
|
"tabWidth": 2,
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"trailingComma": "none",
|
"trailingComma": "none",
|
||||||
"printWidth": 100,
|
"printWidth": 100,
|
||||||
|
|
|
||||||
44
package.json
44
package.json
|
|
@ -13,37 +13,39 @@
|
||||||
"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/adapter-vercel": "^1.0.0",
|
||||||
|
"@sveltejs/kit": "^1.0.1",
|
||||||
"@types/cookie": "^0.5.1",
|
"@types/cookie": "^0.5.1",
|
||||||
"@types/node": "^18.11.9",
|
"@types/node": "^18.11.18",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.42.1",
|
"@typescript-eslint/eslint-plugin": "^5.47.1",
|
||||||
"@typescript-eslint/parser": "^5.42.1",
|
"@typescript-eslint/parser": "^5.47.1",
|
||||||
"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 +53,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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
1355
pnpm-lock.yaml
1355
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
|
@ -1,8 +1,12 @@
|
||||||
<!-- Taken from carbon design system svelte -->
|
<!-- Taken from carbon design system svelte -->
|
||||||
<!-- https://github.com/carbon-design-system/carbon-components-svelte/blob/master/src/SkeletonPlaceholder/SkeletonPlaceholder.svelte -->
|
<!-- https://github.com/carbon-design-system/carbon-components-svelte/blob/master/src/SkeletonPlaceholder/SkeletonPlaceholder.svelte -->
|
||||||
|
<script lang="ts">
|
||||||
|
export let style: string;
|
||||||
|
</script>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
|
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
|
||||||
<div
|
<div
|
||||||
|
{style}
|
||||||
class:bx--skeleton__placeholder={true}
|
class:bx--skeleton__placeholder={true}
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
on:click
|
on:click
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -5,30 +5,12 @@
|
||||||
import { ToastType } from '$root/lib/types';
|
import { ToastType } from '$root/lib/types';
|
||||||
import { toast } from '../../toast/toast';
|
import { toast } from '../../toast/toast';
|
||||||
|
|
||||||
// async function handleSubmit(event: SubmitEvent) {
|
|
||||||
// // submitting = true;
|
|
||||||
// boredState.update((n) => ({ ...n, loading: true }));
|
|
||||||
// const form = event.target as HTMLFormElement;
|
|
||||||
// console.log('form', form);
|
|
||||||
// const response = await fetch('/api/games', {
|
|
||||||
// method: 'POST',
|
|
||||||
// headers: { accept: 'application/json' },
|
|
||||||
// body: new FormData(form)
|
|
||||||
// });
|
|
||||||
// const responseData = await response.json();
|
|
||||||
// // submitting = false;
|
|
||||||
// boredState.update((n) => ({ ...n, loading: false }));
|
|
||||||
// gameStore.removeAll();
|
|
||||||
// gameStore.addAll(responseData?.games);
|
|
||||||
// // games = responseData?.games;
|
|
||||||
// }
|
|
||||||
|
|
||||||
let submitting = $boredState?.loading;
|
let submitting = $boredState?.loading;
|
||||||
let checked = true;
|
let checked = true;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
action="/search"
|
action="/search?/random"
|
||||||
method="POST"
|
method="POST"
|
||||||
use:enhance={() => {
|
use:enhance={() => {
|
||||||
gameStore.removeAll();
|
gameStore.removeAll();
|
||||||
|
|
@ -58,7 +40,7 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<fieldset aria-busy={submitting} disabled={submitting}>
|
<fieldset aria-busy={submitting} disabled={submitting}>
|
||||||
<input type="checkbox" id="random" name="random" hidden {checked} />
|
<!-- <input type="checkbox" id="random" name="random" hidden {checked} /> -->
|
||||||
<button class="btn" type="submit" disabled={submitting}>Random Game 🎲</button>
|
<button class="btn" type="submit" disabled={submitting}>Random Game 🎲</button>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -33,27 +33,24 @@
|
||||||
let gameToRemove: GameType | SavedGameType;
|
let gameToRemove: GameType | SavedGameType;
|
||||||
let numberOfGameSkeleton = 1;
|
let numberOfGameSkeleton = 1;
|
||||||
let submitButton: HTMLElement;
|
let submitButton: HTMLElement;
|
||||||
let pageSize = 10;
|
let pageSize = +data?.limit || 10;
|
||||||
let page = +form?.data?.page || 1;
|
let totalItems = +data?.totalCount || 0;
|
||||||
let totalItems = form?.totalCount || data?.totalCount || 0;
|
let offset = +data?.skip || 0;
|
||||||
|
let page = Math.floor(offset / pageSize) + 1 || 1;
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -81,11 +78,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handlePerPageEvent(event: CustomEvent) {
|
async function handlePerPageEvent(event: CustomEvent) {
|
||||||
console.log('Per Page Event called', event.detail);
|
|
||||||
page = 1;
|
page = 1;
|
||||||
pageSize = event.detail.pageSize;
|
pageSize = event.detail.pageSize;
|
||||||
await tick();
|
await tick();
|
||||||
// console.log('New limit value DOM: ', document.getElementById('limit')?.getAttribute('value'));
|
|
||||||
submitButton.click();
|
submitButton.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -141,22 +136,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 +173,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 +193,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 +234,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">
|
||||||
|
|
|
||||||
208
src/lib/types.ts
208
src/lib/types.ts
|
|
@ -1,125 +1,125 @@
|
||||||
import type { SvelteComponent } from "svelte";
|
import type { SvelteComponent } from 'svelte';
|
||||||
|
|
||||||
export type Dialog = {
|
export type Dialog = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
content?: typeof SvelteComponent;
|
content?: typeof SvelteComponent;
|
||||||
additionalData?: SavedGameType | GameType;
|
additionalData?: SavedGameType | GameType;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type Search = {
|
export type Search = {
|
||||||
name: string;
|
name: string;
|
||||||
minAge: string;
|
minAge: string;
|
||||||
minPlayers: string;
|
minPlayers: string;
|
||||||
maxPlayers: string;
|
maxPlayers: string;
|
||||||
exactMinAge: string;
|
exactMinAge: string;
|
||||||
exactMinPlayers: string;
|
exactMinPlayers: string;
|
||||||
exactMaxPlayers: string;
|
exactMaxPlayers: string;
|
||||||
skip: number;
|
skip: number;
|
||||||
currentPage: number;
|
currentPage: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type BoredStore = {
|
export type BoredStore = {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
dialog: Dialog;
|
dialog: Dialog;
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum ToastType {
|
export enum ToastType {
|
||||||
INFO = 'INFO',
|
INFO = 'INFO',
|
||||||
ERROR = 'ERROR',
|
ERROR = 'ERROR',
|
||||||
WARNING = 'WARNING'
|
WARNING = 'WARNING'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ToastData = {
|
export type ToastData = {
|
||||||
id: number;
|
id: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
dismissible: boolean;
|
dismissible: boolean;
|
||||||
showButton: boolean;
|
showButton: boolean;
|
||||||
autoDismiss: boolean;
|
autoDismiss: boolean;
|
||||||
type: ToastType;
|
type: ToastType;
|
||||||
message: string;
|
message: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SavedGameType = {
|
export type SavedGameType = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
thumb_url: string;
|
thumb_url: string;
|
||||||
players: string;
|
players: string;
|
||||||
playtime: string;
|
playtime: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type GameType = {
|
export type GameType = {
|
||||||
id: string;
|
id: string;
|
||||||
handle: string;
|
handle: string;
|
||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
edit_url: string;
|
edit_url: string;
|
||||||
thumb_url: string;
|
thumb_url: string;
|
||||||
image_url: string;
|
image_url: string;
|
||||||
price: number;
|
price: number;
|
||||||
price_ca: number;
|
price_ca: number;
|
||||||
price_uk: number;
|
price_uk: number;
|
||||||
price_au: number;
|
price_au: number;
|
||||||
msrp: number;
|
msrp: number;
|
||||||
year_published: number;
|
year_published: number;
|
||||||
min_players: number;
|
min_players: number;
|
||||||
max_players: number;
|
max_players: number;
|
||||||
min_playtime: number;
|
min_playtime: number;
|
||||||
max_playtime: number;
|
max_playtime: number;
|
||||||
min_age: number;
|
min_age: number;
|
||||||
description: string;
|
description: string;
|
||||||
description_preview: string;
|
description_preview: string;
|
||||||
players: string;
|
players: string;
|
||||||
playtime: string;
|
playtime: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SearchQuery = {
|
export type SearchQuery = {
|
||||||
client_id: string;
|
client_id: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
skip?: number;
|
skip?: number;
|
||||||
ids?: string[];
|
ids?: string[];
|
||||||
list_id?: string;
|
list_id?: string;
|
||||||
kickstarter?: boolean;
|
kickstarter?: boolean;
|
||||||
random?: boolean;
|
random?: boolean;
|
||||||
name?: string;
|
name?: string;
|
||||||
exact?: boolean;
|
exact?: boolean;
|
||||||
fuzzy_match?: boolean;
|
fuzzy_match?: boolean;
|
||||||
designer?: string;
|
designer?: string;
|
||||||
publisher?: string;
|
publisher?: string;
|
||||||
artist?: string;
|
artist?: string;
|
||||||
mechanics?: string;
|
mechanics?: string;
|
||||||
categories?: string;
|
categories?: string;
|
||||||
order_by?: string;
|
order_by?: string;
|
||||||
ascending?: boolean;
|
ascending?: boolean;
|
||||||
min_players?: number;
|
min_players?: number;
|
||||||
max_players?: number;
|
max_players?: number;
|
||||||
min_playtime?: number;
|
min_playtime?: number;
|
||||||
max_playtime?: number;
|
max_playtime?: number;
|
||||||
min_age?: number;
|
min_age?: number;
|
||||||
year_published?: number;
|
year_published?: number;
|
||||||
gt_min_players?: number;
|
gt_min_players?: number;
|
||||||
gt_max_players?: number;
|
gt_max_players?: number;
|
||||||
gt_min_playtime?: number;
|
gt_min_playtime?: number;
|
||||||
gt_max_playtime?: number;
|
gt_max_playtime?: number;
|
||||||
gt_min_age?: number;
|
gt_min_age?: number;
|
||||||
gt_year_published?: number;
|
gt_year_published?: number;
|
||||||
gt_price?: bigint;
|
gt_price?: bigint;
|
||||||
gt_msrp?: bigint;
|
gt_msrp?: bigint;
|
||||||
gt_discount?: bigint;
|
gt_discount?: bigint;
|
||||||
gt_reddit_count?: number;
|
gt_reddit_count?: number;
|
||||||
gt_reddit_week_count?: number;
|
gt_reddit_week_count?: number;
|
||||||
gt_reddit_day_count?: number;
|
gt_reddit_day_count?: number;
|
||||||
lt_min_players?: number;
|
lt_min_players?: number;
|
||||||
lt_max_players?: number;
|
lt_max_players?: number;
|
||||||
lt_min_playtime?: number;
|
lt_min_playtime?: number;
|
||||||
lt_max_playtime?: number;
|
lt_max_playtime?: number;
|
||||||
lt_min_age?: number;
|
lt_min_age?: number;
|
||||||
lt_year_published?: number;
|
lt_year_published?: number;
|
||||||
lt_price?: bigint;
|
lt_price?: bigint;
|
||||||
lt_msrp?: bigint;
|
lt_msrp?: bigint;
|
||||||
lt_discount?: bigint;
|
lt_discount?: bigint;
|
||||||
lt_reddit_count?: number;
|
lt_reddit_count?: number;
|
||||||
lt_reddit_week_count?: number;
|
lt_reddit_week_count?: number;
|
||||||
lt_reddit_day_count?: number;
|
lt_reddit_day_count?: number;
|
||||||
fields?: string;
|
fields?: string;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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, {
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,25 @@
|
||||||
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 = {
|
export const load: PageServerLoad = async ({ params, setHeaders }) => {
|
||||||
params: {
|
const queryParams = {
|
||||||
id: string;
|
ids: `${params?.id}`
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ params }: GamePageParams) => {
|
const response = await boardGameApi('get', `search`, queryParams);
|
||||||
const queryParams = {
|
|
||||||
ids: `${params?.id}`
|
|
||||||
};
|
|
||||||
|
|
||||||
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]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error(response.status, 'not found');
|
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);
|
$: 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) {
|
||||||
|
|
@ -223,6 +225,7 @@
|
||||||
place-items: center;
|
place-items: center;
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
margin: 1rem;
|
margin: 1rem;
|
||||||
|
line-height: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.overflow-description {
|
.overflow-description {
|
||||||
|
|
|
||||||
|
|
@ -1,149 +1,203 @@
|
||||||
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 } from '@sveltejs/kit';
|
||||||
import type { GameType, Search, SearchQuery } from '$root/lib/types';
|
import type { GameType, RandomSearch, 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> => {
|
random: async ({ request }: RequestEvent): Promise<any> => {
|
||||||
console.log("In search action specific")
|
const queryParams: SearchQuery = {
|
||||||
// Do things in here
|
order_by: 'rank',
|
||||||
const formData = Object.fromEntries(await request.formData()) as Search;
|
ascending: false,
|
||||||
console.log('formData', formData);
|
client_id: BOARD_GAME_ATLAS_CLIENT_ID,
|
||||||
console.log('passed in limit:', formData?.limit)
|
random: true
|
||||||
console.log('passed in skip:', formData?.skip)
|
};
|
||||||
const limit = formData?.limit || 10;
|
|
||||||
const skip = formData?.skip || 0;
|
|
||||||
|
|
||||||
const queryParams: SearchQuery = {
|
const newQueryParams: Record<string, string> = {};
|
||||||
order_by: 'rank',
|
for (const key in queryParams) {
|
||||||
ascending: false,
|
newQueryParams[key] = `${queryParams[key as keyof SearchQuery]}`;
|
||||||
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
|
const urlQueryParams = new URLSearchParams(newQueryParams);
|
||||||
|
|
||||||
const random = formData?.random === 'on';
|
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 (random) {
|
if (!response.ok) {
|
||||||
console.log('Random');
|
console.log('Status not 200', response.status);
|
||||||
queryParams.random = random;
|
throw error(response.status);
|
||||||
} else {
|
}
|
||||||
try {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
minAge,
|
|
||||||
minPlayers,
|
|
||||||
maxPlayers,
|
|
||||||
exactMinAge,
|
|
||||||
exactMinPlayers,
|
|
||||||
exactMaxPlayers
|
|
||||||
} = search_schema.parse(formData);
|
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
if (minAge) {
|
const gameResponse = await response.json();
|
||||||
if (exactMinAge) {
|
// console.log('gameResponse', gameResponse);
|
||||||
queryParams.min_age = minAge;
|
const gameList = gameResponse?.games;
|
||||||
} else {
|
const totalCount = gameResponse?.count;
|
||||||
queryParams.gt_min_age = minAge === 1 ? 0 : minAge - 1;
|
console.log('totalCount', totalCount);
|
||||||
}
|
const games: GameType[] = [];
|
||||||
}
|
gameList.forEach((game) => {
|
||||||
|
games.push(mapAPIGameToBoredGame(game));
|
||||||
|
});
|
||||||
|
|
||||||
if (minPlayers) {
|
// console.log('returning from search', games)
|
||||||
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) {
|
return {
|
||||||
queryParams.name = name;
|
games
|
||||||
}
|
};
|
||||||
} catch (error: unknown) {
|
}
|
||||||
if (error instanceof ZodError) {
|
} catch (e) {
|
||||||
console.log(error);
|
console.log(`Error searching board games ${e}`);
|
||||||
|
}
|
||||||
const { fieldErrors: errors } = error.flatten();
|
return {
|
||||||
return invalid(400, { data: formData, errors });
|
games: []
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
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 {
|
|
||||||
games,
|
|
||||||
totalCount,
|
|
||||||
limit: parseInt(limit),
|
|
||||||
skip: parseInt(skip),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log(`Error searching board games ${e}`);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
games: [],
|
|
||||||
totalCount: 0,
|
|
||||||
limit,
|
|
||||||
skip
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
export let form: ActionData;
|
export let form: ActionData;
|
||||||
|
|
||||||
|
console.log('data limit', data?.limit);
|
||||||
|
|
||||||
$: if (data?.games) {
|
$: if (data?.games) {
|
||||||
gameStore.removeAll();
|
gameStore.removeAll();
|
||||||
gameStore.addAll(data?.games);
|
gameStore.addAll(data?.games);
|
||||||
|
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
import { error } from '@sveltejs/kit';
|
|
||||||
import { api } from './api';
|
|
||||||
import type { PageServerLoad, Action } from './$types';
|
|
||||||
|
|
||||||
type Todo = {
|
|
||||||
uid: string;
|
|
||||||
created_at: Date;
|
|
||||||
text: string;
|
|
||||||
done: boolean;
|
|
||||||
pending_delete: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ locals }) => {
|
|
||||||
// locals.userid comes from src/hooks.js
|
|
||||||
const response = await api('GET', `todos/${locals.userid}`);
|
|
||||||
|
|
||||||
if (response.status === 404) {
|
|
||||||
// user hasn't created a todo list.
|
|
||||||
// start with an empty array
|
|
||||||
return {
|
|
||||||
todos: [] as Todo[]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.status === 200) {
|
|
||||||
return {
|
|
||||||
todos: (await response.json()) as Todo[]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error(response.status);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const POST: Action = async ({ request, locals }) => {
|
|
||||||
const form = await request.formData();
|
|
||||||
|
|
||||||
await api('POST', `todos/${locals.userid}`, {
|
|
||||||
text: form.get('text')
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PATCH: Action = async ({ request, locals }) => {
|
|
||||||
const form = await request.formData();
|
|
||||||
|
|
||||||
await api('PATCH', `todos/${locals.userid}/${form.get('uid')}`, {
|
|
||||||
text: form.has('text') ? form.get('text') : undefined,
|
|
||||||
done: form.has('done') ? !!form.get('done') : undefined
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DELETE: Action = async ({ request, locals }) => {
|
|
||||||
const form = await request.formData();
|
|
||||||
|
|
||||||
await api('DELETE', `todos/${locals.userid}/${form.get('uid')}`);
|
|
||||||
};
|
|
||||||
|
|
@ -1,180 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import type { PageData } from './$types';
|
|
||||||
import { enhance } from '$lib/form';
|
|
||||||
import { scale } from 'svelte/transition';
|
|
||||||
import { flip } from 'svelte/animate';
|
|
||||||
|
|
||||||
export let data: PageData;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svelte:head>
|
|
||||||
<title>Todos</title>
|
|
||||||
<meta name="description" content="A todo list app" />
|
|
||||||
</svelte:head>
|
|
||||||
|
|
||||||
<div class="todos">
|
|
||||||
<h1>Todos</h1>
|
|
||||||
|
|
||||||
<form
|
|
||||||
class="new"
|
|
||||||
action="/todos"
|
|
||||||
method="post"
|
|
||||||
use:enhance={{
|
|
||||||
result: async ({ form }) => {
|
|
||||||
form.reset();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<input name="text" aria-label="Add todo" placeholder="+ tap to add a todo" />
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{#each data.todos as todo (todo.uid)}
|
|
||||||
<div
|
|
||||||
class="todo"
|
|
||||||
class:done={todo.done}
|
|
||||||
transition:scale|local={{ start: 0.7 }}
|
|
||||||
animate:flip={{ duration: 200 }}
|
|
||||||
>
|
|
||||||
<form
|
|
||||||
action="/todos?_method=PATCH"
|
|
||||||
method="post"
|
|
||||||
use:enhance={{
|
|
||||||
pending: ({ data }) => {
|
|
||||||
todo.done = !!data.get('done');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<input type="hidden" name="uid" value={todo.uid} />
|
|
||||||
<input type="hidden" name="done" value={todo.done ? '' : 'true'} />
|
|
||||||
<button class="toggle" aria-label="Mark todo as {todo.done ? 'not done' : 'done'}" />
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<form class="text" action="/todos?_method=PATCH" method="post" use:enhance>
|
|
||||||
<input type="hidden" name="uid" value={todo.uid} />
|
|
||||||
<input aria-label="Edit todo" type="text" name="text" value={todo.text} />
|
|
||||||
<button class="save" aria-label="Save todo" />
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<form
|
|
||||||
action="/todos?_method=DELETE"
|
|
||||||
method="post"
|
|
||||||
use:enhance={{
|
|
||||||
pending: () => (todo.pending_delete = true)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<input type="hidden" name="uid" value={todo.uid} />
|
|
||||||
<button class="delete" aria-label="Delete todo" disabled={todo.pending_delete} />
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.todos {
|
|
||||||
width: 100%;
|
|
||||||
max-width: var(--column-width);
|
|
||||||
margin: var(--column-margin-top) auto 0 auto;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.new {
|
|
||||||
margin: 0 0 0.5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
border: 1px solid transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:focus-visible {
|
|
||||||
box-shadow: inset 1px 1px 6px rgba(0, 0, 0, 0.1);
|
|
||||||
border: 1px solid #ff3e00 !important;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.new input {
|
|
||||||
font-size: 28px;
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.5em 1em 0.3em 1em;
|
|
||||||
box-sizing: border-box;
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
border-radius: 8px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.todo {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 2rem 1fr 2rem;
|
|
||||||
grid-gap: 0.5rem;
|
|
||||||
align-items: center;
|
|
||||||
margin: 0 0 0.5rem 0;
|
|
||||||
padding: 0.5rem;
|
|
||||||
background-color: white;
|
|
||||||
border-radius: 8px;
|
|
||||||
filter: drop-shadow(2px 4px 6px rgba(0, 0, 0, 0.1));
|
|
||||||
transform: translate(-1px, -1px);
|
|
||||||
transition: filter 0.2s, transform 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.done {
|
|
||||||
transform: none;
|
|
||||||
opacity: 0.4;
|
|
||||||
filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.1));
|
|
||||||
}
|
|
||||||
|
|
||||||
form.text {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.todo input {
|
|
||||||
flex: 1;
|
|
||||||
padding: 0.5em 2em 0.5em 0.8em;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.todo button {
|
|
||||||
width: 2em;
|
|
||||||
height: 2em;
|
|
||||||
border: none;
|
|
||||||
background-color: transparent;
|
|
||||||
background-position: 50% 50%;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.toggle {
|
|
||||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
|
||||||
border-radius: 50%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
background-size: 1em auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.done .toggle {
|
|
||||||
background-image: url("data:image/svg+xml,%3Csvg width='22' height='16' viewBox='0 0 22 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M20.5 1.5L7.4375 14.5L1.5 8.5909' stroke='%23676778' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
|
||||||
}
|
|
||||||
|
|
||||||
.delete {
|
|
||||||
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4.5 5V22H19.5V5H4.5Z' fill='%23676778' stroke='%23676778' stroke-width='1.5' stroke-linejoin='round'/%3E%3Cpath d='M10 10V16.5' stroke='white' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M14 10V16.5' stroke='white' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M2 5H22' stroke='%23676778' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M8 5L9.6445 2H14.3885L16 5H8Z' fill='%23676778' stroke='%23676778' stroke-width='1.5' stroke-linejoin='round'/%3E%3C/svg%3E%0A");
|
|
||||||
opacity: 0.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.delete:hover,
|
|
||||||
.delete:focus {
|
|
||||||
transition: opacity 0.2s;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.save {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
opacity: 0;
|
|
||||||
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M20.5 2H3.5C2.67158 2 2 2.67157 2 3.5V20.5C2 21.3284 2.67158 22 3.5 22H20.5C21.3284 22 22 21.3284 22 20.5V3.5C22 2.67157 21.3284 2 20.5 2Z' fill='%23676778' stroke='%23676778' stroke-width='1.5' stroke-linejoin='round'/%3E%3Cpath d='M17 2V11H7.5V2H17Z' fill='white' stroke='white' stroke-width='1.5' stroke-linejoin='round'/%3E%3Cpath d='M13.5 5.5V7.5' stroke='%23676778' stroke-width='1.5' stroke-linecap='round'/%3E%3Cpath d='M5.99844 2H18.4992' stroke='%23676778' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E%0A");
|
|
||||||
}
|
|
||||||
|
|
||||||
.todo input:focus + .save,
|
|
||||||
.save:focus {
|
|
||||||
transition: opacity 0.2s;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
/*
|
|
||||||
This module is used by the /todos endpoint to
|
|
||||||
make calls to api.svelte.dev, which stores todos
|
|
||||||
for each user.
|
|
||||||
|
|
||||||
(The data on the todo app will expire periodically; no
|
|
||||||
guarantees are made. Don't use it to organise your life.)
|
|
||||||
*/
|
|
||||||
|
|
||||||
const base = 'https://api.svelte.dev';
|
|
||||||
|
|
||||||
export function api(method: string, resource: string, data?: Record<string, unknown>) {
|
|
||||||
return fetch(`${base}/${resource}`, {
|
|
||||||
method,
|
|
||||||
headers: {
|
|
||||||
'content-type': 'application/json'
|
|
||||||
},
|
|
||||||
body: data && JSON.stringify(data)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
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 adapter from '@sveltejs/adapter-vercel';
|
||||||
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(),
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue