aboutsummaryrefslogtreecommitdiff
path: root/web/ui/src/routes/dashboard/account/+page.svelte
blob: 287c2104f34302aed33f3619ee6139c5f4a34177 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
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>