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

826 lines
38 KiB
TypeScript

export const SessionsScript = () => {
return (
<script
dangerouslySetInnerHTML={{
__html: `
let lastMintedToken = "";
function showNotice(msg, isError) {
const banner = document.getElementById('status-banner');
banner.textContent = msg;
banner.style.display = 'block';
banner.style.background = isError ? 'var(--danger-bg)' : 'var(--success-bg)';
banner.style.color = isError ? 'var(--danger-text)' : 'var(--success-text)';
banner.style.border = isError ? '1px solid var(--danger-border)' : '1px solid var(--success-border)';
setTimeout(() => { banner.style.display = 'none'; }, 5000);
}
function openDelegateDrawer() {
const drawer = document.getElementById('delegateDrawer');
drawer.style.display = 'block';
drawer.scrollIntoView({ behavior: 'smooth' });
// Reset WorkshopDrawer 2-State Machine on open
const eventCreateState = document.getElementById('eventCreateState');
if (eventCreateState) eventCreateState.style.display = 'block';
const eventHandoffState = document.getElementById('eventHandoffState');
if (eventHandoffState) eventHandoffState.style.display = 'none';
const eventForm = document.getElementById('eventForm');
if (eventForm) eventForm.reset();
// Default back to Single Session Tab
switchDelegateTab('tabDirectPass');
}
function closeDelegateDrawer() {
document.getElementById('delegateDrawer').style.display = 'none';
}
function switchDelegateTab(tabId) {
document.getElementById('tabDirectPass').style.display = 'none';
document.getElementById('tabWorkshopPass').style.display = 'none';
const btnDirect = document.getElementById('tabBtnDirectPass');
const btnWorkshop = document.getElementById('tabBtnWorkshopPass');
btnDirect.classList.remove('active');
btnDirect.setAttribute('aria-selected', 'false');
btnWorkshop.classList.remove('active');
btnWorkshop.setAttribute('aria-selected', 'false');
document.getElementById(tabId).style.display = 'block';
if (tabId === 'tabDirectPass') {
btnDirect.classList.add('active');
btnDirect.setAttribute('aria-selected', 'true');
} else {
btnWorkshop.classList.add('active');
btnWorkshop.setAttribute('aria-selected', 'true');
}
}
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) {
document.querySelectorAll('.lifespan-pill').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
document.getElementById('delegateHours').value = hours;
}
function selectMode(btn, mode) {
document.querySelectorAll('.mode-pill').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
document.getElementById('delegateMode').value = mode;
const accordion = document.getElementById('customScopesSection');
if (mode === 'custom') {
accordion.open = true;
}
}
async function handleDelegateSession(e) {
e.preventDefault();
const label = document.getElementById('delegateLabel').value.trim();
const lifespanHours = parseInt(document.getElementById('delegateHours').value) || 1;
let mode = document.getElementById('delegateMode').value;
const targetApp = document.getElementById('delegateTargetApp') ? document.getElementById('delegateTargetApp').value : '';
const customScopes = [];
if (targetApp) {
customScopes.push('app:' + targetApp);
}
if (mode === 'custom' || targetApp) {
document.querySelectorAll('.scope-checkbox:checked').forEach(cb => {
if (!customScopes.includes(cb.value)) {
customScopes.push(cb.value);
}
});
if (targetApp && mode !== 'custom' && mode !== 'admin') {
mode = 'custom';
}
}
const btn = document.getElementById('submitDelegateBtn');
btn.disabled = true;
btn.textContent = 'Minting...';
try {
const res = await fetch('/api/sessions/delegate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ label, lifespanHours, mode, customScopes }),
});
const data = await res.json();
if (res.ok) {
lastMintedToken = data.token;
document.getElementById('handoffMagicLinkText').textContent = window.location.origin + '/pass?token=' + data.token;
document.getElementById('handoffCliText').textContent = 'export AUTH_YES_TOKEN="' + data.token + '"';
document.getElementById('handoffCurlText').textContent = '-H "Authorization: Bearer ' + data.token + '"';
document.getElementById('handoffModal').style.display = 'block';
showNotice('Delegated session created for: ' + data.label, false);
} else {
showNotice(data.error || 'Failed to delegate session', true);
}
} catch (err) {
showNotice('Network error delegating session', true);
} finally {
btn.disabled = false;
btn.textContent = 'Mint & Delegate Session';
}
}
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('eventCreateState').style.display = 'none';
document.getElementById('eventHandoffState').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() {
closeDelegateDrawer();
window.location.reload();
}
function copyHandoff(type) {
let text = '';
if (type === 'link') text = document.getElementById('handoffLinkText').textContent;
if (type === 'cli') text = document.getElementById('handoffCliText').textContent;
if (type === 'curl') text = document.getElementById('handoffCurlText').textContent;
if (type === 'magic') text = document.getElementById('handoffMagicLinkText').textContent;
navigator.clipboard.writeText(text);
showNotice('Copied to clipboard: ' + text, false);
}
function closeHandoffModal() {
document.getElementById('handoffModal').style.display = 'none';
window.location.reload();
}
async function extendSession(sessionId, hours) {
try {
const res = await fetch('/api/sessions/' + sessionId + '/extend', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ extendHours: hours }),
});
if (res.ok) {
showNotice('Session extended by ' + hours + ' hour(s)!', false);
setTimeout(() => window.location.reload(), 600);
} else {
const data = await res.json();
showNotice(data.error || 'Failed to extend session', true);
}
} catch (err) {
showNotice('Network error extending session', true);
}
}
function openEditScopesModal(sessionId, label, scopesJson) {
const scopes = JSON.parse(scopesJson || '[]');
document.getElementById('editScopesSessionId').value = sessionId;
document.getElementById('editScopesLabel').textContent = label;
document.querySelectorAll('.edit-scope-chk').forEach(cb => {
cb.checked = scopes.includes(cb.value);
});
document.getElementById('editScopesModal').style.display = 'flex';
}
function closeEditScopesModal() {
document.getElementById('editScopesModal').style.display = 'none';
}
async function saveUpdatedScopes() {
const sessionId = document.getElementById('editScopesSessionId').value;
const customScopes = [];
document.querySelectorAll('.edit-scope-chk:checked').forEach(cb => {
customScopes.push(cb.value);
});
try {
const res = await fetch('/api/sessions/' + sessionId + '/scopes', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ customScopes }),
});
if (res.ok) {
showNotice('Session scopes updated!', false);
setTimeout(() => window.location.reload(), 600);
} else {
const data = await res.json();
showNotice(data.error || 'Failed to update scopes', true);
}
} catch (err) {
showNotice('Network error updating scopes', true);
}
}
function copyText(text) {
navigator.clipboard.writeText(text);
showNotice('Copied to clipboard!', false);
}
async function rotatePin(eventId) {
try {
const res = await fetch('/api/events/' + eventId + '/rotate-pin', {
method: 'POST'
});
const data = await res.json();
if (res.ok && data.success) {
const pinElem = document.getElementById('pin-' + eventId);
if (pinElem) {
pinElem.textContent = data.pinCode;
}
const compactPinElem = document.getElementById('compact-pin-' + eventId);
if (compactPinElem) {
compactPinElem.textContent = data.pinCode;
}
if (data.slug) {
const linkUrl = window.location.origin + '/e/' + data.slug;
// Extract original base URL from cli code to preserve RP_ID
let cliCmd = 'curl -sSL ' + window.location.origin + '/join/' + data.slug + '?format=env | source /dev/stdin';
const cliCodeElem = document.getElementById('cli-code-' + eventId);
if (cliCodeElem && cliCodeElem.textContent) {
const parts = cliCodeElem.textContent.trim().split(' ');
if (parts.length > 2 && parts[2].startsWith('http')) {
try {
const urlObj = new URL(parts[2]);
cliCmd = 'curl -sSL ' + urlObj.origin + '/join/' + data.slug + '?format=env | source /dev/stdin';
} catch (e) {
// ignore
}
}
}
// Grid View
const linkCodeElem = document.getElementById('link-code-' + eventId);
if (linkCodeElem) linkCodeElem.textContent = '/e/' + data.slug;
const linkBtnElem = document.getElementById('link-btn-' + eventId);
if (linkBtnElem) {
linkBtnElem.onclick = () => copyText(linkUrl);
}
if (cliCodeElem) cliCodeElem.textContent = cliCmd;
const cliBtnElem = document.getElementById('cli-btn-' + eventId);
if (cliBtnElem) {
cliBtnElem.onclick = (e) => {
if (e && e.stopPropagation) e.stopPropagation();
copyText(cliCmd);
};
}
const cliTextareaElem = document.getElementById('cli-textarea-' + eventId);
if (cliTextareaElem) cliTextareaElem.value = cliCmd;
// Compact View
const compactLinkElem = document.getElementById('compact-link-' + eventId);
if (compactLinkElem) {
compactLinkElem.onclick = () => copyText(linkUrl);
}
const compactCliElem = document.getElementById('compact-cli-' + eventId);
if (compactCliElem) {
compactCliElem.onclick = () => copyText(cliCmd);
}
}
showNotice('Event credentials rotated successfully', false);
} else {
showNotice(data.error || 'Failed to rotate credentials', true);
}
} catch (err) {
showNotice('Network error rotating credentials', true);
}
}
async function expandSeats(eventId, count) {
try {
const res = await fetch('/api/events/' + eventId + '/expand', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ addSeats: count }),
});
const data = await res.json();
if (res.ok && data.success) {
showNotice('Expanded capacity by ' + count + ' seats', false);
setTimeout(() => window.location.reload(), 600);
} else {
showNotice(data.error || 'Failed to expand seats', true);
}
} catch (err) {
showNotice('Network error expanding seats', true);
}
}
function closeAttendeesDrawer() {
const drawer = document.getElementById('attendeesDrawer');
if (drawer) {
drawer.classList.remove('open');
const panel = drawer.querySelector('.drawer-panel');
if (panel) panel.classList.remove('open');
// Delay hiding until animation finishes
setTimeout(() => {
drawer.style.display = 'none';
}, 250);
}
}
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
const drawer = document.getElementById('attendeesDrawer');
if (drawer && drawer.classList.contains('open')) {
closeAttendeesDrawer();
}
}
});
document.addEventListener('click', (e) => {
const drawer = document.getElementById('attendeesDrawer');
if (drawer && drawer.classList.contains('open') && e.target === drawer) {
closeAttendeesDrawer();
}
});
async function openAttendeesDrawer(eventId, eventName) {
document.getElementById('guestDrawerTitle').textContent = eventName + ' Guests';
document.getElementById('attendeesDrawerContent').innerHTML = '<div style="display: flex; justify-content: center; align-items: center; height: 100%; color: var(--text-muted); font-size: 0.9rem;">Loading attendees...</div>';
const drawer = document.getElementById('attendeesDrawer');
if (drawer) {
drawer.style.display = 'block';
// Force reflow
drawer.offsetHeight;
drawer.classList.add('open');
const panel = drawer.querySelector('.drawer-panel');
if (panel) panel.classList.add('open');
}
try {
const res = await fetch('/api/events/' + eventId + '/attendees');
let data = {};
try {
data = await res.json();
} catch (_) {
// Non-JSON payload
}
if (res.ok && data.success) {
const attendees = data.attendees || [];
// Update Context Header with null safety
const claimedElem = document.getElementById('guestDrawerClaimed');
if (claimedElem) claimedElem.textContent = attendees.length;
if (data.event) {
const maxElem = document.getElementById('guestDrawerMax');
if (maxElem) maxElem.textContent = data.event.max_seats;
const expiresAtElem = document.getElementById('guestDrawerExpiresAt');
if (expiresAtElem && data.event.expires_at) {
const expDate = new Date(data.event.expires_at);
expiresAtElem.textContent = expDate.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
}
const countdownElem = document.getElementById('guestDrawerCountdown');
if (countdownElem && data.event.expires_at) {
countdownElem.setAttribute('data-expires-at', data.event.expires_at);
}
// Trigger countdown update
if (typeof updateAllCountdowns === 'function') updateAllCountdowns();
}
const contentElem = document.getElementById('attendeesDrawerContent');
if (attendees.length === 0) {
if (contentElem) {
contentElem.innerHTML = '<div style="padding: 1rem; text-align: center; color: var(--text-muted);">No attendees currently active.</div>';
}
return;
}
let html = '<div style="display: flex; flex-direction: column; gap: 0.5rem;">';
for (const att of attendees) {
const isPaused = att.is_paused === true;
// e.g. guest_deno-lab_1 -> Seat #1
const usernameParts = att.username.split('_');
const seatNumber = usernameParts.length > 2 ? usernameParts[usernameParts.length - 1] : '?';
const joinText = formatNaturalJoinTime(att.created_at, !isPaused);
const lastAction = att.last_activity_action || 'ForwardAuth Ingress';
let timeAgo = 'just now';
if (att.last_activity_at) {
const lastActDate = new Date(att.last_activity_at);
const now = new Date();
const diffMinsAct = Math.floor((now - lastActDate) / 60000);
timeAgo = diffMinsAct < 1 ? 'just now' : diffMinsAct + 'm ago';
}
const clientIcon = lastAction.includes('CLI') ? '📟 CLI' : '💻 Web';
html += \`
<div class="card" style="padding: 0.75rem; margin: 0; display: flex; justify-content: space-between; align-items: center; border-left: 3px solid \${isPaused ? 'var(--warning)' : 'var(--success)'}; opacity: \${isPaused ? '0.7' : '1'};">
<div>
<div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.25rem;">
<div style="font-weight: 600; font-size: 0.95rem; color: var(--text-primary); \${isPaused ? 'text-decoration: line-through;' : ''}">Seat #\${seatNumber}</div>
<div style="font-size: 0.75rem; padding: 2px 6px; border-radius: 4px; background: \${isPaused ? 'var(--warning-light)' : 'var(--success-light)'}; color: \${isPaused ? 'var(--warning)' : 'var(--success)'};">
\${isPaused ? '⏸️ Paused' : '🟢 Active'}
</div>
</div>
<div style="font-size: 0.75rem; color: var(--text-secondary);" title="\${att.username}">
\${joinText}
</div>
<div style="font-size: 0.75rem; color: var(--text-secondary); margin-top: 0.15rem;">
Last Action: \${lastAction} &middot; \${timeAgo} &middot; \${clientIcon}
</div>
</div>
<div style="display: flex; gap: 0.5rem;">
<button type="button" class="btn-outline" style="padding: 0.25rem 0.5rem; font-size: 0.75rem; min-height: 28px;" onclick="toggleSessionPause('\${att.id}', \${!isPaused}, '\${eventId}', '\${eventName}')">
\${isPaused ? '▶️ Resume' : '⏸️ Pause'}
</button>
<button type="button" class="btn-danger revoke-btn" data-session-id="\${att.id}" style="padding: 0.25rem 0.5rem; font-size: 0.75rem; min-height: 28px;" aria-label="Revoke">
🗑️ Revoke
</button>
</div>
</div>
\`;
}
html += '</div>';
document.getElementById('attendeesDrawerContent').innerHTML = html;
// Re-bind revoke buttons
document.getElementById('attendeesDrawerContent').querySelectorAll('.revoke-btn').forEach(btn => {
btn.addEventListener('click', async (e) => {
if (!confirm('Revoke this session immediately?')) return;
const sessionId = e.currentTarget.getAttribute('data-session-id');
const originalText = e.currentTarget.textContent;
e.currentTarget.textContent = 'Revoking...';
e.currentTarget.disabled = true;
try {
const revokeRes = await fetch('/api/sessions/' + sessionId, { method: 'DELETE' });
if (revokeRes.ok) {
openAttendeesDrawer(eventId, eventName); // Refresh list
} else {
const revokeData = await revokeRes.json();
showNotice(revokeData.error || 'Failed to revoke session', true);
e.currentTarget.textContent = originalText;
e.currentTarget.disabled = false;
}
} catch (err) {
showNotice('Network error revoking session', true);
e.currentTarget.textContent = originalText;
e.currentTarget.disabled = false;
}
});
});
} else {
const contentElem = document.getElementById('attendeesDrawerContent');
if (contentElem) {
contentElem.innerHTML = '<div style="padding: 1rem; color: var(--danger-text);">Failed to load attendees: ' + (data.error || 'HTTP ' + res.status) + '</div>';
}
}
} catch (err) {
console.error('[Attendees Drawer Error]', err);
const contentElem = document.getElementById('attendeesDrawerContent');
if (contentElem) {
contentElem.innerHTML = '<div style="padding: 1rem; color: var(--danger-text);">Error loading attendees: ' + (err && err.message ? err.message : 'Network failure') + '</div>';
}
}
}
async function toggleSessionPause(sessionId, shouldPause, eventId, eventName) {
try {
const res = await fetch('/api/sessions/' + sessionId + '/pause', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ is_paused: shouldPause }),
});
const data = await res.json();
if (res.ok && data.success) {
showNotice('Session ' + (shouldPause ? 'paused' : 'unpaused') + ' successfully', false);
openAttendeesDrawer(eventId, eventName); // Refresh list
} else {
showNotice(data.error || 'Failed to toggle pause state', true);
}
} catch (err) {
showNotice('Network error toggling pause state', true);
}
}
// Event Cockpit Actions
document.querySelectorAll('.extend-event-btn').forEach(btn => {
btn.addEventListener('click', async (e) => {
const eventId = e.currentTarget.getAttribute('data-event-id');
e.currentTarget.disabled = true;
try {
const res = await fetch('/api/events/' + eventId + '/extend', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ extendHours: 1 })
});
let data = {};
try {
data = await res.json();
} catch (_) {
// Non-JSON payload
}
if (res.ok && data.success) {
showNotice('Event extended by 1 hour!', false);
setTimeout(() => window.location.reload(), 600);
} else {
showNotice(data.error || 'Failed to extend event (HTTP ' + res.status + ')', true);
e.currentTarget.disabled = false;
}
} catch (err) {
showNotice('Network error extending event', true);
e.currentTarget.disabled = false;
}
});
});
document.querySelectorAll('.end-event-btn').forEach(btn => {
btn.addEventListener('click', async (e) => {
if (!confirm('Are you sure you want to end this workshop? This will immediately revoke ALL active guest sessions and they will lose access.')) return;
const eventId = e.currentTarget.getAttribute('data-event-id');
e.currentTarget.disabled = true;
e.currentTarget.textContent = 'Revoking...';
try {
const res = await fetch('/api/events/' + eventId + '/end', {
method: 'POST',
});
if (res.ok) {
showNotice('Workshop ended and all sessions revoked.', false);
setTimeout(() => window.location.reload(), 600);
} else {
const data = await res.json();
showNotice(data.error || 'Failed to end event', true);
e.currentTarget.disabled = false;
e.currentTarget.textContent = '🔴 End & Revoke All';
}
} catch (err) {
showNotice('Network error ending event', true);
e.currentTarget.disabled = false;
e.currentTarget.textContent = '🔴 End & Revoke All';
}
});
});
// Revoke handler
document.querySelectorAll('.revoke-btn').forEach(btn => {
btn.addEventListener('click', async (e) => {
if (!confirm('Revoke this session immediately?')) return;
const sessionId = e.currentTarget.getAttribute('data-session-id');
const originalText = e.currentTarget.textContent;
e.currentTarget.textContent = 'Revoking...';
e.currentTarget.disabled = true;
try {
const res = await fetch('/api/sessions/' + sessionId, {
method: 'DELETE'
});
if (res.ok) {
window.location.reload();
} else {
const data = await res.json();
showNotice(data.error || 'Failed to revoke session', true);
e.currentTarget.textContent = originalText;
e.currentTarget.disabled = false;
}
} catch (err) {
showNotice('Network error revoking session', true);
e.currentTarget.textContent = originalText;
e.currentTarget.disabled = false;
}
});
});
// Set Event View Mode (Grid vs Compact)
function setEventViewMode(mode) {
const container = document.getElementById('eventDeckContainer');
const gridBtn = document.getElementById('viewModeGrid');
const compactBtn = document.getElementById('viewModeCompact');
if (container && gridBtn && compactBtn) {
if (mode === 'compact') {
container.classList.remove('grid-view');
container.classList.add('compact-view');
gridBtn.classList.remove('active');
compactBtn.classList.add('active');
localStorage.setItem('auth_yes_event_view_mode', 'compact');
} else {
container.classList.remove('compact-view');
container.classList.add('grid-view');
compactBtn.classList.remove('active');
gridBtn.classList.add('active');
localStorage.setItem('auth_yes_event_view_mode', 'grid');
}
}
}
// Standardized formatters
function formatNaturalExpiry(expiresAt) {
const expDate = new Date(expiresAt);
const now = new Date();
const diffMs = expDate.getTime() - now.getTime();
const timeStr = expDate.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
const fullISO = expDate.toISOString();
let dateStr = '';
const isToday = expDate.getDate() === now.getDate() && expDate.getMonth() === now.getMonth() && expDate.getFullYear() === now.getFullYear();
const tomorrow = new Date(now);
tomorrow.setDate(tomorrow.getDate() + 1);
const isTomorrow = expDate.getDate() === tomorrow.getDate() && expDate.getMonth() === tomorrow.getMonth() && expDate.getFullYear() === tomorrow.getFullYear();
if (isToday) {
dateStr = 'Today';
} else if (isTomorrow) {
dateStr = 'Tomorrow';
} else if (expDate.getFullYear() === now.getFullYear()) {
dateStr = expDate.toLocaleDateString([], { month: 'short', day: 'numeric' });
} else {
dateStr = expDate.toLocaleDateString([], { month: 'short', day: 'numeric', year: 'numeric' });
}
let badge = '';
if (diffMs <= 0) {
badge = '<span class="status-badge-red" title="' + fullISO + '" style="background:rgba(239,68,68,0.15);color:#dc2626;padding:2px 6px;border-radius:4px;font-size:0.75rem;font-weight:600;">Expired</span>';
} else {
const totalMins = Math.floor(diffMs / 60000);
const hours = totalMins / 60;
const days = hours / 24;
const months = days / 30;
const years = days / 365;
let timeRemainingStr = '';
let badgeStyle = 'background:rgba(34,197,94,0.15);color:#16a34a;padding:2px 6px;border-radius:4px;font-size:0.75rem;font-weight:600;'; // green
if (hours < 1) {
timeRemainingStr = totalMins + 'm left';
badgeStyle = 'background:rgba(245,158,11,0.15);color:#d97706;padding:2px 6px;border-radius:4px;font-size:0.75rem;font-weight:600;'; // amber
} else if (hours < 24) {
const h = Math.floor(hours);
const m = totalMins % 60;
timeRemainingStr = h + 'h ' + m + 'm left';
badgeStyle = 'background:rgba(245,158,11,0.15);color:#d97706;padding:2px 6px;border-radius:4px;font-size:0.75rem;font-weight:600;'; // amber
} else if (days <= 60) {
timeRemainingStr = Math.floor(days) + 'd left';
} else if (months <= 12) {
timeRemainingStr = months.toFixed(1) + ' mos left';
} else {
timeRemainingStr = years.toFixed(1) + ' yrs left';
}
badge = '<span style="' + badgeStyle + '" title="' + fullISO + '">' + timeRemainingStr + '</span>';
}
return dateStr + ' &middot; ' + timeStr + ' &middot; ' + badge;
}
function formatNaturalJoinTime(createdAt, isActive) {
const createdDate = new Date(createdAt);
const now = new Date();
const diffMs = now.getTime() - createdDate.getTime();
const diffMins = Math.floor(diffMs / 60000);
const timeStr = createdDate.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
const isToday = createdDate.getDate() === now.getDate() && createdDate.getMonth() === now.getMonth() && createdDate.getFullYear() === now.getFullYear();
const dateStr = isToday ? 'Today' : createdDate.toLocaleDateString([], { month: 'short', day: 'numeric' });
let durationStr = '';
if (diffMins < 60) {
durationStr = diffMins + 'm';
} else {
const h = Math.floor(diffMins / 60);
const m = diffMins % 60;
durationStr = h + 'h ' + m + 'm';
}
const badgeStyle = isActive ? 'background:rgba(34,197,94,0.15);color:#16a34a;padding:2px 6px;border-radius:4px;font-size:0.75rem;font-weight:600;' : 'background:rgba(234,179,8,0.15);color:#ca8a04;padding:2px 6px;border-radius:4px;font-size:0.75rem;font-weight:600;';
const badgeText = isActive ? 'Active ' + durationStr : 'Paused';
const activeBadge = '<span style="' + badgeStyle + '">' + badgeText + '</span>';
return 'Joined ' + dateStr + ' &middot; ' + timeStr + ' &middot; ' + activeBadge;
}
// Dynamic Countdown Updater
function updateAllCountdowns() {
const pills = document.querySelectorAll('.countdown-pill, #guestDrawerCountdown');
pills.forEach(pill => {
const expiresAtStr = pill.getAttribute('data-expires-at');
if (!expiresAtStr) return;
if (pill.id === 'guestDrawerCountdown') {
const now = new Date();
const expDate = new Date(expiresAtStr);
const diffMs = expDate - now;
if (diffMs <= 0) {
pill.textContent = '⏳ Expired';
} else {
const totalMins = Math.floor(diffMs / 60000);
const hours = Math.floor(totalMins / 60);
const mins = totalMins % 60;
pill.textContent = \`⏳ \${hours}h \${mins}m left\`;
}
} else {
pill.innerHTML = formatNaturalExpiry(expiresAtStr);
}
});
}
// Initialize on DOM load
document.addEventListener('DOMContentLoaded', () => {
// 1. Initialize Event View Mode
const storedViewMode = localStorage.getItem('auth_yes_event_view_mode') || 'grid';
setEventViewMode(storedViewMode);
// 2. Initialize Countdowns
updateAllCountdowns();
setInterval(updateAllCountdowns, 30000);
});
`,
}}
>
</script>
);
};