feat: participants directory and account page
This commit is contained in:
parent
636ef129bb
commit
99f3052651
2 changed files with 481 additions and 0 deletions
296
src/pages/account.astro
Normal file
296
src/pages/account.astro
Normal file
|
|
@ -0,0 +1,296 @@
|
||||||
|
---
|
||||||
|
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>
|
||||||
185
src/pages/participants.astro
Normal file
185
src/pages/participants.astro
Normal file
|
|
@ -0,0 +1,185 @@
|
||||||
|
---
|
||||||
|
import AppLayout from '../layouts/AppLayout.astro';
|
||||||
|
import { getAllUsersPublic } from '../lib/db';
|
||||||
|
import type { UserPublic, Role } from '../lib/db';
|
||||||
|
|
||||||
|
const user = Astro.locals.user;
|
||||||
|
|
||||||
|
const allUsers = getAllUsersPublic().filter((u) => u.active);
|
||||||
|
|
||||||
|
// Group by organisation
|
||||||
|
const orgs = new Map<string, UserPublic[]>();
|
||||||
|
for (const u of allUsers) {
|
||||||
|
if (!orgs.has(u.organisation)) orgs.set(u.organisation, []);
|
||||||
|
orgs.get(u.organisation)!.push(u);
|
||||||
|
}
|
||||||
|
|
||||||
|
const roleLabels: Record<Role, string> = {
|
||||||
|
pilot: 'Pilot',
|
||||||
|
cab: 'CAB',
|
||||||
|
fenja: 'Fenja',
|
||||||
|
};
|
||||||
|
|
||||||
|
const roleColors: Record<Role, string> = {
|
||||||
|
pilot: 'var(--pigment-copper)',
|
||||||
|
cab: 'var(--pigment-indigo)',
|
||||||
|
fenja: 'var(--secondary)',
|
||||||
|
};
|
||||||
|
---
|
||||||
|
<AppLayout title="Participants" user={user}>
|
||||||
|
<div class="page">
|
||||||
|
|
||||||
|
<header class="page-header">
|
||||||
|
<p class="label-sm eyebrow">Participants</p>
|
||||||
|
<h1 class="display-md page-title">The people.</h1>
|
||||||
|
<p class="lead subtitle">
|
||||||
|
Everyone in the Bifrost pilot — who they are, where they are from,
|
||||||
|
and what they bring.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="orgs">
|
||||||
|
{[...orgs.entries()].map(([orgName, members]) => (
|
||||||
|
<section class="org-section">
|
||||||
|
<h2 class="headline-sm org-name">{orgName}</h2>
|
||||||
|
<ul class="member-list">
|
||||||
|
{members.map((member) => (
|
||||||
|
<li class="member-card">
|
||||||
|
<div class="member-header">
|
||||||
|
<span class="body-md member-name">{member.name}</span>
|
||||||
|
<span
|
||||||
|
class="role-badge label-sm"
|
||||||
|
style={`color: ${roleColors[member.role]}`}
|
||||||
|
>
|
||||||
|
{roleLabels[member.role]}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{member.bio && (
|
||||||
|
<p class="body-sm member-bio">{member.bio}</p>
|
||||||
|
)}
|
||||||
|
{member.id === user.id && (
|
||||||
|
<a href="/account" class="edit-bio-link label-sm">
|
||||||
|
Edit your bio
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</AppLayout>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.page {
|
||||||
|
padding: var(--space-12) var(--space-20) var(--space-16);
|
||||||
|
max-width: var(--content-max);
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Header ──────────────────────────────────────────────────────── */
|
||||||
|
.page-header {
|
||||||
|
max-width: 44rem;
|
||||||
|
margin-bottom: var(--space-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.eyebrow {
|
||||||
|
letter-spacing: var(--tracking-wider);
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--on-surface-muted);
|
||||||
|
margin-bottom: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title { margin-bottom: var(--space-5); }
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: var(--on-surface-variant);
|
||||||
|
max-width: var(--reading-max);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Orgs ────────────────────────────────────────────────────────── */
|
||||||
|
.orgs {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-12);
|
||||||
|
max-width: 52rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-name {
|
||||||
|
font-family: var(--font-serif);
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: var(--tracking-snug);
|
||||||
|
margin: 0;
|
||||||
|
padding-bottom: var(--space-4);
|
||||||
|
border-bottom: var(--ghost-border);
|
||||||
|
color: var(--on-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Member cards ────────────────────────────────────────────────── */
|
||||||
|
.member-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card {
|
||||||
|
background: var(--surface-container-lowest);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: var(--space-5);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--on-surface);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-badge {
|
||||||
|
letter-spacing: var(--tracking-wide);
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 600;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-bio {
|
||||||
|
color: var(--on-surface-variant);
|
||||||
|
margin: 0;
|
||||||
|
line-height: var(--leading-relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-bio-link {
|
||||||
|
color: var(--on-surface-muted);
|
||||||
|
letter-spacing: var(--tracking-wide);
|
||||||
|
text-transform: uppercase;
|
||||||
|
text-decoration: none;
|
||||||
|
border-bottom: none;
|
||||||
|
transition: color var(--duration-fast) var(--ease-standard);
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
.edit-bio-link:hover {
|
||||||
|
color: var(--on-surface-variant);
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
Add table
Reference in a new issue