This commit finalizes Phase 4 of the Event & Session Overhaul: 1. Implements session pause logic across PostgreSQL schema, Valkey cache, and `auth_forward.ts` edge check (`is_paused`). 2. Implements non-destructive operational endpoints (`/api/events/:id/rotate-pin`, `/api/events/:id/expand`, `/api/events/:id/attendees`) with Zero-Trust Ownership verification. 3. Upgrades existing `end` and `extend` endpoints in `events.ts` to utilize robust Zero-Trust Ownership queries (created_by OR isGlobalAdmin). 4. Creates `EventAttendeesDrawer.tsx` to handle live participant inspection and individual session controls (Pause, Revoke). 5. Updates `EventCockpitDeck.tsx` and `SessionsScript.tsx` to mount and drive the new controls via vanilla JavaScript, respecting zero-framework guidelines. 6. Ensures `deno fmt`, `deno task lint`, `deno task check` and `deno test` execute successfully against the new schema and API guards. Co-authored-by: mrteye <1945243+mrteye@users.noreply.github.com>
166 lines
7.4 KiB
TypeScript
166 lines
7.4 KiB
TypeScript
export const EventCockpitDeck = ({ eventPasses }: { eventPasses: any[] }) => {
|
|
if (!eventPasses || eventPasses.length === 0) return null;
|
|
|
|
return (
|
|
<div style="margin-bottom: 2rem;">
|
|
<h2 style="font-size: 1.25rem; font-weight: 700; margin: 0 0 1rem 0; color: var(--text-primary); border-bottom: 1px solid var(--border-subtle); padding-bottom: 0.5rem;">
|
|
Event Cockpit Deck (Live Workshops)
|
|
</h2>
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 1rem;">
|
|
{eventPasses.map((event) => (
|
|
<div
|
|
key={event.id}
|
|
class="card"
|
|
style="border-left: 4px solid var(--primary); margin: 0;"
|
|
>
|
|
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.75rem;">
|
|
<div>
|
|
<h3 style="margin: 0 0 0.25rem 0; font-size: 1.1rem; color: var(--text-primary);">
|
|
{event.name}
|
|
</h3>
|
|
<div style="font-size: 0.8rem; color: var(--text-secondary);">
|
|
Expires: {new Date(event.expires_at).toLocaleString()}
|
|
</div>
|
|
</div>
|
|
<span class="badge badge-success">Active</span>
|
|
</div>
|
|
|
|
{/* Progress Bar for Seats */}
|
|
<div style="margin-bottom: 1rem;">
|
|
<div style="display: flex; justify-content: space-between; font-size: 0.8rem; margin-bottom: 0.25rem; color: var(--text-secondary);">
|
|
<span>Seats Claimed</span>
|
|
<strong style="color: var(--text-primary);">
|
|
{event.seats_claimed} /{" "}
|
|
{event.max_seats === 0 ? "∞" : event.max_seats}
|
|
</strong>
|
|
</div>
|
|
<div style="width: 100%; height: 8px; background: var(--surface-muted); border-radius: 4px; overflow: hidden;">
|
|
<div
|
|
style={`height: 100%; background: var(--primary); width: ${
|
|
event.max_seats > 0
|
|
? Math.min(
|
|
(event.seats_claimed / event.max_seats) * 100,
|
|
100,
|
|
)
|
|
: 100
|
|
}%;`}
|
|
>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Quick Copy Snippets */}
|
|
<div style="display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 1rem;">
|
|
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
|
<span style="font-size: 0.8rem; font-weight: 600; width: 60px; color: var(--text-muted);">
|
|
PIN:
|
|
</span>
|
|
<code
|
|
style="flex: 1; padding: 0.35rem 0.5rem; background: var(--surface-muted); border-radius: var(--radius-sm); font-family: monospace; font-size: 0.9rem; text-align: center; letter-spacing: 2px;"
|
|
id={`pin-${event.id}`}
|
|
>
|
|
{event.pin_code}
|
|
</code>
|
|
<button
|
|
type="button"
|
|
class="btn-outline"
|
|
style="padding: 0.25rem 0.5rem; font-size: 0.75rem; min-height: 28px;"
|
|
onclick={`copyText(document.getElementById('pin-${event.id}').textContent.trim())`}
|
|
>
|
|
Copy
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn-outline"
|
|
style="padding: 0.25rem 0.5rem; font-size: 0.75rem; min-height: 28px;"
|
|
onclick={`rotatePin('${event.id}')`}
|
|
>
|
|
🔄 Rotate
|
|
</button>
|
|
</div>
|
|
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
|
<span style="font-size: 0.8rem; font-weight: 600; width: 60px; color: var(--text-muted);">
|
|
Link:
|
|
</span>
|
|
<code style="flex: 1; padding: 0.35rem 0.5rem; background: var(--surface-muted); border-radius: var(--radius-sm); font-family: monospace; font-size: 0.75rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
|
|
/e/{event.slug}
|
|
</code>
|
|
<button
|
|
type="button"
|
|
class="btn-outline"
|
|
style="padding: 0.25rem 0.5rem; font-size: 0.75rem; min-height: 28px;"
|
|
onclick={`copyText(window.location.origin + '/e/${event.slug}')`}
|
|
>
|
|
Copy
|
|
</button>
|
|
</div>
|
|
|
|
<details style="font-size: 0.8rem;">
|
|
<summary style="cursor: pointer; color: var(--primary); font-weight: 600;">
|
|
Show CLI 1-liner handoff
|
|
</summary>
|
|
<div style="margin-top: 0.5rem; display: flex; align-items: center; gap: 0.5rem;">
|
|
<code style="flex: 1; padding: 0.35rem 0.5rem; background: var(--surface-muted); border-radius: var(--radius-sm); font-family: monospace; font-size: 0.75rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
|
|
curl -sSL {Deno.env.get("RP_ID")
|
|
? `https://${Deno.env.get("RP_ID")}`
|
|
: ""}/join/{event.slug}?format=env | source /dev/stdin
|
|
</code>
|
|
<button
|
|
type="button"
|
|
class="btn-outline"
|
|
style="padding: 0.25rem 0.5rem; font-size: 0.75rem; min-height: 28px;"
|
|
onclick={`copyText("curl -sSL " + window.location.origin + "/join/${event.slug}?format=env | source /dev/stdin")`}
|
|
>
|
|
Copy
|
|
</button>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
|
|
{/* Cockpit Actions */}
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; margin-bottom: 0.5rem;">
|
|
<button
|
|
type="button"
|
|
class="btn-outline"
|
|
style="justify-content: center; min-height: 38px; font-size: 0.85rem;"
|
|
onclick={`openAttendeesDrawer('${event.id}', '${
|
|
event.name.replace(/'/g, "\\'")
|
|
}')`}
|
|
>
|
|
👥 Manage Attendees ({event.seats_claimed})
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn-outline"
|
|
style="justify-content: center; min-height: 38px; font-size: 0.85rem;"
|
|
onclick={`expandSeats('${event.id}', 5)`}
|
|
>
|
|
+5 Seats
|
|
</button>
|
|
</div>
|
|
<div style="display: flex; gap: 0.5rem;">
|
|
<button
|
|
type="button"
|
|
class="btn-outline extend-event-btn"
|
|
data-event-id={event.id}
|
|
style="flex: 1; justify-content: center; min-height: 38px; font-size: 0.85rem;"
|
|
>
|
|
+1h Extend
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn-danger end-event-btn"
|
|
data-event-id={event.id}
|
|
aria-label="End Event and Revoke All"
|
|
style="flex: 1; justify-content: center; min-height: 38px; font-size: 0.85rem;"
|
|
>
|
|
🔴 End & Revoke All
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|