Control: add 'Add applicant' form to Applicants tab

Owner can add a tester directly (email/label) via the existing
POST /api/control/admin/applicants proxy route, instead of relying on
the not-yet-built public /register page.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-06-15 07:29:18 +10:00
parent ce0e9b3846
commit 93a13c9885

View File

@@ -75,6 +75,23 @@ async function renderApplicants(panel) {
try { rows = await api.get(`${A}/applicants?status=pending`); }
catch (e) { return mount(panel, errBox(e)); }
// Add-applicant form (owner adds a tester directly; the public /register page
// is a later addition). Posts to POST /admin/applicants {email,label}.
const emailI = el('input', { class: 'lk-url', type: 'email', placeholder: 'email (optional)' });
const labelI = el('input', { class: 'lk-url', placeholder: 'label / name' });
const addMsg = el('span', { class: 'muted', style: { fontSize: '0.78rem' } }, '');
const addBtn = btn('Add applicant', async () => {
if (!emailI.value.trim() && !labelI.value.trim()) return notify(addMsg, 'email or label required', false);
try {
await api.post(`${A}/applicants`, { email: emailI.value.trim(), label: labelI.value.trim() });
emailI.value = ''; labelI.value = '';
renderApplicants(panel);
} catch (e) { notify(addMsg, e.message || 'add failed', false); }
}, 'primary');
const addCard = el('div', { class: 'card', style: { display: 'grid', gap: '0.5rem', marginBottom: '0.9rem', gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))', alignItems: 'end' } },
field('Email', emailI), field('Label', labelI),
el('div', { style: { display: 'flex', alignItems: 'center', gap: '0.5rem' } }, addBtn, addMsg));
const body = rows.map(a => {
const msg = el('span', { class: 'muted', style: { fontSize: '0.75rem' } }, '');
const groupSel = select([{ value: '', label: '(default group)' }, ...groupsCache.map(g => ({ value: g.id, label: g.name }))]);
@@ -104,6 +121,7 @@ async function renderApplicants(panel) {
mount(panel,
el('div', { class: 'term-bar' }, el('span', { class: 'term-title' }, '◆ Pending applicants'),
btn('Refresh', () => renderApplicants(panel), 'ghost')),
addCard,
rows.length ? table(['Applicant', 'Note', 'Status', 'Action', ''], body)
: el('p', { class: 'muted' }, 'No pending applicants.'));
}