aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2024-02-20 23:17:57 -0800
committerAdam Soltys <[email protected]>2024-04-25 12:47:16 -0700
commit5b995bc5b0a57af8b8bce6474a4bf81a0a77286a (patch)
tree0c914a89464be9a100570a59c1a7794db1914c57 /src
parent2ef0d75f69051e267f534d07c535a053f064778d (diff)
downloadcoinos-ui-5b995bc5b0a57af8b8bce6474a4bf81a0a77286a.tar.gz
coinos-ui-5b995bc5b0a57af8b8bce6474a4bf81a0a77286a.zip
item drag and drop
Diffstat (limited to 'src')
-rw-r--r--src/components/ItemForm.svelte16
-rw-r--r--src/locales/en.json10
-rw-r--r--src/routes/(app)/[username]/(profile)/+page.svelte388
-rw-r--r--src/routes/(app)/[username]/items/[id]/+page.server.js3
-rw-r--r--src/routes/(app)/[username]/items/[id]/+page.svelte10
-rw-r--r--src/routes/(app)/[username]/items/create/+page.server.js6
-rw-r--r--src/routes/(app)/[username]/items/create/+page.svelte9
7 files changed, 305 insertions, 137 deletions
diff --git a/src/components/ItemForm.svelte b/src/components/ItemForm.svelte
index 71114c29..bd300344 100644
--- a/src/components/ItemForm.svelte
+++ b/src/components/ItemForm.svelte
@@ -5,6 +5,7 @@
import { applyAction, deserialize } from "$app/forms";
import Spinner from "$comp/Spinner.svelte";
export let item;
+ export let user;
let fileInput, formElement, file, submitting, progress;
let select = () => fileInput.click();
@@ -79,7 +80,18 @@
<div>
<label for="price" class="font-bold mb-1 block">{$t("items.price")}</label>
- <input name="price" bind:value={item.price} />
+ <div class="flex">
+ <input
+ name="price"
+ bind:value={item.price}
+ class="border-r-none rounded-r-none"
+ />
+ <div
+ class="text-gray-600 rounded-r-2xl p-4 my-auto rounded-l-none rounded border bg-gray-100"
+ >
+ {user.currency}
+ </div>
+ </div>
</div>
<div class="w-full">
@@ -123,7 +135,7 @@
</div>
<button
- class="rounded-full py-5 px-6 font-bold hover:opacity-80 flex bg-black text-white text-2xl"
+ class="rounded-full py-5 px-6 font-bold hover:opacity-80 bg-black text-white text-2xl w-full text-center"
>
{#if submitting}
<Spinner />
diff --git a/src/locales/en.json b/src/locales/en.json
index 292c669a..e1b5465b 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -335,5 +335,13 @@
"sendMessage": "Write a message...",
"fundsClaimed": "Funds claimed!",
"print": "Print"
- }
+ },
+ "items": {
+ "add": "Add item",
+ "edit": "Edit item",
+ "name": "Name",
+ "price": "Price",
+ "submit": "Submit",
+ "image": "Image"
+ }
}
diff --git a/src/routes/(app)/[username]/(profile)/+page.svelte b/src/routes/(app)/[username]/(profile)/+page.svelte
index 4868ac86..1dd8f573 100644
--- a/src/routes/(app)/[username]/(profile)/+page.svelte
+++ b/src/routes/(app)/[username]/(profile)/+page.svelte
@@ -1,28 +1,116 @@
<script>
- import { onMount } from "svelte";
+ import { browser } from "$app/environment";
+ import { goto, invalidate } from "$app/navigation";
+ import { btc, f, sat, post } from "$lib/utils";
import Icon from "$comp/Icon.svelte";
- import Avatar from "$comp/Avatar.svelte";
import Balance from "$comp/Balance.svelte";
- import Feed from "$comp/Feed.svelte";
import DeleteItem from "$comp/DeleteItem.svelte";
import { t } from "$lib/translations";
- import { btc, ease, f, sat, sats } from "$lib/utils";
- import { sign, send, encrypt, decrypt } from "$lib/nostr";
- import { event as e, password } from "$lib/store";
- import { browser } from "$app/environment";
+ import { fade } from "svelte/transition";
+ import { cubicIn } from "svelte/easing";
+ import { flip } from "svelte/animate";
+ import { installPrompt } from "$lib/store";
+
+ import {
+ dndzone,
+ SHADOW_ITEM_MARKER_PROPERTY_NAME,
+ SOURCES,
+ TRIGGERS,
+ } from "svelte-dnd-action";
export let data;
+
+ let flipDurationMs = 200;
+ let dropTargetStyle = {};
+
let { subject, user, items, rates } = data;
+ $: ({ currency } = subject);
+ $: rate = rates[currency];
$: refresh(data);
let refresh = (d) => ({ items, subject, user, rates } = d);
+ let dragDisabled =
+ subject?.id !== user?.id || (browser && window.innerWidth < 768);
let deleting;
$: total = items.reduce((a, b) => a + b.price * b.quantity, 0);
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);
+
+ let install = async () => {
+ if (!$installPrompt) return;
+ await $installPrompt.prompt();
+ $installPrompt = null;
+ };
</script>
+<svelte:window on:resize={reset} />
+
{#if deleting}
<DeleteItem bind:item={deleting} />
{/if}
@@ -30,166 +118,214 @@
<div class="space-y-5">
{#if user?.id === subject.id}
<div class="flex justify-center lg:justify-start">
- <Balance {user} rate={rates[user.currency]} />
+ <Balance {user} {rate} />
</div>
{#if !user.balance}
- <p
- class="text-secondary dark:text-gray-300 text-lg max-w-2xl text-center lg:text-start mx-auto"
- >
- {$t("user.welcome")}
- </p>
+ <div class="mb-8">
+ <p class="text-secondary text-lg">
+ {$t("user.welcome")}
+ </p>
+ </div>
{/if}
- <div class="flex flex-wrap gap-4 justify-center w-full">
- <a href={`/send`} class="w-full md:w-40">
+ <div class="flex flex-wrap gap-3 justify-center w-full text-xl">
+ <a href={`/send`} class="contents grow">
<button
- class="rounded-full border dark:border-white py-3 px-6 font-bold hover:opacity-80 flex h-14 w-full"
+ type="button"
+ class="rounded-2xl border py-5 px-6 font-bold hover:opacity-80 flex bg-primary grow"
>
- <div class="mx-auto my-auto flex">
- <Icon
- icon="send"
- style="my-auto h-6 mr-2 text-red-300 dark:invert"
- />
+ <div class="mx-auto flex gap-2">
+ <Icon icon="send" style="w-8" />
<div class="my-auto">{$t("user.dashboard.send")}</div>
</div>
</button>
</a>
- <a href={`/${user.username}/receive`} class="w-full md:w-40 my-auto">
+ <a href={`/${user.username}/receive`} class="contents">
<button
- class="rounded-full border dark:border-white py-3 px-6 font-bold hover:opacity-80 flex h-14 w-full"
+ class="rounded-2xl border py-5 px-6 font-bold hover:opacity-80 flex bg-primary grow"
>
- <div class="mx-auto flex">
- <Icon icon="numpad" style="my-auto h-6 mr-2 dark:invert" />
- <div class="my-auto mt-1">{$t("user.dashboard.receive")}</div>
+ <div class="mx-auto flex gap-2">
+ <Icon icon="numpad" style="w-8" />
+ <div class="my-auto">{$t("user.dashboard.receive")}</div>
</div>
</button>
</a>
- <a href={`/buy`} class="w-full md:w-52">
+ {#if $installPrompt}
<button
- class="rounded-full border dark:border-white py-3 px-6 font-bold hover:opacity-80 flex h-14 w-full"
+ class="rounded-2xl border py-5 px-6 font-bold hover:opacity-80 flex bg-black text-white grow basis-full fixed bottom-16 xl:hidden"
+ on:click={install}
>
- <div class="mx-auto flex">
- <!-- <Icon icon="plus" style="my-auto h-6 mr-2" /> -->
-
- <img
- src="/images/bitcoin.svg"
- class="w-8 border-4 border-transparent mr-2"
- alt="Bitcoin"
- />
- <div class="my-auto mt-1">Buy Bitcoin</div>
+ <div class="mx-auto flex gap-2">
+ <Icon icon="save" style="w-8 mx-auto invert" />
+ <div class="my-auto text-xl whitespace-nowrap">
+ {$t("user.install")}
+ </div>
</div>
</button>
- </a>
+ {/if}
</div>
+ {/if}
- {#if user?.username === subject.username}
- <div>
- <a href={`/${user.username}/items/create`} class="w-full">
- <button
- class="rounded-2xl border py-5 px-6 font-bold hover:opacity-80 flex w-full bg-black text-white"
- >
- <div class="mx-auto flex">
- <div class="my-auto text-2xl">
- {$t("items.add")}
+ {#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>
- </button>
- </a>
- </div>
- {/if}
-
- <div class="grid grid-cols-2 gap-4">
- {#each items as i}
- <div
- class="w-full text-center cursor-pointer hover:opacity-80"
- on:click={() => i.quantity++}
- >
- <div class="relative w-full h-64 overflow-hidden rounded-xl">
- {#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"
- on:click={select}
- on:keydown={select}
- alt="Banner"
- />
- {/if}
- {#if user?.username === subject.username}
- <div class="flex gap-2 justify-center absolute top-2 right-2">
- <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--}
>
- <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>
- </div>
+ <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
- class="bg-black rounded-full w-12 h-12 bg-opacity-40 hover:bg-opacity-100"
- on:click|stopPropagation={() => del(i)}
+ type="button"
+ on:click|stopPropagation={() => i.quantity++}
>
- <Icon icon="trash" style="w-8 mx-auto invert" />
+ <Icon icon="plus" style="w-10 min-w-10" />
</button>
- </div>
- {/if}
- </div>
- <div class="bg-white rounded-xl p-2">
- <div class="flex">
- <div class="text-2xl mr-auto">{i.name}</div>
- <div class="flex gap-2 my-auto">
- <div class="font-semibold">
- {f(i.price, subject.currency)}
- </div>
- <div class="text-secondary">
- {sat(btc(i.price, rates[subject.currency]))}
- </div>
- </div>
- </div>
- <div class="flex gap-2">
- <div
- class="ml-auto"
- on:click|stopPropagation={() => i.quantity > 0 && i.quantity--}
- >
- <Icon icon="minus" style="w-10" />
- </div>
- <div class="text-2xl my-auto">{i.quantity}</div>
- <Icon icon="plus" style="w-10" />
+ {/if}
</div>
</div>
</div>
- {/each}
- </div>
- {/if}
-</div>
-
-{#if total > 0}
- <div>
- <a
- href={`/send/${subject.username}/${total}/${subject.currency}`}
- class="w-full"
- >
+ </button>
+ {/each}
+ </div>
+ <div
+ class="fixed inset-x-0 mx-auto flex bottom-16"
+ class:static={!items.length}
+ >
+ {#if total > 0}
<button
- class="rounded-2xl border py-5 px-6 font-bold hover:opacity-80 flex w-full bg-black text-white"
+ class="rounded-2xl py-5 px-6 font-bold hover:bg-neutral-700 flex bg-black text-white mx-auto"
+ on:click={checkout}
>
<div class="mx-auto flex">
<div class="my-auto text-2xl">
- {f(total, subject.currency)}
{$t("user.dashboard.checkout")}
+ {f(total, currency)}
+ <span class="text-base">
+ {sat(btc(total, rate), currency)}
+ </span>
</div>
</div>
</button>
- </a>
+ {:else if user?.username !== subject.username}
+ <a href={`/pay/${subject.username}`} class="contents">
+ <button
+ class="rounded-2xl py-5 px-6 font-bold hover:bg-neutral-700 flex bg-black text-white mx-auto"
+ >
+ <div class="mx-auto flex gap-2">
+ <Icon icon="send" style="w-8 invert" />
+ <div class="my-auto text-2xl">{$t("user.dashboard.payNow")}</div>
+ </div>
+ </button>
+ </a>
+ {/if}
</div>
-{/if}
+</div>
+
+<style>
+ .custom-shadow-item {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ visibility: visible;
+ background: #333;
+ opacity: 0.5;
+ margin: 0;
+ }
+
+ .bottom-half {
+ @apply bottom-1/2;
+ }
+</style>
diff --git a/src/routes/(app)/[username]/items/[id]/+page.server.js b/src/routes/(app)/[username]/items/[id]/+page.server.js
index 960f9af4..4cfc952c 100644
--- a/src/routes/(app)/[username]/items/[id]/+page.server.js
+++ b/src/routes/(app)/[username]/items/[id]/+page.server.js
@@ -1,7 +1,7 @@
import { fd, auth, get, post } from "$lib/utils";
import { fail, redirect } from "@sveltejs/kit";
- export let load = async ({ cookies, params: { id } }) => {
+export let load = async ({ cookies, params: { id } }) => {
let item = await get(`/items/${id}`);
return { item };
};
@@ -12,7 +12,6 @@ export const actions = {
try {
await post(`/items`, form, auth(cookies));
} catch (e) {
- console.log(e);
return fail(400, { message: e.message });
}
diff --git a/src/routes/(app)/[username]/items/[id]/+page.svelte b/src/routes/(app)/[username]/items/[id]/+page.svelte
index ddf4cf1d..49619a33 100644
--- a/src/routes/(app)/[username]/items/[id]/+page.svelte
+++ b/src/routes/(app)/[username]/items/[id]/+page.svelte
@@ -1,14 +1,18 @@
<script>
import { t } from "$lib/translations";
import ItemForm from "$comp/ItemForm.svelte";
+ import { fail } from "$lib/utils";
+
export let data;
- let { item } = data;
- console.log(item)
+ export let form;
+
+ $: form?.message && fail(form.message);
+ let { item, user } = data;
</script>
<div class="container max-w-xl mx-auto">
<h1 class="px-3 md:px-0 text-center text-3xl md:text-4xl font-semibold mb-10">
{$t("items.edit")}
</h1>
- <ItemForm {item} />
+ <ItemForm {item} {user} />
</div>
diff --git a/src/routes/(app)/[username]/items/create/+page.server.js b/src/routes/(app)/[username]/items/create/+page.server.js
index ea0571e7..6740f5f9 100644
--- a/src/routes/(app)/[username]/items/create/+page.server.js
+++ b/src/routes/(app)/[username]/items/create/+page.server.js
@@ -6,12 +6,14 @@ export const actions = {
let form = await fd(request);
try {
await post("/items", form, auth(cookies));
+
+ console.log("YEAH");
} catch (e) {
- console.log(e);
+ console.log("E", e)
return fail(400, { message: e.message });
}
- let success = `/${url.pathname.split('/')[1]}`
+ let success = `/${url.pathname.split("/")[1]}`;
redirect(307, success);
},
};
diff --git a/src/routes/(app)/[username]/items/create/+page.svelte b/src/routes/(app)/[username]/items/create/+page.svelte
index 46ee8e0c..b81f0423 100644
--- a/src/routes/(app)/[username]/items/create/+page.svelte
+++ b/src/routes/(app)/[username]/items/create/+page.svelte
@@ -1,6 +1,13 @@
<script>
import { t } from "$lib/translations";
import ItemForm from "$comp/ItemForm.svelte";
+ import { fail } from "$lib/utils";
+
+ export let data;
+ export let form;
+
+ $: form?.message && fail(form.message);
+ let { user } = data;
let item = {};
</script>
@@ -8,5 +15,5 @@
<h1 class="px-3 md:px-0 text-center text-3xl md:text-4xl font-semibold mb-10">
{$t("items.add")}
</h1>
- <ItemForm {item} />
+ <ItemForm {item} {user} />
</div>