aboutsummaryrefslogtreecommitdiffstats
path: root/src/routes/(static)
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2024-09-10 20:39:47 -0700
committerAdam Soltys <[email protected]>2024-09-17 09:18:36 -0700
commit1f63848ca9fb01336f04b41434de1cc06a5b3d93 (patch)
tree2affa3041863be365f33d99e1f32bda60cf93a32 /src/routes/(static)
parent6e48c584605bec5623c12c470d04d07303983801 (diff)
downloadcoinos-ui-1f63848ca9fb01336f04b41434de1cc06a5b3d93.tar.gz
coinos-ui-1f63848ca9fb01336f04b41434de1cc06a5b3d93.zip
move routes
Diffstat (limited to 'src/routes/(static)')
-rw-r--r--src/routes/(static)/+layout.svelte2
-rw-r--r--src/routes/(static)/login/+page.server.ts29
-rw-r--r--src/routes/(static)/login/+page.svelte171
-rw-r--r--src/routes/(static)/register/+page.server.ts41
-rw-r--r--src/routes/(static)/register/+page.svelte292
-rw-r--r--src/routes/(static)/support/+page.svelte134
-rw-r--r--src/routes/(static)/support/+server.ts9
7 files changed, 677 insertions, 1 deletions
diff --git a/src/routes/(static)/+layout.svelte b/src/routes/(static)/+layout.svelte
index 13073d5a..4b350bd0 100644
--- a/src/routes/(static)/+layout.svelte
+++ b/src/routes/(static)/+layout.svelte
@@ -9,7 +9,7 @@
<SvelteToast options={{ reversed: true, intro: { y: 192 } }} />
<LandingHeader {user} />
-<main class="container flex px-10 md:px-40 mx-auto min-h-[600px] pb-20">
+<main class="container flex px-4 md:px-40 mx-auto min-h-[600px] py-28">
<slot />
</main>
<Footer />
diff --git a/src/routes/(static)/login/+page.server.ts b/src/routes/(static)/login/+page.server.ts
new file mode 100644
index 00000000..fd7fec8c
--- /dev/null
+++ b/src/routes/(static)/login/+page.server.ts
@@ -0,0 +1,29 @@
+import { fail, redirect } from "@sveltejs/kit";
+import { login, fd } from "$lib/utils";
+
+export const load = async ({ parent }) => {
+ let { user } = await parent();
+ if (user?.pubkey) redirect(307, `/${user.username}`);
+};
+
+export const actions = {
+ default: async ({ cookies, request }) => {
+ let form = await fd(request);
+ let { username, password, token, loginRedirect } = form;
+ let user = { username, password, token };
+
+ if (loginRedirect === "undefined") loginRedirect = undefined;
+
+ try {
+ await login(
+ user,
+ cookies,
+ request.headers.get("cf-connecting-ip")
+ );
+ } catch (e: any) {
+ return fail(400, { error: "Login failed", message: e.message, ...form });
+ }
+
+ redirect(307, loginRedirect || `/${user.username}`);
+ },
+};
diff --git a/src/routes/(static)/login/+page.svelte b/src/routes/(static)/login/+page.svelte
new file mode 100644
index 00000000..84e1ea89
--- /dev/null
+++ b/src/routes/(static)/login/+page.svelte
@@ -0,0 +1,171 @@
+<script>
+ import { onMount } from "svelte";
+ import { applyAction, deserialize } from "$app/forms";
+ import { tick } from "svelte";
+ import { fly } from "svelte/transition";
+ import { enhance } from "$app/forms";
+ import Pin from "$comp/Pin.svelte";
+ import Icon from "$comp/Icon.svelte";
+ import Spinner from "$comp/Spinner.svelte";
+ import { focus, fail } from "$lib/utils";
+ import { password, pin, loginRedirect } from "$lib/store";
+ import { t } from "$lib/translations";
+ import { page } from "$app/stores";
+ import { generate } from "$lib/nostr";
+ import { invalidateAll } from "$app/navigation";
+
+ onMount(() => {
+ $pin = undefined;
+ localStorage.clear();
+ sessionStorage.clear();
+
+ let lang = $page.url.searchParams.get("lang");
+ if (lang) document.cookie = `lang=${lang} ;`;
+ });
+
+ export let form;
+
+ let token, formElement;
+ let code = [];
+ let redirect;
+
+ $password = undefined;
+
+ $: need2fa = form?.message === "2fa";
+ let cancel = () => (need2fa = false);
+ $: if (form?.message === "2fa" && form.token === token) token = "";
+
+ let username, email, btn;
+
+ $: update(form);
+ let update = (form) => form && ({ username, password: $password } = form);
+
+ $: token && token?.length === 6 && tick().then(() => btn.click());
+
+ let revealPassword = false;
+
+ let loading;
+ async function handleSubmit(e) {
+ loading = true;
+
+ let data = new FormData(this);
+ let user = Object.fromEntries(data);
+ await generate(user);
+
+ for (let k in user) {
+ data.set(k, user[k]);
+ }
+
+ const response = await fetch("/login", {
+ method: "POST",
+ body: data,
+ });
+
+ const result = deserialize(await response.text());
+
+ if (result.type === "success") {
+ await invalidateAll();
+ }
+
+ applyAction(result);
+ loading = false;
+ }
+</script>
+
+{#if need2fa}
+ <Pin bind:value={token} title="Enter 2FA Code" {cancel} notify={false} />
+{/if}
+
+<div class="mx-auto md:shadow-xl rounded-3xl max-w-xl w-full md:w-[480px] md:p-8">
+ <h1 class="text-2xl font-bold text-center">{$t("login.signIn")}</h1>
+
+ {#if form?.error}
+ <div class="text-red-600 text-center" in:fly>
+ {form.error}
+ </div>
+ {/if}
+
+ <form class="space-y-5" on:submit|preventDefault={handleSubmit} method="POST">
+ <input
+ type="hidden"
+ name="loginRedirect"
+ value={$loginRedirect || $page.url.searchParams.get("redirect")}
+ />
+ <input type="hidden" name="token" value={token} />
+
+ <div>
+ <label for="username" class="font-semibold">{$t("login.username")}</label>
+ <input
+ name="username"
+ type="text"
+ required
+ class="bg-primary"
+ bind:value={username}
+ use:focus
+ autocapitalize="none"
+ />
+ </div>
+
+ <div class="relative">
+ <label for="password" class="block font-semibold"
+ >{$t("login.password")}</label
+ >
+ {#if revealPassword}
+ <input
+ name="password"
+ type="text"
+ required
+ class="bg-primary"
+ bind:value={$password}
+ autocapitalize="none"
+ />
+ {:else}
+ <input
+ name="password"
+ type="password"
+ required
+ class="bg-primary"
+ bind:value={$password}
+ autocapitalize="none"
+ />
+ {/if}
+ <button
+ type="button"
+ on:click={() => (revealPassword = !revealPassword)}
+ class="absolute right-5 top-10"
+ >
+ <Icon icon={revealPassword ? "eye" : "eye-off"} />
+ </button>
+ </div>
+
+ <div class="flex justify-end items-center">
+ <a href="/forgot" class="underline underline-offset-4 text-black"
+ >{$t("login.forgotUserOrPassword")}</a
+ >
+ </div>
+
+ <button
+ type="submit"
+ class="bg-black text-white w-full rounded-2xl p-4 font-semibold hover:opacity-80"
+ disabled={loading}
+ bind:this={btn}
+ >
+ {#if loading}
+ <Spinner />
+ {:else}
+ {$t("login.signIn")}
+ {/if}
+ </button>
+
+
+ <p class="text-secondary text-center font-medium">
+ {$t("login.noAccount")}
+ <a
+ href={"/register"}
+ class="block md:inline text-black underline underline-offset-4 hover:opacity-80"
+ >
+ {$t("login.register")}
+ </a>
+ </p>
+ </form>
+</div>
diff --git a/src/routes/(static)/register/+page.server.ts b/src/routes/(static)/register/+page.server.ts
new file mode 100644
index 00000000..731530c4
--- /dev/null
+++ b/src/routes/(static)/register/+page.server.ts
@@ -0,0 +1,41 @@
+import { fail, redirect } from "@sveltejs/kit";
+import { fd, post, login } from "$lib/utils";
+
+export const load = async ({ parent }) => {
+ let { user } = await parent();
+ if (user) redirect(307, `/${user.username}`);
+
+ let index = Math.floor(Math.random() * 64) + 1;
+
+ return { index };
+};
+
+export const actions = {
+ default: async ({ cookies, request }) => {
+ let ip = request.headers.get("cf-connecting-ip");
+
+ let form = await fd(request);
+ let { profile, username, password, cipher, salt, pubkey, loginRedirect } =
+ form;
+ let user = { profile, username, password, cipher, salt, pubkey };
+ let error;
+
+ if (loginRedirect === "undefined") loginRedirect = undefined;
+
+ try {
+ await post("/register", { user }, { "cf-connecting-ip": ip });
+ } catch (e: any) {
+ error = e.message;
+ }
+
+ try {
+ await login(user, cookies, ip);
+ error = null;
+ } catch (e: any) {
+ error ||= e.message;
+ }
+
+ if (error) return fail(400, { error });
+ redirect(307, loginRedirect || `/${user.username}`);
+ },
+};
diff --git a/src/routes/(static)/register/+page.svelte b/src/routes/(static)/register/+page.svelte
new file mode 100644
index 00000000..b6a4c44e
--- /dev/null
+++ b/src/routes/(static)/register/+page.svelte
@@ -0,0 +1,292 @@
+<script>
+ import punks from "$lib/punks";
+ import { upload } from "$lib/upload";
+ import { afterNavigate, invalidateAll } from "$app/navigation";
+ import { applyAction, deserialize } from "$app/forms";
+ import { tick } from "svelte";
+ import { fly } from "svelte/transition";
+ import { enhance } from "$app/forms";
+
+ import Pin from "$comp/Pin.svelte";
+ import Icon from "$comp/Icon.svelte";
+ import Spinner from "$comp/Spinner.svelte";
+
+ import { focus, fail } from "$lib/utils";
+ import { avatar, password, loginRedirect } from "$lib/store";
+ import { t } from "$lib/translations";
+ import { page } from "$app/stores";
+ import { generate } from "$lib/nostr";
+ import {
+ NumberDictionary,
+ uniqueNamesGenerator,
+ colors,
+ adjectives,
+ animals,
+ } from "unique-names-generator";
+
+ export let form;
+ export let data;
+
+ $: data && ($avatar = undefined);
+ let username;
+ let { index } = data;
+
+ let cleared;
+ let clear = () => {
+ if (!cleared) {
+ cleared = true;
+ username = "";
+ $password = "";
+ revealPassword = false;
+ }
+ };
+
+ let refresh = async () => {
+ username = uniqueNamesGenerator({
+ dictionaries: [animals, NumberDictionary.generate({ min: 10, max: 99 })],
+ length: 2,
+ separator: "",
+ });
+
+ $password = uniqueNamesGenerator({
+ dictionaries: [colors, NumberDictionary.generate({ min: 100, max: 999 })],
+ length: 2,
+ separator: "",
+ });
+
+ revealPassword = true;
+ };
+
+ afterNavigate(async () => {
+ try {
+ await invalidateAll();
+ } catch (e) {
+ console.log(e);
+ }
+ });
+
+ let token, formElement;
+ let code = [];
+ let redirect;
+
+ $: need2fa = form?.message === "2fa";
+ let cancel = () => (need2fa = false);
+ $: if (form?.message === "2fa" && form.token === token) token = "";
+
+ let email, btn;
+
+ $: update(form);
+ let update = (form) => form && ({ username, password: $password } = form);
+
+ $: token && token?.length === 6 && tick().then(() => btn.click());
+
+ let revealPassword;
+
+ let loading;
+ async function handleSubmit(e) {
+ loading = true;
+
+ let data = new FormData(this);
+ let user = Object.fromEntries(data);
+ await generate(user);
+
+ for (let k in user) {
+ data.set(k, user[k]);
+ }
+
+ data.set("profile", punks[index]);
+ if ($avatar) {
+ try {
+ let { hash } = JSON.parse(
+ await upload($avatar.file, $avatar.type, $avatar.progress, token)
+ );
+
+ data.set("profile", hash);
+ await fetch(`/api/public/${hash}.webp`, {
+ cache: "reload",
+ mode: "no-cors",
+ });
+ } catch (e) {
+ console.log("problem uploading avatar", e);
+ }
+ }
+
+ const response = await fetch("/register", {
+ method: "POST",
+ body: data,
+ });
+
+ const result = deserialize(await response.text());
+
+ if (result.type === "success") {
+ await invalidateAll();
+ }
+
+ applyAction(result);
+ loading = false;
+ }
+
+ let avatarInput;
+ $: src = `/api/public/${punks[index]}.webp`;
+ let decr = () => (index = index <= 0 ? 63 : index - 1);
+ let incr = () => (index = index >= 63 ? 0 : index + 1);
+ let selectAvatar = () => avatarInput.click();
+
+ let progress;
+ let handleFile = async ({ target }) => {
+ let type = "profile";
+ let file = target.files[0];
+ if (!file) return;
+
+ if (file.size > 10000000) form.error = "File too large";
+ $avatar = { file, type, progress };
+
+ var reader = new FileReader();
+ reader.onload = async (e) => {
+ src = e.target.result;
+ };
+
+ reader.readAsDataURL(file);
+ };
+</script>
+
+{#if need2fa}
+ <Pin bind:value={token} title="Enter 2FA Code" {cancel} notify={false} />
+{/if}
+
+<div
+ class="mx-auto md:shadow-xl rounded-3xl max-w-xl w-full md:w-[480px] md:p-8 space-y-5"
+>
+ <h1 class="text-2xl font-bold text-center">{$t("login.createAccount")}</h1>
+ <input
+ type="file"
+ class="hidden"
+ bind:this={avatarInput}
+ on:change={(e) => handleFile(e, "profile")}
+ />
+
+ <div class="relative">
+ <button
+ class="absolute w-8 h-12 left-12 bg-white rounded top-12"
+ on:click={decr}
+ >
+ <Icon icon="chevron-left" style="w-8 " />
+ </button>
+ <button class="block relative w-32 mx-auto" on:click={selectAvatar}>
+ <button
+ class="w-32 h-32 rounded-full border-4 border-white overflow-hidden flex mx-auto relative"
+ >
+ <img
+ {src}
+ class="w-full h-full object-cover object-center overflow-hidden"
+ alt={username}
+ />
+ </button>
+ <button
+ class="absolute bg-white rounded-full p-2 mx-auto right-0 bottom-0 z-10 bg-opacity-80"
+ >
+ <Icon icon="upload" style="w-8" />
+ </button>
+ </button>
+ <button
+ class="absolute w-8 h-12 right-12 bg-white rounded top-12"
+ on:click={incr}
+ >
+ <Icon icon="chevron-left" style="w-8 rotate-180" />
+ </button>
+ </div>
+
+ {#if form?.error}
+ <div class="text-red-600 text-center" in:fly>
+ {form.error}
+ </div>
+ {/if}
+
+ <form class="space-y-5" on:submit|preventDefault={handleSubmit} method="POST">
+ <input
+ type="hidden"
+ name="loginRedirect"
+ value={$loginRedirect || $page.url.searchParams.get("redirect")}
+ />
+ <input type="hidden" name="token" value={token} />
+
+ <div class="relative">
+ <label for="username" class="font-semibold">{$t("login.username")}</label>
+ <input
+ use:focus
+ name="username"
+ type="text"
+ required
+ class="bg-primary"
+ bind:value={username}
+ on:focus={clear}
+ autocapitalize="none"
+ />
+ <button
+ tabindex="-1"
+ type="button"
+ on:click={refresh}
+ class="absolute right-0 top-4"
+ >
+ <Icon icon="random" style="w-10 m-4" />
+ </button>
+ </div>
+
+ <div class="relative">
+ <label for="password" class="block font-semibold"
+ >{$t("login.password")}</label
+ >
+ {#if revealPassword}
+ <input
+ name="password"
+ type="text"
+ required
+ class="bg-primary"
+ bind:value={$password}
+ autocapitalize="none"
+ />
+ {:else}
+ <input
+ name="password"
+ type="password"
+ required
+ class="bg-primary"
+ bind:value={$password}
+ autocapitalize="none"
+ on:focus={clear}
+ />
+ {/if}
+ <button
+ type="button"
+ tabindex="-1"
+ on:click={() => (revealPassword = !revealPassword)}
+ class="absolute right-0 top-4"
+ >
+ <Icon icon={revealPassword ? "eye" : "eye-off"} style="m-6" />
+ </button>
+ </div>
+
+ <button
+ type="submit"
+ class="bg-black text-white w-full rounded-2xl p-4 font-semibold hover:opacity-80"
+ disabled={loading}
+ bind:this={btn}
+ >
+ {#if loading}
+ <Spinner />
+ {:else}
+ {$t("login.register")}
+ {/if}
+ </button>
+
+ <p class="text-secondary text-center font-medium">
+ {$t("login.haveAccount")}
+ <a
+ href={"/login"}
+ class="block md:inline text-black underline underline-offset-4 hover:opacity-80"
+ >
+ {$t("login.signIn")}
+ </a>
+ </p>
+ </form>
+</div>
diff --git a/src/routes/(static)/support/+page.svelte b/src/routes/(static)/support/+page.svelte
new file mode 100644
index 00000000..20e41280
--- /dev/null
+++ b/src/routes/(static)/support/+page.svelte
@@ -0,0 +1,134 @@
+<script>
+ import { browser } from "$app/environment";
+ import { onDestroy } from "svelte";
+ import { fail, success, post } from "$lib/utils";
+ import { scale } from "svelte/transition";
+ import Icon from "$comp/Icon.svelte";
+ import { t } from "$lib/translations";
+
+ export let data;
+
+ let { user } = data;
+ $: if (user) {
+ data.subject = { ...user };
+ }
+
+ let email;
+ let message;
+ let sent;
+ let recaptchaSiteKey = "6LfCd8YkAAAAANmVJgzN3SQY3n3fv1RhiS5PgMYM";
+
+ let submit = (e) => {
+ e.preventDefault();
+ grecaptcha.ready(() => {
+ grecaptcha.execute(recaptchaSiteKey, { action: "submit" }).then((token) =>
+ post("/support", { username: user?.username, email, message, token })
+ .then(() => (sent = true))
+ .catch(() => fail("problem submitting"))
+ );
+ });
+ };
+
+ onDestroy(() => {
+ if (!browser) return;
+ const nodeBadge = document.querySelector(".grecaptcha-badge");
+ if (nodeBadge) {
+ document.body.removeChild(nodeBadge.parentNode);
+ }
+
+ const scriptSelector =
+ "script[src='https://www.google.com/recaptcha/api.js?render=" +
+ recaptchaSiteKey +
+ "']";
+ const script = document.querySelector(scriptSelector);
+ if (script) {
+ script.remove();
+ }
+ });
+</script>
+
+<svelte:head
+ ><script
+ src="https://www.google.com/recaptcha/api.js?render=6LfCd8YkAAAAANmVJgzN3SQY3n3fv1RhiS5PgMYM"
+ ></script></svelte:head
+>
+
+<div
+ class="container max-w-lg mx-auto mt-20 space-y-8 shadow-xl rounded-3xl p-10"
+>
+ {#if sent}
+ <h1 class="text-center text-3xl md:text-4xl font-semibold">Thank you!</h1>
+ <div class="text-center text-lg text-secondary">
+ Someone will be in touch shortly.
+ </div>
+
+ <div>
+ <a href={user ? `/${user.username}` : "/"}>
+ <button
+ class="rounded-full border py-2 px-5 font-bold hover:opacity-80 w-full bg-black text-white"
+ >Ok</button
+ >
+ </a>
+ </div>
+ {:else}
+ <h1 class="text-center text-3xl md:text-4xl font-semibold mb-8">
+ {$t("user.support.header")}
+ </h1>
+
+ <p class="text-secondary text-lg">
+ Fill out this form or email us directly at <a
+ class="underline"
+ > and we'll do our best to get back to you in a timely manner.
+ </p>
+
+ <form on:submit={submit}>
+ <div class="mb-4">
+ <label for="account" class="font-semibold"
+ >{$t("user.support.accountName")}</label
+ >
+ <input
+ class="bg-primary"
+ type="text"
+ name="account"
+ required
+ value={user?.username || ""}
+ />
+ </div>
+
+ <div class="mb-4">
+ <label for="email" class="font-semibold"
+ >{$t("user.support.email")}</label
+ >
+ <input
+ class="bg-primary"
+ type="email"
+ name="email"
+ bind:value={email}
+ required
+ />
+ </div>
+
+ <div class="mb-4">
+ <label for="message" class="font-semibold"
+ >{$t("user.support.message")}</label
+ >
+ <textarea
+ rows={5}
+ class="bg-primary"
+ type="text"
+ name="message"
+ bind:value={message}
+ required
+ />
+ </div>
+
+ <button
+ type="submit"
+ disabled={!email || !message}
+ class="g-recaptcha bg-black text-white font-bold rounded-xl py-3 w-full mx-auto hover:opacity-80"
+ >{$t("user.support.send")}</button
+ >
+ </form>
+ {/if}
+</div>
diff --git a/src/routes/(static)/support/+server.ts b/src/routes/(static)/support/+server.ts
new file mode 100644
index 00000000..e9a16cfc
--- /dev/null
+++ b/src/routes/(static)/support/+server.ts
@@ -0,0 +1,9 @@
+import { json } from "@sveltejs/kit";
+import { auth, post } from "$lib/utils";
+
+export const prerender = false;
+
+export async function POST({ cookies, request }) {
+ let result = await post("/email", await request.json(), auth(cookies));
+ return json(result);
+}