feat(ui): add workshop pass creator drawer and 1-click app destination picker
This commit is contained in:
parent
8d4069709d
commit
e4955f4bd2
@ -136,32 +136,352 @@ export const SessionsPage = ({
|
|||||||
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 1.5rem; flex-wrap: wrap; gap: 1rem;">
|
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 1.5rem; flex-wrap: wrap; gap: 1rem;">
|
||||||
<div>
|
<div>
|
||||||
<h1 style="font-size: 1.75rem; font-weight: 700; margin: 0 0 0.5rem 0; color: var(--text-primary);">
|
<h1 style="font-size: 1.75rem; font-weight: 700; margin: 0 0 0.5rem 0; color: var(--text-primary);">
|
||||||
Active Sessions & Delegations
|
Active Sessions & Passes
|
||||||
</h1>
|
</h1>
|
||||||
<p style="color: var(--text-secondary); margin: 0; font-size: 0.95rem;">
|
<p style="color: var(--text-secondary); margin: 0; font-size: 0.95rem;">
|
||||||
Manage browser logins and mint isolated, scoped sessions for 1-click
|
Manage logins, mint 1:1 ephemeral links & CLI tokens, or launch
|
||||||
passes, team members, CLI tools, or AI agents.
|
multi-claim workshop event passes.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<div style="display: flex; gap: 0.75rem; flex-wrap: wrap;">
|
||||||
type="button"
|
<button
|
||||||
class="btn-primary"
|
type="button"
|
||||||
style="min-height: 42px; box-shadow: var(--shadow-sm); display: inline-flex; align-items: center; gap: 0.4rem;"
|
class="btn-outline"
|
||||||
onclick="openDelegateDrawer()"
|
style="min-height: 42px; display: inline-flex; align-items: center; gap: 0.4rem;"
|
||||||
>
|
onclick="openEventDrawer()"
|
||||||
<svg
|
|
||||||
width="18"
|
|
||||||
height="18"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2.5"
|
|
||||||
>
|
>
|
||||||
<path d="M12 5v14M5 12h14"></path>
|
<span style="font-size: 1.1rem;">🎟️</span>
|
||||||
</svg>
|
<span>+ Create Workshop Pass</span>
|
||||||
<span>Delegate Session</span>
|
</button>
|
||||||
</button>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-primary"
|
||||||
|
style="min-height: 42px; box-shadow: var(--shadow-sm); display: inline-flex; align-items: center; gap: 0.4rem;"
|
||||||
|
onclick="openDelegateDrawer()"
|
||||||
|
>
|
||||||
|
<span style="font-size: 1.1rem;">🔑</span>
|
||||||
|
<span>+ Delegate 1:1 Pass / Session</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Create Multi-Claim Event Pass Drawer */}
|
||||||
|
<div
|
||||||
|
id="eventDrawer"
|
||||||
|
class="card"
|
||||||
|
style="display: none; border-left: 4px solid var(--success); margin-bottom: 1.5rem;"
|
||||||
|
>
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.5rem;">
|
||||||
|
<div>
|
||||||
|
<h3 style="margin: 0 0 0.25rem 0; color: var(--text-primary);">
|
||||||
|
Create Multi-Claim Workshop / Event Pass
|
||||||
|
</h3>
|
||||||
|
<p style="color: var(--text-secondary); font-size: 0.9rem; margin: 0;">
|
||||||
|
Generate a shared event pass with a universal 6-digit PIN, vanity
|
||||||
|
link (<code style="font-size: 0.8rem;">/e/slug</code>), and CLI
|
||||||
|
1-liner for workshops, hackathons, and multi-user kiosks.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick="closeEventDrawer()"
|
||||||
|
style="background: none; border: none; font-size: 1.3rem; color: var(--text-muted); cursor: pointer;"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form
|
||||||
|
id="eventForm"
|
||||||
|
onsubmit="handleCreateEvent(event)"
|
||||||
|
style="margin-top: 1rem;"
|
||||||
|
>
|
||||||
|
{/* Event Name */}
|
||||||
|
<div style="margin-bottom: 1.25rem;">
|
||||||
|
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.875rem; color: var(--text-secondary);">
|
||||||
|
Workshop / Event Name *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="eventName"
|
||||||
|
placeholder="e.g. Deno & SPIRE Zero-Trust Workshop, Elite Fleet Live Demo"
|
||||||
|
required
|
||||||
|
style="width: 100%;"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1rem; margin-bottom: 1.25rem;">
|
||||||
|
{/* Target App */}
|
||||||
|
<div>
|
||||||
|
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.875rem; color: var(--text-secondary);">
|
||||||
|
Target App Destination
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="eventAppId"
|
||||||
|
style="width: 100%; padding: 0.5rem 0.75rem; background: var(--surface-card); border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); color: var(--text-primary); font-size: 0.9rem;"
|
||||||
|
>
|
||||||
|
<option value="">Universal Sandbox (Default)</option>
|
||||||
|
{apps.map((app) => (
|
||||||
|
<option value={app.id}>
|
||||||
|
{app.name} ({app.domain || "Internal"})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Guest Role */}
|
||||||
|
<div>
|
||||||
|
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.875rem; color: var(--text-secondary);">
|
||||||
|
Attendee Access Role
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="eventRole"
|
||||||
|
style="width: 100%; padding: 0.5rem 0.75rem; background: var(--surface-card); border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); color: var(--text-primary); font-size: 0.9rem;"
|
||||||
|
>
|
||||||
|
<option value="viewer">Viewer (Read-Only Demo)</option>
|
||||||
|
<option value="operator">Operator (Workshop Actions)</option>
|
||||||
|
<option value="user">Standard User</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Seat Capacity Presets */}
|
||||||
|
<div style="margin-bottom: 1.25rem;">
|
||||||
|
<label style="display: block; font-weight: 600; margin-bottom: 0.45rem; font-size: 0.875rem; color: var(--text-secondary);">
|
||||||
|
Seat Capacity Limit
|
||||||
|
</label>
|
||||||
|
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center;">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="pill-btn seat-pill"
|
||||||
|
onclick="selectSeats(this, 25)"
|
||||||
|
>
|
||||||
|
25 Seats
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="pill-btn seat-pill active"
|
||||||
|
onclick="selectSeats(this, 50)"
|
||||||
|
>
|
||||||
|
50 Seats
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="pill-btn seat-pill"
|
||||||
|
onclick="selectSeats(this, 100)"
|
||||||
|
>
|
||||||
|
100 Seats
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="pill-btn seat-pill"
|
||||||
|
onclick="selectSeats(this, 0)"
|
||||||
|
>
|
||||||
|
∞ Unlimited
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="eventMaxSeats"
|
||||||
|
value="50"
|
||||||
|
min="0"
|
||||||
|
max="10000"
|
||||||
|
style="width: 100px; padding: 0.35rem 0.5rem; text-align: center;"
|
||||||
|
title="Max seats (0 = unlimited)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Duration Presets */}
|
||||||
|
<div style="margin-bottom: 1.25rem;">
|
||||||
|
<label style="display: block; font-weight: 600; margin-bottom: 0.45rem; font-size: 0.875rem; color: var(--text-secondary);">
|
||||||
|
Event Duration & Expiration
|
||||||
|
</label>
|
||||||
|
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="pill-btn event-lifespan-pill"
|
||||||
|
onclick="selectEventLifespan(this, 1)"
|
||||||
|
>
|
||||||
|
⚡ 1 Hour
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="pill-btn event-lifespan-pill active"
|
||||||
|
onclick="selectEventLifespan(this, 3)"
|
||||||
|
>
|
||||||
|
🛠️ 3 Hours (Standard Workshop)
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="pill-btn event-lifespan-pill"
|
||||||
|
onclick="selectEventLifespan(this, 12)"
|
||||||
|
>
|
||||||
|
📅 12 Hours (All-Day Hackathon)
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="pill-btn event-lifespan-pill"
|
||||||
|
onclick="selectEventLifespan(this, 24)"
|
||||||
|
>
|
||||||
|
🗓️ 24 Hours
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<input type="hidden" id="eventLifespanHours" value="3" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Custom Codes Accordion (Optional) */}
|
||||||
|
<details style="margin-bottom: 1.5rem; background: var(--surface-muted); padding: 0.75rem 1rem; border-radius: var(--radius-sm); border: 1px solid var(--border-subtle);">
|
||||||
|
<summary style="font-size: 0.875rem; font-weight: 600; color: var(--text-primary); cursor: pointer;">
|
||||||
|
▸ Custom Vanity Slug & Custom PIN Code (Optional)
|
||||||
|
</summary>
|
||||||
|
<div style="margin-top: 0.75rem; display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;">
|
||||||
|
<div>
|
||||||
|
<label style="display: block; font-size: 0.8rem; font-weight: 600; color: var(--text-secondary); margin-bottom: 0.25rem;">
|
||||||
|
Vanity Slug (e.g. /e/deno-lab)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="eventSlug"
|
||||||
|
placeholder="auto-generated if blank"
|
||||||
|
style="width: 100%; font-size: 0.85rem;"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style="display: block; font-size: 0.8rem; font-weight: 600; color: var(--text-secondary); margin-bottom: 0.25rem;">
|
||||||
|
Custom 6-Digit PIN (e.g. 749-123)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="eventPinCode"
|
||||||
|
placeholder="auto-generated if blank"
|
||||||
|
style="width: 100%; font-size: 0.85rem;"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 0.75rem;">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
id="submitEventBtn"
|
||||||
|
class="btn-primary"
|
||||||
|
style="min-height: 42px; background: var(--success); border-color: var(--success);"
|
||||||
|
>
|
||||||
|
🎟️ Launch Workshop Pass
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-outline"
|
||||||
|
onclick="closeEventDrawer()"
|
||||||
|
style="min-height: 42px;"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* Event Launch Success Modal / Card */}
|
||||||
|
<div
|
||||||
|
id="eventHandoffModal"
|
||||||
|
style="display: none; margin-top: 1.5rem; padding: 1.25rem; background: var(--surface-card); border: 2px solid var(--success); border-radius: var(--radius-md); box-shadow: var(--shadow-md);"
|
||||||
|
>
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem;">
|
||||||
|
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||||
|
<span style="font-size: 1.3rem;">🎟️</span>
|
||||||
|
<strong
|
||||||
|
id="createdEventTitle"
|
||||||
|
style="color: var(--text-primary); font-size: 1.1rem;"
|
||||||
|
>
|
||||||
|
Workshop Pass Live!
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
<span class="badge badge-success">Active Now</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="font-size: 0.85rem; color: var(--text-secondary); margin: 0 0 1rem 0;">
|
||||||
|
Share the 6-digit PIN, 1-click link, or CLI 1-liner with attendees.
|
||||||
|
You can monitor live claimed seats and trigger the master kill
|
||||||
|
switch from the Event Cockpit below.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style="display: flex; flex-direction: column; gap: 0.75rem; margin-bottom: 1.25rem;">
|
||||||
|
{/* PIN Code */}
|
||||||
|
<div>
|
||||||
|
<label style="display: block; font-size: 0.75rem; font-weight: 700; text-transform: uppercase; color: var(--text-muted); margin-bottom: 0.25rem;">
|
||||||
|
6-Digit Universal PIN (for /join or Kiosk Entry)
|
||||||
|
</label>
|
||||||
|
<div style="display: flex; gap: 0.5rem;">
|
||||||
|
<code
|
||||||
|
id="eventHandoffPin"
|
||||||
|
style="flex: 1; padding: 0.5rem 0.75rem; background: var(--surface-muted); border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); font-family: monospace; font-size: 1.1rem; text-align: center; letter-spacing: 3px; font-weight: 700; color: var(--primary);"
|
||||||
|
>
|
||||||
|
</code>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-primary"
|
||||||
|
style="min-height: 36px; padding: 0 0.85rem; font-size: 0.8rem;"
|
||||||
|
onclick="copyEventHandoff('pin')"
|
||||||
|
>
|
||||||
|
Copy PIN
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 1-Click Vanity Link */}
|
||||||
|
<div>
|
||||||
|
<label style="display: block; font-size: 0.75rem; font-weight: 700; text-transform: uppercase; color: var(--text-muted); margin-bottom: 0.25rem;">
|
||||||
|
Direct 1-Click Workshop Entrance URL
|
||||||
|
</label>
|
||||||
|
<div style="display: flex; gap: 0.5rem;">
|
||||||
|
<code
|
||||||
|
id="eventHandoffLink"
|
||||||
|
style="flex: 1; padding: 0.5rem 0.75rem; background: var(--surface-muted); border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); font-family: monospace; font-size: 0.85rem; overflow-x: auto; white-space: nowrap; color: var(--success);"
|
||||||
|
>
|
||||||
|
</code>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-outline"
|
||||||
|
style="min-height: 36px; padding: 0 0.85rem; font-size: 0.8rem;"
|
||||||
|
onclick="copyEventHandoff('link')"
|
||||||
|
>
|
||||||
|
Copy URL
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CLI 1-Liner */}
|
||||||
|
<div>
|
||||||
|
<label style="display: block; font-size: 0.75rem; font-weight: 700; text-transform: uppercase; color: var(--text-muted); margin-bottom: 0.25rem;">
|
||||||
|
Terminal 1-Liner (Environment Injector)
|
||||||
|
</label>
|
||||||
|
<div style="display: flex; gap: 0.5rem;">
|
||||||
|
<code
|
||||||
|
id="eventHandoffCli"
|
||||||
|
style="flex: 1; padding: 0.5rem 0.75rem; background: var(--surface-muted); border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); font-family: monospace; font-size: 0.8rem; overflow-x: auto; white-space: nowrap; color: var(--text-primary);"
|
||||||
|
>
|
||||||
|
</code>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-outline"
|
||||||
|
style="min-height: 36px; padding: 0 0.85rem; font-size: 0.8rem;"
|
||||||
|
onclick="copyEventHandoff('cli')"
|
||||||
|
>
|
||||||
|
Copy 1-Liner
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-outline"
|
||||||
|
style="width: 100%; min-height: 38px; justify-content: center; font-size: 0.85rem;"
|
||||||
|
onclick="closeEventHandoffModal()"
|
||||||
|
>
|
||||||
|
Done & Refresh Cockpit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Delegate Session Drawer */}
|
{/* Delegate Session Drawer */}
|
||||||
@ -173,7 +493,7 @@ export const SessionsPage = ({
|
|||||||
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.5rem;">
|
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.5rem;">
|
||||||
<div>
|
<div>
|
||||||
<h3 style="margin: 0 0 0.25rem 0; color: var(--text-primary);">
|
<h3 style="margin: 0 0 0.25rem 0; color: var(--text-primary);">
|
||||||
Delegate New Session
|
Delegate New 1:1 Pass / Session
|
||||||
</h3>
|
</h3>
|
||||||
<p style="color: var(--text-secondary); font-size: 0.9rem; margin: 0;">
|
<p style="color: var(--text-secondary); font-size: 0.9rem; margin: 0;">
|
||||||
Mint an isolated, scoped child session for 1-click magic links,
|
Mint an isolated, scoped child session for 1-click magic links,
|
||||||
@ -203,12 +523,35 @@ export const SessionsPage = ({
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="delegateLabel"
|
id="delegateLabel"
|
||||||
placeholder="e.g. Workshop Guest Pass, Antigravity Assistant, CI/CD Runner, Terminal CLI"
|
placeholder="e.g. Friend Demo Pass, Antigravity Assistant, CI/CD Runner, Terminal CLI"
|
||||||
required
|
required
|
||||||
style="width: 100%;"
|
style="width: 100%;"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Direct 1-Click Destination (App Selection) */}
|
||||||
|
<div style="margin-bottom: 1.25rem;">
|
||||||
|
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.875rem; color: var(--text-secondary);">
|
||||||
|
1-Click Destination App (Optional)
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="delegateTargetApp"
|
||||||
|
onchange="handleTargetAppChange(this.value)"
|
||||||
|
style="width: 100%; padding: 0.5rem 0.75rem; background: var(--surface-card); border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); color: var(--text-primary); font-size: 0.9rem;"
|
||||||
|
>
|
||||||
|
<option value="">Auth-Yes Central Dashboard (Default)</option>
|
||||||
|
{apps.map((app) => (
|
||||||
|
<option value={app.name}>
|
||||||
|
{app.name} ({app.domain || "Internal App"})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<p style="font-size: 0.75rem; color: var(--text-muted); margin: 0.35rem 0 0 0;">
|
||||||
|
Selecting an app scopes the pass and automatically redirects the
|
||||||
|
recipient to that app upon 1-click redemption.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Lifespan Presets */}
|
{/* Lifespan Presets */}
|
||||||
<div style="margin-bottom: 1.25rem;">
|
<div style="margin-bottom: 1.25rem;">
|
||||||
<label style="display: block; font-weight: 600; margin-bottom: 0.45rem; font-size: 0.875rem; color: var(--text-secondary);">
|
<label style="display: block; font-weight: 600; margin-bottom: 0.45rem; font-size: 0.875rem; color: var(--text-secondary);">
|
||||||
@ -854,6 +1197,8 @@ export const SessionsPage = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openDelegateDrawer() {
|
function openDelegateDrawer() {
|
||||||
|
const eventDrawer = document.getElementById('eventDrawer');
|
||||||
|
if (eventDrawer) eventDrawer.style.display = 'none';
|
||||||
document.getElementById('delegateDrawer').style.display = 'block';
|
document.getElementById('delegateDrawer').style.display = 'block';
|
||||||
document.getElementById('delegateDrawer').scrollIntoView({ behavior: 'smooth' });
|
document.getElementById('delegateDrawer').scrollIntoView({ behavior: 'smooth' });
|
||||||
}
|
}
|
||||||
@ -862,6 +1207,38 @@ export const SessionsPage = ({
|
|||||||
document.getElementById('delegateDrawer').style.display = 'none';
|
document.getElementById('delegateDrawer').style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openEventDrawer() {
|
||||||
|
const delegateDrawer = document.getElementById('delegateDrawer');
|
||||||
|
if (delegateDrawer) delegateDrawer.style.display = 'none';
|
||||||
|
document.getElementById('eventDrawer').style.display = 'block';
|
||||||
|
document.getElementById('eventDrawer').scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeEventDrawer() {
|
||||||
|
document.getElementById('eventDrawer').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectSeats(btn, count) {
|
||||||
|
document.querySelectorAll('.seat-pill').forEach(b => b.classList.remove('active'));
|
||||||
|
btn.classList.add('active');
|
||||||
|
document.getElementById('eventMaxSeats').value = count;
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectEventLifespan(btn, hours) {
|
||||||
|
document.querySelectorAll('.event-lifespan-pill').forEach(b => b.classList.remove('active'));
|
||||||
|
btn.classList.add('active');
|
||||||
|
document.getElementById('eventLifespanHours').value = hours;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTargetAppChange(appName) {
|
||||||
|
if (appName) {
|
||||||
|
const scopeVal = 'app:' + appName;
|
||||||
|
document.querySelectorAll('.scope-checkbox').forEach(cb => {
|
||||||
|
if (cb.value === scopeVal) cb.checked = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function selectLifespan(btn, hours) {
|
function selectLifespan(btn, hours) {
|
||||||
document.querySelectorAll('.lifespan-pill').forEach(b => b.classList.remove('active'));
|
document.querySelectorAll('.lifespan-pill').forEach(b => b.classList.remove('active'));
|
||||||
btn.classList.add('active');
|
btn.classList.add('active');
|
||||||
@ -883,13 +1260,22 @@ export const SessionsPage = ({
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const label = document.getElementById('delegateLabel').value.trim();
|
const label = document.getElementById('delegateLabel').value.trim();
|
||||||
const lifespanHours = parseInt(document.getElementById('delegateHours').value) || 1;
|
const lifespanHours = parseInt(document.getElementById('delegateHours').value) || 1;
|
||||||
const mode = document.getElementById('delegateMode').value;
|
let mode = document.getElementById('delegateMode').value;
|
||||||
|
const targetApp = document.getElementById('delegateTargetApp') ? document.getElementById('delegateTargetApp').value : '';
|
||||||
|
|
||||||
const customScopes = [];
|
const customScopes = [];
|
||||||
if (mode === 'custom') {
|
if (targetApp) {
|
||||||
|
customScopes.push('app:' + targetApp);
|
||||||
|
}
|
||||||
|
if (mode === 'custom' || targetApp) {
|
||||||
document.querySelectorAll('.scope-checkbox:checked').forEach(cb => {
|
document.querySelectorAll('.scope-checkbox:checked').forEach(cb => {
|
||||||
customScopes.push(cb.value);
|
if (!customScopes.includes(cb.value)) {
|
||||||
|
customScopes.push(cb.value);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
if (targetApp && mode !== 'custom' && mode !== 'admin') {
|
||||||
|
mode = 'custom';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const btn = document.getElementById('submitDelegateBtn');
|
const btn = document.getElementById('submitDelegateBtn');
|
||||||
@ -921,6 +1307,60 @@ export const SessionsPage = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleCreateEvent(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const name = document.getElementById('eventName').value.trim();
|
||||||
|
const appId = document.getElementById('eventAppId').value || null;
|
||||||
|
const role = document.getElementById('eventRole').value || 'viewer';
|
||||||
|
const maxSeats = parseInt(document.getElementById('eventMaxSeats').value) || 0;
|
||||||
|
const lifespanHours = parseInt(document.getElementById('eventLifespanHours').value) || 3;
|
||||||
|
const slug = document.getElementById('eventSlug').value.trim() || undefined;
|
||||||
|
const pinCode = document.getElementById('eventPinCode').value.trim() || undefined;
|
||||||
|
|
||||||
|
const btn = document.getElementById('submitEventBtn');
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = 'Launching...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/events', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ name, appId, role, maxSeats, lifespanHours, slug, pinCode }),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (res.ok && data.success) {
|
||||||
|
const ev = data.event;
|
||||||
|
document.getElementById('createdEventTitle').textContent = '🎟️ ' + ev.name + ' Live!';
|
||||||
|
document.getElementById('eventHandoffPin').textContent = ev.pin_code;
|
||||||
|
document.getElementById('eventHandoffLink').textContent = window.location.origin + '/e/' + ev.slug;
|
||||||
|
document.getElementById('eventHandoffCli').textContent = 'curl -sSL ' + window.location.origin + '/join/' + ev.slug + '?format=env | source /dev/stdin';
|
||||||
|
document.getElementById('eventHandoffModal').style.display = 'block';
|
||||||
|
showNotice('Workshop pass created: ' + ev.name, false);
|
||||||
|
} else {
|
||||||
|
showNotice(data.error || 'Failed to create event pass', true);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
showNotice('Network error creating event pass', true);
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = '🎟️ Launch Workshop Pass';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyEventHandoff(type) {
|
||||||
|
let text = '';
|
||||||
|
if (type === 'pin') text = document.getElementById('eventHandoffPin').textContent;
|
||||||
|
if (type === 'link') text = document.getElementById('eventHandoffLink').textContent;
|
||||||
|
if (type === 'cli') text = document.getElementById('eventHandoffCli').textContent;
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
showNotice('Copied to clipboard: ' + text, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeEventHandoffModal() {
|
||||||
|
document.getElementById('eventHandoffModal').style.display = 'none';
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
function copyHandoff(type) {
|
function copyHandoff(type) {
|
||||||
let text = '';
|
let text = '';
|
||||||
if (type === 'link') text = document.getElementById('handoffLinkText').textContent;
|
if (type === 'link') text = document.getElementById('handoffLinkText').textContent;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user