add a test
This commit is contained in:
parent
a1bab69ee8
commit
ef1e9c9ae8
@ -1,6 +1,6 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
for (const url of ['/', '/stats/', '/about/']) {
|
||||
for (const url of ['/', '/stats/', '/about/', '/cart/']) {
|
||||
test(url + ' render', async ({ page }) => {
|
||||
await page.goto(url);
|
||||
await expect(
|
||||
|
@ -40,6 +40,7 @@
|
||||
"better-sqlite3": "^9.3.0",
|
||||
"cheerio": "1.0.0-rc.12",
|
||||
"node-fetch": "^3.3.2",
|
||||
"remeda": "^1.40.1",
|
||||
"svelte-persisted-store": "^0.9.0",
|
||||
"vite-multiple-assets": "^1.2.10"
|
||||
}
|
||||
|
@ -40,12 +40,26 @@ games_hidden_store.subscribe((data) =>
|
||||
localStorage.setItem('games_hidden', JSON.stringify(data)),
|
||||
);
|
||||
|
||||
let cart_store_stored = localStorage.getItem('cart');
|
||||
if (cart_store_stored) cart_store_stored = JSON.parse(cart_store_stored);
|
||||
export const cart_store = writable(cart_store_stored || {});
|
||||
cart_store.subscribe((data) =>
|
||||
localStorage.setItem('cart', JSON.stringify(data)),
|
||||
);
|
||||
|
||||
export const games_store = derived(
|
||||
[_games_store, games_hidden_store],
|
||||
([$games, $hidden]) =>
|
||||
$games.map((game) => ({ ...game, is_hidden: $hidden[game.id] })),
|
||||
[_games_store, games_hidden_store, cart_store],
|
||||
([$games, $hidden, $cart]) =>
|
||||
$games.map((game) => ({
|
||||
...game,
|
||||
is_hidden: $hidden[game.id],
|
||||
is_in_cart: $cart[game.id],
|
||||
})),
|
||||
);
|
||||
|
||||
games_store.toggle_hidden = (id) => {
|
||||
games_hidden_store.update(u.updateIn(id, (v) => !v));
|
||||
};
|
||||
games_store.toggle_cart = (id) => {
|
||||
cart_store.update(u.updateIn(id, (v) => !v));
|
||||
};
|
||||
|
@ -2,7 +2,7 @@
|
||||
<title>Ottawa board games, trades and sales</title>
|
||||
</svelte:head>
|
||||
<main class="responsive">
|
||||
<AppTop />
|
||||
<AppTop cart={$cart_store} />
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
@ -15,9 +15,14 @@
|
||||
import { persisted } from 'svelte-persisted-store';
|
||||
import u from '@yanick/updeep-remeda';
|
||||
import AppTop from './AppTop.svelte';
|
||||
import { games_store, sellers_store } from '$lib/stores/index.js';
|
||||
import {
|
||||
games_store,
|
||||
sellers_store,
|
||||
cart_store,
|
||||
} from '$lib/stores/index.js';
|
||||
setContext('games', games_store);
|
||||
setContext('sellers', sellers_store);
|
||||
setContext('cart', cart_store);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
@ -12,7 +12,8 @@
|
||||
games={$games}
|
||||
sellers={$sellers}
|
||||
on:toggle_game_visibility={({ detail }) =>
|
||||
games.toggle_hidden(detail)} />
|
||||
games.toggle_hidden(detail)}
|
||||
on:toggle_cart={({ detail }) => games.toggle_cart(detail)} />
|
||||
{/if}
|
||||
</article>
|
||||
|
||||
|
@ -5,7 +5,16 @@
|
||||
on:click={() => (show_menu = !show_menu)}>
|
||||
<i>menu</i>
|
||||
</button>
|
||||
<h5 class="max left-align">Ottawa board games for sale and trade</h5>
|
||||
<h5 class="max left-align">
|
||||
<a href="/">Ottawa board games for sale and trade</a>
|
||||
</h5>
|
||||
|
||||
<a class="" href="/cart/">
|
||||
{#if cart_size}
|
||||
<span class="badge circle">{cart_size}</span>
|
||||
{/if}
|
||||
|
||||
<i>shopping_cart</i></a>
|
||||
</nav>
|
||||
</header>
|
||||
<dialog
|
||||
@ -19,6 +28,10 @@
|
||||
<i>list</i>
|
||||
<span class="max">listing</span>
|
||||
</a>
|
||||
<a href="/cart/" on:click={hide_menu}>
|
||||
<i>shopping_cart</i>
|
||||
<span>cart</span>
|
||||
</a>
|
||||
<a href="/stats/" on:click={hide_menu}>
|
||||
<i>numbers</i>
|
||||
<span>stats</span>
|
||||
@ -38,6 +51,9 @@
|
||||
import clickOutside from '$lib/directives/clickOutside.js';
|
||||
let show_menu = false;
|
||||
const hide_menu = () => (show_menu = false);
|
||||
|
||||
export let cart = [];
|
||||
$: cart_size = Object.values(cart).filter((x) => x).length;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
@ -3,13 +3,18 @@
|
||||
class:hidden={is_hidden}
|
||||
class="game">
|
||||
<div class="grid top-align">
|
||||
<div class="s12 m2">
|
||||
<a
|
||||
class="s12 m2"
|
||||
target="_blank"
|
||||
href={`https://boardgamegeek.com/boardgame/${bgg_id}`}>
|
||||
<img alt="" loading="lazy" src={thumbnail} height="80" /></a>
|
||||
</div>
|
||||
<div class="grid s12 m10">
|
||||
<div class="s8 left-align">
|
||||
<div class="max">
|
||||
{#if is_in_cart}
|
||||
<i class="star">star</i>
|
||||
{/if}
|
||||
<strong>
|
||||
<a
|
||||
class="game-desc"
|
||||
@ -18,6 +23,7 @@
|
||||
{name}</a>
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="s3">
|
||||
<div>
|
||||
<a
|
||||
@ -39,6 +45,20 @@
|
||||
</div>
|
||||
<div class="notes s8">
|
||||
{notes}
|
||||
<div class="actions">
|
||||
<button on:click={toggle_cart}
|
||||
><i>
|
||||
{#if is_in_cart}
|
||||
remove_shopping_cart{:else}
|
||||
add_shopping_cart
|
||||
{/if}
|
||||
</i></button>
|
||||
<button title="hide game" on:click={toggle_game_visibility}
|
||||
>{#if is_hidden}<i>visibility_off</i>{:else}
|
||||
<i>visibility</i>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="s3 updated-at">
|
||||
{#if updated_at}
|
||||
@ -46,22 +66,17 @@
|
||||
{/if}
|
||||
</div>
|
||||
<div class="s1">
|
||||
<div>
|
||||
<button
|
||||
class="transparent round"
|
||||
title="hide game"
|
||||
on:click={toggle_game_visibility}
|
||||
>{#if is_hidden}<i>visibility_off</i>{:else}
|
||||
<i>visibility</i>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// TODO show cart icon with badge
|
||||
// show cart page
|
||||
// cart store
|
||||
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { slide, fade } from 'svelte/transition';
|
||||
|
||||
@ -74,6 +89,7 @@
|
||||
export let price = null;
|
||||
export let notes = '';
|
||||
export let updated_at = null;
|
||||
export let is_in_cart = false;
|
||||
|
||||
function pretty_date(date) {
|
||||
if (!date) return '';
|
||||
@ -83,6 +99,7 @@
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const toggle_game_visibility = () => dispatch('toggle_visibility');
|
||||
const toggle_cart = () => dispatch('toggle_cart');
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@ -111,4 +128,16 @@
|
||||
.hidden {
|
||||
opacity: 60%;
|
||||
}
|
||||
.actions {
|
||||
margin-top: 1em;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
}
|
||||
.actions button {
|
||||
margin: 0.5em 2em 0em 1em;
|
||||
}
|
||||
.star {
|
||||
color: darkorange;
|
||||
font-size: var(--font-size-12);
|
||||
}
|
||||
</style>
|
||||
|
@ -21,6 +21,7 @@
|
||||
{#if show_hidden || !game.is_hidden}
|
||||
<Game
|
||||
on:toggle_visibility={() => toggle_visibility(game.id)}
|
||||
on:toggle_cart={() => toggle_cart(game.id)}
|
||||
{...game}
|
||||
seller={sellers[game.username]} />
|
||||
<div class="medium-divider"></div>
|
||||
@ -41,6 +42,7 @@
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const toggle_visibility = (id) => dispatch('toggle_game_visibility', id);
|
||||
const toggle_cart = (id) => dispatch('toggle_cart', id);
|
||||
|
||||
// add filter
|
||||
// add sort (user, game, price)
|
||||
|
62
src/routes/cart/+page.svelte
Normal file
62
src/routes/cart/+page.svelte
Normal file
@ -0,0 +1,62 @@
|
||||
<article>
|
||||
{#if games.length == 0}
|
||||
<div class="medium-height middle-align center-align">
|
||||
<div class="center-align">
|
||||
<h4 class="center-align">Such restraint</h4>
|
||||
<h5>No games in cart (yet)</h5>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<GameList
|
||||
{games}
|
||||
sellers={$sellers}
|
||||
on:toggle_cart={({ detail }) => all_games.toggle_cart(detail)} />
|
||||
{/if}
|
||||
</article>
|
||||
|
||||
<script>
|
||||
import { getContext } from 'svelte';
|
||||
import GameList from './GameList.svelte';
|
||||
|
||||
const all_games = getContext('games');
|
||||
const sellers = getContext('sellers');
|
||||
|
||||
$: games = $all_games.filter(({ is_in_cart }) => is_in_cart);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
p {
|
||||
font-size: var(--font-size-10);
|
||||
max-width: 60em;
|
||||
text-align: justify;
|
||||
margin-left: 3em;
|
||||
margin-right: 3em;
|
||||
}
|
||||
a {
|
||||
color: var(--primary);
|
||||
text-decoration: underline;
|
||||
}
|
||||
.game-cell {
|
||||
display: flex;
|
||||
}
|
||||
.game-cell > * {
|
||||
display: block;
|
||||
}
|
||||
.game-desc {
|
||||
flex: 1;
|
||||
}
|
||||
td {
|
||||
font-size: var(--font-size-10);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 730px) {
|
||||
h1 {
|
||||
font-size: var(--font-size-12);
|
||||
}
|
||||
p {
|
||||
font-size: inherit;
|
||||
margin-left: 1em;
|
||||
margin-right: 1em;
|
||||
}
|
||||
}
|
||||
</style>
|
113
src/routes/cart/Game.svelte
Normal file
113
src/routes/cart/Game.svelte
Normal file
@ -0,0 +1,113 @@
|
||||
<div
|
||||
transition:slide={{ delay: 250, duration: 300 }}
|
||||
class:hidden={is_hidden}
|
||||
class="game">
|
||||
<div class="grid top-align">
|
||||
<div class="s12 m2">
|
||||
<a
|
||||
target="_blank"
|
||||
href={`https://boardgamegeek.com/boardgame/${bgg_id}`}>
|
||||
<img alt="" loading="lazy" src={thumbnail} height="80" /></a>
|
||||
</div>
|
||||
<div class="grid s12 m10">
|
||||
<div class="s8 left-align">
|
||||
<div class="max">
|
||||
<strong>
|
||||
<a
|
||||
class="game-desc"
|
||||
target="_blank"
|
||||
href={`https://boardgamegeek.com/boardgame/${bgg_id}`}>
|
||||
{name}</a>
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="s3"></div>
|
||||
<div class="s1 right-align">
|
||||
{price ? '$' + price : ''}
|
||||
</div>
|
||||
<div class="notes s8">
|
||||
{notes}
|
||||
</div>
|
||||
<div class="s3 updated-at">
|
||||
{#if updated_at}
|
||||
{pretty_date(updated_at)}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="actions s1">
|
||||
<button on:click={toggle_cart}
|
||||
><i>
|
||||
{#if is_in_cart}
|
||||
remove_shopping_cart{:else}
|
||||
add_shopping_cart
|
||||
{/if}
|
||||
</i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// TODO show cart icon with badge
|
||||
// show cart page
|
||||
// cart store
|
||||
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { slide, fade } from 'svelte/transition';
|
||||
|
||||
export let username = '';
|
||||
export let name = '';
|
||||
export let is_hidden = false;
|
||||
export let bgg_id = '';
|
||||
export let thumbnail = '';
|
||||
export let seller = {};
|
||||
export let price = null;
|
||||
export let notes = '';
|
||||
export let updated_at = null;
|
||||
export let is_in_cart = false;
|
||||
|
||||
function pretty_date(date) {
|
||||
if (!date) return '';
|
||||
|
||||
return date.replace(/T.*/, '');
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const toggle_game_visibility = () => dispatch('toggle_visibility');
|
||||
const toggle_cart = () => dispatch('toggle_cart');
|
||||
</script>
|
||||
|
||||
<style>
|
||||
a {
|
||||
color: var(--primary);
|
||||
text-decoration: underline;
|
||||
}
|
||||
.grid {
|
||||
margin-right: 2em;
|
||||
font-size: var(--font-size-10);
|
||||
}
|
||||
.notes {
|
||||
margin-top: 1em;
|
||||
margin-left: 1em;
|
||||
}
|
||||
.updated-at {
|
||||
font-size: smaller;
|
||||
text-align: left;
|
||||
}
|
||||
.updated-at::before {
|
||||
content: 'last update: ';
|
||||
}
|
||||
.neighbourhood {
|
||||
font-size: smaller;
|
||||
}
|
||||
.hidden {
|
||||
opacity: 60%;
|
||||
}
|
||||
.actions {
|
||||
margin-top: 1em;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
}
|
||||
.actions button {
|
||||
margin: 0.5em 2em 0em 1em;
|
||||
}
|
||||
</style>
|
107
src/routes/cart/GameList.svelte
Normal file
107
src/routes/cart/GameList.svelte
Normal file
@ -0,0 +1,107 @@
|
||||
{#each games_sellers as seller}
|
||||
<h4>
|
||||
<BggUser username={seller} />
|
||||
|
||||
<a
|
||||
target="_blank"
|
||||
href={`https://boardgamegeek.com/geekmail/compose?touser=${seller}`}>
|
||||
<i>email</i></a>
|
||||
<span class="neighbourhood">
|
||||
{sellers[seller]?.neighbourhood ?? ''}
|
||||
</span>
|
||||
<span class="nbr-games"
|
||||
>{games.filter((game) => game.username == seller).length} game(s)</span>
|
||||
</h4>
|
||||
<div class="games">
|
||||
{#each games.filter((game) => {
|
||||
return game.username == seller;
|
||||
}) as game}
|
||||
{#if show_hidden || !game.is_hidden}
|
||||
<Game
|
||||
on:toggle_visibility={() => toggle_visibility(game.id)}
|
||||
on:toggle_cart={() => toggle_cart(game.id)}
|
||||
{...game}
|
||||
seller={sellers[game.username]} />
|
||||
<div class="medium-divider"></div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<script>
|
||||
import { getContext, createEventDispatcher } from 'svelte';
|
||||
import Game from './Game.svelte';
|
||||
import BggUser from '$lib/components/BggUser.svelte';
|
||||
import u from '@yanick/updeep-remeda';
|
||||
import * as R from 'remeda';
|
||||
|
||||
export let games = [];
|
||||
export let sellers = {};
|
||||
|
||||
$: games_sellers = R.uniq(games.map(({ username }) => username).sort());
|
||||
|
||||
let show_hidden = false;
|
||||
|
||||
let search_text = '';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const toggle_visibility = (id) => dispatch('toggle_game_visibility', id);
|
||||
const toggle_cart = (id) => dispatch('toggle_cart', id);
|
||||
|
||||
// add filter
|
||||
// add sort (user, game, price)
|
||||
</script>
|
||||
|
||||
<style>
|
||||
h4 {
|
||||
align-items: baseline;
|
||||
display: flex;
|
||||
}
|
||||
.neighbourhood {
|
||||
margin-left: 2em;
|
||||
}
|
||||
.games {
|
||||
margin-top: 3em;
|
||||
}
|
||||
a {
|
||||
color: var(--primary);
|
||||
text-decoration: underline;
|
||||
}
|
||||
.grid {
|
||||
margin-right: 2em;
|
||||
font-size: var(--font-size-10);
|
||||
}
|
||||
.notes {
|
||||
margin-top: 1em;
|
||||
margin-left: 1em;
|
||||
}
|
||||
.updated-at {
|
||||
font-size: smaller;
|
||||
text-align: left;
|
||||
}
|
||||
.updated-at::before {
|
||||
content: 'last update: ';
|
||||
}
|
||||
.neighbourhood {
|
||||
font-size: smaller;
|
||||
}
|
||||
.hidden {
|
||||
opacity: 60%;
|
||||
}
|
||||
.switch span {
|
||||
font-size: var(--font-size-10);
|
||||
padding-left: 1em;
|
||||
}
|
||||
.options {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
.options > div {
|
||||
margin-right: 1em;
|
||||
}
|
||||
.nbr-games {
|
||||
font-size: smaller;
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
Loading…
Reference in New Issue
Block a user