feat(ui): redesign account page and style CLI callback page
- Add page header, card-wrapped sections, and surface-alt item cards to the account page for visual consistency with the rest of the app - Style CLI callback page with centered icon + heading layout matching the login page, with dark-mode-safe colors - Add dark mode variants to all error/success alerts on account page - Add check_circle icon for CLI callback success state
This commit is contained in:
parent
db786c8387
commit
c4914d6ec6
3 changed files with 229 additions and 196 deletions
|
|
@ -1,24 +1,39 @@
|
|||
{% extends "base.html" %} {% import "partials/icons.html" as icons %}
|
||||
{% block title %}Brewlog · Admin{% endblock %}
|
||||
{% block title %}Brewlog · Account{% endblock %}
|
||||
{% block head %}
|
||||
<script src="/webauthn.js"></script>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<!-- Passkeys -->
|
||||
<section data-signals:_show-passkey-form="false">
|
||||
<h2 class="text-lg font-semibold text-accent mb-3">Passkeys</h2>
|
||||
<header class="flex flex-col gap-2">
|
||||
<h1 class="text-3xl font-semibold">Account</h1>
|
||||
<p class="max-w-2xl text-sm text-text-secondary">Manage your passkeys, API tokens, and data.</p>
|
||||
</header>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
<!-- Passkeys -->
|
||||
<section data-signals:_show-passkey-form="false" class="rounded-lg border bg-surface p-5">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-text">Passkeys</h2>
|
||||
<p class="mt-1 text-sm text-text-secondary">Passkeys let you sign in securely without a password.</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
{% for passkey in passkeys %}
|
||||
<div class="rounded-lg border bg-surface p-4 flex flex-col justify-between">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div class="flex items-center justify-between gap-4 rounded-md bg-surface-alt px-4 py-3">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
{% call icons::key("h-4 w-4 text-accent shrink-0") %}
|
||||
<div class="min-w-0">
|
||||
<span class="block font-semibold text-text">{% call icons::key("inline h-4 w-4 mr-1 text-accent") %}{{ passkey.name }}</span>
|
||||
<span class="block text-sm font-semibold text-text">{{ passkey.name }}</span>
|
||||
<span class="block text-xs text-text-muted">
|
||||
Added {{ passkey.created_at }}
|
||||
· {% if let Some(last_used) = passkey.last_used_at %}Last used {{ last_used }}{% else %}Never used{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{% if passkeys.len() > 1 %}
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 rounded-md p-1 text-text-muted transition hover:text-red-600"
|
||||
class="shrink-0 inline-flex h-8 w-8 items-center justify-center rounded-md p-1 text-text-muted transition hover:text-red-600"
|
||||
onclick="deletePasskey({{ passkey.id }}, '{{ passkey.name }}')"
|
||||
aria-label="Delete passkey"
|
||||
>
|
||||
|
|
@ -26,26 +41,17 @@
|
|||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="mt-2 space-y-0.5 text-sm text-text-muted">
|
||||
<p>Added {{ passkey.created_at }}</p>
|
||||
{% if let Some(last_used) = passkey.last_used_at %}
|
||||
<p>Last used {{ last_used }}</p>
|
||||
{% else %}
|
||||
<p>Never used</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if passkeys.len() <= 1 %}
|
||||
<p class="mt-2 text-xs text-text-muted">Add another passkey before removing your only one.</p>
|
||||
<p class="text-xs text-text-muted">Add another passkey before removing your only one.</p>
|
||||
{% endif %}
|
||||
|
||||
<!-- Add passkey form (hidden by default) -->
|
||||
<div id="add-passkey-form" class="mt-3 rounded-lg border bg-surface p-5"
|
||||
<!-- Add passkey form -->
|
||||
<div id="add-passkey-form" class="rounded-md border bg-surface-alt p-4"
|
||||
data-show="$_showPasskeyForm" style="display: none">
|
||||
<div id="add-passkey-error" class="mb-3 hidden rounded-md bg-red-100 border border-red-300 p-2 text-sm text-red-800"></div>
|
||||
<div id="add-passkey-error" class="mb-3 hidden rounded-md bg-red-100 border border-red-300 p-2 text-sm text-red-800 dark:bg-red-950/40 dark:border-red-800 dark:text-red-300"></div>
|
||||
<div class="flex items-end gap-3">
|
||||
<label class="flex-1 flex flex-col gap-1 text-sm">
|
||||
<span class="text-text">Passkey Name</span>
|
||||
|
|
@ -79,63 +85,65 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
data-show="!$_showPasskeyForm"
|
||||
data-on:click="$_showPasskeyForm = true; setTimeout(() => document.getElementById('passkey-name').focus(), 50)"
|
||||
class="mt-3 rounded-md bg-accent px-4 py-2 text-sm font-medium text-accent-text transition hover:bg-accent-hover"
|
||||
class="rounded-md bg-accent px-4 py-2 text-sm font-medium text-accent-text transition hover:bg-accent-hover"
|
||||
>
|
||||
Add Passkey
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- API Tokens -->
|
||||
<section
|
||||
class="rounded-lg border bg-surface p-5"
|
||||
data-signals:_show-token-form="false"
|
||||
data-signals:_creating-token="false"
|
||||
data-signals:_token-created="false"
|
||||
data-signals:_token-value="''"
|
||||
data-signals:_token-error="''"
|
||||
>
|
||||
<h2 class="text-lg font-semibold text-accent mb-3">API Tokens</h2>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-text">API Tokens</h2>
|
||||
<p class="mt-1 text-sm text-text-secondary">Use tokens to authenticate the CLI or REST API.</p>
|
||||
</div>
|
||||
|
||||
{% if tokens.is_empty() %}
|
||||
<p class="text-sm text-text-muted">No active tokens. Create one to use the CLI or API.</p>
|
||||
<p class="text-sm text-text-muted">No active tokens.</p>
|
||||
{% else %}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
<div class="flex flex-col gap-2">
|
||||
{% for token in tokens %}
|
||||
<div class="rounded-lg border bg-surface p-4 flex flex-col justify-between">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div class="flex items-center justify-between gap-4 rounded-md bg-surface-alt px-4 py-3">
|
||||
<div class="min-w-0">
|
||||
<span class="block font-semibold text-text">{{ token.name }}</span>
|
||||
<span class="block text-sm font-semibold text-text">{{ token.name }}</span>
|
||||
<span class="block text-xs text-text-muted">
|
||||
Created {{ token.created_at }}
|
||||
· {% if let Some(last_used) = token.last_used_at %}Last used {{ last_used }}{% else %}Never used{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 rounded-md p-1 text-text-muted transition hover:text-red-600"
|
||||
class="shrink-0 inline-flex h-8 w-8 items-center justify-center rounded-md p-1 text-text-muted transition hover:text-red-600"
|
||||
onclick="revokeToken({{ token.id }}, '{{ token.name }}')"
|
||||
aria-label="Revoke token"
|
||||
>
|
||||
{% call icons::delete("h-4 w-4") %}
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2 space-y-0.5 text-sm text-text-muted">
|
||||
<p>Created {{ token.created_at }}</p>
|
||||
{% if let Some(last_used) = token.last_used_at %}
|
||||
<p>Last used {{ last_used }}</p>
|
||||
{% else %}
|
||||
<p>Never used</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Create token form -->
|
||||
<div class="mt-3 rounded-lg border bg-surface p-5"
|
||||
<div class="rounded-md border bg-surface-alt p-4"
|
||||
data-show="$_showTokenForm && !$_tokenCreated" style="display: none">
|
||||
<p data-show="$_tokenError" data-text="$_tokenError" style="display: none"
|
||||
class="mb-3 rounded-md bg-red-100 border border-red-300 p-2 text-sm text-red-800"></p>
|
||||
class="mb-3 rounded-md bg-red-100 border border-red-300 p-2 text-sm text-red-800 dark:bg-red-950/40 dark:border-red-800 dark:text-red-300"></p>
|
||||
<form
|
||||
data-on:submit="$_creatingToken = true; $_tokenError = ''; @post('/api/v1/tokens', {contentType: 'form'})"
|
||||
data-on:datastar-fetch="if (!$_creatingToken) return;
|
||||
|
|
@ -173,7 +181,7 @@
|
|||
|
||||
<!-- One-time token display -->
|
||||
<div data-show="$_tokenCreated" style="display: none"
|
||||
class="mt-3 rounded-lg border border-green-300 dark:border-green-800 bg-green-50 dark:bg-green-950/40 p-5">
|
||||
class="rounded-md border border-green-300 dark:border-green-800 bg-green-50 dark:bg-green-950/40 p-4">
|
||||
<p class="text-sm font-medium text-green-800 dark:text-green-300">Token created! Copy it now — you won't see it again.</p>
|
||||
<div class="mt-3 flex items-center gap-2">
|
||||
<code id="token-value" data-text="$_tokenValue"
|
||||
|
|
@ -195,20 +203,26 @@
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
data-show="!$_showTokenForm && !$_tokenCreated"
|
||||
data-on:click="$_showTokenForm = true"
|
||||
class="mt-3 rounded-md bg-accent px-4 py-2 text-sm font-medium text-accent-text transition hover:bg-accent-hover"
|
||||
class="rounded-md bg-accent px-4 py-2 text-sm font-medium text-accent-text transition hover:bg-accent-hover"
|
||||
>
|
||||
Create Token
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Data -->
|
||||
<section>
|
||||
<h2 class="text-lg font-semibold text-accent mb-3">Data</h2>
|
||||
<p class="text-sm text-text-muted mb-3">Export all coffee data as JSON, or restore from a previous backup.</p>
|
||||
<section class="rounded-lg border bg-surface p-5">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-text">Data</h2>
|
||||
<p class="mt-1 text-sm text-text-secondary">Export all coffee data as JSON, or restore from a previous backup.</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<a
|
||||
|
|
@ -220,7 +234,7 @@
|
|||
</a>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md border bg-surface px-4 py-2 text-sm font-medium text-text-secondary transition hover:bg-surface-alt hover:text-text"
|
||||
class="rounded-md border bg-surface-alt px-4 py-2 text-sm font-medium text-text-secondary transition hover:bg-surface-alt hover:text-text"
|
||||
onclick="document.getElementById('restore-file-input').click()"
|
||||
>
|
||||
Restore from Backup
|
||||
|
|
@ -230,28 +244,32 @@
|
|||
<input type="file" id="restore-file-input" accept=".json" class="hidden"
|
||||
onchange="restoreFromFile(this)" />
|
||||
|
||||
<div id="backup-status" class="mt-3 hidden rounded-lg border border-green-300 bg-green-50 p-4 text-sm text-green-800"></div>
|
||||
<div id="backup-error" class="mt-3 hidden rounded-md bg-red-100 border border-red-300 p-3 text-sm text-red-800"></div>
|
||||
<div id="backup-status" class="hidden rounded-md border border-green-300 bg-green-50 p-4 text-sm text-green-800 dark:bg-green-950/40 dark:border-green-800 dark:text-green-300"></div>
|
||||
<div id="backup-error" class="hidden rounded-md bg-red-100 border border-red-300 p-3 text-sm text-red-800 dark:bg-red-950/40 dark:border-red-800 dark:text-red-300"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- OpenRouter Usage -->
|
||||
<!-- AI Usage -->
|
||||
{% if let Some(usage) = ai_usage %}
|
||||
<section>
|
||||
<h2 class="text-lg font-semibold text-accent mb-3">OpenRouter Usage</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<div class="rounded-lg border bg-surface p-4">
|
||||
<section class="rounded-lg border bg-surface p-5">
|
||||
<div class="flex flex-col gap-4">
|
||||
<h2 class="text-lg font-semibold text-text">AI Usage</h2>
|
||||
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<span class="block text-sm text-text-muted">Total Cost</span>
|
||||
<span class="mt-1 block text-lg font-semibold text-text">{{ usage.cost }}</span>
|
||||
</div>
|
||||
<div class="rounded-lg border bg-surface p-4">
|
||||
<div>
|
||||
<span class="block text-sm text-text-muted">API Calls</span>
|
||||
<span class="mt-1 block text-lg font-semibold text-text">{{ usage.calls }}</span>
|
||||
</div>
|
||||
<div class="rounded-lg border bg-surface p-4">
|
||||
<div>
|
||||
<span class="block text-sm text-text-muted">Total Tokens</span>
|
||||
<span class="mt-1 block text-lg font-semibold text-text">{{ usage.tokens }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,23 +1,32 @@
|
|||
{% extends "base.html" %}
|
||||
{% extends "base.html" %} {% import "partials/icons.html" as icons %}
|
||||
{% block title %}Brewlog · CLI Authentication{% endblock %}
|
||||
{% block content %}
|
||||
<div class="mx-auto max-w-md">
|
||||
<div class="rounded-lg border bg-surface p-6">
|
||||
{% if token.is_some() %}
|
||||
<h1 class="text-2xl font-semibold text-green-700">Authenticated</h1>
|
||||
<p class="mt-2 text-sm text-text-secondary">
|
||||
Your CLI has been authenticated. You can close this window.
|
||||
<div class="flex flex-col items-center text-center gap-3">
|
||||
{% call icons::check_circle("h-10 w-10 text-emerald-600 dark:text-emerald-400") %}
|
||||
<h1 class="text-2xl font-semibold text-accent">CLI Authenticated</h1>
|
||||
<p class="text-sm text-text-secondary">
|
||||
Your CLI has been authenticated successfully. You can close this window and return to the terminal.
|
||||
</p>
|
||||
</div>
|
||||
{% else if error.is_some() %}
|
||||
<h1 class="text-2xl font-semibold text-red-700">Authentication Failed</h1>
|
||||
<div class="mt-4 rounded-md bg-red-100 border border-red-300 p-3 text-sm text-red-800">
|
||||
<div class="flex flex-col items-center text-center gap-3">
|
||||
{% call icons::x_circle("h-10 w-10 text-red-600 dark:text-red-400") %}
|
||||
<h1 class="text-2xl font-semibold text-accent">Authentication Failed</h1>
|
||||
<div class="w-full rounded-md bg-red-100 border border-red-300 p-3 text-sm text-red-800 dark:bg-red-950/40 dark:border-red-800 dark:text-red-300">
|
||||
{{ error.as_ref().unwrap() }}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="flex flex-col items-center text-center gap-3">
|
||||
{% call icons::spinner("h-10 w-10") %}
|
||||
<h1 class="text-2xl font-semibold text-accent">CLI Authentication</h1>
|
||||
<p class="mt-2 text-sm text-text-secondary">
|
||||
Processing authentication...
|
||||
<p class="text-sm text-text-secondary">
|
||||
Processing authentication…
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -188,6 +188,12 @@
|
|||
</svg>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro check_circle(class) %}
|
||||
<svg class="{{ class }}" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm3.857-9.809a.75.75 0 0 0-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 1 0-1.06 1.061l2.5 2.5a.75.75 0 0 0 1.137-.089l4-5.5Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro key(class) %}
|
||||
<svg class="{{ class }}" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M8 7a5 5 0 1 1 3.61 4.804l-1.903 1.903A1 1 0 0 1 9 14H8v1a1 1 0 0 1-1 1H6v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1v-2a1 1 0 0 1 .293-.707L8.196 8.39A5.002 5.002 0 0 1 8 7Zm5-3a.75.75 0 0 0 0 1.5A1.5 1.5 0 0 1 14.5 7 .75.75 0 0 0 16 7a3 3 0 0 0-3-3Z" clip-rule="evenodd" />
|
||||
|
|
|
|||
Loading…
Reference in a new issue