auth-yes/ui/components/sessions/EventCockpitDeck.tsx

434 lines
17 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

export const EventCockpitDeck = ({ eventPasses }: { eventPasses: any[] }) => {
if (!eventPasses || eventPasses.length === 0) return null;
return (
<div style="margin-bottom: 2rem;">
<div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border-subtle); padding-bottom: 0.5rem; margin-bottom: 1rem;">
<h2 style="font-size: 1.25rem; font-weight: 700; margin: 0; color: var(--text-primary);">
Events
</h2>
<div style="display: flex; background: var(--surface-muted); padding: 2px; border-radius: var(--radius-sm); border: 1px solid var(--border-subtle); gap: 2px;">
<button
type="button"
id="viewModeGrid"
class="view-mode-btn active"
onclick="setEventViewMode('grid')"
aria-label="Grid View"
aria-pressed="true"
>
🗂 Grid
</button>
<button
type="button"
id="viewModeCompact"
class="view-mode-btn"
onclick="setEventViewMode('compact')"
aria-label="Compact View"
aria-pressed="false"
>
📋 Compact
</button>
</div>
</div>
<div id="eventDeckContainer" class="grid-view">
{eventPasses.map((event) => {
const expDate = new Date(event.expires_at);
const timeString = expDate.toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
});
return (
<div
key={event.id}
class="card event-card"
style="border-left: 4px solid var(--primary); margin: 0;"
>
<div class="event-card-header">
<div>
<h3 style="margin: 0 0 0.25rem 0; font-size: 1.1rem; color: var(--text-primary);">
{event.name}
</h3>
<div
class="countdown-pill"
data-expires-at={event.expires_at}
>
0h 0m left · (Expires {timeString})
</div>
</div>
<span class="badge badge-success status-badge">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
class="event-card-snippets"
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: 45px; 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"
aria-label={`Copy PIN code for ${event.name}`}
style="padding: 0.25rem 0.5rem; font-size: 0.75rem; min-height: 28px;"
onclick={`copyText(document.getElementById('pin-${event.id}').textContent.trim())`}
>
Copy
</button>
</div>
<div style="display: flex; align-items: center; gap: 0.5rem;">
<span style="font-size: 0.8rem; font-weight: 600; width: 45px; color: var(--text-muted);">
Link:
</span>
<code
id={`link-code-${event.id}`}
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"
id={`link-btn-${event.id}`}
class="btn-outline"
aria-label={`Copy direct link for ${event.name}`}
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
class="cli-details"
style="margin-top: 0.1rem; width: 100%;"
>
<summary style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer; user-select: none;">
<span style="font-size: 0.8rem; font-weight: 600; width: 45px; color: var(--text-muted);">
CLI:
</span>
<code
id={`cli-code-${event.id}`}
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; cursor: pointer;"
>
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"
id={`cli-btn-${event.id}`}
class="btn-outline"
aria-label={`Copy CLI command for ${event.name}`}
style="padding: 0.25rem 0.5rem; font-size: 0.75rem; min-height: 28px;"
onclick={`event.stopPropagation(); copyText("curl -sSL " + window.location.origin + "/join/${event.slug}?format=env | source /dev/stdin")`}
>
Copy
</button>
<span style="font-size: 0.75rem; padding-right: 0.25rem;">
[ ]
</span>
</summary>
<textarea
id={`cli-textarea-${event.id}`}
readonly
rows={2}
style="width: 100%; margin-top: 0.4rem; padding: 0.4rem 0.5rem; font-family: monospace; font-size: 0.75rem; background: var(--surface-muted); color: var(--text-primary); border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); resize: vertical; box-sizing: border-box;"
onclick="this.select()"
>
{`curl -sSL ${
Deno.env.get("RP_ID")
? `https://${Deno.env.get("RP_ID")}`
: ""
}/join/${event.slug}?format=env | source /dev/stdin`}
</textarea>
</details>
</div>
{/* Cockpit Actions */}
<div class="event-card-actions">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; margin-bottom: 0.5rem;">
<button
type="button"
class="btn-outline"
aria-label={`Manage attendees for ${event.name}`}
style="justify-content: center; min-height: 38px; font-size: 0.85rem;"
onclick={`openAttendeesDrawer('${event.id}', '${
event.name.replace(/'/g, "\\'")
}')`}
>
👥 Manage Guests ({event.seats_claimed})
</button>
<button
type="button"
class="btn-outline"
aria-label={`Add 5 seats to ${event.name}`}
style="justify-content: center; min-height: 38px; font-size: 0.85rem;"
onclick={`expandSeats('${event.id}', 5)`}
>
+5 Seats
</button>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 0.5rem;">
<button
type="button"
class="btn-outline"
aria-label={`Rotate Credentials for ${event.name}`}
style="justify-content: center; min-height: 38px; font-size: 0.82rem; padding: 0 0.25rem;"
onclick={`rotatePin('${event.id}')`}
>
🔄 Rotate Credentials
</button>
<button
type="button"
class="btn-outline extend-event-btn"
data-event-id={event.id}
aria-label={`Extend lifespan by 1 hour for ${event.name}`}
style="justify-content: center; min-height: 38px; font-size: 0.82rem; padding: 0 0.25rem;"
>
+1h Extend
</button>
<button
type="button"
class="btn-danger end-event-btn"
data-event-id={event.id}
aria-label={`End event ${event.name} and revoke all attendees`}
style="justify-content: center; min-height: 38px; font-size: 0.82rem; padding: 0 0.25rem;"
>
End Event
</button>
</div>
</div>
{/* Compact Actions (only visible in compact mode) */}
<div class="event-card-compact-row-2">
<div class="compact-pills">
<span
class="compact-pill"
onclick={`copyText(document.getElementById('pin-${event.id}').textContent.trim())`}
>
PIN:{" "}
<span id={`compact-pin-${event.id}`}>{event.pin_code}</span>
{" "}
(Copy)
</span>
<span
id={`compact-link-${event.id}`}
class="compact-pill"
onclick={`copyText(window.location.origin + '/e/${event.slug}')`}
>
Link (Copy)
</span>
<span
id={`compact-cli-${event.id}`}
class="compact-pill"
onclick={`copyText("curl -sSL " + window.location.origin + "/join/${event.slug}?format=env | source /dev/stdin")`}
>
CLI (Copy)
</span>
</div>
<div class="compact-actions-right">
<button
type="button"
class="btn-outline"
style="padding: 0.25rem 0.5rem; font-size: 0.8rem; min-height: 28px;"
onclick={`openAttendeesDrawer('${event.id}', '${
event.name.replace(/'/g, "\\'")
}')`}
>
👥 Guests ({event.seats_claimed})
</button>
<button
type="button"
class="btn-outline extend-event-btn"
data-event-id={event.id}
style="padding: 0.25rem 0.5rem; font-size: 0.8rem; min-height: 28px;"
>
🔄 +1h
</button>
</div>
</div>
</div>
);
})}
</div>
<style>
{`
.cli-details summary {
list-style: none;
}
.cli-details summary::-webkit-details-marker {
display: none;
}
.view-mode-btn {
padding: 0.25rem 0.75rem;
border-radius: var(--radius-sm);
border: none;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
transition: all 0.15s;
background: transparent;
color: var(--text-secondary);
opacity: 0.75;
}
.view-mode-btn.active {
background: var(--primary);
color: #ffffff;
font-weight: 700;
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
border-radius: var(--radius-sm);
opacity: 1;
}
/* Grid View Layout */
.grid-view {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 1rem;
}
.grid-view .event-card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 0.75rem;
}
.grid-view .event-card-compact-row-2 {
display: none;
}
/* Compact View Layout */
.compact-view {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.compact-view .event-card {
display: flex;
flex-direction: column;
justify-content: center;
padding: 0.75rem 1rem;
box-sizing: border-box;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
min-height: 70px;
}
.compact-view .event-card > div {
margin-bottom: 0 !important; /* Reset component margins */
}
.compact-view .event-card-header {
display: flex;
align-items: center;
gap: 0.5rem;
width: 100%;
margin-bottom: 0.5rem !important;
}
.compact-view .event-card-header > div {
display: flex;
align-items: center;
gap: 0.5rem;
flex: 1;
min-width: 0;
}
.compact-view .event-card-header h3 {
margin: 0 !important;
font-size: 1rem !important;
font-weight: 700;
color: var(--text-primary);
max-width: 280px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
}
.compact-view .event-card-header > .status-badge {
margin-left: auto;
}
.compact-view .event-card-compact-row-2 {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.compact-view .compact-pills {
display: flex;
gap: 0.5rem;
align-items: center;
}
.compact-view .compact-pill {
padding: 2px 6px;
font-size: 0.75rem;
background: var(--surface-muted);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-sm);
cursor: pointer;
color: var(--text-secondary);
font-family: monospace;
}
.compact-view .compact-pill:hover {
border-color: var(--primary);
color: var(--primary);
}
.compact-view .compact-actions-right {
display: flex;
gap: 0.5rem;
align-items: center;
}
.compact-view .event-card-snippets,
.compact-view .event-card-actions {
display: none !important; /* Hide heavy elements in compact */
}
/* Dynamic Countdown Colors */
.countdown-pill {
font-size: 0.8rem;
color: var(--text-secondary);
font-weight: 500;
}
.countdown-pill.status-green { color: var(--success); }
.countdown-pill.status-amber { color: var(--warning); }
.countdown-pill.status-red { color: var(--danger-text); }
`}
</style>
</div>
);
};