New Dispatches tab — full CRUD matching the existing pattern (?tab= querystring, plain HTML POST, hidden action field, redirect-with-?msg=). Author select is restricted to Fenja-role users (defaults to the current admin). Create form has a status toggle (draft / publish on save). publish_dispatch stamps published_at via the existing helper; archive preserves it. Body is a monospace textarea so admins can see markdown without proportional kerning confusion. Participants tab gains a per-row Edit link. When ?tab=participants&edit=ID is set, the table is replaced by <UserEditTab>: title input, comma- separated focus_tags input (parsed server-side via parseFocusTags), a pull_quote textarea with a 200-char live counter, and a read-only member_number display (set on role transition to cab). The inline role dropdown + deactivate stay on the table. EventsTab — adds audience, duration_label, action_label, notes_url inputs. Kind <select> now labels office_hours as 'Studio hours' and exposes working_session as the new fifth option. Admin action-link / action-cell styles were missing on admin/index.astro (they were defined only inside per-tab components); added to the page stylesheet so the new Participants Edit link inherits the same look. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
90 lines
3.8 KiB
Text
90 lines
3.8 KiB
Text
---
|
||
import type { UserPublic } from '../../lib/db';
|
||
import { readFocusTags } from '../../lib/format';
|
||
|
||
interface Props {
|
||
member: UserPublic;
|
||
}
|
||
|
||
const { member } = Astro.props;
|
||
const tagsStr = readFocusTags(member.focus_tags).join(', ');
|
||
---
|
||
<div class="tab-content">
|
||
<section class="section">
|
||
<a href="/admin?tab=participants" class="action-link label-sm">← Back to participants</a>
|
||
<h2 class="label-sm section-heading">Edit member — {member.name}</h2>
|
||
|
||
<form method="POST" class="invite-form" novalidate>
|
||
<input type="hidden" name="action" value="update_user_admin" />
|
||
<input type="hidden" name="user_id" value={member.id} />
|
||
|
||
<div class="form-grid">
|
||
<div class="field">
|
||
<label class="label-sm field-label">Name</label>
|
||
<input type="text" class="input body-md" value={member.name} disabled />
|
||
</div>
|
||
<div class="field">
|
||
<label class="label-sm field-label">Email</label>
|
||
<input type="text" class="input body-md" value={member.email} disabled />
|
||
</div>
|
||
<div class="field">
|
||
<label class="label-sm field-label">Organisation</label>
|
||
<input type="text" class="input body-md" value={member.organisation} disabled />
|
||
</div>
|
||
<div class="field">
|
||
<label class="label-sm field-label">Member number {member.role === 'cab' ? '(allocated)' : '(only set for cab role)'}</label>
|
||
<input type="text" class="input body-md" value={member.member_number ?? '—'} disabled />
|
||
</div>
|
||
<div class="field">
|
||
<label for="title" class="label-sm field-label">Job title</label>
|
||
<input type="text" id="title" name="title" class="input body-md" value={member.title ?? ''} placeholder="e.g. Senior Adviser" />
|
||
</div>
|
||
<div class="field">
|
||
<label for="focus_tags" class="label-sm field-label">Focus tags (comma-separated, max 3 × 24 chars)</label>
|
||
<input type="text" id="focus_tags" name="focus_tags" class="input body-md" value={tagsStr} placeholder="GDPR, Telemetry, Policy" />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="field">
|
||
<label for="pull_quote" class="label-sm field-label">Pull quote (one sentence in their voice — max 200 chars)</label>
|
||
<textarea id="pull_quote" name="pull_quote" class="input body-md" rows="3" maxlength="200" data-counter>{member.pull_quote ?? ''}</textarea>
|
||
<span class="char-counter label-sm" data-counter-for="pull_quote">{(member.pull_quote ?? '').length} / 200</span>
|
||
</div>
|
||
|
||
<div class="form-actions">
|
||
<button type="submit" class="btn-primary label-sm">Save changes</button>
|
||
<a href="/admin?tab=participants" class="action-link label-sm">Cancel</a>
|
||
</div>
|
||
</form>
|
||
|
||
<p class="body-sm note">
|
||
Role transitions and deactivation live in the participants table.
|
||
A member-number is allocated the first time a user becomes CAB and is never reused.
|
||
</p>
|
||
</section>
|
||
</div>
|
||
|
||
<script>
|
||
// Tiny live counter for the 200-char pull-quote field — no framework.
|
||
document.querySelectorAll<HTMLTextAreaElement>('[data-counter]').forEach((el) => {
|
||
const counter = document.querySelector<HTMLElement>(`[data-counter-for="${el.id}"]`);
|
||
if (!counter) return;
|
||
const update = () => { counter.textContent = `${el.value.length} / 200`; };
|
||
el.addEventListener('input', update);
|
||
});
|
||
</script>
|
||
|
||
<style>
|
||
.form-actions { display: flex; gap: var(--space-3); align-items: center; }
|
||
.char-counter { color: var(--on-surface-muted); margin-top: var(--space-1); display: inline-block; }
|
||
.note {
|
||
color: var(--on-surface-muted);
|
||
margin-top: var(--space-4);
|
||
max-width: var(--reading-max);
|
||
}
|
||
.input:disabled {
|
||
color: var(--on-surface-muted);
|
||
background: var(--surface-container-low);
|
||
cursor: not-allowed;
|
||
}
|
||
</style>
|