aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2024-04-16 10:45:13 -0700
committerAdam Soltys <[email protected]>2024-04-16 10:45:13 -0700
commit1acda0eb21d3832e2005603a73b1d2503fd684aa (patch)
treeb0ca5339d2fc810774d029b943c1f6070095d9e3
parentde921f5bcc58e2ed5682340112c821c0a006b4d5 (diff)
downloadcoinos-ui-separate-homepage.tar.gz
coinos-ui-separate-homepage.zip
-rw-r--r--src/components/AppHeader.svelte4
-rw-r--r--src/routes/(app)/[username]/(profile)/+layout.svelte4
-rw-r--r--src/routes/(app)/[username]/(profile)/+page.svelte163
-rw-r--r--src/routes/(app)/[username]/home/+layout.server.js26
-rw-r--r--src/routes/(app)/[username]/home/+layout.svelte276
-rw-r--r--src/routes/(app)/[username]/home/+page.server.js11
-rw-r--r--src/routes/(app)/[username]/home/+page.svelte201
-rw-r--r--src/routes/(app)/[username]/home/event/[id]/+page.server.js12
-rw-r--r--src/routes/(app)/[username]/home/event/[id]/+page.svelte7
-rw-r--r--src/routes/(app)/[username]/home/followers/+page.svelte52
-rw-r--r--src/routes/(app)/[username]/home/follows/+page.svelte51
11 files changed, 641 insertions, 166 deletions
diff --git a/src/components/AppHeader.svelte b/src/components/AppHeader.svelte
index 6f68c41a..18d024a1 100644
--- a/src/components/AppHeader.svelte
+++ b/src/components/AppHeader.svelte
@@ -51,10 +51,10 @@
>
<nav class="flex justify-end items-center space-x-4 p-5">
{#if user}
- <a href={`/${user.username}`}>
+ <a href={`/${user.username}/home`}>
<button
class="bg-white p-2 rounded-full w-12 h-12 sm:w-16 sm:h-16 drop-shadow-xl border border-black/10 {$page
- .url.pathname === `/${user.username}`
+ .url.pathname === `/${user.username}/home`
? 'opacity-100'
: 'opacity-70 hover:opacity-80'}"
><Icon icon="home" style="mx-auto w-6 sm:w-8" />
diff --git a/src/routes/(app)/[username]/(profile)/+layout.svelte b/src/routes/(app)/[username]/(profile)/+layout.svelte
index 0520f1b8..3d4f191b 100644
--- a/src/routes/(app)/[username]/(profile)/+layout.svelte
+++ b/src/routes/(app)/[username]/(profile)/+layout.svelte
@@ -149,7 +149,7 @@
</a>
{/if}
- {#if !subject.anon && subject.username !== user?.username && !subject.hidepay}
+ {#if !subject.anon && subject.username !== user?.username}
<a
href={user
? `/pay/${subject.username}`
@@ -168,7 +168,6 @@
</a>
{/if}
- {#if user?.username !== subject.username}
<button
type="button"
on:click={toggleDetails}
@@ -177,7 +176,6 @@
<Icon icon="profile" style="w-8 mr-2" />
<div class="mt-1">{$t("user.info")}</div>
</button>
- {/if}
{#if user?.admin && user.username !== subject.username}
<form class="w-full flex" on:submit|preventDefault={reset}>
diff --git a/src/routes/(app)/[username]/(profile)/+page.svelte b/src/routes/(app)/[username]/(profile)/+page.svelte
index 5878d252..c74f8e6d 100644
--- a/src/routes/(app)/[username]/(profile)/+page.svelte
+++ b/src/routes/(app)/[username]/(profile)/+page.svelte
@@ -1,22 +1,9 @@
<script>
+ import { btc, f, sat, post } from "$lib/utils";
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 Balance from "$comp/Balance.svelte";
- import DeleteItem from "$comp/DeleteItem.svelte";
import { t } from "$lib/translations";
- 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;
@@ -28,8 +15,6 @@
$: 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);
@@ -37,40 +22,6 @@
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);
@@ -98,82 +49,12 @@
if (user) goto(`/send/invoice/${id}`, { invalidateAll: true });
else 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}
-
<div class="space-y-5">
- {#if user?.id === subject.id}
- <div class="flex justify-center lg:justify-start">
- <Balance {user} {rate} />
- </div>
-
- {#if !user.balance}
- <div class="mb-8">
- <p class="text-secondary text-lg">
- {$t("user.welcome")}
- </p>
- </div>
- {/if}
-
- <div class="flex flex-wrap gap-3 justify-center w-full text-xl">
- <a href={`/send`} class="contents grow">
- <button
- type="button"
- class="rounded-2xl border py-5 px-6 font-bold hover:opacity-80 flex bg-primary grow"
- >
- <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="contents">
- <button
- class="rounded-2xl border py-5 px-6 font-bold hover:opacity-80 flex bg-primary grow"
- >
- <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>
-
- {#if $installPrompt}
- <button
- 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 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>
- {/if}
- </div>
- {/if}
-
<div
class="grid sm:grid-cols-2 gap-4"
class:pb-20={total > 0}
- use:dndzone={{ items, flipDurationMs, dropTargetStyle, dragDisabled }}
- on:consider={handleConsider}
- on:finalize={handleFinalize}
>
{#each items as i (i.id)}
<button
@@ -182,12 +63,7 @@
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}
+ {#if i.image}
<img
src={`/api/public/${i.image}.webp`}
alt={i.name}
@@ -199,41 +75,6 @@
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">
diff --git a/src/routes/(app)/[username]/home/+layout.server.js b/src/routes/(app)/[username]/home/+layout.server.js
new file mode 100644
index 00000000..a508de92
--- /dev/null
+++ b/src/routes/(app)/[username]/home/+layout.server.js
@@ -0,0 +1,26 @@
+import { get } from "$lib/utils";
+
+export async function load({ parent }) {
+ let { subject, user } = await parent();
+
+ subject.follows = [];
+ subject.followers = [];
+
+ try {
+ subject.follows = await get(`/${subject.pubkey}/follows`);
+ } catch (e) {
+ console.log("problem fetching follows", e);
+ }
+
+ try {
+ subject.followers = await get(`/${subject.pubkey}/followers`);
+ } catch (e) {
+ console.log("problem fetching followers", e);
+ }
+
+ if (user) {
+ user.follows = await get(`/${user.pubkey}/follows?tagsonly=true`);
+ }
+
+ return { subject, user };
+}
diff --git a/src/routes/(app)/[username]/home/+layout.svelte b/src/routes/(app)/[username]/home/+layout.svelte
new file mode 100644
index 00000000..0520f1b8
--- /dev/null
+++ b/src/routes/(app)/[username]/home/+layout.svelte
@@ -0,0 +1,276 @@
+<script>
+ import { hexToUint8Array } from "uint8array-extras";
+ import { copy, f, post, s, sats, success, fail } from "$lib/utils";
+ import Icon from "$comp/Icon.svelte";
+ import { t } from "$lib/translations";
+ import { sign, send } from "$lib/nostr";
+ import { bech32m } from "@scure/base";
+ import { page } from "$app/stores";
+
+ export let data;
+
+ let { encode, toWords } = bech32m;
+ let events, user, subject, src, text;
+
+ $: ({ events, user, subject, src, rates, text } = data);
+ $: ({ currency, username: n, display } = subject);
+
+ $: username = n.length > 60 ? n.substr(0, 6) : display || n;
+ $: npub =
+ subject.pubkey &&
+ bech32m.encode("npub", toWords(hexToUint8Array(subject.pubkey)), 180);
+ $: lnaddr = `${n}@${$page.url.host}`;
+ $: profile = `${$page.url.host}/${n}`;
+
+ let follow = async () => {
+ user.follows.push([
+ "p",
+ subject.pubkey,
+ "wss://nostr.coinos.io",
+ subject.username,
+ ]);
+ update();
+ };
+
+ let unfollow = async () => {
+ user.follows.splice(
+ user.follows.findIndex((t) => t[1] === subject.pubkey),
+ 1
+ );
+ update();
+ };
+
+ let update = async () => {
+ let event = {
+ pubkey: user.pubkey,
+ created_at: Math.floor(Date.now() / 1000),
+ kind: 3,
+ content: "",
+ tags: user.follows,
+ };
+
+ await sign({ event, user });
+ await send(event);
+
+ user.follows = user.follows;
+ };
+
+ $: following = !!user?.follows.find((t) => t.includes(subject.pubkey));
+
+ let showBio;
+ let toggleBio = () => (showBio = !showBio);
+
+ let showDetails;
+ let toggleDetails = () => (showDetails = !showDetails);
+
+ let password;
+
+ let reset = async () => {
+ try {
+ await post(`/${subject.username}/reset`, { password });
+ success("Password reset");
+ } catch (e) {
+ fail(e.message);
+ }
+ };
+</script>
+
+<div class="container mx-auto w-full px-4 flex flex-wrap lg:flex-nowrap">
+ <div class="hidden lg:block lg:w-[280px] xl:w-[360px]" />
+ <div
+ class="w-full lg:w-[280px] xl:w-[360px] lg:absolute space-y-5 left-20 mx-auto mb-5"
+ >
+ <div
+ class="flex text-3xl font-bold text-center mx-auto justify-center gap-2"
+ >
+ <div class="my-auto break-words w-full">{display || username}</div>
+ </div>
+
+ {#if subject.address}
+ <div
+ class="text-secondary mx-auto text-center lg:mx-0"
+ class:line-clamp-2={!showBio}
+ on:click={toggleBio}
+ on:keydown={toggleBio}
+ >
+ {subject.address}
+ </div>
+ {/if}
+
+ <div class="flex justify-center gap-2">
+ <a href={`/${subject.pubkey}/follows`}
+ ><b>{subject.follows.length}</b>
+ <span class="text-secondary">{$t("user.following")}</span></a
+ >
+ <a href={`/${subject.pubkey}/followers`}
+ ><b>{subject.followers.length}</b>
+ <span class="text-secondary">{$t("user.followers")}</span></a
+ >
+ </div>
+
+ <div class="flex flex-wrap gap-2 w-full text-xl">
+ {#if user && user.username !== subject.username && subject.pubkey}
+ {#if following}
+ <button
+ class="mx-auto rounded-2xl border py-5 px-6 font-bold hover:opacity-80 flex bg-black text-white grow"
+ on:click={unfollow}
+ >
+ <div class="mx-auto flex">
+ <Icon icon={"profile"} style="my-auto w-8 mr-2 invert" />
+ <div class="my-auto">{$t("user.following")}</div>
+ </div>
+ </button>
+ {:else}
+ <button
+ class="mx-auto rounded-2xl border py-5 px-6 font-bold hover:opacity-80 flex grow"
+ on:click={follow}
+ >
+ <div class="mx-auto flex">
+ <Icon icon={"profile"} style="my-auto h-6 mr-2" />
+ <div class="my-auto">{$t("user.follow")}</div>
+ </div>
+ </button>
+ {/if}
+ {/if}
+
+ {#if user && user.username !== subject.username && subject.pubkey}
+ <a
+ href={`/${user.username}/messages/${subject.username}`}
+ class="contents"
+ >
+ <button
+ class="rounded-2xl border py-5 px-6 font-bold hover:opacity-80 flex w-60 grow"
+ >
+ <div class="mx-auto flex">
+ <Icon icon="message" style="w-8 mr-2 my-auto" />
+ <div class="mt-1 my-auto">{$t("user.message")}</div>
+ </div>
+ </button>
+ </a>
+ {/if}
+
+ {#if !subject.anon && subject.username !== user?.username && !subject.hidepay}
+ <a
+ href={user
+ ? `/pay/${subject.username}`
+ : `/${subject.username}/receive`}
+ class="contents"
+ id="payButton"
+ >
+ <button
+ class="rounded-2xl border py-5 px-6 font-bold hover:opacity-80 flex w-60 grow"
+ >
+ <div class="mx-auto flex">
+ <Icon icon="send" style="w-8 mr-2" />
+ <div class="mt-1">{$t("user.pay")}</div>
+ </div>
+ </button>
+ </a>
+ {/if}
+
+ {#if user?.username !== subject.username}
+ <button
+ type="button"
+ on:click={toggleDetails}
+ class="rounded-2xl border py-5 px-6 font-bold hover:opacity-80 flex justify-center w-full"
+ >
+ <Icon icon="profile" style="w-8 mr-2" />
+ <div class="mt-1">{$t("user.info")}</div>
+ </button>
+ {/if}
+
+ {#if user?.admin && user.username !== subject.username}
+ <form class="w-full flex" on:submit|preventDefault={reset}>
+ <input placeholder="Password reset" bind:value={password} />
+ <button
+ type="submit"
+ class="rounded-2xl border py-5 px-6 font-bold hover:opacity-80 flex w-60"
+ >
+ <div class="mx-auto flex">
+ <Icon icon="clock" style="mr-2 my-auto w-8" />
+ <div class="mt-1 my-auto">Reset</div>
+ </div>
+ </button>
+ </form>
+ {/if}
+ </div>
+ {#if showDetails}
+ <div class="text-xl space-y-5">
+ <div>
+ <div class="font-bold">{$t("user.profilePage")}</div>
+ <div class="flex gap-4">
+ <div class="break-all grow text-secondary">
+ {profile}
+ </div>
+ <div class="flex mb-auto gap-1">
+ <button class="my-auto" on:click={() => copy(profile)}
+ ><Icon icon="copy" style="max-w-max w-8 min-w-[32px]" /></button
+ >
+ <a href={`/qr/${encodeURIComponent(profile)}`} class="my-auto">
+ <Icon icon="qr" style="invert max-w-max min-w-[32px]" />
+ </a>
+ </div>
+ </div>
+ </div>
+ <div>
+ <div class="font-bold">{$t("user.lightningAddress")}</div>
+ <div class="flex gap-4">
+ <div class="break-all grow text-secondary">
+ {lnaddr}
+ </div>
+ <div class="flex mb-auto gap-1">
+ <button class="my-auto" on:click={() => copy(lnaddr)}
+ ><Icon icon="copy" style="max-w-max w-8 min-w-[32px]" /></button
+ >
+ <a href={`/qr/${encodeURIComponent(lnaddr)}`} class="my-auto">
+ <Icon icon="qr" style="invert max-w-max min-w-[32px]" />
+ </a>
+ </div>
+ </div>
+ </div>
+ <div>
+ <div class="font-bold">{$t("user.nostrPubkey")}</div>
+ <div class="flex gap-4">
+ <div class="break-all grow text-secondary">
+ {npub}
+ </div>
+ <div class="flex my-auto gap-1">
+ <button class="my-auto" on:click={() => copy(npub)}
+ ><Icon icon="copy" style="max-w-max w-8 min-w-[32px]" /></button
+ >
+ <a href={`/qr/${encodeURIComponent(npub)}`} class="my-auto">
+ <Icon icon="qr" style="invert max-w-max min-w-[32px]" />
+ </a>
+ </div>
+ </div>
+ </div>
+ </div>
+ {/if}
+ </div>
+
+ <div class="w-full">
+ <div class="mx-auto space-y-5 lg:max-w-xl lg:pl-10 xl:max-w-2xl lg:pl-10">
+ <slot />
+ </div>
+ </div>
+</div>
+
+{#if currency}
+ <div
+ class="flex fixed w-full px-4 bg-white py-2 pb-3 bottom-0 bg-opacity-90 tabular-nums"
+ >
+ <div class="text-secondary flex mr-auto">
+ <div class="flex mr-1">
+ <div class="my-auto mr-1">1</div>
+ <img src="/images/bitcoin.svg" class="w-5 my-auto" alt="Bitcoin" />
+ </div>
+ <div>= {f(rates[currency], currency)}</div>
+ </div>
+ <div class="text-secondary flex ml-auto">
+ <div class="flex">
+ <div class="mr-1">⚡️{s((1 * sats) / rates[currency])} =</div>
+ <div>{f(1, currency)}</div>
+ </div>
+ </div>
+ </div>
+{/if}
diff --git a/src/routes/(app)/[username]/home/+page.server.js b/src/routes/(app)/[username]/home/+page.server.js
new file mode 100644
index 00000000..59eb44d5
--- /dev/null
+++ b/src/routes/(app)/[username]/home/+page.server.js
@@ -0,0 +1,11 @@
+import { auth, get } from "$lib/utils";
+
+export async function load({ cookies, depends, parent }) {
+ depends("app:items");
+
+ let { subject } = await parent();
+ let items = await get(`/${subject.id}/items`, auth(cookies));
+ items.map((i) => (i.quantity = 0));
+
+ return { items };
+}
diff --git a/src/routes/(app)/[username]/home/+page.svelte b/src/routes/(app)/[username]/home/+page.svelte
new file mode 100644
index 00000000..4e6365ff
--- /dev/null
+++ b/src/routes/(app)/[username]/home/+page.svelte
@@ -0,0 +1,201 @@
+<script>
+ 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 Balance from "$comp/Balance.svelte";
+ import DeleteItem from "$comp/DeleteItem.svelte";
+ import { t } from "$lib/translations";
+ 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`);
+ }
+ if (user) goto(`/send/invoice/${id}`, { invalidateAll: true });
+ else 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}
+
+<div class="space-y-5">
+ {#if user?.id === subject.id}
+ <div class="flex justify-center lg:justify-start">
+ <Balance {user} {rate} />
+ </div>
+
+ {#if !user.balance}
+ <div class="mb-8">
+ <p class="text-secondary text-lg">
+ {$t("user.welcome")}
+ </p>
+ </div>
+ {/if}
+
+ <div class="flex flex-wrap gap-3 justify-center w-full text-xl">
+ <a href={`/send`} class="contents grow">
+ <button
+ type="button"
+ class="rounded-2xl border py-5 px-6 font-bold hover:opacity-80 flex bg-primary grow"
+ >
+ <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="contents">
+ <button
+ class="rounded-2xl border py-5 px-6 font-bold hover:opacity-80 flex bg-primary grow"
+ >
+ <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>
+
+ {#if $installPrompt}
+ <button
+ 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 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>
+ {/if}
+ </div>
+ {/if}
+
+ {#if total > 0}
+ <button
+ class="rounded-2xl py-5 px-6 font-bold hover:opacity-80 flex bg-black text-white fixed bottom-16 right-4"
+ on:click={checkout}
+ >
+ <div class="mx-auto flex">
+ <div class="my-auto text-2xl">
+ {$t("user.dashboard.checkout")}
+ {f(total, currency)}
+ <span class="text-base">
+ {sat(btc(total, rate), currency)}
+ </span>
+ </div>
+ </div>
+ </button>
+ {/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;
+ }
+</style>
diff --git a/src/routes/(app)/[username]/home/event/[id]/+page.server.js b/src/routes/(app)/[username]/home/event/[id]/+page.server.js
new file mode 100644
index 00000000..24f2b611
--- /dev/null
+++ b/src/routes/(app)/[username]/home/event/[id]/+page.server.js
@@ -0,0 +1,12 @@
+import { get } from "$lib/utils";
+import { error } from "@sveltejs/kit";
+
+export async function load({ params: { id } }) {
+ try {
+ let event = await get(`/event/${id}`);
+ if (!event) error(500, "Event not found");
+ return { event };
+ } catch (e) {
+ error(500, e.message);
+ }
+}
diff --git a/src/routes/(app)/[username]/home/event/[id]/+page.svelte b/src/routes/(app)/[username]/home/event/[id]/+page.svelte
new file mode 100644
index 00000000..74ce1067
--- /dev/null
+++ b/src/routes/(app)/[username]/home/event/[id]/+page.svelte
@@ -0,0 +1,7 @@
+<script>
+ import Event from "$comp/Event.svelte";
+ export let data;
+ let { event } = data;
+</script>
+
+<Event {event} />
diff --git a/src/routes/(app)/[username]/home/followers/+page.svelte b/src/routes/(app)/[username]/home/followers/+page.svelte
new file mode 100644
index 00000000..e087ad42
--- /dev/null
+++ b/src/routes/(app)/[username]/home/followers/+page.svelte
@@ -0,0 +1,52 @@
+<script>
+ import { t } from "$lib/translations";
+ import Avatar from "$comp/Avatar.svelte";
+ import { browser } from "$app/environment";
+ import VirtualScroll from "svelte-virtual-scroll-list";
+
+ export let data;
+ let {
+ subject: { followers },
+ } = data;
+
+ let w;
+</script>
+
+<svelte:window bind:innerWidth={w} />
+
+<div class="px-3 md:px-0 w-full md:w-[400px] mx-auto space-y-8">
+ <h1 class="px-3 md:px-0 text-center text-3xl md:text-4xl font-semibold">
+ {$t("user.followers")}
+ </h1>
+
+ {#if browser}
+ {#if followers.length}
+ <VirtualScroll data={followers} key="pubkey" let:data pageMode={true}>
+ <a href={`/${data.pubkey}`}>
+ <div
+ class="flex border-b last:border-b-0 py-4 text-sm lg:text-lg text-secondary"
+ :key={data.pubkey}
+ >
+ <div class="mb-auto mr-2">
+ <div class="md:hidden">
+ <Avatar size={12} user={data} disabled={true} />
+ </div>
+ <div class="hidden md:block">
+ <Avatar size={20} user={data} disabled={true} />
+ </div>
+ </div>
+
+ <div class="w-full flex pb-1 text-black my-auto">
+ <div>
+ {data.display_name || ""}
+ <span class="text-secondary">@{data.username}</span>
+ </div>
+ </div>
+ </div>
+ </a>
+ </VirtualScroll>
+ {:else}
+ <div>No followers</div>
+ {/if}
+ {/if}
+</div>
diff --git a/src/routes/(app)/[username]/home/follows/+page.svelte b/src/routes/(app)/[username]/home/follows/+page.svelte
new file mode 100644
index 00000000..e4363796
--- /dev/null
+++ b/src/routes/(app)/[username]/home/follows/+page.svelte
@@ -0,0 +1,51 @@
+<script>
+ import { t } from "$lib/translations";
+ import Avatar from "$comp/Avatar.svelte";
+ import { browser } from "$app/environment";
+ import VirtualScroll from "svelte-virtual-scroll-list";
+
+ export let data;
+ let {
+ subject: { follows },
+ } = data;
+ let w;
+</script>
+
+<svelte:window bind:innerWidth={w} />
+
+<div class="px-3 md:px-0 w-full md:w-[400px] mx-auto space-y-8">
+ <h1 class="px-3 md:px-0 text-center text-3xl md:text-4xl font-semibold">
+ {$t("user.following")}
+ </h1>
+
+ {#if browser}
+ {#if follows.length}
+ <VirtualScroll data={follows} key="pubkey" let:data pageMode={true}>
+ <a href={`/${data.pubkey}`}>
+ <div
+ class="flex py-4 text-sm lg:text-lg text-secondary"
+ :key={data.pubkey}
+ >
+ <div class="mb-auto mr-2">
+ <div class="md:hidden">
+ <Avatar size={12} user={data} disabled={true} />
+ </div>
+ <div class="hidden md:block">
+ <Avatar size={20} user={data} disabled={true} />
+ </div>
+ </div>
+
+ <div class="w-full flex pb-1 text-black my-auto">
+ <div>
+ {data.display_name || ""}
+ <span class="text-secondary">@{data.username}</span>
+ </div>
+ </div>
+ </div>
+ </a>
+ </VirtualScroll>
+ {:else}
+ <div>No follows</div>
+ {/if}
+ {/if}
+</div>