auth-yes/ui/components/sessions/EventCockpitDeck.tsx
google-labs-jules[bot] d86642ce31 feat: Sessions UI overhaul and backend revocation fix
- Update `DELETE /api/sessions/:id` in `server/routes/sessions.ts` to allow event creators to delete guests' sessions.
- Update page hierarchy and top headings in `ui/components/SessionsPage.tsx`.
- Refactor `EventAttendeesDrawer.tsx` to `EventGuestsDrawer.tsx` as a fixed slide-over overlay.
- Add multi-event compact view toggle with `localStorage` persistence in `EventCockpitDeck.tsx`.
- Standardize dynamic countdown pills across `EventCockpitDeck.tsx`, `EventGuestsDrawer.tsx`, `SessionDeck.tsx`, and `SessionTable.tsx`.
- Optimize mobile session deck in `SessionDeck.tsx`.

Co-authored-by: mrteye <1945243+mrteye@users.noreply.github.com>
2026-08-27 04:06:19 +00:00

345 lines
14 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);">
Event Passes
</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"
>
🗂 Grid
</button>
<button
type="button"
id="viewModeCompact"
class="view-mode-btn"
onclick="setEventViewMode('compact')"
aria-label="Compact View"
>
📋 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 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"
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>
<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);">
CLI:
</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;">
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"
aria-label={`Copy CLI command for ${event.name}`}
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 style="font-size: 0.75rem; margin-top: 0.1rem;">
<summary style="cursor: pointer; color: var(--primary); font-weight: 600; user-select: none;">
Expand full CLI command
</summary>
<textarea
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 PIN code for ${event.name}`}
style="justify-content: center; min-height: 38px; font-size: 0.82rem; padding: 0 0.25rem;"
onclick={`rotatePin('${event.id}')`}
>
🔄 Rotate PIN
</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-actions">
<button
type="button"
class="btn-outline"
style="padding: 0.25rem 0.5rem; font-size: 0.8rem; min-height: 32px;"
onclick={`openAttendeesDrawer('${event.id}', '${
event.name.replace(/'/g, "\\'")
}')`}
>
👥 Guests ({event.seats_claimed})
</button>
<button
type="button"
class="btn-outline"
style="padding: 0.25rem 0.5rem; font-size: 0.8rem; min-height: 32px;"
onclick="alert('Expand functionality for compact view to be implemented if needed')"
>
Details
</button>
</div>
</div>
);
})}
</div>
<style>
{`
.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);
}
.view-mode-btn.active {
background: var(--surface-card);
color: var(--primary);
box-shadow: var(--shadow-xs);
}
/* 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-actions {
display: none;
}
/* Compact View Layout */
.compact-view {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.compact-view .event-card {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1rem;
gap: 1rem;
}
.compact-view .event-card > div {
margin-bottom: 0 !important; /* Reset component margins */
}
.compact-view .event-card-header {
display: flex;
align-items: center;
gap: 1rem;
flex: 1;
min-width: 0;
}
.compact-view .event-card-header h3 {
margin: 0 !important;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.compact-view .event-card-header > div {
display: flex;
align-items: center;
gap: 1rem;
}
.compact-view .event-card-snippets,
.compact-view .event-card-actions,
.compact-view .status-badge {
display: none; /* Hide heavy elements in compact */
}
.compact-view .event-card-compact-actions {
display: flex;
gap: 0.5rem;
align-items: center;
flex-shrink: 0;
}
.compact-view .countdown-pill {
font-size: 0.75rem;
}
/* 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>
);
};