aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/Items.svelte
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/Items.svelte')
-rw-r--r--src/components/Items.svelte209
1 files changed, 209 insertions, 0 deletions
diff --git a/src/components/Items.svelte b/src/components/Items.svelte
new file mode 100644
index 00000000..f449215c
--- /dev/null
+++ b/src/components/Items.svelte
@@ -0,0 +1,209 @@
+<script>
+ import { btc, f, sat, post } from "$lib/utils";
+ import { browser } from "$app/environment";
+ import { goto, invalidate } from "$app/navigation";
+ import { fade } from "svelte/transition";
+ import { cubicIn } from "svelte/easing";
+ import { flip } from "svelte/animate";
+ import {
+ dndzone,
+ SHADOW_ITEM_MARKER_PROPERTY_NAME,
+ SOURCES,
+ TRIGGERS,
+ } from "svelte-dnd-action";
+ import DeleteItem from "$comp/DeleteItem.svelte";
+ import Icon from "$comp/Icon.svelte";
+
+ export let subject, user, items, total, rate, currency;
+
+ let flipDurationMs = 200;
+ let dropTargetStyle = {};
+
+ let dragDisabled =
+ subject?.id !== user?.id || (browser && window.innerWidth < 768);
+
+ let deleting;
+ let del = (item) => {
+ deleting = item;
+ };
+
+ function handleConsider(e) {
+ const {
+ items: newItems,
+ info: { source, trigger },
+ } = e.detail;
+ items = newItems;
+ if (source === SOURCES.KEYBOARD && trigger === TRIGGERS.DRAG_STOPPED)
+ reset();
+ }
+
+ async function handleFinalize(e) {
+ const {
+ items: newItems,
+ info: { source },
+ } = e.detail;
+ items = newItems;
+ if (source === SOURCES.POINTER) reset();
+
+ if (subject?.id === user?.id) {
+ await post("/api/items/sort", { items });
+ invalidate("app:items");
+ }
+ }
+
+ function startDrag(e) {
+ e.preventDefault();
+ dragDisabled = false;
+ }
+
+ function handleKeyDown(e) {
+ if ((e.key === "Enter" || e.key === " ") && dragDisabled)
+ dragDisabled = false;
+ }
+
+ $: if (total > 0 && browser) {
+ let payButton = document.querySelector("#payButton");
+ if (payButton) payButton.addEventListener("click", checkout);
+ }
+
+ let checkout = async () => {
+ let invoice = {
+ type: "lightning",
+ amount: btc(total, rate),
+ currency,
+ items: items.filter((i) => i.quantity > 0),
+ prompt: subject.prompt,
+ memoPrompt: subject.memoPrompt,
+ rate,
+ };
+
+ let { id } = await post(`/${subject.username}/invoice`, {
+ invoice,
+ user: { username: subject.username },
+ });
+
+ if (invoice.memoPrompt) {
+ return goto(`/${subject.username}/invoice/${id}/memo`);
+ }
+ goto(`/${subject.username}/invoice/${id}`);
+ };
+
+ let reset = () =>
+ (dragDisabled =
+ user?.username !== subject?.username || window.innerWidth < 768);
+</script>
+
+<svelte:window on:resize={reset} />
+
+{#if deleting}
+ <DeleteItem bind:item={deleting} />
+{/if}
+
+{#if items.length}
+ <h2 class="text-2xl font-bold">Select items</h2>
+{/if}
+<div
+ class="grid sm:grid-cols-2 gap-4 pb-20"
+ use:dndzone={{ items, flipDurationMs, dropTargetStyle, dragDisabled }}
+ on:consider={handleConsider}
+ on:finalize={handleFinalize}
+>
+ {#each items as i (i.id)}
+ <button
+ type="button"
+ class="w-full text-center cursor-pointer hover:opacity-80 mb-auto"
+ on:click={() => i.quantity++}
+ >
+ <div class="relative w-full h-64 overflow-hidden rounded-xl">
+ {#if i[SHADOW_ITEM_MARKER_PROPERTY_NAME]}
+ <div
+ in:fade={{ duration: 200, easing: cubicIn }}
+ class="custom-shadow-item"
+ />
+ {:else if i.image}
+ <img
+ src={`/api/public/${i.image}.webp`}
+ alt={i.name}
+ class="mx-auto object-cover h-full w-full"
+ />
+ {:else}
+ <div
+ class="bg-gradient-to-r from-primary to-gradient mb-4 cursor-pointer hover:opacity-80 w-full h-full"
+ alt="Banner"
+ />
+ {/if}
+ {#if user?.username === subject.username}
+ <div class="flex gap-2 justify-center absolute top-2 right-2">
+ <button type="button" on:click|stopPropagation={() => {}}>
+ <a href={`/${user.username}/items/${i.id}`}>
+ <button
+ class="bg-black rounded-full w-12 h-12 bg-opacity-40 hover:bg-opacity-100"
+ >
+ <Icon icon="edit" style="w-8 mx-auto invert" />
+ </button>
+ </a>
+ </button>
+ <button
+ class="bg-black rounded-full w-12 h-12 bg-opacity-40 hover:bg-opacity-100"
+ on:click|stopPropagation={() => del(i)}
+ >
+ <Icon icon="trash" style="w-8 mx-auto invert" />
+ </button>
+ <button
+ type="button"
+ on:click|stopPropagation={() => {}}
+ class="sm:hidden"
+ on:mousedown={startDrag}
+ on:touchstart={startDrag}
+ on:keydown={handleKeyDown}
+ >
+ <a href={`/${user.username}/items/${i.id}`}>
+ <button
+ class="bg-black rounded-full w-12 h-12 bg-opacity-40 hover:bg-opacity-100"
+ >
+ <Icon icon="move" style="w-8 mx-auto invert" />
+ </button>
+ </a>
+ </button>
+ </div>
+ {/if}
+ </div>
+ <div class="bg-white rounded-xl py-2">
+ <div class="flex text-left gap-2">
+ <div class="overflow-hidden space-y-1">
+ <div class="text-2xl break-words">{i.name}</div>
+ <div class="flex gap-2">
+ <div class="font-semibold text-lg">
+ {f(i.price, currency)}
+ </div>
+ <div class="text-secondary my-auto">
+ {sat(btc(i.price, rate))}
+ </div>
+ </div>
+ </div>
+ <div class="flex gap-1 ml-auto mb-auto justify-around tabular-nums">
+ {#if i.quantity}
+ <button
+ type="button"
+ on:click|stopPropagation={() => i.quantity > 0 && i.quantity--}
+ >
+ <Icon icon="minus" style="w-10 min-w-10" />
+ </button>
+ {/if}
+ <div class="text-2xl my-auto text-center">
+ {#if i.quantity}{i.quantity}{:else}&mdash;{/if}
+ </div>
+ {#if i.quantity}
+ <button
+ type="button"
+ on:click|stopPropagation={() => i.quantity++}
+ >
+ <Icon icon="plus" style="w-10 min-w-10" />
+ </button>
+ {/if}
+ </div>
+ </div>
+ </div>
+ </button>
+ {/each}
+</div>