Updating og image generation, updating Lucia Beta, and using Shadcn Form for the search form.

This commit is contained in:
Bradley Shellnut 2024-01-26 16:35:02 -08:00
parent 019798eb0b
commit 386d4e7e3a
22 changed files with 879 additions and 573 deletions

View file

@ -28,16 +28,16 @@
"devDependencies": {
"@melt-ui/pp": "^0.3.0",
"@melt-ui/svelte": "^0.70.0",
"@playwright/test": "^1.41.0",
"@resvg/resvg-js": "^2.4.1",
"@sveltejs/adapter-auto": "^3.1.0",
"@sveltejs/adapter-vercel": "^4.0.5",
"@sveltejs/kit": "^2.3.5",
"@playwright/test": "^1.41.1",
"@resvg/resvg-js": "^2.6.0",
"@sveltejs/adapter-auto": "^3.1.1",
"@sveltejs/enhanced-img": "^0.1.8",
"@sveltejs/kit": "^2.5.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@types/cookie": "^0.5.4",
"@types/node": "^18.19.8",
"@typescript-eslint/eslint-plugin": "^6.19.0",
"@typescript-eslint/parser": "^6.19.0",
"@types/node": "^18.19.10",
"@typescript-eslint/eslint-plugin": "^6.19.1",
"@typescript-eslint/parser": "^6.19.1",
"autoprefixer": "^10.4.17",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
@ -59,14 +59,14 @@
"svelte-meta-tags": "^3.1.0",
"svelte-preprocess": "^5.1.3",
"svelte-sequential-preprocessor": "^2.0.1",
"sveltekit-flash-message": "^2.3.1",
"sveltekit-superforms": "^1.13.3",
"sveltekit-flash-message": "^2.4.1",
"sveltekit-superforms": "^1.13.4",
"tailwindcss": "^3.4.1",
"ts-node": "^10.9.2",
"tslib": "^2.6.1",
"typescript": "^5.3.3",
"vite": "^5.0.11",
"vitest": "^1.2.1",
"vite": "^5.0.12",
"vitest": "^1.2.2",
"zod": "^3.22.4"
},
"type": "module",
@ -78,32 +78,33 @@
"@fontsource/fira-mono": "^4.5.10",
"@iconify-icons/line-md": "^1.2.26",
"@iconify-icons/mdi": "^1.2.47",
"@lucia-auth/adapter-prisma": "4.0.0-beta.9",
"@lucia-auth/adapter-prisma": "4.0.0-beta.10",
"@lukeed/uuid": "^2.0.1",
"@paralleldrive/cuid2": "^2.2.2",
"@prisma/client": "^5.8.1",
"@sentry/sveltekit": "^7.88.0",
"@sveltejs/adapter-vercel": "^5.1.0",
"@types/feather-icons": "^4.29.4",
"@vercel/og": "^0.5.13",
"bits-ui": "^0.11.8",
"@vercel/og": "^0.5.20",
"bits-ui": "^0.15.1",
"boardgamegeekclient": "^1.9.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"cookie": "^0.5.0",
"feather-icons": "^4.29.1",
"formsnap": "^0.4.2",
"formsnap": "^0.4.3",
"html-entities": "^2.4.0",
"iconify-icon": "^1.0.8",
"just-kebab-case": "^4.2.0",
"loader": "^2.1.1",
"lucia": "3.0.0-beta.14",
"lucia": "3.0.0-beta.15",
"lucide-svelte": "^0.298.0",
"open-props": "^1.6.16",
"oslo": "^0.27.1",
"open-props": "^1.6.17",
"oslo": "^1.0.1",
"radix-svelte": "^0.9.0",
"svelte-french-toast": "^1.2.0",
"svelte-lazy-loader": "^1.0.0",
"tailwind-merge": "^2.2.0",
"tailwind-merge": "^2.2.1",
"tailwind-variants": "^0.1.19",
"tailwindcss-animate": "^1.0.6",
"zod-to-json-schema": "^3.22.3"

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,39 @@
<script lang="ts">
import type { SuperValidated } from 'sveltekit-superforms';
import { search_schema, type SearchSchema } from '$lib/zodValidation';
import * as Form from "$lib/components/ui/form";
export let form: SuperValidated<SearchSchema>;
</script>
<search>
<Form.Root id="search-form" action="/search" method="GET" data-sveltekit-reload {form} schema={search_schema} let:config>
<fieldset>
<Form.Item>
<Form.Field {config} name="q">
<Form.Label for="label">Search</Form.Label>
<Form.Input />
<Form.Validation />
</Form.Field>
<Form.Field {config} name="skip">
<Form.Input type="hidden" />
</Form.Field>
<Form.Field {config} name="limit">
<Form.Input type="hidden" />
</Form.Field>
</Form.Item>
</fieldset>
<fieldset>
<div class="flex items-center space-x-2">
<Form.Field {config} name="exact">
<Form.Label>Exact Search</Form.Label>
<Form.Checkbox class="mt-0" />
</Form.Field>
</div>
</fieldset>
<Form.Button>Submit</Form.Button>
</Form.Root>
</search>
<style lang="postcss">
</style>

View file

@ -1,76 +0,0 @@
<script lang="ts">
import { superForm } from 'sveltekit-superforms/client';
import SuperDebug from 'sveltekit-superforms/client/SuperDebug.svelte';
import type { SuperValidated } from 'sveltekit-superforms';
import type { SearchSchema } from '$lib/zodValidation';
import { Label } from '$lib/components/ui/label';
import { Input } from '$lib/components/ui/input';
import { Button } from '$lib/components/ui/button';
export let data;
console.log("text search data", data);
export let showButton: boolean = false;
export let advancedSearch: boolean = false;
const { form, errors }: SuperValidated<SearchSchema> = superForm(data.form);
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>
{#if dev}
<SuperDebug collapsible data={$form} />
{/if}
<search>
<form id="search-form" action="/search" method="GET">
<div class="search">
<fieldset class="text-search">
<Label for="label">Search</Label>
<Input type="text" id="q" class={$errors.q && "outline outline-destructive"} name="q" placeholder="Search board games" data-invalid={$errors.q} bind:value={$form.q} />
{#if $errors.q}
<p class="text-sm text-destructive">{$errors.q}</p>
{/if}
<input id="skip" type="hidden" name="skip" bind:value={$form.skip} />
<input id="limit" type="hidden" name="limit" bind:value={$form.limit} />
</fieldset>
</div>
{#if showButton}
<Button type="submit">Submit</Button>
{/if}
</form>
</search>
<style lang="postcss">
:global(.disclosure-button) {
display: flex;
gap: 0.25rem;
place-items: center;
}
#search-form {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
button {
padding: 1rem;
margin: 1.5rem 0;
}
label {
display: grid;
grid-template-columns: auto auto;
gap: 1rem;
place-content: start;
place-items: center;
@media (max-width: 850px) {
display: flex;
flex-wrap: wrap;
}
}
</style>

View file

@ -7,12 +7,10 @@ const buttonVariants = tv({
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline"
},

View file

@ -8,9 +8,6 @@
export { className as class };
</script>
<FormPrimitive.Description
class={cn("text-sm text-muted-foreground", className)}
{...$$restProps}
>
<FormPrimitive.Description class={cn("text-sm text-muted-foreground", className)} {...$$restProps}>
<slot />
</FormPrimitive.Description>

View file

@ -12,10 +12,6 @@
const { errors, ids } = getFormField();
</script>
<Label
for={$ids.input}
class={cn($errors && "text-destructive", className)}
{...$$restProps}
>
<Label for={$ids.input} class={cn($errors && "text-destructive", className)} {...$$restProps}>
<slot />
</Label>

View file

@ -7,17 +7,12 @@
placeholder?: string;
};
type $$Events = SelectPrimitive.TriggerEvents;
const { attrStore } = getFormField();
const { attrStore, value } = getFormField();
export let placeholder = "";
</script>
<Select.Trigger
{...$$restProps}
{...$attrStore}
on:click
on:keydown
type="button"
>
<Select.Value {placeholder} />
<slot />
<Select.Trigger {...$$restProps} {...$attrStore} on:click on:keydown type="button">
<slot value={$value}>
<Select.Value {placeholder} />
</slot>
</Select.Trigger>

View file

@ -2,10 +2,7 @@
import { getFormField } from "formsnap";
import type { HTMLTextareaAttributes } from "svelte/elements";
import type { TextareaGetFormField } from ".";
import {
Textarea,
type TextareaEvents
} from "$lib/components/ui/textarea";
import { Textarea, type TextareaEvents } from "$lib/components/ui/textarea";
type $$Props = HTMLTextareaAttributes;
type $$Events = TextareaEvents;

View file

@ -27,10 +27,7 @@ const SelectGroup = SelectComp.Group;
const SelectItem = SelectComp.Item;
const SelectSeparator = SelectComp.Separator;
export type TextareaGetFormField = Omit<
ReturnType<typeof getFormField>,
"value"
> & {
export type TextareaGetFormField = Omit<ReturnType<typeof getFormField>, "value"> & {
value: Writable<string>;
};

View file

@ -8,6 +8,8 @@ export type InputEvents = {
change: FormInputEvent<Event>;
click: FormInputEvent<MouseEvent>;
focus: FormInputEvent<FocusEvent>;
focusin: FormInputEvent<FocusEvent>;
focusout: FormInputEvent<FocusEvent>;
keydown: FormInputEvent<KeyboardEvent>;
keypress: FormInputEvent<KeyboardEvent>;
keyup: FormInputEvent<KeyboardEvent>;

View file

@ -21,6 +21,8 @@
on:change
on:click
on:focus
on:focusin
on:focusout
on:keydown
on:keypress
on:keyup

View file

@ -9,10 +9,6 @@
export { className as class };
</script>
<RadioGroupPrimitive.Root
bind:value
class={cn("grid gap-2", className)}
{...$$restProps}
>
<RadioGroupPrimitive.Root bind:value class={cn("grid gap-2", className)} {...$$restProps}>
<slot />
</RadioGroupPrimitive.Root>

View file

@ -8,7 +8,4 @@
export { className as class };
</script>
<SelectPrimitive.Separator
class={cn("-mx-1 my-1 h-px bg-muted", className)}
{...$$restProps}
/>
<SelectPrimitive.Separator class={cn("-mx-1 my-1 h-px bg-muted", className)} {...$$restProps} />

View file

@ -12,7 +12,7 @@
<SelectPrimitive.Trigger
class={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 line-clamp-1 truncate",
className
)}
{...$$restProps}

45
src/lib/renderImage.ts Normal file
View file

@ -0,0 +1,45 @@
import satori from 'satori';
import { Resvg } from '@resvg/resvg-js';
import { html as toReactNode } from 'satori-html';
import { dev } from '$app/environment';
import { read } from '$app/server';
// we use a Vite plugin to turn this import into the result of fs.readFileSync during build
import firaSansSemiBold from '$lib/fonts/FiraSans-SemiBold.ttf';
const fontData = read(firaSansSemiBold).arrayBuffer();
export async function componentToPng(component,
props: Record<string, string | undefined>,
height: number, width: number) {
const result = component.render(props);
const markup = toReactNode(`${result.html}<style lang="css">${result.css.code}</style>`);
const svg = await satori(markup, {
fonts: [
{
name: 'Fira Sans',
data: await fontData,
style: 'normal'
}
],
height: +height,
width: +width
});
const resvg = new Resvg(svg, {
fitTo: {
mode: 'width',
value: +width
}
});
const image = resvg.render();
return new Response(image.asPng(), {
headers: {
'content-type': 'image/png',
'cache-control': dev ? 'no-cache, no-store' : 'public, immutable, no-transform, max-age=86400'
}
});
}

View file

@ -2,7 +2,6 @@ import { superValidate } from 'sveltekit-superforms/server';
import { search_schema } from '$lib/zodValidation';
import type { MetaTagsProps } from 'svelte-meta-tags';
import type { PageServerLoad } from './$types';
import prisma from '$lib/prisma';
import type { Game } from '@prisma/client';
export const load: PageServerLoad = async ({ fetch, url }) => {

View file

@ -95,7 +95,6 @@ async function searchForGames(
};
} catch (e) {
console.log(`Error searching board games ${e}`);
// throw error(500, { message: 'Something went wrong' });
}
return {
totalCount: 0,
@ -103,21 +102,27 @@ async function searchForGames(
};
}
const defaults = {
limit: 10,
skip: 0,
order: 'name',
sort: 'asc',
q: '',
exact: false,
};
export const load: PageServerLoad = async ({ locals, fetch, url }) => {
const defaults = {
limit: 10,
skip: 0,
order: 'asc',
sort: 'name'
};
const searchParams = Object.fromEntries(url?.searchParams);
console.log('searchParams', searchParams);
searchParams.limit = searchParams.limit || `${defaults.limit}`;
searchParams.skip = searchParams.skip || `${defaults.skip}`;
searchParams.order = searchParams.order || defaults.order;
searchParams.sort = searchParams.sort || defaults.sort;
const form = await superValidate(searchParams, search_schema);
// const modifyListForm = await superValidate(listGameSchema);
searchParams.q = searchParams.q || defaults.q;
const form = await superValidate({
...searchParams,
skip: Number(searchParams.skip || defaults.skip),
limit: Number(searchParams.limit || defaults.limit),
exact: searchParams.exact ? searchParams.exact === 'true' : defaults.exact
}, search_schema);
const queryParams: SearchQuery = {
limit: form.data?.limit,
@ -125,7 +130,6 @@ export const load: PageServerLoad = async ({ locals, fetch, url }) => {
q: form.data?.q
};
// fields: ('id,name,min_age,min_players,max_players,thumb_url,min_playtime,max_playtime,min_age,description');
try {
if (form.data?.q === '') {
return {
@ -169,6 +173,8 @@ export const load: PageServerLoad = async ({ locals, fetch, url }) => {
const urlQueryParams = new URLSearchParams(newQueryParams);
const searchData = await searchForGames(locals, fetch, urlQueryParams);
console.log('search data', JSON.stringify(searchData, null, 2));
return {
form,
// modifyListForm,
@ -178,6 +184,8 @@ export const load: PageServerLoad = async ({ locals, fetch, url }) => {
} catch (e) {
console.log(`Error searching board games ${e}`);
}
console.log('returning default no data')
return {
form,
searchData: {

View file

@ -4,21 +4,26 @@
import { superForm } from 'sveltekit-superforms/client';
import SuperDebug from 'sveltekit-superforms/client/SuperDebug.svelte';
import { createPagination, createToolbar, melt } from '@melt-ui/svelte';
import { ChevronLeft, ChevronRight, LayoutList, LayoutGrid } from 'lucide-svelte';
import type { SearchSchema } from '$lib/zodValidation';
import { ChevronLeft, ChevronRight, LayoutList, LayoutGrid, Check } from 'lucide-svelte';
import { search_schema, type SearchSchema } from '$lib/zodValidation';
import Game from '$components/Game.svelte';
import { Label } from '$lib/components/ui/label';
import { Input } from '$lib/components/ui/input';
import { Button } from '$lib/components/ui/button';
import { Checkbox } from "$lib/components/ui/checkbox";
import { Checkbox } from '$lib/components/ui/checkbox';
import * as Form from "$lib/components/ui/form";
import GameSearchForm from '$components/search/GameSearchForm.svelte';
export let data;
const { form, errors }: SuperValidated<SearchSchema> = superForm(data.form);
const { games, totalCount } = data?.searchData;
console.log('data found', data);
console.log('found games', games);
console.log('found totalCount', totalCount);
let submitButton: HTMLElement;
let pageSize = +form?.limit || 10;
let pageSize: number = data.form.limit || 10;
$: showPagination = totalCount > pageSize;
@ -48,33 +53,10 @@
<div class="game-search">
{#if dev}
<SuperDebug collapsible data={$form} />
<SuperDebug collapsible data={data.form} />
{/if}
<search>
<form id="search-form" action="/search" method="GET">
<fieldset>
<Label for="label">Search</Label>
<Input type="text" id="q" class={$errors.q && "outline outline-destructive"} name="q" placeholder="Search board games" data-invalid={$errors.q} bind:value={$form.q} />
{#if $errors.q}
<p class="text-sm text-destructive">{$errors.q}</p>
{/if}
<input id="skip" type="hidden" name="skip" bind:value={$form.skip} />
<input id="limit" type="hidden" name="limit" bind:value={$form.limit} />
</fieldset>
<fieldset class="flex items-center space-x-2">
<Checkbox id="exact" bind:checked={$form.exact} aria-labelledby="exact-label" />
<Label
id="exact-label"
for="exact"
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Exact Search
</Label>
</fieldset>
<Button type="submit">Submit</Button>
</form>
</search>
<GameSearchForm form={data.form} />
<section class="games">
<div>
@ -139,15 +121,11 @@
button {
display: grid;
place-items: center;
border-radius: 2px;
background-color: rgb(var(--color-white) / 1);
color: rgb(var(--color-magnum-700) / 1);
box-shadow: 0px 1px 2px 0px rgb(var(--color-black) / 0.05);
font-size: 14px;
padding-inline: 0.75rem;
height: 2rem;
}
button:hover {

View file

@ -1,56 +1,27 @@
import type { RequestHandler } from '@sveltejs/kit';
import satori from 'satori';
import { Resvg } from '@resvg/resvg-js';
import { html as toReactNode } from 'satori-html';
import NotoSans from '$lib/fonts/NotoSans-Regular.ttf';
import SocialImageCard from '$components/socialImageCard.svelte';
import { componentToPng } from '$lib/renderImage.js';
const height = 630;
const width = 1200;
export const GET: RequestHandler = async ({ url }) => {
/** @type {import('./$types').RequestHandler} */
export async function GET({ url }) {
try {
const ogImage = `${new URL(url.origin).href}images/bored-game.png`;
const faviconImageLocation = 'images/bored-game.png';
const image = `${new URL(url.origin).href}${faviconImageLocation}`;
const header = url.searchParams.get('header') ?? undefined;
const page = url.searchParams.get('page') ?? undefined;
const content = url.searchParams.get('content') ?? '';
const result = SocialImageCard.render({
return componentToPng(SocialImageCard, {
header,
page,
content,
image: ogImage,
width,
height,
image,
width: `${width}`,
height: `${height}`,
url: new URL(url.origin).href
});
console.log('result', result);
const element = toReactNode(`${result.html}<style>${result.css.code}</style>`);
const svg = await satori(element, {
fonts: [
{
name: 'Noto Sans',
data: Buffer.from(NotoSans),
style: 'normal'
}
],
height,
width
});
const resvg = new Resvg(svg, {
fitTo: {
mode: 'width',
value: width
}
});
const image = resvg.render();
return new Response(image.asPng(), {
headers: {
'content-type': 'image/png'
}
});
}, height, width);
} catch (e) {
console.error(e);
}

View file

@ -132,7 +132,7 @@
--toast-error-background: var(--tomatoOrange);
/* Media Queryies - Not yet supported in CSS */
/*
/*
--xsmall: 340px;
--small: 500px;
--large: 960px;

View file

@ -1,7 +1,6 @@
import { sentrySvelteKit } from "@sentry/sveltekit";
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
// import fs from 'fs';
export default defineConfig({
plugins: [
@ -19,6 +18,9 @@ export default defineConfig({
test: {
include: ['src/**/*.{test,spec}.{js,ts}']
},
define: {
SUPERFORMS_LEGACY: true
},
css: {
devSourcemap: true,
preprocessorOptions: {
@ -43,15 +45,3 @@ export default defineConfig({
}
}
});
// function rawFonts(ext) {
// return {
// name: 'vite-plugin-raw-fonts',
// transform(code, id) {
// if (ext.some((e) => id.endsWith(e))) {
// const buffer = fs.readFileSync(id);
// return { code: `export default ${JSON.stringify(buffer)}`, map: null };
// }
// }
// };
// }