434 lines
17 KiB
TypeScript
434 lines
17 KiB
TypeScript
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>
|
||
);
|
||
};
|