project-bifrost-platform/src/pages/account.astro

296 lines
8.2 KiB
Text

---
import AppLayout from '../layouts/AppLayout.astro';
import { updateUserProfile } from '../lib/db';
const user = Astro.locals.user;
let success = false;
let error: string | null = null;
if (Astro.request.method === 'POST') {
const data = await Astro.request.formData();
const name = String(data.get('name') ?? '').trim();
const bio = String(data.get('bio') ?? '').trim();
if (name.length < 2) {
error = 'Name must be at least 2 characters.';
} else if (bio.length > 280) {
error = 'Bio must be 280 characters or fewer.';
} else {
updateUserProfile(user.id, name, bio);
return Astro.redirect('/account?saved=1');
}
}
const saved = Astro.url.searchParams.get('saved') === '1';
---
<AppLayout title="Account" user={user}>
<div class="page">
<header class="page-header">
<p class="label-sm eyebrow">Account</p>
<h1 class="headline-lg page-title">{user.name}</h1>
<p class="body-md org">{user.organisation}</p>
</header>
<div class="content">
{saved && (
<p class="success-msg body-sm" role="status">Profile updated.</p>
)}
{error && (
<p class="error-msg body-sm" role="alert">{error}</p>
)}
<form method="POST" class="account-form" novalidate>
<div class="field">
<label for="name" class="label-sm field-label">
Display name
</label>
<input
type="text"
id="name"
name="name"
class="input body-md"
value={user.name}
required
minlength="2"
maxlength="80"
/>
</div>
<div class="field">
<label for="bio" class="label-sm field-label">
Bio
<span class="label-sm field-hint">Optional · max 280 characters · shown on participants page</span>
</label>
<textarea
id="bio"
name="bio"
class="textarea body-md"
rows="4"
maxlength="280"
placeholder="A sentence or two about your work and why you are here."
>{user.bio}</textarea>
</div>
<div class="form-actions">
<button type="submit" class="btn-primary body-md">Save changes</button>
</div>
</form>
<div class="account-info">
<h2 class="label-sm section-heading">Account details</h2>
<dl class="info-list">
<div class="info-row">
<dt class="label-sm info-label">Email</dt>
<dd class="body-md info-value">{user.email}</dd>
</div>
<div class="info-row">
<dt class="label-sm info-label">Role</dt>
<dd class="body-md info-value" style="text-transform: capitalize">{user.role}</dd>
</div>
<div class="info-row">
<dt class="label-sm info-label">Organisation</dt>
<dd class="body-md info-value">{user.organisation}</dd>
</div>
</dl>
<p class="body-sm reset-note">
To change your email or password, contact Fenja — we will issue a new invite link.
</p>
</div>
</div>
</div>
</AppLayout>
<style>
.page {
padding: var(--space-10) var(--space-20) var(--space-16);
max-width: var(--content-max);
margin: 0 auto;
}
/* ── Header ──────────────────────────────────────────────────────── */
.page-header {
margin-bottom: var(--space-10);
}
.eyebrow {
letter-spacing: var(--tracking-wider);
text-transform: uppercase;
color: var(--on-surface-muted);
margin-bottom: var(--space-3);
}
.page-title {
font-family: var(--font-serif);
font-weight: 400;
letter-spacing: var(--tracking-snug);
margin-bottom: var(--space-2);
}
.org {
color: var(--on-surface-variant);
margin: 0;
}
/* ── Content ─────────────────────────────────────────────────────── */
.content {
max-width: 36rem;
display: flex;
flex-direction: column;
gap: var(--space-10);
}
/* ── Status messages ─────────────────────────────────────────────── */
.success-msg {
padding: var(--space-3) var(--space-4);
background: rgba(109, 140, 124, 0.1);
border-radius: var(--radius-sm);
color: var(--pigment-copper);
margin: 0;
}
.error-msg {
padding: var(--space-3) var(--space-4);
background: rgba(185, 107, 88, 0.08);
border-radius: var(--radius-sm);
color: var(--pigment-terracotta);
margin: 0;
}
/* ── Form ────────────────────────────────────────────────────────── */
.account-form {
display: flex;
flex-direction: column;
gap: var(--space-6);
}
.field {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.field-label {
letter-spacing: var(--tracking-wide);
text-transform: uppercase;
color: var(--on-surface-variant);
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.field-hint {
color: var(--on-surface-muted);
text-transform: none;
letter-spacing: var(--tracking-normal);
font-weight: 400;
}
.input {
width: 100%;
padding: var(--space-3) var(--space-4);
background: var(--surface-container-lowest);
border: var(--ghost-border);
border-radius: var(--radius-sm);
font-family: var(--font-sans);
font-size: var(--text-body-md);
color: var(--on-surface);
outline: none;
transition: border-color var(--duration-fast) var(--ease-standard),
box-shadow var(--duration-fast) var(--ease-standard);
box-sizing: border-box;
}
.input:focus {
border-color: var(--secondary);
box-shadow: 0 0 0 3px rgba(120, 95, 83, 0.12);
}
.textarea {
width: 100%;
padding: var(--space-3) var(--space-4);
background: var(--surface-container-lowest);
border: var(--ghost-border);
border-radius: var(--radius-sm);
font-family: var(--font-sans);
font-size: var(--text-body-md);
color: var(--on-surface);
outline: none;
resize: vertical;
transition: border-color var(--duration-fast) var(--ease-standard),
box-shadow var(--duration-fast) var(--ease-standard);
box-sizing: border-box;
line-height: var(--leading-relaxed);
}
.textarea:focus {
border-color: var(--secondary);
box-shadow: 0 0 0 3px rgba(120, 95, 83, 0.12);
}
.textarea::placeholder { color: var(--on-surface-muted); }
.form-actions {
display: flex;
justify-content: flex-end;
}
.btn-primary {
padding: var(--space-3) var(--space-6);
background: linear-gradient(180deg, var(--secondary) 0%, var(--secondary-dim) 100%);
color: var(--on-secondary);
border: none;
border-radius: var(--radius-md);
font-family: var(--font-sans);
font-size: var(--text-body-md);
font-weight: 600;
cursor: pointer;
transition: opacity var(--duration-fast) var(--ease-standard);
}
.btn-primary:hover { opacity: 0.9; }
/* ── Account info ────────────────────────────────────────────────── */
.account-info {
display: flex;
flex-direction: column;
gap: var(--space-4);
padding-top: var(--space-8);
border-top: var(--ghost-border);
}
.section-heading {
letter-spacing: var(--tracking-wider);
text-transform: uppercase;
color: var(--on-surface-muted);
}
.info-list {
display: flex;
flex-direction: column;
gap: var(--space-2);
margin: 0;
padding: 0;
}
.info-row {
display: flex;
gap: var(--space-4);
align-items: baseline;
}
.info-label {
letter-spacing: var(--tracking-wide);
text-transform: uppercase;
color: var(--on-surface-muted);
min-width: 6rem;
flex-shrink: 0;
}
.info-value {
color: var(--on-surface-variant);
margin: 0;
}
.reset-note {
color: var(--on-surface-muted);
margin: 0;
}
</style>