diff options
Diffstat (limited to 'web/ui/src/routes/dashboard/account/+page.svelte')
| -rw-r--r-- | web/ui/src/routes/dashboard/account/+page.svelte | 238 |
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 · 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> {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> |
