Adding superforms, image lazy loader, and image icons.

This commit is contained in:
Bradley Shellnut 2023-05-14 21:08:30 -07:00
parent b09f71244f
commit 240bf4aa9e
12 changed files with 1096 additions and 984 deletions

View file

@ -13,55 +13,59 @@
"format": "prettier --write --plugin-search-dir=. ."
},
"devDependencies": {
"@iconify-icons/line-md": "^1.2.21",
"@iconify-icons/mdi": "^1.2.44",
"@playwright/test": "^1.31.2",
"@playwright/test": "^1.33.0",
"@rgossiaux/svelte-headlessui": "1.0.2",
"@rgossiaux/svelte-heroicons": "^0.1.2",
"@sveltejs/adapter-auto": "^1.0.3",
"@sveltejs/adapter-vercel": "^1.0.6",
"@sveltejs/kit": "^1.11.0",
"@sveltejs/kit": "^1.16.3",
"@types/cookie": "^0.5.1",
"@types/node": "^18.15.0",
"@typescript-eslint/eslint-plugin": "^5.54.1",
"@typescript-eslint/parser": "^5.54.1",
"@types/node": "^18.16.9",
"@typescript-eslint/eslint-plugin": "^5.59.5",
"@typescript-eslint/parser": "^5.59.5",
"autoprefixer": "^10.4.14",
"eslint": "^8.36.0",
"eslint-config-prettier": "^8.7.0",
"eslint-plugin-svelte3": "^4.0.0",
"eslint": "^8.40.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-svelte": "^2.28.0",
"just-clone": "^6.2.0",
"just-debounce-it": "^3.2.0",
"postcss": "^8.4.21",
"postcss": "^8.4.23",
"postcss-color-functional-notation": "^4.2.4",
"postcss-custom-media": "^9.1.2",
"postcss-custom-media": "^9.1.3",
"postcss-env-function": "^4.0.6",
"postcss-import": "^15.1.0",
"postcss-load-config": "^4.0.1",
"postcss-media-minmax": "^5.0.0",
"postcss-nested": "^6.0.1",
"prettier": "^2.8.4",
"prettier-plugin-svelte": "^2.9.0",
"sass": "^1.59.2",
"svelte": "^3.56.0",
"prettier": "^2.8.8",
"prettier-plugin-svelte": "^2.10.0",
"sass": "^1.62.1",
"svelte": "^3.59.1",
"svelte-check": "^2.10.3",
"svelte-preprocess": "^4.10.7",
"sveltekit-superforms": "^0.8.6",
"tslib": "^2.5.0",
"typescript": "^4.9.5",
"vite": "^4.1.4",
"vitest": "^0.25.3"
"vite": "^4.3.5",
"vitest": "^0.25.3",
"zod": "^3.21.4"
},
"type": "module",
"dependencies": {
"@fontsource/fira-mono": "^4.5.10",
"@iconify-icons/line-md": "^1.2.22",
"@iconify-icons/mdi": "^1.2.45",
"@leveluptuts/svelte-side-menu": "^1.0.5",
"@leveluptuts/svelte-toy": "^2.0.3",
"@lukeed/uuid": "^2.0.0",
"@lukeed/uuid": "^2.0.1",
"@types/feather-icons": "^4.29.1",
"cookie": "^0.5.0",
"feather-icons": "^4.29.0",
"iconify-icon": "^1.0.7",
"loader": "^2.1.1",
"open-props": "^1.5.8",
"svelte-lazy": "^1.2.1",
"svelte-lazy-loader": "^1.0.0",
"zod": "^3.21.4",
"zod-to-json-schema": "^3.20.4"
"zod-to-json-schema": "^3.21.1"
}
}

File diff suppressed because it is too large Load diff

View file

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

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { tick } from 'svelte';
import { applyAction, enhance, type SubmitFunction } from '$app/forms';
import type { ActionData, PageData } from './$types';
import SuperDebug from 'sveltekit-superforms/client/SuperDebug.svelte';
import { fade } from 'svelte/transition';
import { Disclosure, DisclosureButton, DisclosurePanel } from '@rgossiaux/svelte-headlessui';
import { ChevronRightIcon } from '@rgossiaux/svelte-heroicons/solid';
@ -21,11 +21,9 @@
detail: GameType | SavedGameType;
}
export let data: PageData;
// console.log('search page data', data);
export let form: ActionData;
// console.log('search page form', form);
const errors = data?.errors;
export let form;
export let errors;
export let constraints;
export let showButton: boolean = false;
export let advancedSearch: boolean = false;
@ -33,13 +31,13 @@
let gameToRemove: GameType | SavedGameType;
let numberOfGameSkeleton = 1;
let submitButton: HTMLElement;
let pageSize = +data?.limit || 10;
let totalItems = +data?.totalCount || 0;
let offset = +data?.skip || 0;
let pageSize = +form?.limit || 10;
let totalItems = +form?.searchData?.totalCount || 0;
let offset = +form?.skip || 0;
let page = Math.floor(offset / pageSize) + 1 || 1;
let submitting = $boredState?.loading;
let name = data?.name || '';
let disclosureOpen = errors || false;
let name = form?.name || '';
let disclosureOpen = $errors.length > 0 || false;
$: skip = (page - 1) * pageSize;
$: showPagination = $gameStore?.length > 1;
@ -121,8 +119,8 @@
await applyAction(result);
} else if (result.type === 'success') {
gameStore.removeAll();
gameStore.addAll(result?.data?.games);
totalItems = result?.data?.totalCount;
gameStore.addAll(result?.data?.searchData?.games);
totalItems = result?.data?.searchData?.totalCount;
// toast.send('Success!', { duration: 3000, type: ToastType.INFO, dismissible: true });
await applyAction(result);
} else {
@ -131,28 +129,36 @@
};
};
const dev = process.env.NODE_ENV !== 'production';
// TODO: Keep all Pagination Values on back and forth browser
// TODO: Add cache for certain number of pages so back and forth doesn't request data again
</script>
<form id="search-form" action="/search" method="get" on:submit={() => {
{#if dev}
<SuperDebug data={$form} />
{/if}
<form id="search-form" action="/search" method="GET" on:submit={() => {
skip = 0;
}}>
<div class="search">
<fieldset class="text-search" aria-busy={submitting} disabled={submitting}>
<label for="q">
Search
<input
id="q"
name="q"
bind:value={name}
type="text"
aria-label="Search boardgame"
placeholder="Search boardgame"
/>
</label>
<input id="skip" type="hidden" name="skip" bind:value={skip} />
<input id="limit" type="hidden" name="limit" bind:value={pageSize} />
<label for="q">Search</label>
<input
id="q"
name="q"
bind:value={$form.q}
data-invalid={$errors?.q}
{...$constraints.q}
type="text"
aria-label="Search board games"
placeholder="Search board games"
/>
{#if $errors?.q}<span class="invalid">{$errors?.q}</span>{/if}
<input id="skip" type="hidden" name="skip" bind:value={$form.skip} />
<input id="limit" type="hidden" name="limit" bind:value={$form.limit} />
</fieldset>
{#if advancedSearch}
<Disclosure>
@ -174,7 +180,9 @@
<!-- Using `static`, `DisclosurePanel` is always rendered,
and ignores the `open` state -->
<DisclosurePanel static>
<AdvancedSearch {data} />
{#if disclosureOpen}
<AdvancedSearch {form} {errors} {constraints} />
{/if}
</DisclosurePanel>
</div>
{/if}

5
src/lib/config.ts Normal file
View file

@ -0,0 +1,5 @@
import { dev } from '$app/environment';
export const title = 'Bored Game';
export const description = 'Bored? Find a game! Bored Game!';
export const url = dev ? 'http://localhost:5173' : 'https://boredgame.vercel.app';

View file

@ -31,13 +31,15 @@ function IntegerString<schema extends ZodNumber | ZodOptional<ZodNumber>>(schema
export const search_schema = z
.object({
name: z.string().trim().optional(),
q: z.string().trim().optional().default(''),
minAge: IntegerString(z.number().min(1).max(120).optional()),
minPlayers: IntegerString(z.number().min(1).max(50).optional()),
maxPlayers: IntegerString(z.number().min(1).max(50).optional()),
exactMinAge: z.preprocess((a) => Boolean(a), z.boolean().optional()),
exactMinPlayers: z.preprocess((a) => Boolean(a), z.boolean().optional()),
exactMaxPlayers: z.preprocess((a) => Boolean(a), z.boolean().optional())
exactMaxPlayers: z.preprocess((a) => Boolean(a), z.boolean().optional()),
limit: z.number().min(10).max(100).default(10),
skip: z.number().min(0).default(0)
})
.superRefine(
({ minPlayers, maxPlayers, minAge, exactMinAge, exactMinPlayers, exactMaxPlayers }, ctx) => {

View file

@ -1,8 +1,16 @@
import type { PageServerLoad, Actions } from './$types';
import { superValidate } from 'sveltekit-superforms/server';
import { search_schema } from '$lib/zodValidation';
export const actions: Actions = {
default: async ({ request, locals }): Promise<any> => {
// Do things in here
return {};
}
}
export const load = async ({ fetch, url }) => {
const formData = Object.fromEntries(url?.searchParams);
formData.name = formData?.q;
const form = await superValidate(formData, search_schema);
return { form };
};
export const actions = {
default: async ({ request, locals }): Promise<any> => {
// Do things in here
return {};
}
};

View file

@ -1,13 +1,11 @@
<script lang="ts">
import type { ActionData, PageData } from './$types';
import { superForm } from 'sveltekit-superforms/client';
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';
export let data: PageData;
// console.log('data', data);
export let form: ActionData;
// console.log('form', form);
export let data;
const { form, errors, constraints } = superForm(data?.form);
</script>
<svelte:head>
@ -26,7 +24,7 @@
<Random />
</div>
</section>
<TextSearch showButton advancedSearch {data} {form} />
<TextSearch showButton advancedSearch {form} {errors} {constraints} />
</div>
<style lang="scss">

View file

@ -1,8 +1,7 @@
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import { boardGameApi } from '../../api';
export const load: PageServerLoad = async ({ params, setHeaders }) => {
export const load = async ({ params, setHeaders }) => {
const queryParams = {
ids: `${params?.id}`,
fields:

View file

@ -1,93 +1,12 @@
import type { Actions, PageServerLoad, RequestEvent } from '../$types';
import type { Actions, RequestEvent } from '../$types';
import { BOARD_GAME_ATLAS_CLIENT_ID } from '$env/static/private';
import { error } from '@sveltejs/kit';
import type { GameType, SearchQuery } from '$root/lib/types';
import { mapAPIGameToBoredGame } from '$root/lib/util/gameMapper';
import { search_schema } from '$root/lib/zodValidation';
import { ZodError } from 'zod';
export const load: PageServerLoad = async ({ fetch, url }) => {
const formData = Object.fromEntries(url?.searchParams);
formData.name = formData?.q;
const limit = parseInt(formData?.limit) || 10;
const skip = parseInt(formData?.skip) || 0;
const queryParams: SearchQuery = {
order_by: 'rank',
ascending: false,
limit,
skip,
client_id: BOARD_GAME_ATLAS_CLIENT_ID,
fuzzy_match: true,
name: '',
fields:
'id,name,min_age,min_players,max_players,thumb_url,min_playtime,max_playtime,min_age,description'
};
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) {
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);
import { superValidate } from 'sveltekit-superforms/server';
import type { GameType, SearchQuery } from '$lib/types';
import { mapAPIGameToBoredGame } from '$lib/util/gameMapper';
import { search_schema } from '$lib/zodValidation';
async function searchForGames(urlQueryParams) {
try {
const url = `https://api.boardgameatlas.com/api/search${
urlQueryParams ? `?${urlQueryParams}` : ''
@ -98,20 +17,19 @@ export const load: PageServerLoad = async ({ fetch, url }) => {
'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 games: GameType[] = [];
let totalCount = 0;
if (response.ok) {
const gameResponse = await response.json();
// console.log('gameResponse', gameResponse);
const gameList = gameResponse?.games;
const totalCount = gameResponse?.count;
totalCount = gameResponse?.count;
console.log('totalCount', totalCount);
const games: GameType[] = [];
gameList.forEach((game) => {
if (game?.min_players && game?.max_players) {
game.players = `${game.min_players}-${game.max_players}`;
@ -119,30 +37,75 @@ export const load: PageServerLoad = async ({ fetch, url }) => {
}
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
};
}
return {
totalCount,
games
};
} catch (e) {
console.log(`Error searching board games ${e}`);
}
return {
games: [],
totalCount: 0,
limit,
skip
games: []
};
}
export const load = async ({ fetch, url }) => {
const defaults = {
limit: 10,
skip: 0
};
const searchParams = Object.fromEntries(url?.searchParams);
searchParams.limit = searchParams.limit || `${defaults.limit}`;
searchParams.skip = searchParams.skip || `${defaults.skip}`;
const form = await superValidate(searchParams, search_schema);
const queryParams: SearchQuery = {
order_by: 'rank',
ascending: false,
limit: form.data?.limit,
skip: form.data?.skip,
client_id: BOARD_GAME_ATLAS_CLIENT_ID,
fuzzy_match: true,
name: form.data?.q,
fields:
'id,name,min_age,min_players,max_players,thumb_url,min_playtime,max_playtime,min_age,description'
};
if (form.data?.minAge) {
if (form.data?.exactMinAge) {
queryParams.min_age = form.data?.minAge;
} else {
queryParams.gt_min_age = form.data?.minAge === 1 ? 0 : form.data?.minAge - 1;
}
}
if (form.data?.minPlayers) {
if (form.data?.exactMinPlayers) {
queryParams.min_players = form.data?.minPlayers;
} else {
queryParams.gt_min_players = form.data?.minPlayers === 1 ? 0 : form.data?.minPlayers - 1;
}
}
if (form.data?.maxPlayers) {
if (form.data?.exactMaxPlayers) {
queryParams.max_players = form.data?.maxPlayers;
} else {
queryParams.lt_max_players = form.data?.maxPlayers + 1;
}
}
const newQueryParams: Record<string, string> = {};
for (const key in queryParams) {
newQueryParams[key] = `${queryParams[key as keyof SearchQuery]}`;
}
const urlQueryParams = new URLSearchParams(newQueryParams);
return {
form,
searchData: await searchForGames(urlQueryParams)
};
};

View file

@ -1,22 +1,17 @@
<script lang="ts">
import type { ActionData, PageData } from './$types';
import { superForm } from 'sveltekit-superforms/client';
import { gameStore } from '$lib/stores/gameSearchStore';
import TextSearch from '$lib/components/search/textSearch/index.svelte';
export let data: PageData;
export let form: ActionData;
export let data;
const { form, errors, constraints } = superForm(data?.form);
$: if (data?.games) {
$: if (data?.searchData?.games) {
gameStore.removeAll();
gameStore.addAll(data?.games);
}
$: if (form?.games) {
gameStore.removeAll();
gameStore.addAll(form?.games);
gameStore.addAll(data?.searchData?.games);
}
</script>
<div class="game-search">
<TextSearch showButton advancedSearch {data} {form} />
<TextSearch showButton advancedSearch {form} {errors} {constraints} />
</div>

View file

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