From 4bed3a5fe048cd6158973b7c890fc6b13ae2a553 Mon Sep 17 00:00:00 2001 From: Jonathan Date: Sun, 19 Apr 2026 20:29:09 +0200 Subject: [PATCH] feat: join_requests table and join CTA flow --- migrations/0002_join_requests.sql | 5 +++ src/lib/db.ts | 30 +++++++++++++ src/pages/admin/index.astro | 70 ++++++++++++++++++++++++++++++- src/pages/api/join.ts | 9 ++++ 4 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 migrations/0002_join_requests.sql create mode 100644 src/pages/api/join.ts diff --git a/migrations/0002_join_requests.sql b/migrations/0002_join_requests.sql new file mode 100644 index 0000000..eb1d157 --- /dev/null +++ b/migrations/0002_join_requests.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS join_requests ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL UNIQUE REFERENCES users(id), + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); diff --git a/src/lib/db.ts b/src/lib/db.ts index b513f3c..5155e55 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -329,3 +329,33 @@ export function getAllAttendance(meetingSlug: string): { user_id: number; status WHERE a.meeting_slug = ? `).all(meetingSlug) as { user_id: number; status: AttendanceStatus; name: string }[]; } + +// ── Join requests ──────────────────────────────────────────────── + +export function createJoinRequest(userId: number): void { + db.prepare('INSERT OR IGNORE INTO join_requests (user_id) VALUES (?)').run(userId); +} + +export function hasJoinRequest(userId: number): boolean { + const r = db.prepare('SELECT id FROM join_requests WHERE user_id = ?').get(userId); + return r !== undefined; +} + +export interface JoinRequest { + id: number; + user_id: number; + created_at: string; + user_name: string; + user_email: string; + user_organisation: string; +} + +export function getAllJoinRequests(): JoinRequest[] { + return db.prepare(` + SELECT jr.id, jr.user_id, jr.created_at, + u.name AS user_name, u.email AS user_email, u.organisation AS user_organisation + FROM join_requests jr + JOIN users u ON u.id = jr.user_id + ORDER BY jr.created_at DESC + `).all() as JoinRequest[]; +} diff --git a/src/pages/admin/index.astro b/src/pages/admin/index.astro index aa7906d..8ae9fb5 100644 --- a/src/pages/admin/index.astro +++ b/src/pages/admin/index.astro @@ -3,6 +3,7 @@ import AppLayout from '../../layouts/AppLayout.astro'; import { getAllInvites, getAllUsersPublic, revokeInvite, createInvite, updateUserRole, deactivateUser, + getAllJoinRequests, } from '../../lib/db'; import { generateInviteToken, inviteExpiresAt } from '../../lib/auth'; import { fmtDate } from '../../lib/markdown'; @@ -64,8 +65,9 @@ if (Astro.request.method === 'POST') { } } -const invites = getAllInvites(); -const users = getAllUsersPublic(); +const invites = getAllInvites(); +const users = getAllUsersPublic(); +const joinRequests = getAllJoinRequests(); actionMsg = Astro.url.searchParams.get('msg'); --- @@ -86,6 +88,12 @@ actionMsg = Astro.url.searchParams.get('msg'); href="/admin?tab=participants" class:list={['tab label-sm', { active: tab === 'participants' }]} >Participants + + Join requests {joinRequests.length > 0 && {joinRequests.length}} + {actionMsg && ( @@ -255,6 +263,43 @@ actionMsg = Astro.url.searchParams.get('msg'); )} + + {tab === 'join' && ( +
+
+

Join requests

+

+ Users who clicked "I want to join" on the home page. Use this to prioritise + follow-up and generate invite links. +

+ {joinRequests.length === 0 ? ( +

No join requests yet.

+ ) : ( + + + + + + + + + + + {joinRequests.map((jr) => ( + + + + + + + ))} + +
NameEmailOrganisationRequested
{jr.user_name}{jr.user_email}{jr.user_organisation}{fmtDate(jr.created_at)}
+ )} +
+
+ )} +
@@ -303,6 +348,27 @@ actionMsg = Astro.url.searchParams.get('msg'); .tab:hover { color: var(--on-surface-variant); background: var(--surface-container-low); border-bottom: none; } .tab.active { color: var(--on-surface); background: var(--surface-container); } + .tab-count { + display: inline-flex; + align-items: center; + justify-content: center; + background: var(--secondary); + color: var(--on-secondary); + border-radius: var(--radius-full); + font-size: var(--text-label-sm); + font-weight: 700; + min-width: 1.25rem; + height: 1.25rem; + padding: 0 var(--space-1); + margin-left: var(--space-2); + } + + .section-note { + color: var(--on-surface-muted); + margin: 0; + max-width: var(--reading-max); + } + /* ── Messages ────────────────────────────────────────────────────── */ .action-msg { padding: var(--space-3) var(--space-4); diff --git a/src/pages/api/join.ts b/src/pages/api/join.ts new file mode 100644 index 0000000..fb18e76 --- /dev/null +++ b/src/pages/api/join.ts @@ -0,0 +1,9 @@ +import type { APIRoute } from 'astro'; +import { createJoinRequest } from '../../lib/db'; + +export const POST: APIRoute = ({ locals }) => { + createJoinRequest(locals.user.id); + return new Response(JSON.stringify({ ok: true }), { + headers: { 'Content-Type': 'application/json' }, + }); +};