mirror of
https://github.com/BradNut/boredgame
synced 2025-09-08 17:40:22 +00:00
commit
8b090b5a4c
32 changed files with 1783 additions and 707 deletions
11
.prettierrc
11
.prettierrc
|
|
@ -1,6 +1,9 @@
|
||||||
{
|
{
|
||||||
"useTabs": false,
|
"useTabs": true,
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"trailingComma": "none",
|
"trailingComma": "none",
|
||||||
"printWidth": 100
|
"printWidth": 100,
|
||||||
|
"plugins": ["prettier-plugin-svelte"],
|
||||||
|
"pluginSearchDirs": ["."],
|
||||||
|
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
37
package.json
37
package.json
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "boredgame",
|
"name": "boredgame",
|
||||||
"version": "0.0.1",
|
"version": "0.0.2",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "NODE_OPTIONS=\"--inspect\" vite dev --host",
|
"dev": "NODE_OPTIONS=\"--inspect\" vite dev --host",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
|
|
@ -13,40 +13,41 @@
|
||||||
"format": "prettier --write --plugin-search-dir=. ."
|
"format": "prettier --write --plugin-search-dir=. ."
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.25.1",
|
"@playwright/test": "^1.27.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": "1.0.0-next.71",
|
"@sveltejs/adapter-auto": "1.0.0-next.72",
|
||||||
"@sveltejs/kit": "1.0.0-next.461",
|
"@sveltejs/kit": "1.0.0-next.480",
|
||||||
"@types/cookie": "^0.5.1",
|
"@types/cookie": "^0.5.1",
|
||||||
"@types/node": "^18.7.14",
|
"@types/node": "^18.11.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.36.1",
|
"@typescript-eslint/eslint-plugin": "^5.41.0",
|
||||||
"@typescript-eslint/parser": "^5.36.1",
|
"@typescript-eslint/parser": "^5.41.0",
|
||||||
"carbon-components-svelte": "^0.70.4",
|
"carbon-components-svelte": "^0.70.12",
|
||||||
"carbon-icons-svelte": "^11.2.0",
|
"carbon-icons-svelte": "^11.4.0",
|
||||||
"eslint": "^8.23.0",
|
"eslint": "^8.26.0",
|
||||||
"eslint-config-prettier": "^8.1.0",
|
"eslint-config-prettier": "^8.1.0",
|
||||||
"eslint-plugin-svelte3": "^4.0.0",
|
"eslint-plugin-svelte3": "^4.0.0",
|
||||||
"just-debounce-it": "^3.1.1",
|
"just-debounce-it": "^3.1.1",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.7.1",
|
||||||
"prettier-plugin-svelte": "^2.7.0",
|
"prettier-plugin-svelte": "^2.8.0",
|
||||||
"sass": "^1.54.8",
|
"sass": "^1.55.0",
|
||||||
"svelte": "^3.49.0",
|
"svelte": "^3.52.0",
|
||||||
"svelte-check": "^2.9.0",
|
"svelte-check": "^2.9.2",
|
||||||
"svelte-preprocess": "^4.10.7",
|
"svelte-preprocess": "^4.10.7",
|
||||||
"tslib": "^2.0.0",
|
"tslib": "^2.0.0",
|
||||||
"typescript": "^4.8.2",
|
"typescript": "^4.8.4",
|
||||||
"vite": "^3.1.0-beta.1"
|
"vite": "^3.2.0"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/fira-mono": "^4.5.9",
|
"@fontsource/fira-mono": "^4.5.10",
|
||||||
"@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.7.0",
|
||||||
"cookie": "^0.5.0",
|
"cookie": "^0.5.0",
|
||||||
"feather-icons": "^4.29.0",
|
"feather-icons": "^4.29.0",
|
||||||
"zod": "^3.18.0"
|
"zod": "^3.19.1",
|
||||||
|
"zod-to-json-schema": "^3.18.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import type { PlaywrightTestConfig } from '@playwright/test';
|
import type { PlaywrightTestConfig } from '@playwright/test';
|
||||||
|
|
||||||
const config: PlaywrightTestConfig = {
|
const config: PlaywrightTestConfig = {
|
||||||
webServer: {
|
webServer: {
|
||||||
command: 'npm run build && npm run preview',
|
command: 'npm run build && npm run preview',
|
||||||
port: 3000
|
port: 4173
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|
|
||||||
658
pnpm-lock.yaml
658
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
15
src/app.d.ts
vendored
15
src/app.d.ts
vendored
|
|
@ -1,11 +1,14 @@
|
||||||
/// <reference types="@sveltejs/kit" />
|
|
||||||
|
|
||||||
// See https://kit.svelte.dev/docs/types#app
|
// See https://kit.svelte.dev/docs/types#app
|
||||||
// for information about these interfaces
|
// for information about these interfaces
|
||||||
// and what to do when importing types
|
// and what to do when importing types
|
||||||
declare namespace App {
|
declare namespace App {
|
||||||
// interface Locals {}
|
interface Locals {
|
||||||
// interface Platform {}
|
userid: string;
|
||||||
// interface Session {}
|
}
|
||||||
// interface Stuff {}
|
|
||||||
|
// interface PageData {}
|
||||||
|
|
||||||
|
// interface PageError {}
|
||||||
|
|
||||||
|
// interface Platform {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
65
src/db/actions.ts
Normal file
65
src/db/actions.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
function isNumber(str: string): boolean {
|
||||||
|
if (typeof str !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str.trim() === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !Number.isNaN(Number(str));
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertToBoolean(input: string): boolean | undefined {
|
||||||
|
try {
|
||||||
|
return JSON.parse(input.toLowerCase());
|
||||||
|
} catch (e) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getFormDataObject(request: Request): Promise<{ [key: string]: string }> {
|
||||||
|
const formData = await request.formData();
|
||||||
|
let data = formData.entries();
|
||||||
|
var obj = data.next();
|
||||||
|
var retrieved: any = {};
|
||||||
|
while (undefined !== obj.value) {
|
||||||
|
retrieved[obj.value[0]] = obj.value[1];
|
||||||
|
obj = data.next();
|
||||||
|
}
|
||||||
|
return retrieved;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getFormData<T>(request: Request, schema: any): T {
|
||||||
|
const data = await getFormDataObject(request);
|
||||||
|
return transformFormDataTypes<T>(data, schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Modify this as schema refers to a JSON schema helper
|
||||||
|
|
||||||
|
export function transformFormDataTypes<T>(data, schema): T {
|
||||||
|
for (const property in data) {
|
||||||
|
if (isNumber(schema[property])) {
|
||||||
|
data[property] = parseInt(data[property]);
|
||||||
|
} else if (typeof convertToBoolean(schema[property]) === boolean) {
|
||||||
|
data[property] = convertToBoolean(schema[property]); // data[property] === 'true';
|
||||||
|
} else if (Array.isArray(JSON.parse(schema[property]))) {
|
||||||
|
data[property] = JSON.parse(schema[property]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Actions {
|
||||||
|
[key: string]: any // Action
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Games: Actions = {
|
||||||
|
search: async function search({ request, locals }): Promise<any> {
|
||||||
|
|
||||||
|
}
|
||||||
|
// create: async function create({ request, locals }): Promise<any> {
|
||||||
|
// const data = await getFormDataObject<any>(request);
|
||||||
|
// return data;
|
||||||
|
// }
|
||||||
|
}
|
||||||
16
src/hooks.server..ts
Normal file
16
src/hooks.server..ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import type { Handle } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export const handle: Handle = async ({ event, resolve }) => {
|
||||||
|
let userid = event.cookies.get('userid');
|
||||||
|
|
||||||
|
if (!userid) {
|
||||||
|
// if this is the first time the user has visited this app,
|
||||||
|
// set a cookie so that we recognise them when they return
|
||||||
|
userid = crypto.randomUUID();
|
||||||
|
event.cookies.set('userid', userid, { path: '/' });
|
||||||
|
}
|
||||||
|
|
||||||
|
event.locals.userid = userid;
|
||||||
|
|
||||||
|
return resolve(event);
|
||||||
|
};
|
||||||
23
src/hooks.ts
23
src/hooks.ts
|
|
@ -1,23 +0,0 @@
|
||||||
import type { Handle } from '@sveltejs/kit';
|
|
||||||
import * as cookie from 'cookie';
|
|
||||||
|
|
||||||
export const handle: Handle = async ({ event, resolve }) => {
|
|
||||||
const cookies = cookie.parse(event.request.headers.get('cookie') || '');
|
|
||||||
event.locals.userid = cookies['userid'] || crypto.randomUUID();
|
|
||||||
|
|
||||||
const response = await resolve(event);
|
|
||||||
|
|
||||||
if (!cookies['userid']) {
|
|
||||||
// if this is the first time the user has visited this app,
|
|
||||||
// set a cookie so that we recognise them when they return
|
|
||||||
response.headers.set(
|
|
||||||
'set-cookie',
|
|
||||||
cookie.serialize('userid', event.locals.userid, {
|
|
||||||
path: '/',
|
|
||||||
httpOnly: true
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
};
|
|
||||||
0
src/lib/apis/game.ts
Normal file
0
src/lib/apis/game.ts
Normal file
0
src/lib/components/GameSearchContainer.svelte
Normal file
0
src/lib/components/GameSearchContainer.svelte
Normal file
|
|
@ -1,47 +1,138 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
import {
|
||||||
|
Listbox,
|
||||||
|
ListboxButton,
|
||||||
|
ListboxOption,
|
||||||
|
ListboxOptions
|
||||||
|
} from '@rgossiaux/svelte-headlessui';
|
||||||
|
import {
|
||||||
|
CheckIcon,
|
||||||
|
ChevronLeftIcon,
|
||||||
|
ChevronRightIcon
|
||||||
|
} from '@rgossiaux/svelte-heroicons/outline';
|
||||||
import { boredState } from '$root/lib/stores/boredState';
|
import { boredState } from '$root/lib/stores/boredState';
|
||||||
|
|
||||||
const totalCount = $boredState.search.totalCount; // TODO: Check default value
|
const dispatch = createEventDispatcher();
|
||||||
console.log('totalCount', totalCount);
|
// export let pageSize = 10;
|
||||||
$: pageSize = $boredState.search.pageSize;
|
export let pageSize: number; // Reactive, bind
|
||||||
console.log('pageSize', pageSize);
|
export let currentPage: number; // Reactive, bind
|
||||||
$: currentPage = $boredState.search.currentPage;
|
export let totalItems: number;
|
||||||
console.log('currentPage', currentPage);
|
export let pageSizes: ReadonlyArray<Number> = [10];
|
||||||
$: skip = $boredState.search.skip;
|
export let forwardText: string;
|
||||||
console.log('skip', skip);
|
export let backwardText: string;
|
||||||
|
export let pageSizeInputDisabled: boolean = false;
|
||||||
|
|
||||||
const totalPages: number = Math.ceil(totalCount / pageSize);
|
const totalPages: number = Math.ceil(totalItems / pageSize);
|
||||||
console.log('totalPages', totalPages);
|
console.log('totalPages', totalPages);
|
||||||
const prevPage: number = currentPage - 1;
|
const prevPage: number = currentPage - 1;
|
||||||
const nextPage: number = currentPage + 1;
|
const nextPage: number = currentPage + 1;
|
||||||
const hasNextPage: boolean = nextPage <= totalPages;
|
const hasNextPage: boolean = nextPage <= totalPages;
|
||||||
const hasPrevPage: boolean = prevPage >= 1;
|
const hasPrevPage: boolean = prevPage >= 1;
|
||||||
const itemsLeft: number =
|
const itemsLeft: number =
|
||||||
totalCount - currentPage * pageSize >= 0 ? totalCount - currentPage * pageSize : 0;
|
totalItems - currentPage * $boredState.search.pageSize >= 0
|
||||||
|
? totalItems - currentPage * $boredState.search.pageSize
|
||||||
const pageArray = Array.from({ length: 10 }, (_, i) => i + 1);
|
: 0;
|
||||||
// console.log('pageArray', pageArray);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<button type="button" class="btn" disabled={!hasPrevPage}>Prev</button>
|
<span>
|
||||||
{#each pageArray as page}
|
<p>Items per-page:</p>
|
||||||
<button
|
<div class="list-container">
|
||||||
type="button"
|
<Listbox
|
||||||
class="btn"
|
class="list-box"
|
||||||
aria-current={page === currentPage}
|
value={$boredState.search.pageSize}
|
||||||
class:current={page === currentPage}>{page}</button
|
on:change={(e) => {
|
||||||
>
|
dispatch('pageSizeEvent', e.detail);
|
||||||
{/each}
|
// boredState.update((n) => ({
|
||||||
<button type="button" class="btn" disabled={!hasNextPage}>Next</button>
|
// ...n,
|
||||||
|
// search: { totalCount, pageSize: e.detail, skip, currentPage }
|
||||||
|
// }));
|
||||||
|
}}
|
||||||
|
let:open
|
||||||
|
>
|
||||||
|
<ListboxButton>{pageSize}</ListboxButton>
|
||||||
|
{#if open}
|
||||||
|
<div transition:fade>
|
||||||
|
<ListboxOptions static class="list-options">
|
||||||
|
{#each pageSizes as size (size)}
|
||||||
|
<ListboxOption
|
||||||
|
value={size}
|
||||||
|
disabled={pageSizeInputDisabled}
|
||||||
|
class={({ active }) => (active ? 'active' : '')}
|
||||||
|
let:selected
|
||||||
|
>
|
||||||
|
{#if selected}
|
||||||
|
<CheckIcon />
|
||||||
|
{/if}
|
||||||
|
<p>{size.toString()}</p>
|
||||||
|
</ListboxOption>
|
||||||
|
{/each}
|
||||||
|
</ListboxOptions>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Listbox>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
<p>
|
||||||
|
Page {currentPage || 1} of {totalPages || 1}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{itemsLeft} Item{itemsLeft > 1 || itemsLeft === 0 ? 's' : ''} Left
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn"
|
||||||
|
disabled={!hasPrevPage}
|
||||||
|
on:click={() => dispatch('previousPageEvent', prevPage)}
|
||||||
|
><ChevronLeftIcon width="24" height="24" />
|
||||||
|
<p class="word">{backwardText || 'Prev'}</p></button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn"
|
||||||
|
disabled={!hasNextPage}
|
||||||
|
on:click={() => dispatch('nextPageEvent', nextPage)}
|
||||||
|
><p class="word">{forwardText || 'Next'}</p>
|
||||||
|
<ChevronRightIcon width="24" height="24" /></button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.container {
|
.container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
margin: 3rem 0;
|
margin: 3rem 0;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
.word {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-container :global(.list-box) {
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-container :global(.list-options) {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-container :global(.active) {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
|
|
|
||||||
|
|
@ -1,82 +1,86 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { boredState } from '$lib/stores/boredState';
|
import { boredState } from '$lib/stores/boredState';
|
||||||
|
|
||||||
let submitting = $boredState?.loading;
|
export let form: ActionData;
|
||||||
let minAge = 1;
|
console.log('form', form);
|
||||||
let minPlayers = 1;
|
let submitting = $boredState?.loading;
|
||||||
let maxPlayers = 1;
|
let minAge = 1;
|
||||||
let exactMinPlayers = false;
|
let minPlayers = 1;
|
||||||
let exactMaxPlayers = false;
|
let maxPlayers = 1;
|
||||||
|
let exactMinPlayers = false;
|
||||||
|
let exactMaxPlayers = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- <form on:submit|preventDefault={handleSubmit} method="post"> -->
|
|
||||||
<fieldset class="advanced-search" aria-busy={submitting} disabled={submitting}>
|
<fieldset class="advanced-search" aria-busy={submitting} disabled={submitting}>
|
||||||
<div>
|
<div>
|
||||||
<label for="minAge">
|
<label for="minAge">
|
||||||
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>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="minPlayers">
|
<label for="minPlayers">
|
||||||
Min Players
|
Min Players
|
||||||
<input
|
<input
|
||||||
id="minPlayers"
|
id="minPlayers"
|
||||||
name="minPlayers"
|
name="minPlayers"
|
||||||
bind:value={minPlayers}
|
bind:value={minPlayers}
|
||||||
type="number"
|
type="number"
|
||||||
min={0}
|
min="1"
|
||||||
max={50}
|
max="50"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label for="exactMinPlayers" style="display: flex; gap: 1rem; place-items: center;">
|
<label for="exactMinPlayers" style="display: flex; gap: 1rem; place-items: center;">
|
||||||
<span>Exact?</span>
|
<span>Exact?</span>
|
||||||
<input
|
<input
|
||||||
id="exactMinPlayers"
|
id="exactMinPlayers"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name="exactMinPlayers"
|
name="exactMinPlayers"
|
||||||
bind:checked={exactMinPlayers}
|
bind:checked={exactMinPlayers}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
{#if form?.error?.id === 'minPlayers'}
|
||||||
<div>
|
{form.error.message}
|
||||||
<label for="maxPlayers">
|
{/if}
|
||||||
Max Players
|
</div>
|
||||||
<input
|
<div>
|
||||||
id="maxPlayers"
|
<label for="maxPlayers">
|
||||||
name="maxPlayers"
|
Max Players
|
||||||
bind:value={maxPlayers}
|
<input
|
||||||
type="number"
|
id="maxPlayers"
|
||||||
min={0}
|
name="maxPlayers"
|
||||||
max={50}
|
bind:value={maxPlayers}
|
||||||
/>
|
type="number"
|
||||||
</label>
|
min="1"
|
||||||
<label for="exactMaxPlayers" style="display: flex; gap: 1rem; place-items: center;">
|
max="50"
|
||||||
<span>Exact?</span>
|
/>
|
||||||
<input
|
</label>
|
||||||
id="exactMaxPlayers"
|
<label for="exactMaxPlayers" style="display: flex; gap: 1rem; place-items: center;">
|
||||||
type="checkbox"
|
<span>Exact?</span>
|
||||||
name="exactMaxPlayers"
|
<input
|
||||||
bind:checked={exactMaxPlayers}
|
id="exactMaxPlayers"
|
||||||
/>
|
type="checkbox"
|
||||||
</label>
|
name="exactMaxPlayers"
|
||||||
</div>
|
bind:checked={exactMaxPlayers}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<!-- <button type="submit" disabled={submitting}>Submit</button> -->
|
<!-- <button type="submit" disabled={submitting}>Submit</button> -->
|
||||||
|
|
||||||
<!-- </form> -->
|
<!-- </form> -->
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
fieldset {
|
fieldset {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
|
||||||
@media (max-width: 800px) {
|
@media (max-width: 800px) {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
display: grid;
|
display: grid;
|
||||||
margin: 1rem;
|
margin: 1rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,72 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { boredState } from '$lib/stores/boredState';
|
import { applyAction, enhance } from '$app/forms';
|
||||||
import { gameStore } from '$lib/stores/gameSearchStore';
|
import { boredState } from '$lib/stores/boredState';
|
||||||
|
import { gameStore } from '$lib/stores/gameSearchStore';
|
||||||
|
import { ToastType } from '$root/lib/types';
|
||||||
|
import { toast } from '../../toast/toast';
|
||||||
|
|
||||||
async function handleSubmit(event: SubmitEvent) {
|
// async function handleSubmit(event: SubmitEvent) {
|
||||||
// submitting = true;
|
// // submitting = true;
|
||||||
boredState.update((n) => ({ ...n, loading: true }));
|
// boredState.update((n) => ({ ...n, loading: true }));
|
||||||
const form = event.target as HTMLFormElement;
|
// const form = event.target as HTMLFormElement;
|
||||||
console.log('form', form);
|
// console.log('form', form);
|
||||||
const response = await fetch('/api/games', {
|
// const response = await fetch('/api/games', {
|
||||||
method: 'POST',
|
// method: 'POST',
|
||||||
headers: { accept: 'application/json' },
|
// headers: { accept: 'application/json' },
|
||||||
body: new FormData(form)
|
// body: new FormData(form)
|
||||||
});
|
// });
|
||||||
const responseData = await response.json();
|
// const responseData = await response.json();
|
||||||
// submitting = false;
|
// // submitting = false;
|
||||||
boredState.update((n) => ({ ...n, loading: false }));
|
// boredState.update((n) => ({ ...n, loading: false }));
|
||||||
gameStore.removeAll();
|
// gameStore.removeAll();
|
||||||
gameStore.addAll(responseData?.games);
|
// gameStore.addAll(responseData?.games);
|
||||||
// games = responseData?.games;
|
// // games = responseData?.games;
|
||||||
}
|
// }
|
||||||
|
|
||||||
let submitting = $boredState?.loading;
|
let submitting = $boredState?.loading;
|
||||||
|
let checked = true;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form on:submit|preventDefault={handleSubmit} method="post">
|
<form
|
||||||
<fieldset aria-busy={submitting} disabled={submitting}>
|
action="/search"
|
||||||
<input type="checkbox" id="random" name="random" hidden checked />
|
method="POST"
|
||||||
<button class="btn" type="submit" disabled={submitting}>Random Game 🎲</button>
|
use:enhance={() => {
|
||||||
</fieldset>
|
boredState.update((n) => ({ ...n, loading: true }));
|
||||||
|
return async ({ result }) => {
|
||||||
|
boredState.update((n) => ({ ...n, loading: false }));
|
||||||
|
console.log('result main page search', result);
|
||||||
|
// `result` is an `ActionResult` object
|
||||||
|
if (result.type === 'success') {
|
||||||
|
console.log('In success');
|
||||||
|
gameStore.removeAll();
|
||||||
|
const resultGames = result?.data?.games;
|
||||||
|
if (resultGames?.length <= 0) {
|
||||||
|
toast.send('No results!', { duration: 3000, type: ToastType.INFO, dismissible: true });
|
||||||
|
}
|
||||||
|
gameStore.addAll(resultGames);
|
||||||
|
console.log(`Frontend result: ${JSON.stringify(result)}`);
|
||||||
|
await applyAction(result);
|
||||||
|
} else {
|
||||||
|
console.log('Invalid');
|
||||||
|
await applyAction(result);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<fieldset aria-busy={submitting} disabled={submitting}>
|
||||||
|
<input type="checkbox" id="random" name="random" hidden {checked} />
|
||||||
|
<button class="btn" type="submit" disabled={submitting}>Random Game 🎲</button>
|
||||||
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
fieldset {
|
fieldset {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
max-width: 450px;
|
max-width: 450px;
|
||||||
padding: var(--spacing-8) var(--spacing-16);
|
padding: var(--spacing-8) var(--spacing-16);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,88 +1,88 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import { Disclosure, DisclosureButton, DisclosurePanel } from '@rgossiaux/svelte-headlessui';
|
import { Disclosure, DisclosureButton, DisclosurePanel } from '@rgossiaux/svelte-headlessui';
|
||||||
import { ChevronRightIcon } from '@rgossiaux/svelte-heroicons/solid';
|
import { ChevronRightIcon } from '@rgossiaux/svelte-heroicons/solid';
|
||||||
import { boredState } from '$lib/stores/boredState';
|
import { boredState } from '$lib/stores/boredState';
|
||||||
import AdvancedSearch from '$lib/components/search/advancedSearch/index.svelte';
|
import AdvancedSearch from '$lib/components/search/advancedSearch/index.svelte';
|
||||||
|
|
||||||
export let showButton: boolean = false;
|
export let showButton: boolean = false;
|
||||||
export let advancedSearch: boolean = false;
|
export let advancedSearch: boolean = false;
|
||||||
|
export let form: ActionData;
|
||||||
|
|
||||||
let submitting = $boredState?.loading;
|
let submitting = $boredState?.loading;
|
||||||
let name = '';
|
let name = '';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- <form on:submit|preventDefault={handleSearch} method="post"> -->
|
|
||||||
<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="name">
|
||||||
Search
|
Search
|
||||||
<input
|
<input
|
||||||
id="name"
|
id="name"
|
||||||
name="name"
|
name="name"
|
||||||
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>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{#if advancedSearch}
|
{#if advancedSearch}
|
||||||
<Disclosure let:open>
|
<Disclosure let:open>
|
||||||
<DisclosureButton class="disclosure-button">
|
<DisclosureButton class="disclosure-button">
|
||||||
<span>Advanced Search?</span>
|
<span>Advanced Search?</span>
|
||||||
<ChevronRightIcon
|
<ChevronRightIcon
|
||||||
class="icon disclosure-icon"
|
class="icon disclosure-icon"
|
||||||
style={open
|
style={open
|
||||||
? 'transform: rotate(90deg); transition: transform 0.5s ease;'
|
? 'transform: rotate(90deg); transition: transform 0.5s ease;'
|
||||||
: 'transform: rotate(0deg); transition: transform 0.5s ease;'}
|
: 'transform: rotate(0deg); transition: transform 0.5s ease;'}
|
||||||
/>
|
/>
|
||||||
</DisclosureButton>
|
</DisclosureButton>
|
||||||
|
|
||||||
{#if open}
|
{#if open}
|
||||||
<div transition:fade>
|
<div transition:fade>
|
||||||
<!-- 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 />
|
<AdvancedSearch form />
|
||||||
</DisclosurePanel>
|
</DisclosurePanel>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</Disclosure>
|
</Disclosure>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if showButton}
|
{#if showButton}
|
||||||
<button class="btn" type="submit" disabled={submitting}>Submit</button>
|
<button class="btn" type="submit" disabled={submitting}>Submit</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- </form> -->
|
<!-- </form> -->
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.search {
|
.search {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.disclosure-button) {
|
:global(.disclosure-button) {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
margin: 1.5rem 0;
|
margin: 1.5rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto auto;
|
grid-template-columns: auto auto;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
place-content: start;
|
place-content: start;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
|
|
||||||
@media (max-width: 850px) {
|
@media (max-width: 850px) {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ const state = () => {
|
||||||
const initial: BoredStore = {
|
const initial: BoredStore = {
|
||||||
loading: false, dialog: initialDialog, search: {
|
loading: false, dialog: initialDialog, search: {
|
||||||
totalCount: 1,
|
totalCount: 1,
|
||||||
pageSize: 25,
|
pageSize: 10,
|
||||||
skip: 0,
|
skip: 0,
|
||||||
currentPage: 1
|
currentPage: 1
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import zodToJsonSchema from 'zod-to-json-schema';
|
||||||
|
|
||||||
export const BoardGameSearch = z.object({
|
export const BoardGameSearch = z.object({
|
||||||
minAge: z.number(),
|
minAge: z.number(),
|
||||||
|
|
@ -7,7 +8,9 @@ export const BoardGameSearch = z.object({
|
||||||
maxPlayers: z.number()
|
maxPlayers: z.number()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Game = z.object({
|
export const Board
|
||||||
|
|
||||||
|
export const game_schema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
handle: z.string(),
|
handle: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
|
|
@ -28,3 +31,9 @@ export const Game = z.object({
|
||||||
players: z.string(),
|
players: z.string(),
|
||||||
playtime: z.string()
|
playtime: z.string()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const game_raw_schema_json = zodToJsonSchema(game_schema, {
|
||||||
|
$refStrategy: 'none',
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Game = z.infer<typeof game_schema>;
|
||||||
|
|
|
||||||
|
|
@ -1,159 +1,159 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import { navigating } from '$app/stores';
|
import { navigating } from '$app/stores';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import debounce from 'just-debounce-it';
|
import debounce from 'just-debounce-it';
|
||||||
import { Toy } from '@leveluptuts/svelte-toy';
|
import { Toy } from '@leveluptuts/svelte-toy';
|
||||||
import Header from '$lib/components/header/Header.svelte';
|
import Header from '$lib/components/header/Header.svelte';
|
||||||
import Loading from '$lib/components/loading.svelte';
|
import Loading from '$lib/components/loading.svelte';
|
||||||
import Transition from '$lib/components/transition/index.svelte';
|
import Transition from '$lib/components/transition/index.svelte';
|
||||||
import Portal from '$lib/Portal.svelte';
|
import Portal from '$lib/Portal.svelte';
|
||||||
import { boredState } from '$lib/stores/boredState';
|
import { boredState } from '$lib/stores/boredState';
|
||||||
import { collectionStore } from '$lib/stores/collectionStore';
|
import { collectionStore } from '$lib/stores/collectionStore';
|
||||||
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.scss';
|
import '$root/styles/styles.scss';
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if ($navigating) {
|
if ($navigating) {
|
||||||
debounce(() => {
|
debounce(() => {
|
||||||
boredState.update((n) => ({ ...n, loading: true }));
|
boredState.update((n) => ({ ...n, loading: true }));
|
||||||
}, 250);
|
}, 250);
|
||||||
}
|
}
|
||||||
if (!$navigating) {
|
if (!$navigating) {
|
||||||
boredState.update((n) => ({ ...n, loading: false }));
|
boredState.update((n) => ({ ...n, loading: false }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: isOpen = $boredState?.dialog?.isOpen;
|
$: isOpen = $boredState?.dialog?.isOpen;
|
||||||
|
|
||||||
if (browser) {
|
if (browser) {
|
||||||
let collectionEmpty = $collectionStore.length === 0 || false;
|
let collectionEmpty = $collectionStore.length === 0 || false;
|
||||||
console.log('collectionEmpty', collectionEmpty);
|
console.log('collectionEmpty', collectionEmpty);
|
||||||
console.log('localStorage.collection', localStorage.collection);
|
console.log('localStorage.collection', localStorage.collection);
|
||||||
if (collectionEmpty && localStorage?.collection && localStorage?.collection?.length !== 0) {
|
if (collectionEmpty && localStorage?.collection && localStorage?.collection?.length !== 0) {
|
||||||
const collection = JSON.parse(localStorage.collection);
|
const collection = JSON.parse(localStorage.collection);
|
||||||
console.log('collection', collection);
|
console.log('collection', collection);
|
||||||
if (collection?.length !== 0) {
|
if (collection?.length !== 0) {
|
||||||
collectionStore.addAll(collection);
|
collectionStore.addAll(collection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const dev = process.env.NODE_ENV !== 'production';
|
const dev = process.env.NODE_ENV !== 'production';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if dev}
|
{#if dev}
|
||||||
<Toy register={{ boredState, collectionStore, gameStore, toast }} />
|
<Toy register={{ boredState, collectionStore, gameStore, toast }} />
|
||||||
{/if}
|
{/if}
|
||||||
<Transition transition={{ type: 'fade', duration: 250 }}>
|
<Transition transition={{ type: 'fade', duration: 250 }}>
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<Header />
|
<Header />
|
||||||
<Transition transition={{ type: 'page' }}>
|
<Transition transition={{ type: 'page' }}>
|
||||||
<main>
|
<main>
|
||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
</Transition>
|
</Transition>
|
||||||
<footer>
|
<footer>
|
||||||
<p>Built by <a target="__blank" href="https://bradleyshellnut.com">Bradley Shellnut</a></p>
|
<p>Built by <a target="__blank" href="https://bradleyshellnut.com">Bradley Shellnut</a></p>
|
||||||
<p>
|
<p>
|
||||||
<a
|
<a
|
||||||
target="__blank"
|
target="__blank"
|
||||||
href="https://www.flaticon.com/free-icons/board-game"
|
href="https://www.flaticon.com/free-icons/board-game"
|
||||||
title="board game icons">Board game icons created by Freepik - Flaticon</a
|
title="board game icons">Board game icons created by Freepik - Flaticon</a
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
{#if $boredState?.loading}
|
{#if $boredState?.loading}
|
||||||
<Portal>
|
<Portal>
|
||||||
<Transition transition={{ type: 'fade', duration: 0 }}>
|
<Transition transition={{ type: 'fade', duration: 0 }}>
|
||||||
<div class="loading">
|
<div class="loading">
|
||||||
<Loading />
|
<Loading />
|
||||||
<h3>Loading...</h3>
|
<h3>Loading...</h3>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
<div class="background" />
|
<div class="background" />
|
||||||
</Portal>
|
</Portal>
|
||||||
{/if}
|
{/if}
|
||||||
{#if isOpen}
|
{#if isOpen}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<svelte:component this={$boredState?.dialog?.content} />
|
<svelte:component this={$boredState?.dialog?.content} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<Toast />
|
<Toast />
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.loading {
|
.loading {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
z-index: 101;
|
z-index: 101;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.background {
|
.background {
|
||||||
background: black;
|
background: black;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
cursor: none;
|
cursor: none;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wrapper {
|
.wrapper {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto 1fr auto;
|
grid-template-rows: auto 1fr auto;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
max-width: 850px;
|
max-width: 850px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 2rem 0rem;
|
padding: 2rem 0rem;
|
||||||
max-width: 80%;
|
max-width: 80%;
|
||||||
|
|
||||||
@media (min-width: 1500px) {
|
@media (min-width: 1500px) {
|
||||||
max-width: 60%;
|
max-width: 60%;
|
||||||
}
|
}
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer a {
|
footer a {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 480px) {
|
@media (min-width: 480px) {
|
||||||
footer {
|
footer {
|
||||||
padding: 40px 0;
|
padding: 40px 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.dialog-overlay) {
|
:global(.dialog-overlay) {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
background-color: rgb(0 0 0);
|
background-color: rgb(0 0 0);
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
8
src/routes/+page.server.ts
Normal file
8
src/routes/+page.server.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import type { PageServerLoad, Actions } from './$types';
|
||||||
|
|
||||||
|
export const actions: Actions = {
|
||||||
|
default: async ({ request, locals }): Promise<any> => {
|
||||||
|
// Do things in here
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,121 +1,182 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { GameType, SavedGameType } from '$root/lib/types';
|
import { enhance, applyAction } from '$app/forms';
|
||||||
import { gameStore } from '$lib/stores/gameSearchStore';
|
import type { ActionData, PageData } from './$types';
|
||||||
import { boredState } from '$root/lib/stores/boredState';
|
import { ToastType, type GameType, type SavedGameType } from '$root/lib/types';
|
||||||
import RemoveCollectionDialog from '$root/lib/components/dialog/RemoveCollectionDialog.svelte';
|
import { toast } from '$root/lib/components/toast/toast';
|
||||||
import Game from '$lib/components/game/index.svelte';
|
import { gameStore } from '$lib/stores/gameSearchStore';
|
||||||
import TextSearch from '$lib/components/search/textSearch/index.svelte';
|
import { boredState } from '$root/lib/stores/boredState';
|
||||||
import RandomSearch from '$lib/components/search/random/index.svelte';
|
import RemoveCollectionDialog from '$root/lib/components/dialog/RemoveCollectionDialog.svelte';
|
||||||
import Random from '$lib/components/random/index.svelte';
|
import Game from '$lib/components/game/index.svelte';
|
||||||
import Pagination from '$lib/components/pagination/index.svelte';
|
import TextSearch from '$lib/components/search/textSearch/index.svelte';
|
||||||
|
import RandomSearch from '$lib/components/search/random/index.svelte';
|
||||||
|
import Random from '$lib/components/random/index.svelte';
|
||||||
|
import Pagination from '$lib/components/pagination/index.svelte';
|
||||||
|
|
||||||
async function handleSearch(event: SubmitEvent) {
|
export let data: PageData;
|
||||||
boredState.update((n) => ({ ...n, loading: true }));
|
export let form: ActionData;
|
||||||
const form = event.target as HTMLFormElement;
|
console.log('form routesss', form);
|
||||||
console.log('form', form);
|
console.log('Formed data:', JSON.stringify(data));
|
||||||
const response = await fetch('/api/game', {
|
let pageSize: number;
|
||||||
method: 'POST',
|
let currentPage: number;
|
||||||
headers: { accept: 'application/json' },
|
$: totalItems = 0;
|
||||||
body: new FormData(form)
|
console.log('totalItems', totalItems);
|
||||||
});
|
|
||||||
const responseData = await response.json();
|
|
||||||
boredState.update((n) => ({ ...n, loading: false }));
|
|
||||||
gameStore.removeAll();
|
|
||||||
gameStore.addAll(responseData?.games);
|
|
||||||
}
|
|
||||||
|
|
||||||
let isOpen: boolean = false;
|
// async function handleItemsPerPageChange(event) {
|
||||||
let gameToRemove: GameType | SavedGameType;
|
// const perPage = event?.detail;
|
||||||
console.log('isOpen', isOpen);
|
// if ($gameStore.length )
|
||||||
|
// }
|
||||||
|
async function handleNextPageEvent(event: CustomEvent) {
|
||||||
|
console.log('Next page called', event);
|
||||||
|
boredState.update((n) => ({ ...n, loading: true }));
|
||||||
|
const form = event.target as HTMLFormElement;
|
||||||
|
console.log('form', form);
|
||||||
|
const response = await fetch('/api/game', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { accept: 'application/json' },
|
||||||
|
body: new FormData(form)
|
||||||
|
});
|
||||||
|
const responseData = await response.json();
|
||||||
|
boredState.update((n) => ({ ...n, loading: false }));
|
||||||
|
gameStore.removeAll();
|
||||||
|
gameStore.addAll(responseData?.games);
|
||||||
|
const skip = $boredState?.search?.skip;
|
||||||
|
const pageSize = $boredState?.search?.pageSize;
|
||||||
|
const currentPage = $boredState?.search?.currentPage;
|
||||||
|
const totalCount = responseData?.totalCount;
|
||||||
|
boredState.update((n) => ({
|
||||||
|
...n,
|
||||||
|
search: { totalCount, skip, pageSize, currentPage }
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
interface RemoveGameEvent extends Event {
|
let isOpen: boolean = false;
|
||||||
detail: GameType | SavedGameType;
|
let gameToRemove: GameType | SavedGameType;
|
||||||
}
|
console.log('isOpen', isOpen);
|
||||||
|
|
||||||
function handleRemoveGame(event: RemoveGameEvent) {
|
interface RemoveGameEvent extends Event {
|
||||||
console.log('event', event);
|
detail: GameType | SavedGameType;
|
||||||
gameToRemove = event?.detail;
|
}
|
||||||
boredState.update((n) => ({
|
|
||||||
...n,
|
function handleRemoveGame(event: RemoveGameEvent) {
|
||||||
dialog: { isOpen: true, content: RemoveCollectionDialog, additionalData: gameToRemove }
|
console.log('event', event);
|
||||||
}));
|
gameToRemove = event?.detail;
|
||||||
}
|
boredState.update((n) => ({
|
||||||
|
...n,
|
||||||
|
dialog: { isOpen: true, content: RemoveCollectionDialog, additionalData: gameToRemove }
|
||||||
|
}));
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Bored Game | Home</title>
|
<title>Bored Game | Home</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<h1>Search Boardgames!</h1>
|
<h1>Search Boardgames!</h1>
|
||||||
<p style="margin: 1rem 0;">
|
<p style="margin: 1rem 0;">
|
||||||
Input your requirements to search for board game that match your criteria.
|
Input your requirements to search for board games that match your criteria.
|
||||||
</p>
|
</p>
|
||||||
<div class="game-search">
|
<div class="game-search">
|
||||||
<form on:submit|preventDefault={handleSearch} method="post">
|
<form
|
||||||
<TextSearch showButton advancedSearch />
|
action="/search"
|
||||||
</form>
|
method="post"
|
||||||
<section>
|
use:enhance={() => {
|
||||||
<p>Or pick a random game!</p>
|
boredState.update((n) => ({ ...n, loading: true }));
|
||||||
<div class="random-buttons">
|
return async ({ result }) => {
|
||||||
<RandomSearch />
|
boredState.update((n) => ({ ...n, loading: false }));
|
||||||
<Random />
|
console.log('result main page search', result);
|
||||||
</div>
|
// `result` is an `ActionResult` object
|
||||||
</section>
|
if (result.type === 'success') {
|
||||||
|
console.log('In success');
|
||||||
|
gameStore.removeAll();
|
||||||
|
const resultGames = result?.data?.games;
|
||||||
|
if (resultGames?.length <= 0) {
|
||||||
|
toast.send('No results!', { duration: 3000, type: ToastType.INFO, dismissible: true });
|
||||||
|
}
|
||||||
|
gameStore.addAll(resultGames);
|
||||||
|
totalItems = result?.data?.totalCount;
|
||||||
|
console.log(`Frontend result: ${JSON.stringify(result)}`);
|
||||||
|
await applyAction(result);
|
||||||
|
} else {
|
||||||
|
console.log('Invalid');
|
||||||
|
await applyAction(result);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextSearch showButton advancedSearch {form} />
|
||||||
|
</form>
|
||||||
|
<section>
|
||||||
|
<p>Or pick a random game!</p>
|
||||||
|
<div class="random-buttons">
|
||||||
|
<RandomSearch />
|
||||||
|
<Random />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if $gameStore?.length > 0}
|
{#if $gameStore?.length > 0}
|
||||||
<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 $gameStore as game (game.id)}
|
||||||
<Game on:removeGameEvent={handleRemoveGame} {game} />
|
<Game on:removeGameEvent={handleRemoveGame} {game} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<Pagination />
|
<!-- <Pagination
|
||||||
</div>
|
{pageSize}
|
||||||
|
{currentPage}
|
||||||
|
{totalItems}
|
||||||
|
forwardText="Next"
|
||||||
|
backwardText="Prev"
|
||||||
|
pageSizes={[10, 25, 50, 100]}
|
||||||
|
on:nextPageEvent={handleNextPageEvent}
|
||||||
|
on:previousPageEvent={(event) => console.log('Prev page called', event)}
|
||||||
|
on:perPageEvent={(event) => console.log('Per page called', event)}
|
||||||
|
/> -->
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.game-search {
|
.game-search {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
|
|
||||||
section {
|
section {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.games {
|
.games {
|
||||||
margin: 2rem 0rem;
|
margin: 2rem 0rem;
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.games-list {
|
.games-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, minmax(200px, 1fr));
|
grid-template-columns: repeat(3, minmax(200px, 1fr));
|
||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
|
|
||||||
@media (max-width: 800px) {
|
@media (max-width: 800px) {
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 650px) {
|
@media (max-width: 650px) {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.random-buttons {
|
.random-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
place-content: space-between;
|
place-content: space-between;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
|
|
||||||
@media (max-width: 650px) {
|
@media (max-width: 650px) {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
13
src/routes/about/+page.svelte
Normal file
13
src/routes/about/+page.svelte
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<svelte:head>
|
||||||
|
<title>Bored Game | About</title>
|
||||||
|
<meta name="description" content="About Bored Game" />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<h1>About Bored Game</h1>
|
||||||
|
<p>
|
||||||
|
One day we were bored and wanted to play one of our board games. Our problem was that we didn't
|
||||||
|
know which one to play.
|
||||||
|
</p>
|
||||||
|
<p>Rather than just pick one I decided to make this overcomplicated version of choice.</p>
|
||||||
|
</div>
|
||||||
9
src/routes/about/+page.ts
Normal file
9
src/routes/about/+page.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { dev } from '$app/environment';
|
||||||
|
|
||||||
|
// we don't need any JS on this page, though we'll load
|
||||||
|
// it in dev so that we get hot module replacement...
|
||||||
|
export const csr = dev;
|
||||||
|
|
||||||
|
// since there's no dynamic data here, we can prerender
|
||||||
|
// it so that it gets served as a static asset in prod
|
||||||
|
export const prerender = true;
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
(The data on the todo app will expire periodically; no
|
(The data on the todo app will expire periodically; no
|
||||||
guarantees are made. Don't use it to organize your life.)
|
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';
|
import { URLSearchParams } from 'url';
|
||||||
|
|
||||||
const base = 'https://api.boardgameatlas.com/api';
|
const base = 'https://api.boardgameatlas.com/api';
|
||||||
|
|
@ -20,7 +20,7 @@ export function boardGameApi(
|
||||||
data?: Record<string, unknown>
|
data?: Record<string, unknown>
|
||||||
) {
|
) {
|
||||||
// console.log('queryParams', queryParams);
|
// console.log('queryParams', queryParams);
|
||||||
queryParams.client_id = import.meta.env.VITE_PUBLIC_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, {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,8 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||||
const queryParams: SearchQuery = {
|
const queryParams: SearchQuery = {
|
||||||
order_by: 'rank',
|
order_by: 'rank',
|
||||||
ascending: false,
|
ascending: false,
|
||||||
limit: 25,
|
limit: 10,
|
||||||
|
skip: 0,
|
||||||
client_id: import.meta.env.VITE_PUBLIC_CLIENT_ID,
|
client_id: import.meta.env.VITE_PUBLIC_CLIENT_ID,
|
||||||
fuzzy_match: true,
|
fuzzy_match: true,
|
||||||
name: ''
|
name: ''
|
||||||
|
|
@ -54,6 +55,7 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
return json$1({
|
return json$1({
|
||||||
|
totalCount,
|
||||||
games
|
games
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -91,9 +91,9 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||||
games.push(mapAPIGameToBoredGame(game));
|
games.push(mapAPIGameToBoredGame(game));
|
||||||
});
|
});
|
||||||
console.log('games', games);
|
console.log('games', games);
|
||||||
return json$1({
|
return {
|
||||||
games
|
games
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Response(undefined, { status: response.status });
|
return new Response(undefined, { status: response.status });
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
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';
|
||||||
|
// import { Games } from '$lib/db/actions';
|
||||||
|
|
||||||
type GamePageParams = {
|
type GamePageParams = {
|
||||||
params: {
|
params: {
|
||||||
|
|
@ -8,8 +9,12 @@ type GamePageParams = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// export const actions = {
|
||||||
|
// default Games.create,
|
||||||
|
// }
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ params }: GamePageParams) => {
|
export const load: PageServerLoad = async ({ params }: GamePageParams) => {
|
||||||
console.log('params', params);
|
console.log('params', params);
|
||||||
const queryParams = {
|
const queryParams = {
|
||||||
ids: `${params?.id}`
|
ids: `${params?.id}`
|
||||||
};
|
};
|
||||||
|
|
|
||||||
224
src/routes/search/+page.server.ts
Normal file
224
src/routes/search/+page.server.ts
Normal file
|
|
@ -0,0 +1,224 @@
|
||||||
|
import type { Actions, PageServerLoad, RequestEvent } from '../$types';
|
||||||
|
import { BOARD_GAME_ATLAS_CLIENT_ID } from '$env/static/private';
|
||||||
|
import { invalid } from '@sveltejs/kit';
|
||||||
|
import type { GameType, SearchQuery } from '$root/lib/types';
|
||||||
|
import { mapAPIGameToBoredGame } from '$root/lib/util/gameMapper';
|
||||||
|
|
||||||
|
export const load: PageServerLoad = (v) => {
|
||||||
|
console.log('page server load request', v)
|
||||||
|
|
||||||
|
return {
|
||||||
|
games: [],
|
||||||
|
totalCount: 0
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const actions: Actions = {
|
||||||
|
default: async ({ request, locals }: RequestEvent): Promise<any> => {
|
||||||
|
console.log("In search action specific")
|
||||||
|
// Do things in here
|
||||||
|
const form = await request.formData();
|
||||||
|
console.log('action form', form);
|
||||||
|
const queryParams: SearchQuery = {
|
||||||
|
order_by: 'rank',
|
||||||
|
ascending: false,
|
||||||
|
limit: 10,
|
||||||
|
skip: 0,
|
||||||
|
client_id: BOARD_GAME_ATLAS_CLIENT_ID,
|
||||||
|
fuzzy_match: true,
|
||||||
|
name: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
const random = form.get('random') && form.get('random') === 'on';
|
||||||
|
|
||||||
|
if (random) {
|
||||||
|
queryParams.random = random;
|
||||||
|
} else {
|
||||||
|
const minAge = form.get('minAge');
|
||||||
|
const minPlayers = form.get('minPlayers');
|
||||||
|
const maxPlayers = form.get('maxPlayers');
|
||||||
|
const exactMinAge = form.get('exactMinAge') || false;
|
||||||
|
const exactMinPlayers = form.get('exactMinPlayers') || false;
|
||||||
|
const exactMaxPlayers = form.get('exactMaxPlayers') || false;
|
||||||
|
|
||||||
|
if (minAge) {
|
||||||
|
if (exactMinAge) {
|
||||||
|
queryParams.min_age = +minAge;
|
||||||
|
} else {
|
||||||
|
queryParams.gt_min_age = +minAge === 1 ? 0 : +minAge - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minPlayers && maxPlayers) {
|
||||||
|
if (minPlayers > maxPlayers) {
|
||||||
|
return invalid(400, { minPlayers, error: { id: 'minPlayers', message: 'Min must be less than max' } });
|
||||||
|
} else if (maxPlayers < minPlayers) {
|
||||||
|
return invalid(400, { maxPlayers, error: { id: 'maxPlayers', message: 'Max must be greater than min' } });
|
||||||
|
}
|
||||||
|
if (exactMinPlayers) {
|
||||||
|
queryParams.min_players = +minPlayers;
|
||||||
|
} else {
|
||||||
|
queryParams.gt_min_players = +minPlayers === 1 ? 0 : +minPlayers - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exactMaxPlayers) {
|
||||||
|
queryParams.max_players = +maxPlayers;
|
||||||
|
} else {
|
||||||
|
queryParams.lt_max_players = +maxPlayers + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = form.has('name') ? form.get('name') : await request?.text();
|
||||||
|
console.log('name', name);
|
||||||
|
if (name) {
|
||||||
|
queryParams.name = `${name}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.status !== 200) {
|
||||||
|
console.log('Status not 200', response.status)
|
||||||
|
invalid(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: games.length
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`Error searching board games ${e}`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
games: [],
|
||||||
|
totalCount: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// const id = form.get('id');
|
||||||
|
// const ids = form.get('ids');
|
||||||
|
// const minAge = form.get('minAge');
|
||||||
|
// const minPlayers = form.get('minPlayers');
|
||||||
|
// const maxPlayers = form.get('maxPlayers');
|
||||||
|
// const exactMinAge = form.get('exactMinAge') || false;
|
||||||
|
// const exactMinPlayers = form.get('exactMinPlayers') || false;
|
||||||
|
// const exactMaxPlayers = form.get('exactMaxPlayers') || false;
|
||||||
|
// const random = form.get('random') === 'on' || false;
|
||||||
|
|
||||||
|
// 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 (id) {
|
||||||
|
// queryParams.ids = new Array(`${id}`);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (ids) {
|
||||||
|
// // TODO: Pass in ids array from localstorage / game store
|
||||||
|
// queryParams.ids = new Array(ids);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// queryParams.random = random;
|
||||||
|
// console.log('queryParams', queryParams);
|
||||||
|
|
||||||
|
// const newQueryParams: Record<string, string> = {};
|
||||||
|
// for (const key in queryParams) {
|
||||||
|
// newQueryParams[key] = `${queryParams[key as keyof typeof queryParams]}`;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const urlQueryParams = new URLSearchParams(newQueryParams);
|
||||||
|
|
||||||
|
// 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 status', response.status);
|
||||||
|
// console.log('board game response action', response);
|
||||||
|
// if (response.status === 404) {
|
||||||
|
// // user hasn't created a todo list.
|
||||||
|
// // start with an empty array
|
||||||
|
// return {
|
||||||
|
// success: true,
|
||||||
|
// games: [],
|
||||||
|
// totalCount: 0
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (response.status === 200) {
|
||||||
|
// const gameResponse = await response.json();
|
||||||
|
// console.log('gameResponse', gameResponse);
|
||||||
|
// const gameList = gameResponse?.games;
|
||||||
|
// const games: GameType[] = [];
|
||||||
|
// gameList.forEach((game: GameType) => {
|
||||||
|
// games.push(mapAPIGameToBoredGame(game));
|
||||||
|
// });
|
||||||
|
// console.log('action games', games);
|
||||||
|
// return {
|
||||||
|
// games,
|
||||||
|
// totalCount: games.length
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return { success: false };
|
||||||
|
// }
|
||||||
|
// create: async function create({ request, locals }): Promise<any> {
|
||||||
|
// const data = await getFormDataObject<any>(request);
|
||||||
|
// return data;
|
||||||
|
// }
|
||||||
|
}
|
||||||
103
src/routes/search/+page.svelte
Normal file
103
src/routes/search/+page.svelte
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { applyAction, enhance } from '$app/forms';
|
||||||
|
import type { ActionData, PageData } from './$types';
|
||||||
|
import Game from '$lib/components/game/index.svelte';
|
||||||
|
import { gameStore } from '$lib/stores/gameSearchStore';
|
||||||
|
import TextSearch from '$lib/components/search/textSearch/index.svelte';
|
||||||
|
import RemoveCollectionDialog from '$root/lib/components/dialog/RemoveCollectionDialog.svelte';
|
||||||
|
import { ToastType, type GameType, type SavedGameType } from '$root/lib/types';
|
||||||
|
import { boredState } from '$root/lib/stores/boredState';
|
||||||
|
import { toast } from '$root/lib/components/toast/toast';
|
||||||
|
|
||||||
|
export let data: PageData;
|
||||||
|
export let form: ActionData;
|
||||||
|
console.log('search page form', form);
|
||||||
|
console.log('search page data stuff', data);
|
||||||
|
let gameToRemove: GameType | SavedGameType;
|
||||||
|
|
||||||
|
$: if (data?.games) {
|
||||||
|
gameStore.removeAll();
|
||||||
|
gameStore.addAll(data?.games);
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (form?.games) {
|
||||||
|
gameStore.removeAll();
|
||||||
|
gameStore.addAll(form?.games);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RemoveGameEvent extends Event {
|
||||||
|
detail: GameType | SavedGameType;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRemoveGame(event: RemoveGameEvent) {
|
||||||
|
console.log('event', event);
|
||||||
|
gameToRemove = event?.detail;
|
||||||
|
boredState.update((n) => ({
|
||||||
|
...n,
|
||||||
|
dialog: { isOpen: true, content: RemoveCollectionDialog, additionalData: gameToRemove }
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="game-search">
|
||||||
|
<form
|
||||||
|
action="?/search"
|
||||||
|
method="post"
|
||||||
|
use:enhance={() => {
|
||||||
|
boredState.update((n) => ({ ...n, loading: true }));
|
||||||
|
return async ({ result }) => {
|
||||||
|
boredState.update((n) => ({ ...n, loading: false }));
|
||||||
|
console.log(result);
|
||||||
|
// `result` is an `ActionResult` object
|
||||||
|
if (result.type === 'error') {
|
||||||
|
toast.send('Error!', { duration: 3000, type: ToastType.ERROR, dismissible: true });
|
||||||
|
await applyAction(result);
|
||||||
|
} else {
|
||||||
|
gameStore.removeAll();
|
||||||
|
gameStore.addAll(result?.data?.games);
|
||||||
|
totalItems = result?.data?.totalCount;
|
||||||
|
console.log(`Frontend result: ${JSON.stringify(result)}`);
|
||||||
|
toast.send('Sucess!', { duration: 3000, type: ToastType.INFO, dismissible: true });
|
||||||
|
await applyAction(result);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextSearch showButton />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if $gameStore?.length > 0}
|
||||||
|
<div class="games">
|
||||||
|
<h1>Games Found:</h1>
|
||||||
|
<div class="games-list">
|
||||||
|
{#each $gameStore as game (game.id)}
|
||||||
|
<Game on:removeGameEvent={handleRemoveGame} {game} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.games {
|
||||||
|
margin: 2rem 0rem;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.games-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(200px, 1fr));
|
||||||
|
gap: 2rem;
|
||||||
|
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 650px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
181
src/search/actions.ts
Normal file
181
src/search/actions.ts
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
import { invalid, type RequestEvent } from '@sveltejs/kit';
|
||||||
|
import { BOARD_GAME_ATLAS_CLIENT_ID } from '$env/static/private';
|
||||||
|
import type { GameType, SearchQuery } from "$root/lib/types";
|
||||||
|
import { mapAPIGameToBoredGame } from "$root/lib/util/gameMapper";
|
||||||
|
|
||||||
|
interface Actions {
|
||||||
|
[key: string]: any // Action
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Games: Actions = {
|
||||||
|
search: async ({ request, locals }: RequestEvent): Promise<any> => {
|
||||||
|
console.log("In search action specific")
|
||||||
|
// Do things in here
|
||||||
|
const form = await request.formData();
|
||||||
|
console.log('action form', form);
|
||||||
|
const queryParams: SearchQuery = {
|
||||||
|
order_by: 'rank',
|
||||||
|
ascending: false,
|
||||||
|
limit: 10,
|
||||||
|
skip: 0,
|
||||||
|
client_id: BOARD_GAME_ATLAS_CLIENT_ID,
|
||||||
|
fuzzy_match: true,
|
||||||
|
name: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
const name = form.has('name') ? form.get('name') : await request?.text();
|
||||||
|
console.log('name', name);
|
||||||
|
if (name) {
|
||||||
|
queryParams.name = `${name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newQueryParams: Record<string, string> = {};
|
||||||
|
for (const key in queryParams) {
|
||||||
|
console.log('key', key);
|
||||||
|
console.log('queryParams[key]', queryParams[key]);
|
||||||
|
newQueryParams[key] = `${queryParams[key]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlQueryParams = new URLSearchParams(newQueryParams);
|
||||||
|
console.log('urlQueryParams', urlQueryParams);
|
||||||
|
|
||||||
|
try {
|
||||||
|
throw new Error("test error");
|
||||||
|
// 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) {
|
||||||
|
// console.log('Status not 200', response.status)
|
||||||
|
// invalid(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')
|
||||||
|
|
||||||
|
// return {
|
||||||
|
// games,
|
||||||
|
// totalCount: games.length
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return {
|
||||||
|
// games: [],
|
||||||
|
// totalCount: 0
|
||||||
|
// };
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`Error searching board games ${e}`);
|
||||||
|
invalid(400, { reason: 'Exception' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// const id = form.get('id');
|
||||||
|
// const ids = form.get('ids');
|
||||||
|
// const minAge = form.get('minAge');
|
||||||
|
// const minPlayers = form.get('minPlayers');
|
||||||
|
// const maxPlayers = form.get('maxPlayers');
|
||||||
|
// const exactMinAge = form.get('exactMinAge') || false;
|
||||||
|
// const exactMinPlayers = form.get('exactMinPlayers') || false;
|
||||||
|
// const exactMaxPlayers = form.get('exactMaxPlayers') || false;
|
||||||
|
// const random = form.get('random') === 'on' || false;
|
||||||
|
|
||||||
|
// 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 (id) {
|
||||||
|
// queryParams.ids = new Array(`${id}`);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (ids) {
|
||||||
|
// // TODO: Pass in ids array from localstorage / game store
|
||||||
|
// queryParams.ids = new Array(ids);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// queryParams.random = random;
|
||||||
|
// console.log('queryParams', queryParams);
|
||||||
|
|
||||||
|
// const newQueryParams: Record<string, string> = {};
|
||||||
|
// for (const key in queryParams) {
|
||||||
|
// newQueryParams[key] = `${queryParams[key as keyof typeof queryParams]}`;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const urlQueryParams = new URLSearchParams(newQueryParams);
|
||||||
|
|
||||||
|
// 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 status', response.status);
|
||||||
|
// console.log('board game response action', response);
|
||||||
|
// if (response.status === 404) {
|
||||||
|
// // user hasn't created a todo list.
|
||||||
|
// // start with an empty array
|
||||||
|
// return {
|
||||||
|
// success: true,
|
||||||
|
// games: [],
|
||||||
|
// totalCount: 0
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (response.status === 200) {
|
||||||
|
// const gameResponse = await response.json();
|
||||||
|
// console.log('gameResponse', gameResponse);
|
||||||
|
// const gameList = gameResponse?.games;
|
||||||
|
// const games: GameType[] = [];
|
||||||
|
// gameList.forEach((game: GameType) => {
|
||||||
|
// games.push(mapAPIGameToBoredGame(game));
|
||||||
|
// });
|
||||||
|
// console.log('action games', games);
|
||||||
|
// return {
|
||||||
|
// games,
|
||||||
|
// totalCount: games.length
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return { success: false };
|
||||||
|
// }
|
||||||
|
// create: async function create({ request, locals }): Promise<any> {
|
||||||
|
// const data = await getFormDataObject<any>(request);
|
||||||
|
// return data;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
@ -9,14 +9,16 @@ const config = {
|
||||||
kit: {
|
kit: {
|
||||||
adapter: adapter(),
|
adapter: adapter(),
|
||||||
alias: {
|
alias: {
|
||||||
$components: 'src/components',
|
|
||||||
$root: './src'
|
$root: './src'
|
||||||
},
|
},
|
||||||
// Override http methods in the Todo forms
|
},
|
||||||
methodOverride: {
|
vitePlugin: {
|
||||||
allowed: ['PATCH', 'DELETE']
|
experimental: {
|
||||||
}
|
inspector: {
|
||||||
}
|
toggleKeyCombo: 'control-alt-shift',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,17 @@
|
||||||
{
|
{
|
||||||
"extends": "./.svelte-kit/tsconfig.json",
|
"extends": "./.svelte-kit/tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"checkJs": true,
|
"checkJs": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"strict": true
|
"strict": true
|
||||||
}
|
}
|
||||||
|
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
||||||
|
//
|
||||||
|
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||||
|
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
import { sveltekit } from '@sveltejs/kit/vite';
|
|
||||||
|
|
||||||
/** @type {import('vite').UserConfig} */
|
|
||||||
const config = {
|
|
||||||
plugins: [sveltekit()]
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
8
vite.config.ts
Normal file
8
vite.config.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
|
import type { UserConfig } from 'vite';
|
||||||
|
|
||||||
|
const config: UserConfig = {
|
||||||
|
plugins: [sveltekit()]
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
Loading…
Reference in a new issue