360 lines
16 KiB
TypeScript
360 lines
16 KiB
TypeScript
import { EVENT_COCKPIT_CSS } from "./cockpit_styles.ts";
|
||
|
||
export const EventCockpitDeckFragment = (
|
||
{ eventPasses }: { eventPasses: any[] },
|
||
) => {
|
||
if (!eventPasses || eventPasses.length === 0) return null;
|
||
|
||
const now = Date.now();
|
||
const activePasses = eventPasses.filter((e) =>
|
||
!e.expires_at || new Date(e.expires_at).getTime() > now
|
||
);
|
||
const expiredPasses = eventPasses.filter((e) =>
|
||
e.expires_at && new Date(e.expires_at).getTime() <= now
|
||
);
|
||
|
||
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">
|
||
{activePasses.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;"
|
||
data-ignore
|
||
onclick={`copyText('${event.pin_code}')`}
|
||
>
|
||
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;"
|
||
data-ignore
|
||
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;"
|
||
data-ignore
|
||
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 PIN 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 */}
|
||
<div class="event-card-compact-row-2">
|
||
<div class="compact-pills">
|
||
<span
|
||
class="compact-pill"
|
||
data-ignore
|
||
onclick={`copyText('${event.pin_code}')`}
|
||
>
|
||
PIN:{" "}
|
||
<span id={`compact-pin-${event.id}`}>{event.pin_code}</span>
|
||
{" "}
|
||
(Copy)
|
||
</span>
|
||
<span
|
||
id={`compact-link-${event.id}`}
|
||
class="compact-pill"
|
||
data-ignore
|
||
onclick={`copyText(window.location.origin + '/e/${event.slug}')`}
|
||
>
|
||
Link (Copy)
|
||
</span>
|
||
<span
|
||
id={`compact-cli-${event.id}`}
|
||
class="compact-pill"
|
||
data-ignore
|
||
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>
|
||
|
||
{/* Collapsible Expired Events Section */}
|
||
{expiredPasses.length > 0 && (
|
||
<details style="margin-top: 1.5rem; border: 1px solid var(--border-subtle); border-radius: var(--radius-md); padding: 0.75rem 1rem; background: var(--surface-muted);">
|
||
<summary style="font-weight: 600; font-size: 0.95rem; color: var(--text-secondary); cursor: pointer;">
|
||
📁 Expired Events ({expiredPasses.length})
|
||
</summary>
|
||
<div style="margin-top: 1rem; display: flex; flex-direction: column; gap: 0.75rem;">
|
||
{expiredPasses.map((event) => (
|
||
<div
|
||
key={event.id}
|
||
class="card"
|
||
style="padding: 0.75rem 1rem; margin: 0; display: flex; justify-content: space-between; align-items: center; opacity: 0.85; border-left: 3px solid var(--text-muted); flex-wrap: wrap; gap: 0.75rem;"
|
||
>
|
||
<div>
|
||
<div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.25rem;">
|
||
<strong style="color: var(--text-primary); font-size: 0.95rem;">
|
||
{event.name}
|
||
</strong>
|
||
<span class="badge badge-secondary">Expired</span>
|
||
</div>
|
||
<div style="font-size: 0.8rem; color: var(--text-secondary);">
|
||
Expired {new Date(event.expires_at).toLocaleDateString()} ·
|
||
{" "}
|
||
{event.seats_claimed} claimed seats
|
||
</div>
|
||
</div>
|
||
<div style="display: flex; gap: 0.5rem; align-items: center;">
|
||
<button
|
||
type="button"
|
||
class="btn-outline extend-event-btn"
|
||
data-event-id={event.id}
|
||
style="padding: 0.3rem 0.65rem; font-size: 0.8rem; min-height: 32px;"
|
||
>
|
||
🔄 +1h Reopen
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="btn-danger end-event-btn"
|
||
data-event-id={event.id}
|
||
style="padding: 0.3rem 0.65rem; font-size: 0.8rem; min-height: 32px;"
|
||
>
|
||
End Event
|
||
</button>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</details>
|
||
)}
|
||
|
||
<style>{EVENT_COCKPIT_CSS}</style>
|
||
</div>
|
||
);
|
||
};
|