Files
inbuxa-server/resources/html-templates/calendar-rsvp.html
T
jcoffey-dev 7dae9b29fd Import upstream v0.16.22, stripped
Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f
Enterprise-only files removed or emptied: 63
Enterprise-only snippets removed: 117 in 50 files
Dangling module declarations removed: 5
Cargo edits turning enterprise off: 14
Verification: clean
Enterprise feature gates left for rebuilt features: 19 in 18 files

Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
2026-09-18 10:21:56 -07:00

1156 lines
42 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="referrer" content="no-referrer">
<meta name="color-scheme" content="light dark">
<title>Respond to invitation</title>
<style>
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0
}
:root {
color-scheme: light dark;
--bg: #f5f5f4;
--card: #fff;
--border: #e5e5e3;
--line: #ececea;
--text: #1a1a18;
--muted: #6b6b67;
--subtle: #f7f7f5;
--accent: #1a1a18;
--accent-fg: #fff;
--info: #185fa5;
--success: #0f7a3c;
--warning: #a15c07;
--danger: #a0192f;
--brand: #db2d54;
--radius: 10px;
--input-bg: #fafaf9;
}
@media(prefers-color-scheme:dark) {
:root {
--bg: #18181b;
--card: #1f1f23;
--border: #2e2e33;
--line: #2a2a2f;
--text: #f4f4f2;
--muted: #9b9b95;
--subtle: #26262b;
--accent: #f4f4f2;
--accent-fg: #18181b;
--info: #5faee3;
--success: #58c98a;
--warning: #e0a458;
--danger: #f4839a;
--input-bg: #2a2a2e;
}
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
display: flex;
align-items: safe center;
justify-content: center;
padding: 1.5rem 1rem;
-webkit-font-smoothing: antialiased;
}
.card {
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 0.5rem 1.5rem 1.75rem;
width: 100%;
max-width: 34rem;
}
@media (min-width: 40em) {
.card {
padding: 0.75rem 2.5rem 2rem;
max-width: 40rem;
}
}
@media (min-width: 64em) {
.card {
padding: 1rem 3rem 2.25rem;
max-width: 46rem;
}
}
.logo-wrap {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
min-height: 72px;
margin: 0.25rem auto 0.75rem;
}
.logo-wrap svg.default-logo {
display: block;
width: calc(100% + 1rem);
height: auto;
margin: -0.25rem auto 0.1rem;
max-width: 300px;
}
.logo-wrap[data-loading] .default-logo {
visibility: hidden;
}
.logo-wrap img.custom-logo {
display: block;
max-width: 200px;
max-height: 88px;
width: auto;
height: auto;
object-fit: contain;
}
.default-logo .wordmark {
fill: var(--text)
}
.default-logo .symbol {
fill: var(--brand)
}
.eyebrow {
font-size: .6875rem;
font-weight: 600;
letter-spacing: .08em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: .375rem;
}
.eyebrow.cancelled {
color: var(--danger)
}
h1 {
font-size: 1.3125rem;
font-weight: 600;
letter-spacing: -.02em;
line-height: 1.25;
overflow-wrap: anywhere;
}
.byline {
font-size: .8125rem;
color: var(--muted);
margin-top: .375rem;
overflow-wrap: anywhere;
}
.meta {
display: flex;
flex-direction: column;
gap: .625rem;
background: var(--subtle);
border: 1px solid var(--line);
border-radius: 9px;
padding: .875rem;
margin-top: 1.25rem;
}
.meta-row {
display: flex;
align-items: flex-start;
gap: .625rem;
font-size: .875rem;
line-height: 1.4;
min-width: 0;
}
.meta-row>div {
min-width: 0;
overflow-wrap: anywhere;
}
.meta-row svg {
flex-shrink: 0;
margin-top: 1px;
color: var(--muted);
}
.meta-row .tz {
color: var(--muted)
}
.meta-row a {
color: var(--info);
overflow-wrap: anywhere;
}
@media (min-width: 40em) {
.meta {
display: grid;
grid-template-columns: 1fr 1fr;
gap: .75rem 1.25rem;
padding: 1rem 1.125rem;
}
}
.section {
margin-top: 1.375rem
}
.section-title {
font-size: .6875rem;
font-weight: 600;
letter-spacing: .07em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: .5rem;
}
.desc {
font-size: .875rem;
line-height: 1.55;
color: var(--text);
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.desc.clamped {
display: -webkit-box;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
overflow: hidden;
}
.linkish {
display: inline-block;
margin-top: .375rem;
font-size: .8125rem;
font-weight: 500;
color: var(--info);
background: none;
border: none;
padding: 0;
font-family: inherit;
cursor: pointer;
}
.linkish:hover {
text-decoration: underline
}
.people {
display: flex;
flex-direction: column;
gap: .4375rem
}
.person {
display: flex;
align-items: center;
gap: .5rem;
font-size: .8125rem;
line-height: 1.3;
}
.dot {
width: 7px;
height: 7px;
border-radius: 50%;
flex-shrink: 0;
background: var(--muted);
opacity: .45;
}
.dot.accepted {
background: var(--success);
opacity: 1
}
.dot.declined {
background: var(--danger);
opacity: 1
}
.dot.tentative {
background: var(--warning);
opacity: 1
}
.person .who {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.person .tag {
font-size: .6875rem;
color: var(--muted);
border: 1px solid var(--border);
border-radius: 999px;
padding: .0625rem .375rem;
flex-shrink: 0;
}
hr {
border: none;
border-top: 1px solid var(--line);
margin: 1.5rem 0 1.25rem;
}
.reply-as {
font-size: .8125rem;
color: var(--muted);
margin-bottom: .625rem;
overflow-wrap: anywhere;
}
.reply-as strong {
color: var(--text);
font-weight: 600
}
.choices {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: .5rem;
}
.choice {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: .3125rem;
padding: .75rem .25rem;
font-family: inherit;
font-size: .8125rem;
font-weight: 500;
color: var(--text);
background: var(--input-bg);
border: 1px solid var(--border);
border-radius: 9px;
cursor: pointer;
transition: border-color .15s, background .15s, color .15s;
}
.choice svg {
color: var(--muted);
transition: color .15s
}
.choice:hover {
border-color: color-mix(in srgb, var(--text) 25%, transparent)
}
.choice[aria-checked=true] {
background: color-mix(in srgb, var(--info) 9%, transparent);
border-color: color-mix(in srgb, var(--info) 55%, transparent);
color: var(--info);
}
.choice[aria-checked=true] svg {
color: var(--info)
}
.choice[data-value=accepted][aria-checked=true] {
background: color-mix(in srgb, var(--success) 9%, transparent);
border-color: color-mix(in srgb, var(--success) 55%, transparent);
color: var(--success);
}
.choice[data-value=accepted][aria-checked=true] svg {
color: var(--success)
}
.choice[data-value=tentative][aria-checked=true] {
background: color-mix(in srgb, var(--warning) 10%, transparent);
border-color: color-mix(in srgb, var(--warning) 55%, transparent);
color: var(--warning);
}
.choice[data-value=tentative][aria-checked=true] svg {
color: var(--warning)
}
.choice[data-value=declined][aria-checked=true] {
background: color-mix(in srgb, var(--danger) 9%, transparent);
border-color: color-mix(in srgb, var(--danger) 55%, transparent);
color: var(--danger);
}
.choice[data-value=declined][aria-checked=true] svg {
color: var(--danger)
}
.note-wrap {
margin-top: .875rem
}
.note-label {
display: flex;
justify-content: space-between;
align-items: center;
font-size: .75rem;
font-weight: 500;
color: var(--muted);
letter-spacing: .03em;
margin-bottom: .3125rem;
}
textarea {
width: 100%;
min-height: 72px;
resize: vertical;
padding: .5625rem .75rem;
font-size: .875rem;
font-family: inherit;
line-height: 1.45;
background: var(--input-bg);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text);
outline: none;
transition: border-color .15s;
-webkit-appearance: none;
}
textarea:focus {
border-color: color-mix(in srgb, var(--info) 60%, transparent);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--info) 12%, transparent);
}
textarea::placeholder {
color: var(--muted);
opacity: .7
}
.btn {
width: 100%;
margin-top: 1rem;
padding: .6875rem 0;
font-size: .9375rem;
font-weight: 500;
font-family: inherit;
background: var(--accent);
color: var(--accent-fg);
border: none;
border-radius: 8px;
cursor: pointer;
letter-spacing: -.01em;
transition: opacity .15s;
}
.btn:hover {
opacity: .88
}
.btn:active {
opacity: .75;
transform: scale(.995)
}
.btn[disabled] {
opacity: .5;
cursor: not-allowed;
transform: none;
}
.alert {
display: flex;
align-items: flex-start;
gap: .625rem;
padding: .625rem .75rem;
background: color-mix(in srgb, var(--brand) 10%, transparent);
border: 1px solid color-mix(in srgb, var(--brand) 30%, transparent);
border-radius: 8px;
font-size: .8125rem;
line-height: 1.4;
color: var(--danger);
margin-top: .875rem;
}
.alert svg {
flex-shrink: 0;
margin-top: 1px
}
.outcome {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: .625rem;
padding: .25rem 0 0;
}
.badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
border-radius: 50%;
background: color-mix(in srgb, var(--success) 14%, transparent);
color: var(--success);
}
.badge.tentative {
background: color-mix(in srgb, var(--warning) 14%, transparent);
color: var(--warning)
}
.badge.declined {
background: color-mix(in srgb, var(--danger) 14%, transparent);
color: var(--danger)
}
.badge.neutral {
background: color-mix(in srgb, var(--muted) 18%, transparent);
color: var(--muted)
}
.outcome h2 {
font-size: 1rem;
font-weight: 600;
letter-spacing: -.01em;
}
.outcome p {
font-size: .8125rem;
color: var(--muted);
line-height: 1.5;
overflow-wrap: anywhere;
}
.skeleton {
display: flex;
flex-direction: column;
gap: .75rem;
padding: .5rem 0 1rem;
}
.sk {
height: 12px;
border-radius: 6px;
background: linear-gradient(90deg, var(--subtle) 25%, var(--line) 37%, var(--subtle) 63%);
background-size: 400% 100%;
animation: shimmer 1.4s ease infinite;
}
.sk.title {
height: 22px;
width: 70%
}
.sk.short {
width: 45%
}
.sk.box {
height: 84px;
border-radius: 9px
}
@keyframes shimmer {
0% {
background-position: 100% 50%
}
100% {
background-position: 0 50%
}
}
@media (prefers-reduced-motion: reduce) {
.sk {
animation: none
}
}
[hidden] {
display: none !important
}
.noscript {
font-size: .875rem;
line-height: 1.55;
color: var(--text);
text-align: center;
padding: .5rem 0 1rem;
}
.noscript+#view-loading {
display: none
}
</style>
</head>
<body>
<main class="card">
<div class="logo-wrap" id="logo-wrap" data-loading>
<svg class="default-logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 680.5 252.1" aria-label="Logo">
<path class="wordmark"
d="M227.8 143.6c.3 4.2 2.1 7.6 5.1 10.1 3.1 2.5 7.1 3.8 12.1 3.8 4.3 0 7.9-.9 10.5-2.8 2.7-1.9 4-4.5 4-7.8 0-2.4-.7-4.3-2.2-5.7-1.5-1.4-3.4-2.5-6-3.2-2.5-.7-6-1.5-10.6-2.3-4.6-.8-8.6-1.9-11.9-3.2-3.3-1.3-6-3.3-8.1-6.1-2.1-2.7-3.1-6.3-3.1-10.7 0-4.1 1.1-7.7 3.2-10.9s5.1-5.7 9-7.4c3.8-1.8 8.2-2.6 13.2-2.6 5.1 0 9.6 1 13.7 2.9 4 1.9 7.2 4.5 9.5 7.8s3.6 7.1 3.8 11.4h-11.5c-.4-3.7-2-6.6-4.8-8.9-2.8-2.2-6.3-3.4-10.6-3.4-4.1 0-7.5.9-9.9 2.7-2.5 1.8-3.7 4.3-3.7 7.6 0 2.3.7 4.1 2.2 5.5 1.5 1.4 3.4 2.4 5.9 3.1 2.4.7 5.9 1.4 10.5 2.2 4.6.8 8.6 1.9 11.9 3.3 3.3 1.4 6 3.4 8.2 6 2.1 2.6 3.2 6.1 3.2 10.5 0 4.2-1.1 8-3.4 11.3-2.2 3.3-5.4 5.9-9.4 7.8-4 1.9-8.6 2.8-13.7 2.8-5.6 0-10.6-1-14.9-3.1-4.3-2-7.6-4.9-10-8.5-2.4-3.6-3.7-7.8-3.7-12.5l11.5.3zM278.5 102.1l11-2.1v14.6h12.6v9.7h-12.6v27.2c0 2 .4 3.5 1.2 4.3.8.9 2.2 1.3 4.2 1.3h8.4v9.7h-10.6c-5 0-8.6-1.2-10.8-3.5-2.2-2.3-3.4-5.9-3.4-10.7v-50.5zM356.8 114.6v52.2h-9.7l-1.2-7.9c-1.8 2.6-4.2 4.7-7 6.2-2.9 1.6-6.2 2.3-10 2.3-4.8 0-9-1.1-12.7-3.2-3.7-2.1-6.7-5.2-8.8-9.3-2.1-4-3.2-8.8-3.2-14.2 0-5.3 1.1-10 3.2-14s5.1-7.2 8.8-9.4c3.7-2.2 7.9-3.3 12.6-3.3 3.9 0 7.2.7 10.1 2.2 2.9 1.5 5.2 3.5 6.9 6.1l1.3-7.6h9.7zm-15.1 38.7c2.8-3.2 4.2-7.3 4.2-12.4 0-5.2-1.4-9.4-4.2-12.6-2.8-3.3-6.5-4.9-11-4.9-4.6 0-8.2 1.6-11 4.8-2.8 3.2-4.2 7.4-4.2 12.5 0 5.2 1.4 9.4 4.2 12.6 2.8 3.2 6.5 4.8 11 4.8s8.2-1.6 11-4.8zM365.5 97.5l11-2.1v71.3h-11V97.5zM380.3 114.6h11.6l11.9 39.9 11.9-39.9h10.1l11.4 39.9 12.3-39.9h11.2l-17.3 52.2h-11.8l-11-35.5-11.4 35.5-11.9.1-17-52.3zM513.7 114.6v52.2H504l-1.2-7.9c-1.8 2.6-4.2 4.7-7 6.2-2.9 1.6-6.2 2.3-10 2.3-4.8 0-9-1.1-12.7-3.2-3.7-2.1-6.7-5.2-8.8-9.3-2.1-4-3.2-8.8-3.2-14.2 0-5.3 1.1-10 3.2-14s5.1-7.2 8.8-9.4c3.7-2.2 7.9-3.3 12.6-3.3 3.9 0 7.2.7 10.1 2.2 2.9 1.5 5.2 3.5 6.9 6.1l1.3-7.6h9.7zm-15.1 38.7c2.8-3.2 4.2-7.3 4.2-12.4 0-5.2-1.4-9.4-4.2-12.6-2.8-3.3-6.5-4.9-11-4.9-4.6 0-8.2 1.6-11 4.8-2.8 3.2-4.2 7.4-4.2 12.5 0 5.2 1.4 9.4 4.2 12.6 2.8 3.2 6.5 4.8 11 4.8 4.6 0 8.2-1.6 11-4.8zM551.3 114.6v10.3h-4.9c-4.6 0-7.8 1.5-9.9 4.4-2 3-3.1 6.7-3.1 11.3v26.2h-11v-52.2h9.8l1.2 7.8c1.5-2.4 3.4-4.4 5.8-5.8 2.4-1.4 5.6-2.1 9.6-2.1h2.5zM556.3 102.1l11-2.1v14.6h12.6v9.7h-12.6v27.2c0 2 .4 3.5 1.2 4.3.8.9 2.2 1.3 4.2 1.3h8.4v9.7h-10.6c-5 0-8.6-1.2-10.8-3.5s-3.4-5.9-3.4-10.7v-50.5z" />
<path class="symbol"
d="M149.1 84.7h-4.8l-44.8 25.9v8.3l44.8 25.9h4.8l44.8-25.9v-8.3l-44.8-25.9zm32.9 30h-35.3V94.4l35.3 20.3zm-35.3 20.4-35.3-20.4 27-15.6v20.2l6.3 3.6h22.9l-20.9 12.2zM99.5 129.9v11l44.8 25.9h4.8l44.8-25.9v-11l-47.2 27.3zM187.3 166.8l6.6-3.8v-11l-25.7 14.8zM99.5 163l6.6 3.8h19.1L99.5 152z" />
</svg>
</div>
<noscript>
<p class="noscript">This page needs JavaScript to load the invitation. Enable it, or respond from your
calendar application instead.</p>
</noscript>
<div id="view-loading" class="skeleton" aria-live="polite">
<div class="sk short"></div>
<div class="sk title"></div>
<div class="sk box"></div>
</div>
<div id="view-error" class="outcome" hidden aria-live="polite">
<div class="badge neutral" id="error-badge">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
</div>
<h2 id="error-title" tabindex="-1">Something went wrong</h2>
<p id="error-detail"></p>
</div>
<div id="view-event" hidden>
<div class="eyebrow" id="kind">Invitation</div>
<h1 id="summary"></h1>
<div class="byline" id="byline" hidden></div>
<div class="meta" id="meta">
<div class="meta-row" id="row-when" hidden>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="4" width="18" height="18" rx="2" />
<line x1="16" y1="2" x2="16" y2="6" />
<line x1="8" y1="2" x2="8" y2="6" />
<line x1="3" y1="10" x2="21" y2="10" />
</svg>
<div id="when"></div>
</div>
<div class="meta-row" id="row-repeat" hidden>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<polyline points="17 1 21 5 17 9" />
<path d="M3 11V9a4 4 0 0 1 4-4h14" />
<polyline points="7 23 3 19 7 15" />
<path d="M21 13v2a4 4 0 0 1-4 4H3" />
</svg>
<div id="repeat"></div>
</div>
<div class="meta-row" id="row-where" hidden>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z" />
<circle cx="12" cy="10" r="3" />
</svg>
<div id="where"></div>
</div>
<div class="meta-row" id="row-conference" hidden>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<polygon points="23 7 16 12 23 17 23 7" />
<rect x="1" y="5" width="15" height="14" rx="2" ry="2" />
</svg>
<div><a id="conference" href="#" rel="noreferrer noopener" target="_blank"></a></div>
</div>
</div>
<div class="section" id="section-desc" hidden>
<div class="section-title" id="desc-title">Description</div>
<div class="desc clamped" id="desc"></div>
<button type="button" class="linkish" id="desc-toggle" aria-expanded="false"
aria-controls="desc" hidden>Show more</button>
</div>
<div class="section" id="section-people" hidden>
<div class="section-title" id="people-title">Attendees</div>
<div class="people" id="people"></div>
<button type="button" class="linkish" id="people-toggle" aria-expanded="false"
aria-controls="people" hidden></button>
</div>
<div id="respond">
<hr>
<div class="reply-as" id="reply-as"></div>
<div class="choices" id="choices" role="radiogroup" aria-labelledby="reply-as">
<button type="button" class="choice" data-value="accepted" role="radio" aria-checked="false"
tabindex="0">
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="20 6 9 17 4 12" />
</svg>
<span id="label-accepted">Yes</span>
</button>
<button type="button" class="choice" data-value="tentative" role="radio" aria-checked="false"
tabindex="-1">
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
<line x1="12" y1="17" x2="12.01" y2="17" />
<circle cx="12" cy="12" r="10" />
</svg>
<span id="label-tentative">Maybe</span>
</button>
<button type="button" class="choice" data-value="declined" role="radio" aria-checked="false"
tabindex="-1">
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
<span id="label-declined">No</span>
</button>
</div>
<div class="note-wrap" id="note-wrap" hidden>
<div class="note-label">
<label for="note" id="note-label">Note to the organizer (optional)</label>
<span id="note-count"></span>
</div>
<textarea id="note" maxlength="512" rows="3"
placeholder="Running ten minutes late, start without me."></textarea>
</div>
<div class="alert" id="alert" hidden aria-live="polite">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
<span id="alert-msg"></span>
</div>
<button type="button" class="btn" id="send" disabled>Send response</button>
</div>
<div id="recorded" hidden aria-live="polite">
<hr>
<div class="outcome">
<div class="badge" id="recorded-badge">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="20 6 9 17 4 12" />
</svg>
</div>
<h2 id="recorded-title" tabindex="-1">Your response has been recorded</h2>
<p id="recorded-detail"></p>
<button type="button" class="linkish" id="change">Change my response</button>
</div>
</div>
</div>
</main>
<script>
(function () {
'use strict';
var $ = function (id) { return document.getElementById(id); };
function setText(el, text) { if (el) el.textContent = text == null ? '' : String(text); }
function show(el) { if (el) el.hidden = false; }
function hide(el) { if (el) el.hidden = true; }
var FALLBACK = {
invitation: 'Invitation',
updatedInvitation: 'Updated invitation',
cancelled: 'Cancelled',
description: 'Description',
attendees: 'Attendees',
organizer: 'Organizer',
location: 'Location',
conference: 'Conference',
when: 'When',
you: 'you',
yes: 'Yes',
maybe: 'Maybe',
no: 'No',
replyAs: 'Reply as $name',
note: 'Note to the organizer (optional)',
noteHint: 'Add a short message for the organizer.',
send: 'Send response',
update: 'Update response',
change: 'Change my response',
recorded: 'Your RSVP has been recorded',
notified: 'The organizer has been notified.',
accepted: 'Accepted',
tentative: 'Tentative',
declined: 'Declined',
showMore: 'Show more',
showLess: 'Show less',
failed: 'RSVP failed',
invalidLink: 'This RSVP link is not valid.',
error: 'Temporary server failure. If the problem persists, contact your administrator.'
};
var PARTSTATS = ['accepted', 'tentative', 'declined'];
function isPartStat(value) {
return PARTSTATS.indexOf(value) !== -1;
}
var labels = FALLBACK;
function applyDirection(resp) {
if (!resp || !resp.language) return;
var root = document.documentElement;
root.lang = resp.language;
root.dir = resp.dir === 'rtl' ? 'rtl' : 'ltr';
}
function t(key, name) {
var value = (labels && labels[key]) || FALLBACK[key] || key;
if (name != null) value = value.replace('$name', name);
return value;
}
var url = new URL(window.location.href);
var token = url.searchParams.get('i') || '';
var preset = (url.searchParams.get('m') || '').toLowerCase();
var apiUrl = (function () {
var path = url.pathname.replace(/\/+$/, '');
var at = path.lastIndexOf('/calendar/rsvp');
return (at >= 0 ? path.slice(0, at) : '') + '/api/calendar/rsvp';
})();
function post(body) {
return fetch(apiUrl, {
method: 'POST',
credentials: 'omit',
cache: 'no-store',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
}).then(function (res) {
return res.json().catch(function () {
return { type: 'error', reason: 'serverError' };
});
});
}
var data = null;
var selected = '';
var peopleExpanded = false;
var inFlight = false;
function clearAlert() { hide($('alert')); }
function showAlert(msg) { setText($('alert-msg'), msg); show($('alert')); }
function fail(title, message) {
hide($('view-loading'));
hide($('view-event'));
title = title || t('failed');
document.title = title;
setText($('error-title'), title);
setText($('error-detail'), message || t('error'));
show($('view-error'));
$('error-title').focus();
}
function renderPeople() {
var list = (data.attendees || []);
var box = $('people');
while (box.firstChild) box.removeChild(box.firstChild);
if (!list.length) { hide($('section-people')); return; }
var limit = peopleExpanded ? list.length : Math.min(list.length, 5);
for (var i = 0; i < limit; i++) {
var person = list[i];
var row = document.createElement('div');
row.className = 'person';
var dot = document.createElement('span');
dot.className = 'dot ' + String(person.partstat || '').toLowerCase();
row.appendChild(dot);
var who = document.createElement('span');
who.className = 'who';
setText(who, person.name || person.email);
row.appendChild(who);
if (person.isOrganizer) {
var tagOrg = document.createElement('span');
tagOrg.className = 'tag';
setText(tagOrg, t('organizer'));
row.appendChild(tagOrg);
}
if (data.attendee && person.email &&
person.email.toLowerCase() === String(data.attendee.email || '').toLowerCase()) {
var tagYou = document.createElement('span');
tagYou.className = 'tag';
setText(tagYou, t('you'));
row.appendChild(tagYou);
}
box.appendChild(row);
}
var toggle = $('people-toggle');
if (list.length > 5) {
setText(toggle, peopleExpanded ? t('showLess') : t('showMore'));
show(toggle);
} else {
hide(toggle);
}
show($('section-people'));
}
function select(value) {
if (inFlight || !isPartStat(value)) return;
selected = value;
var nodes = $('choices').querySelectorAll('.choice');
for (var i = 0; i < nodes.length; i++) {
var checked = nodes[i].getAttribute('data-value') === value;
nodes[i].setAttribute('aria-checked', checked ? 'true' : 'false');
nodes[i].tabIndex = checked ? 0 : -1;
}
show($('note-wrap'));
$('send').disabled = !value;
clearAlert();
}
function renderRecorded(partstat) {
var badge = $('recorded-badge');
badge.className = 'badge' + (partstat === 'accepted' ? '' : ' ' + partstat);
setText($('recorded-title'), t('recorded'));
setText($('recorded-detail'), t(partstat) + '. ' + t('notified'));
setText($('change'), t('change'));
hide($('respond'));
show($('recorded'));
$('recorded-title').focus();
}
function renderEvent() {
labels = data.labels || FALLBACK;
var kind = data.kind || 'invitation';
var kindEl = $('kind');
setText(kindEl, kind === 'update' ? t('updatedInvitation')
: kind === 'cancel' ? t('cancelled') : t('invitation'));
if (kind === 'cancel') kindEl.className = 'eyebrow cancelled';
setText($('summary'), data.summary || t('invitation'));
document.title = data.summary || t('invitation');
var organizer = findOrganizer(data);
if (organizer) {
setText($('byline'), t('organizer') + ': '
+ (organizer.name || organizer.email));
show($('byline'));
}
if (data.when) {
setText($('when'), data.when);
show($('row-when'));
}
if (data.recurrence) { setText($('repeat'), data.recurrence); show($('row-repeat')); }
if (data.location) { setText($('where'), data.location); show($('row-where')); }
if (data.conference) {
var link = $('conference');
setText(link, data.conference.value);
if (data.conference.url) {
link.href = data.conference.url;
} else {
link.removeAttribute('href');
}
show($('row-conference'));
}
if (!data.when && !data.recurrence && !data.location && !data.conference) {
hide($('meta'));
}
if (data.description) {
setText($('desc-title'), t('description'));
setText($('desc'), data.description);
show($('section-desc'));
}
setText($('people-title'), t('attendees'));
renderPeople();
setText($('label-accepted'), t('yes'));
setText($('label-tentative'), t('maybe'));
setText($('label-declined'), t('no'));
setText($('note-label'), t('note'));
$('note').placeholder = t('noteHint');
updateCount();
var me = data.attendee || {};
setText($('reply-as'), t('replyAs', me.name ? me.name + ' (' + me.email + ')' : me.email));
var current = String(data.partstat || '').toLowerCase();
var known = isPartStat(current);
setText($('send'), known ? t('update') : t('send'));
hide($('view-loading'));
show($('view-event'));
if (data.description) {
var desc = $('desc');
if (desc.scrollHeight - desc.clientHeight > 4) {
setText($('desc-toggle'), t('showMore'));
show($('desc-toggle'));
}
}
if (kind === 'cancel') {
hide($('respond'));
return;
}
if (isPartStat(preset)) {
select(preset);
} else if (known) {
select(current);
}
}
function updateCount() {
var used = $('note').value.length;
setText($('note-count'), used ? used + ' / 512' : '');
}
function send() {
if (inFlight || !isPartStat(selected)) return;
var btn = $('send');
inFlight = true;
btn.disabled = true;
clearAlert();
post({
token: token,
partstat: selected,
comment: $('note').value.trim()
}).then(function (resp) {
if (resp && resp.type === 'recorded') {
data.partstat = resp.partstat || selected;
renderRecorded(String(data.partstat).toLowerCase());
return;
}
inFlight = false;
btn.disabled = false;
showAlert((resp && resp.message) || t('error'));
}).catch(function () {
inFlight = false;
btn.disabled = false;
showAlert(t('error'));
});
}
function loadCustomLogo(domain) {
var wrap = $('logo-wrap');
function showDefault() { wrap.removeAttribute('data-loading'); }
var target = apiUrl.replace(/\/api\/calendar\/rsvp$/, '') + '/logo';
if (domain) target += '?domain=' + encodeURIComponent(domain);
fetch(target, { method: 'GET', credentials: 'omit' })
.then(function (res) {
if (!res.ok) return null;
var ct = (res.headers.get('content-type') || '').toLowerCase();
if (ct.indexOf('image/') !== 0) return null;
return res.blob();
})
.then(function (blob) {
if (!blob) { showDefault(); return; }
var objectUrl = URL.createObjectURL(blob);
var img = new Image();
img.className = 'custom-logo';
img.alt = 'Logo';
img.onload = function () {
while (wrap.firstChild) wrap.removeChild(wrap.firstChild);
wrap.appendChild(img);
wrap.removeAttribute('data-loading');
};
img.onerror = function () { URL.revokeObjectURL(objectUrl); showDefault(); };
img.src = objectUrl;
})
.catch(function () { showDefault(); });
}
function findOrganizer(resp) {
var list = (resp && resp.attendees) || [];
for (var i = 0; i < list.length; i++) {
if (list[i].isOrganizer) return list[i];
}
return null;
}
function organizerDomain(resp) {
var organizer = findOrganizer(resp);
var email = organizer && organizer.email;
var at = email ? String(email).lastIndexOf('@') : -1;
return at !== -1 ? String(email).slice(at + 1).toLowerCase() : '';
}
$('choices').addEventListener('click', function (ev) {
var btn = ev.target.closest ? ev.target.closest('.choice') : null;
if (btn) select(btn.getAttribute('data-value'));
});
$('choices').addEventListener('keydown', function (ev) {
var step = ev.key === 'ArrowRight' || ev.key === 'ArrowDown' ? 1
: ev.key === 'ArrowLeft' || ev.key === 'ArrowUp' ? -1 : 0;
if (!step) return;
ev.preventDefault();
var at = PARTSTATS.indexOf(selected);
var next = PARTSTATS[(at + step + PARTSTATS.length) % PARTSTATS.length];
select(next);
var node = $('choices').querySelector('.choice[data-value="' + next + '"]');
if (node) node.focus();
});
$('send').addEventListener('click', send);
$('note').addEventListener('input', updateCount);
$('desc-toggle').addEventListener('click', function () {
var clamped = $('desc').classList.toggle('clamped');
setText($('desc-toggle'), clamped ? t('showMore') : t('showLess'));
$('desc-toggle').setAttribute('aria-expanded', clamped ? 'false' : 'true');
});
$('people-toggle').addEventListener('click', function () {
peopleExpanded = !peopleExpanded;
$('people-toggle').setAttribute('aria-expanded', peopleExpanded ? 'true' : 'false');
renderPeople();
});
$('change').addEventListener('click', function () {
inFlight = false;
hide($('recorded'));
show($('respond'));
$('send').disabled = !selected;
setText($('send'), t('update'));
});
if (!token) { loadCustomLogo(''); fail(null, FALLBACK.invalidLink); return; }
post({ token: token }).then(function (resp) {
loadCustomLogo(organizerDomain(resp));
applyDirection(resp);
if (resp && resp.labels) labels = resp.labels;
if (!resp || resp.type !== 'invitation') {
fail(resp && resp.title, resp && resp.message);
return;
}
data = resp;
renderEvent();
}).catch(function () {
loadCustomLogo('');
fail();
});
})();
</script>
</body>
</html>