160 lines
9.1 KiB
HTML
160 lines
9.1 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>People</title>
|
|
<meta name="robots" content="noindex">
|
|
<link rel="stylesheet" href="/assets/app.css">
|
|
<style>
|
|
table{width:100%;border-collapse:collapse;font-size:14px}
|
|
th,td{text-align:left;padding:9px 10px;border-bottom:1px solid var(--line);vertical-align:top}
|
|
th{font:600 11px/1 var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--muted)}
|
|
td .sub{color:var(--muted);font-size:12.5px}
|
|
.pill{display:inline-block;font:600 11px/1 var(--mono);padding:4px 8px;border-radius:5px;border:1px solid var(--line);color:var(--muted)}
|
|
.pill.joined{color:var(--good);border-color:var(--good)} .pill.invited{color:var(--accent2);border-color:var(--accent2)}
|
|
.linkbox{margin-top:8px;display:flex;gap:8px}
|
|
.linkbox input{font:12px var(--mono)}
|
|
.btn.small{padding:7px 12px;font-size:13px}
|
|
.state{display:flex;gap:18px;flex-wrap:wrap;color:var(--muted);font-size:14px;margin-bottom:6px}
|
|
.state b{color:var(--ink)}
|
|
.exrow{display:grid;grid-template-columns:150px 1fr;gap:16px;padding:14px 0;border-bottom:1px solid var(--line)}
|
|
.exrow video{width:100%;border-radius:8px;background:#000}
|
|
.exrow input[type=text]{margin-bottom:8px}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="wrap">
|
|
|
|
<header class="page">
|
|
<h1>People</h1>
|
|
<p>Who is waiting, who has been invited, who is in — and which finished videos are shown on the Examples page.</p>
|
|
</header>
|
|
|
|
<div class="msg" id="msg"></div>
|
|
|
|
<section>
|
|
<h2 class="sec">How people get in</h2>
|
|
<div class="card"><div class="state" id="state">Loading…</div>
|
|
<p style="color:var(--muted);margin:8px 0 0;font-size:14px" id="statenote"></p></div>
|
|
</section>
|
|
|
|
<section>
|
|
<h2 class="sec">The waiting list</h2>
|
|
<div class="card" style="overflow-x:auto"><table id="wl"></table></div>
|
|
</section>
|
|
|
|
<section>
|
|
<h2 class="sec">Accounts</h2>
|
|
<div class="card" style="overflow-x:auto"><table id="users"></table></div>
|
|
</section>
|
|
|
|
<section>
|
|
<h2 class="sec">Examples page</h2>
|
|
<div class="card" id="examples">Loading…</div>
|
|
</section>
|
|
</div>
|
|
|
|
<script src="/assets/app.js"></script>
|
|
<script>
|
|
navBar("people");
|
|
requireSession();
|
|
|
|
function day(s) { return s ? when(s) : ""; }
|
|
|
|
async function load() {
|
|
var r = await api("waitlist_list", {});
|
|
if (!r || !r.ok) { showMsg("msg", (r && r.error) || "That could not be loaded."); return; }
|
|
document.getElementById("state").innerHTML =
|
|
'<span>Sign-in today: <b>' + (r.auth === "sms" ? "invitation and a texted code" : "an email address is enough (prototype rule)") + '</b></span>'
|
|
+ '<span>Text messages: <b>' + (r.sms ? "switched on" : "not switched on yet") + '</b></span>';
|
|
document.getElementById("statenote").textContent = r.sms
|
|
? "An invitation link texts a code to the person's phone and signs them in."
|
|
: "Until texting is switched on, an invitation link opens but cannot send its code. Invitations made now work as soon as it is.";
|
|
document.getElementById("wl").innerHTML =
|
|
'<tr><th>Who</th><th>Phone</th><th>Asked</th><th>Where they are</th><th></th></tr>'
|
|
+ (r.waitlist.length ? r.waitlist.slice().reverse().map(function (w) {
|
|
return '<tr><td><b>' + esc(w.first_name) + ' ' + esc(w.last_name) + '</b><div class="sub">' + esc(w.email) + '</div></td>'
|
|
+ '<td>' + esc(w.phone || "") + '</td>'
|
|
+ '<td>' + esc(day(w.created_at)) + '<div class="sub">' + esc(w.site || "") + '</div></td>'
|
|
+ '<td><span class="pill ' + esc(w.status) + '">' + esc(w.status) + '</span>'
|
|
+ (w.invited_at ? '<div class="sub">invited ' + esc(day(w.invited_at)) + (w.opened ? ', link opened' : ', not opened yet') + '</div>' : '')
|
|
+ (w.joined_at ? '<div class="sub">joined ' + esc(day(w.joined_at)) + '</div>' : '') + '</td>'
|
|
+ '<td>' + (w.status === "joined" ? '' : '<button type="button" class="btn small" data-invite="' + esc(w.id) + '">'
|
|
+ (w.invites ? 'New invitation link' : 'Invite') + '</button>') + '<div data-link="' + esc(w.id) + '"></div></td></tr>';
|
|
}).join("") : '<tr><td colspan="5" style="color:var(--muted)">Nobody has joined the list yet. The page people use is <a href="/join.html">/join.html</a>.</td></tr>');
|
|
document.getElementById("users").innerHTML =
|
|
'<tr><th>Account</th><th>Phone</th><th>Since</th><th>Video made</th></tr>'
|
|
+ r.users.map(function (u) {
|
|
return '<tr><td><b>' + esc(u.name || "") + '</b><div class="sub">' + esc(u.email) + '</div></td><td>'
|
|
+ (u.phone ? esc(u.phone) : '<span class="sub">none — cannot receive a code</span> '
|
|
+ '<button type="button" class="btn small ghost" data-phone="' + esc(u.email) + '">Add a phone</button>')
|
|
+ '</td><td>' + esc(day(u.created_at)) + '</td><td>' + esc(u.minutes) + ' min</td></tr>';
|
|
}).join("");
|
|
}
|
|
|
|
async function loadExamples() {
|
|
var r = await api("demo_candidates", {});
|
|
var box = document.getElementById("examples");
|
|
if (!r || !r.ok) { box.textContent = (r && r.error) || "That could not be loaded."; return; }
|
|
box.innerHTML = '<p style="color:var(--muted);margin-top:0">Tick a finished video to show it on <a href="/demo.html">the Examples page</a>, '
|
|
+ 'with its script beside it. Anybody can see that page, signed in or not.</p>'
|
|
+ (r.projects.length ? r.projects.map(function (p) {
|
|
return '<div class="exrow"><video controls preload="none" src="' + esc(p.video) + '"></video><div>'
|
|
+ '<label><input type="checkbox" data-demo="' + esc(p.id) + '"' + (p.demo ? ' checked' : '') + '> Show on the Examples page</label> '
|
|
+ '<span class="sub" style="color:var(--muted);font-size:12.5px">' + esc(p.kind === "book" ? "from a book" : "pet video") + ' · ' + esc(day(p.created_at)) + ' · ' + esc(p.owner || "") + '</span>'
|
|
+ '<input type="text" maxlength="120" data-demotitle="' + esc(p.id) + '" value="' + esc(p.title || "") + '" placeholder="Title shown to visitors">'
|
|
+ '<input type="text" maxlength="400" data-demonote="' + esc(p.id) + '" value="' + esc(p.demo_note || "") + '" placeholder="One line about it (optional)">'
|
|
+ '<button type="button" class="btn small ghost" data-demosave="' + esc(p.id) + '">Save the words</button></div></div>';
|
|
}).join("") : '<p>No finished videos yet.</p>');
|
|
}
|
|
|
|
document.addEventListener("click", async function (e) {
|
|
var b = e.target.closest && e.target.closest("button");
|
|
if (!b) return;
|
|
var id;
|
|
if ((id = b.getAttribute("data-invite"))) {
|
|
b.disabled = true;
|
|
var r = await api("waitlist_invite", { waitlist_id: id });
|
|
b.disabled = false;
|
|
if (!r || !r.ok) { showMsg("msg", (r && r.error) || "The invitation could not be made."); return; }
|
|
var slot = document.querySelector('[data-link="' + id + '"]');
|
|
slot.innerHTML = '<div class="linkbox"><input type="text" readonly value="' + esc(r.link) + '"><button type="button" class="btn small ghost" data-copy="1">Copy</button></div>'
|
|
+ '<div class="sub" style="color:var(--muted);font-size:12.5px;margin-top:4px">Send this link to ' + esc(r.first_name || r.email)
|
|
+ ' yourself for now — we do not send email yet. It works once, until ' + esc(day(r.expires_at)) + ', and is shown only this one time.</div>';
|
|
showMsg("msg", "Invitation made for " + r.email + ". Copy the link below their name and send it to them.", "ok");
|
|
} else if (b.getAttribute("data-copy")) {
|
|
var inp = b.parentElement.querySelector("input");
|
|
inp.select();
|
|
try { await navigator.clipboard.writeText(inp.value); b.textContent = "Copied"; } catch (x) { document.execCommand("copy"); b.textContent = "Copied"; }
|
|
} else if ((id = b.getAttribute("data-phone"))) {
|
|
var ph = prompt("US cell phone for " + id + " (ten digits). A sign-in code will be texted to it.");
|
|
if (!ph) return;
|
|
var r2 = await api("set_user_phone", { target_email: id, phone: ph });
|
|
if (!r2 || !r2.ok) { showMsg("msg", (r2 && r2.error) || "That phone could not be saved."); return; }
|
|
showMsg("msg", "Phone saved for " + id + ": " + r2.phone, "ok");
|
|
load();
|
|
} else if ((id = b.getAttribute("data-demosave"))) {
|
|
var r3 = await api("set_demo", { project_id: id,
|
|
demo: document.querySelector('[data-demo="' + id + '"]').checked,
|
|
demo_title: document.querySelector('[data-demotitle="' + id + '"]').value,
|
|
demo_note: document.querySelector('[data-demonote="' + id + '"]').value });
|
|
showMsg("msg", r3 && r3.ok ? "Saved." : ((r3 && r3.error) || "That could not be saved."), r3 && r3.ok ? "ok" : "err");
|
|
}
|
|
});
|
|
|
|
document.addEventListener("change", async function (e) {
|
|
var id = e.target.getAttribute && e.target.getAttribute("data-demo");
|
|
if (!id) return;
|
|
var r = await api("set_demo", { project_id: id, demo: e.target.checked,
|
|
demo_title: document.querySelector('[data-demotitle="' + id + '"]').value,
|
|
demo_note: document.querySelector('[data-demonote="' + id + '"]').value });
|
|
if (!r || !r.ok) { e.target.checked = !e.target.checked; showMsg("msg", (r && r.error) || "That could not be saved."); return; }
|
|
showMsg("msg", e.target.checked ? "Now on the Examples page." : "Taken off the Examples page.", "ok");
|
|
});
|
|
|
|
load();
|
|
loadExamples();
|
|
</script>
|
|
</body>
|
|
</html>
|