also looking at this
Promote main to production: signed-out chrome and the subscriber tables
#17
54 files
+4252
-1353
Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.
| @@ -30,6 +30,11 @@ function fmtDate(v: string | null | undefined): string { | ||
| 30 | 30 | |
| 31 | 31 | // Derived reads. Plain functions rather than computed(): each is called from a |
| 32 | 32 | // binding, which re-evaluates when the signal it reads changes. |
| 33 | // Signed in / signed out, decided only once /api/me has answered. Both are | |
| 34 | // false while `loaded` is still false, so the page shows its skeleton rather | |
| 35 | // than briefly claiming the visitor is signed out. | |
| 36 | const signedIn = (): boolean => loaded() && !!user() | |
| 37 | const signedOut = (): boolean => loaded() && !user() | |
| 33 | 38 | const displayName = (): string => user()?.name || 'Your account' |
| 34 | 39 | const email = (): string => user()?.email || '' |
| 35 | 40 | // A remote avatar can 404 — a revoked GitHub account, a Gravatar that was |
| @@ -110,8 +115,13 @@ onMount(() => { | ||
| 110 | 115 | <header class="flex items-center justify-between mx-auto px-5 md:px-6 h-16 max-w-2xl"> |
| 111 | 116 | <StxLink to="/" class="text-lg wordmark">loghq</StxLink> |
| 112 | 117 | <div class="flex gap-2.5 items-center"> |
| 113 | <StxLink to="/dashboard" class="px-3.5 py-2 font-semibold text-ink text-sm border border-line rounded-[10px] hover:border-[color-mix(in_srgb,var(--accent)_45%,var(--border))] active:translate-y-px [transition:transform_0.12s_ease,opacity_0.15s_ease,background-color_0.15s_ease]">Dashboard</StxLink> | |
| 114 | <button @click="logout" class="px-3.5 py-2 font-semibold text-ink text-sm border border-line rounded-[10px] hover:border-[color-mix(in_srgb,var(--accent)_45%,var(--border))] active:translate-y-px [transition:transform_0.12s_ease,opacity_0.15s_ease,background-color_0.15s_ease]">Log out</button> | |
| 118 | {{-- This page has no server auth block: the viewer arrives from /api/me | |
| 119 | after paint, so these are bound rather than branched. Ungated, a | |
| 120 | signed-out visitor was offered "Log out", which logs nobody out, above | |
| 121 | a profile card of empty dashes. --}} | |
| 122 | <StxLink to="/dashboard" class="px-3.5 py-2 font-semibold text-ink text-sm border border-line rounded-[10px] hover:border-[color-mix(in_srgb,var(--accent)_45%,var(--border))] active:translate-y-px [transition:transform_0.12s_ease,opacity_0.15s_ease,background-color_0.15s_ease]" :show="signedIn()">Dashboard</StxLink> | |
| 123 | <button @click="logout" class="px-3.5 py-2 font-semibold text-ink text-sm border border-line rounded-[10px] hover:border-[color-mix(in_srgb,var(--accent)_45%,var(--border))] active:translate-y-px [transition:transform_0.12s_ease,opacity_0.15s_ease,background-color_0.15s_ease]" :show="signedIn()">Log out</button> | |
| 124 | <StxLink to="/login" class="px-3.5 py-2 font-semibold text-accent-ink text-sm bg-accent hover:bg-[color-mix(in_srgb,var(--accent)_88%,#000)] rounded-[10px] active:translate-y-px [transition:transform_0.12s_ease,opacity_0.15s_ease,background-color_0.15s_ease]" :show="signedOut()">Sign in</StxLink> | |
| 115 | 125 | </div> |
| 116 | 126 | </header> |
| 117 | 127 | |
| @@ -123,7 +133,16 @@ onMount(() => { | ||
| 123 | 133 | <div class="mx-auto px-5 py-8 md:px-6 max-w-2xl"> |
| 124 | 134 | <h1 class="mb-6 text-2xl wordmark">Account</h1> |
| 125 | 135 | |
| 126 | <section class="mb-5 p-6 bg-panel border border-line rounded-[14px]"> | |
| 136 | {{-- The signed-out resting state. Without it this page sat on a skeleton | |
| 137 | name, an empty email and a Free plan badge for a visitor who has no | |
| 138 | account loaded at all, which reads as a broken profile rather than as a | |
| 139 | page that needs signing in to. --}} | |
| 140 | <div class="py-24 text-center" :show="signedOut()"> | |
| 141 | <p class="mb-4 text-sm text-subtle">You're signed out.</p> | |
| 142 | <StxLink to="/login" class="inline-block px-4 py-2.5 font-semibold text-accent-ink text-sm bg-accent hover:bg-[color-mix(in_srgb,var(--accent)_88%,#000)] rounded-[10px]">Sign in</StxLink> | |
| 143 | </div> | |
| 144 | ||
| 145 | <section class="mb-5 p-6 bg-panel border border-line rounded-[14px]" :show="!signedOut()"> | |
| 127 | 146 | <div class="flex gap-4 items-center"> |
| 128 | 147 | {{-- The initials block is the fallback for BOTH "no avatar" and "the |
| 129 | 148 | avatar failed to load", so the two :show conditions are exact |
| @@ -151,7 +170,7 @@ onMount(() => { | ||
| 151 | 170 | </div> |
| 152 | 171 | </section> |
| 153 | 172 | |
| 154 | <section class="mb-5 bg-panel border border-line rounded-[14px]"> | |
| 173 | <section class="mb-5 bg-panel border border-line rounded-[14px]" :show="!signedOut()"> | |
| 155 | 174 | <div class="flex items-center justify-between px-6 py-4"> |
| 156 | 175 | <div> |
| 157 | 176 | <div class="font-mono text-subtle text-xs tracking-wide uppercase">Plan</div> |
| @@ -183,7 +202,7 @@ onMount(() => { | ||
| 183 | 202 | </div> |
| 184 | 203 | </section> |
| 185 | 204 | |
| 186 | <p class="text-sm text-subtle"> | |
| 205 | <p class="text-sm text-subtle" :show="!signedOut()"> | |
| 187 | 206 | Signed in with a private token. <StxLink to="/" class="text-accent">Back to site</StxLink> |
| 188 | 207 | </p> |
| 189 | 208 | </div> |
| @@ -243,9 +243,16 @@ onMounted(() => { | ||
| 243 | 243 | <div class="grain" aria-hidden="true"></div> |
| 244 | 244 | |
| 245 | 245 | <div class="px-5 py-10 md:px-10 min-h-[100dvh] page-dashboard"> |
| 246 | <StxLink class="chrome-upgrade" to="/pricing" :show="!pro()">Upgrade</StxLink> | |
| 247 | <span class="chrome-pro chrome-upgrade" :show="pro()">Pro</span> | |
| 248 | <button class="chrome-logout" @click="signOut">Log out</button> | |
| 246 | {{-- The whole chrome belongs to a signed-in viewer. Ungated, a signed-out | |
| 247 | visitor got a "Log out" button that logs nobody out and an "Upgrade" call | |
| 248 | to action, floating over the "You're signed out" panel below. `user` is | |
| 249 | resolved server-side from the auth-token cookie, so this is decided before | |
| 250 | paint and there is no flash of the wrong chrome. --}} | |
| 251 | @if (user) | |
| 252 | <StxLink class="chrome-upgrade" to="/pricing" :show="!pro()">Upgrade</StxLink> | |
| 253 | <span class="chrome-pro chrome-upgrade" :show="pro()">Pro</span> | |
| 254 | <button class="chrome-logout" @click="signOut">Log out</button> | |
| 255 | @endif | |
| 249 | 256 | |
| 250 | 257 | <div class="mx-auto max-w-5xl"> |
| 251 | 258 | |
| @@ -647,9 +647,16 @@ onMount(() => { | ||
| 647 | 647 | {{-- Fixed-position chrome. Inside the page container, not document.body, so an |
| 648 | 648 | SPA view swap takes it with the rest of the page. Position is fixed, so |
| 649 | 649 | where it sits in the DOM does not affect layout. --}} |
| 650 | <StxLink class="chrome-upgrade" to="/pricing" :show="!pro()">Upgrade</StxLink> | |
| 651 | <span class="chrome-pro chrome-upgrade" :show="pro()">Pro</span> | |
| 652 | <button class="chrome-logout" @click="signOut">Log out</button> | |
| 650 | {{-- The whole chrome belongs to a signed-in viewer. Ungated, a signed-out | |
| 651 | visitor got a "Log out" button that logs nobody out and an "Upgrade" call | |
| 652 | to action, floating over the "You're signed out" panel below. `user` is | |
| 653 | resolved server-side from the auth-token cookie, so this is decided before | |
| 654 | paint and there is no flash of the wrong chrome. --}} | |
| 655 | @if (user) | |
| 656 | <StxLink class="chrome-upgrade" to="/pricing" :show="!pro()">Upgrade</StxLink> | |
| 657 | <span class="chrome-pro chrome-upgrade" :show="pro()">Pro</span> | |
| 658 | <button class="chrome-logout" @click="signOut">Log out</button> | |
| 659 | @endif | |
| 653 | 660 | |
| 654 | 661 | <div class="mx-auto max-w-5xl"> |
| 655 | 662 | |
| @@ -120,15 +120,28 @@ function logout(): void { | ||
| 120 | 120 | <header class="flex gap-4 items-center justify-between mb-9"> |
| 121 | 121 | <div> |
| 122 | 122 | <h1 class="leading-none text-2xl md:text-[28px] wordmark">Your apps</h1> |
| 123 | <p class="mt-2 text-sm text-subtle">Pick an app to track, or create a new one.</p> | |
| 123 | {{-- The subtitle invites an action, so it only belongs to someone who can | |
| 124 | take it. Signed out, the same line read "Pick an app to track, or | |
| 125 | create a new one." above a panel saying the visitor was signed out. --}} | |
| 126 | @if (user) | |
| 127 | <p class="mt-2 text-sm text-subtle">Pick an app to track, or create a new one.</p> | |
| 128 | @else | |
| 129 | <p class="mt-2 text-sm text-subtle">Sign in to see the apps you track.</p> | |
| 130 | @endif | |
| 124 | 131 | </div> |
| 125 | 132 | <div class="flex gap-2.5 items-center"> |
| 126 | 133 | <button class="inline-flex items-center justify-center h-9 w-9 text-subtle hover:text-ink bg-panel border border-line rounded-[10px] hover:border-[color-mix(in_srgb,var(--accent)_45%,var(--border))] cursor-pointer [transition:color_0.15s_ease,border-color_0.15s_ease] theme-btn" @click="toggleTheme" x-tooltip="Toggle theme" aria-label="Toggle theme"> |
| 127 | 134 | <Icon name="lucide:sun" size="18" class="h-[18px] w-[18px] ic-sun" /> |
| 128 | 135 | <Icon name="lucide:moon" size="18" class="h-[18px] w-[18px] ic-moon" /> |
| 129 | 136 | </button> |
| 130 | <StxLink to="/projects/new" class="px-4 py-2 font-semibold text-accent-ink text-sm bg-accent hover:bg-[color-mix(in_srgb,var(--accent)_88%,#000)] rounded-[10px]">+ New app</StxLink> | |
| 131 | <button @click="logout" class="px-3.5 py-2 font-medium text-ink text-sm bg-panel border border-line rounded-[10px] hover:border-[color-mix(in_srgb,var(--accent)_45%,var(--border))]">Log out</button> | |
| 137 | {{-- Both of these are signed-in affordances and neither was guarded, so a | |
| 138 | signed-out visitor was offered "+ New app", which bounces straight back | |
| 139 | to the login wall, and "Log out", which logs out nobody. The theme | |
| 140 | toggle above stays: it works for anyone. --}} | |
| 141 | @if (user) | |
| 142 | <StxLink to="/projects/new" class="px-4 py-2 font-semibold text-accent-ink text-sm bg-accent hover:bg-[color-mix(in_srgb,var(--accent)_88%,#000)] rounded-[10px]">+ New app</StxLink> | |
| 143 | <button @click="logout" class="px-3.5 py-2 font-medium text-ink text-sm bg-panel border border-line rounded-[10px] hover:border-[color-mix(in_srgb,var(--accent)_45%,var(--border))]">Log out</button> | |
| 144 | @endif | |
| 132 | 145 | </div> |
| 133 | 146 | </header> |
| 134 | 147 | |