aboutsummaryrefslogtreecommitdiff
path: root/web/ui/src/routes/dashboard/account/+page.svelte
diff options
context:
space:
mode:
authorbenj <benj@rse8.com>2026-04-10 11:13:34 +0800
committerbenj <benj@rse8.com>2026-04-10 11:13:34 +0800
commit493746b14c1251a45b061d2e3edd9160c929d2b9 (patch)
tree1607cceb94c1aac1a17a01bb5c0d71b97342e892 /web/ui/src/routes/dashboard/account/+page.svelte
parentc041641634650c31e03c70dcad132fd94cb08e63 (diff)
downloadtidyindex-493746b14c1251a45b061d2e3edd9160c929d2b9.tar
tidyindex-493746b14c1251a45b061d2e3edd9160c929d2b9.tar.gz
tidyindex-493746b14c1251a45b061d2e3edd9160c929d2b9.tar.bz2
tidyindex-493746b14c1251a45b061d2e3edd9160c929d2b9.tar.lz
tidyindex-493746b14c1251a45b061d2e3edd9160c929d2b9.tar.xz
tidyindex-493746b14c1251a45b061d2e3edd9160c929d2b9.tar.zst
tidyindex-493746b14c1251a45b061d2e3edd9160c929d2b9.zip
a basic ui and landing web interface for tidyindex.com
Diffstat (limited to 'web/ui/src/routes/dashboard/account/+page.svelte')
-rw-r--r--web/ui/src/routes/dashboard/account/+page.svelte238
1 files changed, 238 insertions, 0 deletions
diff --git a/web/ui/src/routes/dashboard/account/+page.svelte b/web/ui/src/routes/dashboard/account/+page.svelte
new file mode 100644
index 0000000..287c210
--- /dev/null
+++ b/web/ui/src/routes/dashboard/account/+page.svelte
@@ -0,0 +1,238 @@
+<script lang="ts">
+ import { tick } from 'svelte';
+ import { enhance } from '$app/forms';
+ import { afterNavigate, invalidateAll } from '$app/navigation';
+ import { pushToast } from '$lib/stores/toasts';
+ import { PLANS } from '$lib/plans';
+ import type { PageData, ActionData } from './$types';
+
+ let { data, form }: { data: PageData; form: ActionData } = $props();
+
+ let confirmDelete = $state(false);
+
+ // When arriving via "Sign in" (which links to .../account?focus=email),
+ // drop the cursor straight into the email field. preventScroll keeps the
+ // page from jumping when focus() is called.
+ afterNavigate(async (nav) => {
+ if (nav.to?.url.searchParams.get('focus') !== 'email') return;
+ await tick();
+ document.getElementById('email')?.focus({ preventScroll: true });
+ });
+
+ $effect(() => {
+ if (!form) return;
+ if ('linkSent' in form && form.linkSent) {
+ pushToast(`Sign-in link sent to ${form.email}`, 'success');
+ invalidateAll();
+ } else if ('cancelled' in form && form.cancelled) {
+ pushToast('Sign-in cancelled', 'success');
+ invalidateAll();
+ } else if ('verified' in form && form.verified) {
+ pushToast('Signed in', 'success');
+ invalidateAll();
+ } else if ('switchedTo' in form && form.switchedTo) {
+ pushToast(`Switched to ${PLANS[form.switchedTo].name}`, 'success');
+ invalidateAll();
+ } else if ('error' in form && form.error) {
+ pushToast(form.error, 'error');
+ }
+ });
+</script>
+
+<p class="section-marker">§ 03 &nbsp;&middot;&nbsp; account</p>
+<h1 class="page-title">Account.</h1>
+<p class="page-subtitle">
+ {#if data.account.email}
+ Manage your sign-in and review what we know about you.
+ {:else if data.account.pending_email}
+ Finish signing in to lock your keys to this email.
+ {:else}
+ Sign in with email so your keys follow you across browsers.
+ {/if}
+</p>
+
+<div class="card">
+ <div class="card-head">
+ <h2 class="card-title">Sign in</h2>
+ </div>
+
+ {#if data.account.email}
+ <!-- Signed in -->
+ <p class="card-sub">
+ Signed in as <strong>{data.account.email}</strong>.
+ </p>
+ <form method="POST" action="/auth/logout" class="mt-16">
+ <button type="submit" class="btn btn-ghost btn-sm">Sign out</button>
+ </form>
+ {:else if data.account.pending_email}
+ <!-- Pending verification -->
+ <p class="card-sub">
+ We sent a sign-in link to
+ <strong>{data.account.pending_email}</strong>. Click it from your inbox
+ to finish signing in.
+ </p>
+ <div class="row mt-16">
+ <form method="POST" action="?/requestSignInLink" use:enhance>
+ <input type="hidden" name="email" value={data.account.pending_email} />
+ <button type="submit" class="btn btn-ghost btn-sm">Resend link</button>
+ </form>
+ <form method="POST" action="?/cancelPendingSignIn" use:enhance>
+ <button type="submit" class="btn btn-ghost btn-sm">Cancel</button>
+ </form>
+ <form method="POST" action="?/markVerified" use:enhance>
+ <button type="submit" class="btn btn-accent btn-sm" title="Dev shortcut: skips real verification">
+ (dev) Mark verified
+ </button>
+ </form>
+ </div>
+ {:else}
+ <!-- Anonymous -->
+ <p class="card-sub">
+ You're using an anonymous session. Sign in with an email so you don't
+ lose access to your keys if you clear cookies or switch browsers.
+ </p>
+ <form method="POST" action="?/requestSignInLink" use:enhance class="mt-16">
+ <div class="field">
+ <label class="field-label" for="email">email address</label>
+ <input
+ id="email"
+ name="email"
+ type="email"
+ class="input"
+ placeholder="you@company.com"
+ required
+ />
+ </div>
+ <button type="submit" class="btn btn-accent btn-sm">
+ Send sign-in link
+ </button>
+ </form>
+ {/if}
+</div>
+
+<div class="card mt-24">
+ <div class="card-head">
+ <h2 class="card-title">Plan</h2>
+ </div>
+ <p class="card-sub">
+ Usage resets on the first of every month. You can change plans any time, and
+ we prorate mid-cycle. (Billing is stubbed in this demo — switching plans just
+ updates the dashboard.)
+ </p>
+
+ <div class="plan-grid mt-16">
+ {#each data.plans as plan}
+ {@const isCurrent = plan.id === data.currentPlan}
+ <div class="plan-card {isCurrent ? 'current' : ''}">
+ {#if isCurrent}
+ <span class="badge">Current</span>
+ {/if}
+
+ <p class="plan-name">{plan.name}</p>
+ <p class="plan-price">
+ {plan.priceLabel}
+ {#if plan.period}<small>&nbsp;{plan.period}</small>{/if}
+ </p>
+
+ <ul class="plan-features">
+ {#each plan.features as f}
+ <li>{f}</li>
+ {/each}
+ </ul>
+
+ <div class="plan-cta">
+ {#if plan.id === 'enterprise'}
+ <a
+ href="mailto:contact@tidyindex.com?subject=Enterprise%20inquiry"
+ class="btn btn-ghost"
+ style="width: 100%;"
+ >
+ Contact us
+ </a>
+ {:else if isCurrent}
+ <button class="btn btn-ghost" style="width: 100%;" disabled>
+ Current plan
+ </button>
+ {:else}
+ <form method="POST" action="?/switchPlan" use:enhance>
+ <input type="hidden" name="plan" value={plan.id} />
+ <button
+ class="btn {plan.id === 'pro' ? 'btn-accent' : 'btn-ghost'}"
+ style="width: 100%;"
+ type="submit"
+ >
+ {plan.cta}
+ </button>
+ </form>
+ {/if}
+ </div>
+ </div>
+ {/each}
+ </div>
+
+ <p class="help mt-24">
+ Need something not listed here — custom datasets, on-prem deployment, higher
+ rate limits? Reply to any email from us, or reach out at
+ <a href="mailto:contact@tidyindex.com">contact@tidyindex.com</a>.
+ </p>
+</div>
+
+<div class="card mt-24">
+ <div class="card-head">
+ <h2 class="card-title">What we know about you</h2>
+ </div>
+ <dl class="usage-table" style="display:block;">
+ <div class="row-between" style="padding:10px 0; border-bottom:1px solid var(--c-rule);">
+ <dt class="text-mono text-mute">account id</dt>
+ <dd class="text-mono" style="margin:0;">{data.account.id}</dd>
+ </div>
+ <div class="row-between" style="padding:10px 0; border-bottom:1px solid var(--c-rule);">
+ <dt class="text-mono text-mute">plan</dt>
+ <dd class="text-mono" style="margin:0;">{data.account.plan}</dd>
+ </div>
+ <div class="row-between" style="padding:10px 0; border-bottom:1px solid var(--c-rule);">
+ <dt class="text-mono text-mute">created</dt>
+ <dd class="text-mono" style="margin:0;">
+ {new Date(data.account.created_at).toLocaleDateString('en-US', {
+ month: 'short',
+ day: 'numeric',
+ year: 'numeric'
+ })}
+ </dd>
+ </div>
+ <div class="row-between" style="padding:10px 0;">
+ <dt class="text-mono text-mute">keys on file</dt>
+ <dd class="text-mono" style="margin:0;">{data.keyCount}</dd>
+ </div>
+ </dl>
+</div>
+
+<div class="danger-zone">
+ <p class="danger-zone-title">Danger zone</p>
+ <p class="danger-zone-body">
+ Deleting your account will revoke every key and permanently erase your
+ usage history. This cannot be undone.
+ </p>
+ {#if confirmDelete}
+ <div class="row">
+ <form method="POST" action="?/deleteAccount" use:enhance>
+ <button type="submit" class="btn btn-danger btn-sm">
+ Yes, delete everything
+ </button>
+ </form>
+ <button
+ class="btn btn-ghost btn-sm"
+ onclick={() => (confirmDelete = false)}
+ >
+ Cancel
+ </button>
+ </div>
+ {:else}
+ <button
+ class="btn btn-danger btn-sm"
+ onclick={() => (confirmDelete = true)}
+ >
+ Delete account
+ </button>
+ {/if}
+</div>