diff options
| author | Adam Soltys <[email protected]> | 2024-02-20 23:17:57 -0800 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2024-04-25 12:47:16 -0700 |
| commit | 5b995bc5b0a57af8b8bce6474a4bf81a0a77286a (patch) | |
| tree | 0c914a89464be9a100570a59c1a7794db1914c57 /src | |
| parent | 2ef0d75f69051e267f534d07c535a053f064778d (diff) | |
| download | coinos-ui-5b995bc5b0a57af8b8bce6474a4bf81a0a77286a.tar.gz coinos-ui-5b995bc5b0a57af8b8bce6474a4bf81a0a77286a.zip | |
item drag and drop
Diffstat (limited to 'src')
| -rw-r--r-- | src/components/ItemForm.svelte | 16 | ||||
| -rw-r--r-- | src/locales/en.json | 10 | ||||
| -rw-r--r-- | src/routes/(app)/[username]/(profile)/+page.svelte | 388 | ||||
| -rw-r--r-- | src/routes/(app)/[username]/items/[id]/+page.server.js | 3 | ||||
| -rw-r--r-- | src/routes/(app)/[username]/items/[id]/+page.svelte | 10 | ||||
| -rw-r--r-- | src/routes/(app)/[username]/items/create/+page.server.js | 6 | ||||
| -rw-r--r-- | src/routes/(app)/[username]/items/create/+page.svelte | 9 |
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}—{/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> |
