Refactor component names, add api for random games, and use on main page.

This commit is contained in:
Bradley Shellnut 2023-12-28 12:16:36 -08:00
parent daa9a628d1
commit 994d1d462c
23 changed files with 197 additions and 262 deletions

View file

@ -27,7 +27,7 @@
},
"devDependencies": {
"@melt-ui/pp": "^0.1.4",
"@melt-ui/svelte": "^0.66.3",
"@melt-ui/svelte": "^0.66.4",
"@playwright/test": "^1.40.1",
"@resvg/resvg-js": "^2.4.1",
"@sveltejs/adapter-auto": "^3.0.0",

View file

@ -105,10 +105,10 @@ dependencies:
devDependencies:
'@melt-ui/pp':
specifier: ^0.1.4
version: 0.1.4(@melt-ui/svelte@0.66.3)(svelte@4.2.8)
version: 0.1.4(@melt-ui/svelte@0.66.4)(svelte@4.2.8)
'@melt-ui/svelte':
specifier: ^0.66.3
version: 0.66.3(svelte@4.2.8)
specifier: ^0.66.4
version: 0.66.4(svelte@4.2.8)
'@playwright/test':
specifier: ^1.40.1
version: 1.40.1
@ -1035,14 +1035,14 @@ packages:
- supports-color
dev: true
/@melt-ui/pp@0.1.4(@melt-ui/svelte@0.66.3)(svelte@4.2.8):
/@melt-ui/pp@0.1.4(@melt-ui/svelte@0.66.4)(svelte@4.2.8):
resolution: {integrity: sha512-zR+Kl3CZJPJBHW8V7YcdQCMI/dVcnW9Ct3yGbVaIywYVStVRS7F9uEDOea3xLLT2WTGodQePzPlUn53yKFu87g==}
engines: {pnpm: '>=8.6.3'}
peerDependencies:
'@melt-ui/svelte': '>= 0.29.0'
svelte: ^3.55.0 || ^4.0.0 || ^5.0.0-next.1
dependencies:
'@melt-ui/svelte': 0.66.3(svelte@4.2.8)
'@melt-ui/svelte': 0.66.4(svelte@4.2.8)
estree-walker: 3.0.3
svelte: 4.2.8
dev: true
@ -1061,8 +1061,8 @@ packages:
svelte: 4.2.8
dev: false
/@melt-ui/svelte@0.66.3(svelte@4.2.8):
resolution: {integrity: sha512-inwvI+YjvMWykK8PEYIg9sAx0sQHI29XeX9hfrdtP47mFVa61pQLqrLoADBpTSb5gO9ZzuL01agXGFfzjK1iPw==}
/@melt-ui/svelte@0.66.4(svelte@4.2.8):
resolution: {integrity: sha512-RYzgje5/0WQiN8YYAoeuHP7Ua/Ew2oPqBVOkMqlqRquARyiD1gP1RsfXWH637i06q+T5S+UuIiVkc4b/pbcZ4g==}
peerDependencies:
svelte: '>=3 <5'
dependencies:

View file

@ -7,7 +7,8 @@ import { lucia } from '$lib/server/auth';
Sentry.init({
dsn: 'https://742e43279df93a3c4a4a78c12eb1f879@o4506057768632320.ingest.sentry.io/4506057770401792',
tracesSampleRate: 1,
environment: dev ? 'development' : 'production'
environment: dev ? 'development' : 'production',
enabled: !dev
});
export const authentication: Handle = async function ({ event, resolve }) {

View file

@ -0,0 +1,64 @@
<script lang="ts">
import type { GameType, SavedGameType } from '$lib/types';
import * as Card from "$lib/components/ui/card";
import type { CollectionItem } from '@prisma/client';
export let game: GameType | CollectionItem;
export let detailed: boolean = false;
export let variant: 'default' | 'compact' = 'default';
// Naive and assumes description is only on our GameType at the moment
function isGameType(game: GameType | SavedGameType): game is GameType {
return (game as GameType).description !== undefined;
}
</script>
<article class="grid grid-template-cols-2 gap-4">
<Card.Root class={variant === 'compact' ? 'game-card-compact' : ''}>
<Card.Header>
<Card.Title class="game-card-header">
<span style:--transition-name="game-name-{game.slug}">
{game.name}
{#if game?.year_published}
({game?.year_published})
{/if}
</span>
</Card.Title>
</Card.Header>
<Card.Content class={variant === 'compact' ? 'pt-6' : ''}>
<a
class="thumbnail"
href={`/game/${game.id}`}
title={`View ${game.name}`}
data-sveltekit-preload-data
>
<img src={game.thumb_url} alt={`Image of ${game.name}`} loading="lazy" decoding="async" />
<div class="game-details">
{#if game?.players}
<p>Players: {game.players}</p>
<p>Time: {game.playtime} minutes</p>
{#if isGameType(game) && game?.min_age}
<p>Min Age: {game.min_age}</p>
{/if}
{#if detailed && isGameType(game) && game?.description}
<div class="description">{@html game.description}</div>
{/if}
{/if}
</div>
</a>
</Card.Content>
</Card.Root>
</article>
<style lang="postcss">
:global(.game-card-compact) {
display: flex;
place-items: center;
.game-card-header {
span {
view-transition-name: var(--transition-name);
}
}
}
</style>

View file

@ -1,134 +0,0 @@
<script lang="ts">
import { fade } from 'svelte/transition';
import type { GameType, SavedGameType } from '$lib/types';
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '$components/ui/card';
import { Button } from '$components/ui/button';
import type { CollectionItem } from '@prisma/client';
// export let data: SuperValidated<ListGameSchema>;
export let game: GameType | CollectionItem;
export let detailed: boolean = false;
// Naive and assumes description is only on our GameType at the moment
function isGameType(game: GameType | SavedGameType): game is GameType {
return (game as GameType).description !== undefined;
}
</script>
<article class="grid grid-template-cols-2 gap-4" transition:fade|global>
<Card>
<CardHeader>
<CardTitle>{game.name}</CardTitle>
</CardHeader>
<CardContent>
<a
class="thumbnail"
href={`/game/${game.id}`}
title={`View ${game.name}`}
data-sveltekit-preload-data
>
<img src={game.thumb_url} alt={`Image of ${game.name}`} loading="lazy" decoding="async" />
<div class="game-details">
{#if game?.players}
<p>Players: {game.players}</p>
<p>Time: {game.playtime} minutes</p>
{#if isGameType(game) && game?.min_age}
<p>Min Age: {game.min_age}</p>
{/if}
{#if detailed && isGameType(game) && game?.description}
<div class="description">{@html game.description}</div>
{/if}
{/if}
</div>
</a>
</CardContent>
</Card>
</article>
<!-- <article class="game-container" transition:fade|global>
<h2>{game.name}</h2>
<a
class="thumbnail"
href={`/game/${game.id}`}
title={`View ${game.name}`}
data-sveltekit-preload-data
>
<!-- <Image src={game.thumb_url} alt={`Image of ${game.name}`} /> -->
<!-- <img src={game.thumb_url} alt={`Image of ${game.name}`} loading="lazy" decoding="async" /> -->
<!-- loading="lazy" decoding="async" -->
<!-- </a> -->
<!-- <div class="game-details">
{#if game?.players}
<p>Players: {game.players}</p>
<p>Time: {game.playtime} minutes</p>
{#if isGameType(game) && game?.min_age}
<p>Min Age: {game.min_age}</p>
{/if}
{#if detailed && isGameType(game) && game?.description}
<div class="description">{@html game.description}</div>
{/if}
{/if}
</div>
<div class="game-buttons">
<Button size="md" kind={existsInCollection ? 'danger' : 'primary'} icon on:click={onCollectionClick}>
{collectionText}
{#if existsInCollection}
<iconify-icon icon={minusCircle} width="24" height="24" />
{:else}
<iconify-icon icon={plusCircle} width="24" height="24" />
{/if}
</Button>
<Button size="md" kind={existsInWishlist ? 'danger' : 'primary'} icon on:click={onWishlistClick}>
{wishlistText}
{#if existsInWishlist}
<iconify-icon icon={minusCircle} width="24" height="24" />
{:else}
<iconify-icon icon={plusCircle} width="24" height="24" />
{/if}
</Button>
</div> -->
<!-- </article> -->
<style lang="scss">
.game-container {
display: grid;
/* grid-template-rows: repeat(auto-fill, 1fr); */
grid-template-rows: 0.15fr minmax(250px, 1fr) 0.2fr 0.2fr;
place-items: center;
text-align: center;
@media (max-width: 650px) {
max-width: none;
}
gap: var(--spacing-16);
padding: var(--spacing-16) var(--spacing-16);
transition: all 0.3s;
border-radius: 8px;
background-color: var(--primary);
/* &:hover {
background-color: hsla(222, 9%, 65%, 1);
} */
/* .game-info {
display: grid;
place-items: center;
gap: 0.75rem;
margin: 0.2rem;
} */
.game-details {
p,
a {
padding: 0.25rem;
}
}
.game-buttons {
display: grid;
gap: 1rem;
}
}
</style>

View file

@ -1,21 +0,0 @@
<script lang="ts">
import { cn } from "$lib/utils";
let className: string | undefined | null = undefined;
export { className as class };
</script>
<div
class={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className
)}
{...$$restProps}
on:click
on:focusin
on:focusout
on:mouseenter
on:mouseleave
>
<slot />
</div>

View file

@ -1,10 +0,0 @@
<script lang="ts">
import { cn } from "$lib/utils";
let className: string | undefined | null = undefined;
export { className as class };
</script>
<div class={cn("p-6 pt-0", className)} {...$$restProps}>
<slot />
</div>

View file

@ -1,10 +0,0 @@
<script lang="ts">
import { cn } from "$lib/utils";
let className: string | undefined | null = undefined;
export { className as class };
</script>
<p class={cn("text-sm text-muted-foreground", className)} {...$$restProps}>
<slot />
</p>

View file

@ -1,10 +0,0 @@
<script lang="ts">
import { cn } from "$lib/utils";
let className: string | undefined | null = undefined;
export { className as class };
</script>
<div class={cn("flex items-center p-6 pt-0", className)} {...$$restProps}>
<slot />
</div>

View file

@ -1,10 +0,0 @@
<script lang="ts">
import { cn } from "$lib/utils";
let className: string | undefined | null = undefined;
export { className as class };
</script>
<div class={cn("flex flex-col space-y-1.5 p-6", className)} {...$$restProps}>
<slot />
</div>

View file

@ -1,16 +0,0 @@
<script lang="ts">
import { cn } from "$lib/utils";
let className: string | undefined | null = undefined;
export { className as class };
export let tag: "h1" | "h2" | "h3" | "h4" | "h5" | "h6" = "h3";
</script>
<svelte:element
this={tag}
class={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...$$restProps}
>
<slot />
</svelte:element>

View file

@ -1,6 +1,6 @@
<script lang="ts">
// import { tick, onDestroy } from 'svelte';
import Game from '$lib/components/game/index.svelte';
import Game from '$components/Game.svelte';
export let data;
console.log(`Page data: ${JSON.stringify(data)}`);

View file

@ -1,5 +1,5 @@
<script lang="ts">
import Game from '$lib/components/game/index.svelte';
import Game from '$components/Game.svelte';
export let data;
console.log('data', data);

View file

@ -1,5 +1,5 @@
<script lang="ts">
import Game from '$lib/components/game/index.svelte';
import Game from '$components/Game.svelte';
export let data;
console.log('data', data);

View file

@ -1,7 +1,7 @@
<script lang="ts">
import 'iconify-icon';
import Header from '$components/Header.svelte';
import Footer from '$lib/components/footer.svelte';
import Footer from '$components/Footer.svelte';
export let data;
</script>

View file

@ -44,15 +44,8 @@ export const load: PageServerLoad = async ({ fetch, url }) => {
const form = await superValidate(formData, search_schema);
console.log('form', form);
const count = 5;
const ids: { id: string }[] = await prisma.$queryRaw`SELECT id FROM games ORDER BY RAND() LIMIT ${count}`;
const randomGames: Game[] = await prisma.game.findMany({
where: {
id: {
in: ids.map(id => id.id)
}
}
});
const randomGames: Game[] = await fetch('/api/game/random?limit=6').then(res => res.json());
console.log('randomGames', randomGames);
return { form, metaTagsChild: metaTags, randomGames };
};

View file

@ -1,7 +1,7 @@
<script lang="ts">
// import TextSearch from '$lib/components/search/textSearch/header.svelte';
import RandomSearch from '$lib/components/search/random/index.svelte';
import Game from '$lib/components/game/index.svelte';
import Game from '$components/Game.svelte';
import logo from '$lib/assets/bored-game.png';
// import Random from '$lib/components/random/header.svelte';
@ -26,13 +26,13 @@
<!-- <TextSearch showButton advancedSearch data={data.form} /> -->
</div>
<section>
<section class="random-games">
{#each randomGames as game (game.id)}
<Game {game} />
<Game variant='compact' {game} />
{/each}
</section>
<style lang="scss">
<style lang="postcss">
.game-search {
display: grid;
gap: 2rem;
@ -43,6 +43,13 @@
}
}
.random-games {
margin-top: 1rem;
display: grid;
gap: 0.5rem;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
}
.random-buttons {
display: flex;
place-content: space-between;

View file

@ -27,7 +27,7 @@
<title>{game?.name} | Bored Game</title>
</svelte:head>
<h2>{game?.name}
<h2 style:--transition-name="game-name-{game.slug}">{game?.name}
{#if game?.year_published}
({game?.year_published})
{/if}

View file

@ -3,10 +3,10 @@
import type { SuperValidated } from 'sveltekit-superforms';
import { superForm } from 'sveltekit-superforms/client';
import SuperDebug from 'sveltekit-superforms/client/SuperDebug.svelte';
import { createPagination, melt } from '@melt-ui/svelte';
import { ChevronLeft, ChevronRight } from 'lucide-svelte';
import { createPagination, createToolbar, melt } from '@melt-ui/svelte';
import { ChevronLeft, ChevronRight, LayoutList, LayoutGrid } from 'lucide-svelte';
import type { SearchSchema } from '$lib/zodValidation';
import Game from '$lib/components/game/index.svelte';
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';
@ -23,7 +23,15 @@
$: showPagination = totalCount > pageSize;
const {
elements: { root, pageTrigger, prevButton, nextButton },
elements: { root: toolbarRoot },
builders: { createToolbarGroup },
} = createToolbar();
const {
elements: { group: listStyleGroup, item: listStyleItem },
} = createToolbarGroup();
const {
elements: { root: paginationRoot, pageTrigger, prevButton, nextButton },
states: { pages, range }
} = createPagination({
count: totalCount,
@ -31,6 +39,11 @@
defaultPage: 1,
siblingCount: 1
});
const gameListStyle: 'grid' | 'list' = 'grid';
function handleListStyle(event) {
console.log(event, typeof event);
}
</script>
<div class="game-search">
@ -64,7 +77,19 @@
</search>
<section class="games">
<h1>Games Found:</h1>
<div>
<h1>Games Found:</h1>
<div use:melt={$toolbarRoot}>
<div use:melt={$listStyleGroup} class="search-toolbar">
<button class="style-item" aria-label='list' use:melt={$listStyleItem('list')} on:m-click|preventDefault={(e) => handleListStyle(e)}>
<LayoutList class="square-5" />
</button>
<button class="style-item" aria-label='grid' use:melt={$listStyleItem('grid')} on:m-click|preventDefault={(e) => handleListStyle(e)}>
<LayoutGrid class="square-5" />
</button>
</div>
</div>
</div>
<div class="games-list">
{#if totalCount > 0}
{#each games as game (game.id)}
@ -75,7 +100,7 @@
{/if}
</div>
{#if showPagination}
<nav use:melt={$root}>
<nav use:melt={$paginationRoot}>
<p class="text-center">Showing items {$range.start} - {$range.end}</p>
<div class="buttons">
<button use:melt={$prevButton}><ChevronLeft /></button>
@ -156,6 +181,11 @@
.games-list {
display: grid;
--listColumns: 4;
.list {
--listColumns: 1;
}
grid-template-columns: repeat(var(--listColumns), minmax(250px, 1fr));
gap: 2rem;
@ -175,4 +205,24 @@
--listColumns: 1;
}
}
.search-toolbar {
display: flex;
place-items: center;
gap: 0.15rem;
}
.style-item {
padding: 0.1rem;
border-radius: 0.375rem;
border: 1px solid black;
&:hover {
opacity: 0.75;
}
&[data-state='on'] {
background-color: grey;
}
}
</style>

View file

@ -7,7 +7,7 @@
import 'iconify-icon';
import { page } from '$app/stores';
import { onNavigate } from "$app/navigation";
import Analytics from '$lib/components/analytics.svelte';
import Analytics from '$components/Analytics.svelte';
import Loading from '$lib/components/Loading.svelte';
import { theme } from '$state/theme';
import PageLoadingIndicator from '$lib/page_loading_indicator.svelte';
@ -29,9 +29,9 @@
...$page.data.metaTagsChild
}
const flash = getFlash(page, {
clearAfterMs: 6000
});
// const flash = getFlash(page, {
// clearAfterMs: 6000
// });
onMount(() => {
// set the theme to the user's active theme
@ -39,21 +39,21 @@
document.querySelector('html')?.setAttribute('data-theme', $theme);
});
flash.subscribe(($flash) => {
if (!$flash) return;
// flash.subscribe(($flash) => {
// if (!$flash) return;
if ($flash.type === 'success') {
toast.success($flash.message);
} else {
toast.error($flash.message, {
duration: 5000
});
}
// if ($flash.type === 'success') {
// toast.success($flash.message);
// } else {
// toast.error($flash.message, {
// duration: 5000
// });
// }
// Clearing the flash message could sometimes
// be required here to avoid double-toasting.
flash.set(undefined);
});
// // Clearing the flash message could sometimes
// // be required here to avoid double-toasting.
// flash.set(undefined);
// });
onNavigate(async (navigation) => {
if (!document.startViewTransition) return;
@ -80,7 +80,7 @@
</div>
<Toaster />
<Loading />
<!-- <Loading /> -->
<style lang="postcss">
.layout {

View file

@ -0,0 +1,31 @@
import prisma from '$lib/prisma.js';
import type { Game } from '@prisma/client';
import { error, json } from '@sveltejs/kit';
export const GET = async ({ url, locals }) => {
const searchParams = Object.fromEntries(url.searchParams);
const limit = parseInt(searchParams?.limit) || 1;
if (limit <= 0 || limit > 6) {
error(400, { message: 'Limit must be between 1 and 6' });
}
const totalGames = await prisma.game.count();
const randomIndex = Math.floor(Math.random() * totalGames);
const ids: { id: string }[] = await prisma.$queryRaw`
SELECT id
FROM games
ORDER BY id
LIMIT ${limit}
OFFSET ${randomIndex}
`;
const randomGames: Game[] = await prisma.game.findMany({
where: {
id: {
in: ids.map(id => id.id)
}
}
});
return json(randomGames);
}