Skip to content

Commit 4feb177

Browse files
committed
add rsvp admin page
1 parent 570c1a8 commit 4feb177

1 file changed

Lines changed: 208 additions & 0 deletions

File tree

admin.html

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
<!doctype html>
2+
<html lang="pt-PT">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1" />
6+
<title>RSVP Admin</title>
7+
<link rel="stylesheet" href="https://unpkg.com/bulma@0.8.0/css/bulma.min.css" />
8+
<script src="https://kit.fontawesome.com/2828f7885a.js" crossorigin="anonymous"></script>
9+
10+
<style>
11+
body { background: #0f0f14; }
12+
.wrap { padding: 28px 14px; }
13+
.cardish {
14+
background: rgba(255,255,255,0.06);
15+
border: 1px solid rgba(255,255,255,0.14);
16+
box-shadow: 0 18px 50px rgba(0,0,0,0.28);
17+
border-radius: 18px;
18+
padding: 18px;
19+
}
20+
.title, .label, .help, .table { color: rgba(255,255,255,.92); }
21+
.help { color: rgba(255,255,255,.65); }
22+
.input {
23+
background: rgba(255,255,255,0.08);
24+
border-color: rgba(255,255,255,0.18);
25+
color: rgba(255,255,255,.92);
26+
}
27+
.input::placeholder { color: rgba(255,255,255,.45); }
28+
.table thead th { color: rgba(255,255,255,.85); }
29+
.table td, .table th { border-color: rgba(255,255,255,.10); }
30+
.tag.is-yes { background: rgba(72,199,142,0.2); color: #dff7ec; border: 1px solid rgba(72,199,142,0.35); }
31+
.tag.is-no { background: rgba(241,70,104,0.2); color: #ffe2e8; border: 1px solid rgba(241,70,104,0.35); }
32+
.muted { color: rgba(255,255,255,.65); }
33+
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; }
34+
.table-container { overflow-x: auto; }
35+
</style>
36+
</head>
37+
38+
<body>
39+
<section class="wrap">
40+
<div class="container">
41+
<div class="columns is-centered">
42+
<div class="column is-12 is-10-desktop">
43+
44+
<div class="cardish">
45+
<h1 class="title is-4">RSVP Admin</h1>
46+
<p class="help">
47+
This page loads RSVPs from your Worker. Keep your admin token private.
48+
</p>
49+
50+
<div class="columns is-variable is-3" style="margin-top: 12px;">
51+
<div class="column is-7">
52+
<div class="field">
53+
<label class="label">Admin token</label>
54+
<div class="control has-icons-left">
55+
<input id="token" class="input" type="password" placeholder="Paste token here (not stored)">
56+
<span class="icon is-small is-left"><i class="fas fa-key"></i></span>
57+
</div>
58+
</div>
59+
</div>
60+
61+
<div class="column is-5">
62+
<div class="field">
63+
<label class="label">Actions</label>
64+
<div class="buttons">
65+
<button id="load" class="button is-link is-rounded">
66+
<span class="icon"><i class="fas fa-sync"></i></span>
67+
<span>Load</span>
68+
</button>
69+
<button id="export" class="button is-light is-rounded" disabled>
70+
<span class="icon"><i class="fas fa-file-csv"></i></span>
71+
<span>Export CSV</span>
72+
</button>
73+
</div>
74+
</div>
75+
</div>
76+
</div>
77+
78+
<p id="status" class="muted"></p>
79+
80+
<div class="table-container" style="margin-top: 14px;">
81+
<table class="table is-fullwidth is-hoverable">
82+
<thead>
83+
<tr>
84+
<th>ID</th>
85+
<th>When</th>
86+
<th>Name</th>
87+
<th>Email</th>
88+
<th>Attending</th>
89+
<th>Guests</th>
90+
<th>Notes</th>
91+
</tr>
92+
</thead>
93+
<tbody id="rows"></tbody>
94+
</table>
95+
</div>
96+
97+
<p class="help mono" style="margin-top: 10px;">
98+
Tip: bookmark this page. If you lose the token, generate a new one and update the Worker secret.
99+
</p>
100+
</div>
101+
102+
</div>
103+
</div>
104+
</div>
105+
</section>
106+
107+
<script>
108+
// Replace this with your deployed Worker URL:
109+
// Example: https://rsvp-worker.yourname.workers.dev/admin/rsvps?limit=400
110+
const API_ADMIN_URL = "https://wedding.tiagoalfredo-rocha.workers.dev/admin/rsvps?limit=400";
111+
112+
const tokenEl = document.getElementById("token");
113+
const loadBtn = document.getElementById("load");
114+
const exportBtn = document.getElementById("export");
115+
const statusEl = document.getElementById("status");
116+
const rowsEl = document.getElementById("rows");
117+
118+
let lastResults = [];
119+
120+
function esc(s) {
121+
return String(s ?? "").replace(/[&<>"']/g, c => ({
122+
"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"
123+
}[c]));
124+
}
125+
126+
function toCsv(rows) {
127+
const header = ["id","created_at","name","email","attending","guests","notes"];
128+
const lines = [header.join(",")];
129+
130+
for (const r of rows) {
131+
const vals = [
132+
r.id,
133+
r.created_at,
134+
r.name,
135+
r.email ?? "",
136+
r.attending ? "yes" : "no",
137+
r.guests,
138+
(r.notes ?? "").replace(/\r?\n/g, " ")
139+
].map(v => {
140+
const str = String(v ?? "");
141+
return `"${str.replace(/"/g, '""')}"`;
142+
});
143+
lines.push(vals.join(","));
144+
}
145+
return lines.join("\n");
146+
}
147+
148+
function download(filename, text) {
149+
const blob = new Blob([text], { type: "text/csv;charset=utf-8" });
150+
const url = URL.createObjectURL(blob);
151+
const a = document.createElement("a");
152+
a.href = url;
153+
a.download = filename;
154+
document.body.appendChild(a);
155+
a.click();
156+
a.remove();
157+
URL.revokeObjectURL(url);
158+
}
159+
160+
async function loadRsvps() {
161+
rowsEl.innerHTML = "";
162+
statusEl.textContent = "Loading…";
163+
exportBtn.disabled = true;
164+
165+
const token = tokenEl.value.trim();
166+
if (!token) {
167+
statusEl.textContent = "Missing admin token.";
168+
return;
169+
}
170+
171+
const res = await fetch(API_ADMIN_URL, {
172+
headers: { "Authorization": `Bearer ${token}` }
173+
});
174+
175+
const out = await res.json().catch(() => ({}));
176+
177+
if (!res.ok) {
178+
statusEl.textContent = out.error || "Failed to load RSVPs.";
179+
return;
180+
}
181+
182+
lastResults = out.results || [];
183+
statusEl.textContent = `Loaded ${lastResults.length} RSVPs.`;
184+
exportBtn.disabled = lastResults.length === 0;
185+
186+
rowsEl.innerHTML = lastResults.map(r => `
187+
<tr>
188+
<td>${esc(r.id)}</td>
189+
<td class="mono">${esc(r.created_at)}</td>
190+
<td>${esc(r.name)}</td>
191+
<td>${esc(r.email)}</td>
192+
<td>${r.attending ? '<span class="tag is-yes">Yes</span>' : '<span class="tag is-no">No</span>'}</td>
193+
<td>${esc(r.guests)}</td>
194+
<td>${esc(r.notes)}</td>
195+
</tr>
196+
`).join("");
197+
}
198+
199+
loadBtn.addEventListener("click", loadRsvps);
200+
201+
exportBtn.addEventListener("click", () => {
202+
const csv = toCsv(lastResults);
203+
const ts = new Date().toISOString().slice(0,19).replace(/[:T]/g,"-");
204+
download(`rsvps-${ts}.csv`, csv);
205+
});
206+
</script>
207+
</body>
208+
</html>

0 commit comments

Comments
 (0)