Keep columns side-by-side with horizontal snap-scroll instead of stacking the second column under the first on small screens.
569 lines
22 KiB
HTML
569 lines
22 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<meta name="color-scheme" content="light dark" />
|
|
<script src="/static/stork-common.js"></script>
|
|
<script>StorkUI.applyTheme();</script>
|
|
<title>Stork — family name board</title>
|
|
<link rel="icon" href="/static/favicon.png" type="image/png" />
|
|
<link rel="apple-touch-icon" href="/static/favicon.png" />
|
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,700&family=Source+Sans+3:wght@400;600;700&display=swap" rel="stylesheet" />
|
|
<style>
|
|
|
|
:root, [data-theme="light"] {
|
|
--sky: #d6e4ef; --fog: #eef3f7; --paper: #f8faf8; --ink: #18253a; --muted: #4a5566;
|
|
--line: #c5d3e0; --accent: #2a5f7a; --wing: #b85c48; --up: #2f6b5a; --down: #9a4a3a;
|
|
--rank: #1e3a55; --shadow: 0 10px 32px rgba(24,37,58,.07); --radius: 16px;
|
|
--input-bg: #fff; --panel: #f8faf8; --logo-filter: none;
|
|
--body-bg: radial-gradient(900px 420px at 85% -5%, #f0d8cf 0%, transparent 55%),
|
|
radial-gradient(1100px 520px at -5% 0%, var(--sky) 0%, transparent 50%),
|
|
linear-gradient(180deg, #e7eef4 0%, var(--fog) 45%, #e9eee8 100%);
|
|
}
|
|
[data-theme="dark"] {
|
|
--sky: #1a2433; --fog: #121820; --paper: #161c26; --ink: #e8eef6; --muted: #a8b4c4;
|
|
--line: #2e3a4c; --accent: #7eb6d1; --wing: #e0a090; --up: #5fad93; --down: #d48474;
|
|
--rank: #c5d4e8; --shadow: 0 10px 32px rgba(0,0,0,.35);
|
|
--input-bg: #0f141c; --panel: #1a222e; --logo-filter: invert(1) contrast(1.05);
|
|
--body-bg: radial-gradient(900px 420px at 85% -5%, #2a2030 0%, transparent 55%),
|
|
radial-gradient(1100px 520px at -5% 0%, #1a2433 0%, transparent 50%),
|
|
linear-gradient(180deg, #10151c 0%, #121820 45%, #141a22 100%);
|
|
}
|
|
.skip{position:absolute;left:-9999px;top:0;background:var(--ink);color:var(--paper);padding:.5rem .75rem;z-index:10}
|
|
.skip:focus{left:.5rem;top:.5rem}
|
|
.theme-bar{display:flex;gap:.35rem;flex-wrap:wrap;align-items:center;margin:0 0 .85rem;justify-content:flex-end}
|
|
.theme-bar span{color:var(--muted);font-size:.9rem;margin-right:.25rem}
|
|
.theme-bar button{padding:.35rem .65rem;font-size:.85rem;min-height:2.25rem;background:var(--input-bg);color:var(--ink);border:1px solid var(--line)}
|
|
.theme-bar button.active{background:var(--ink);color:var(--paper);border-color:var(--ink)}
|
|
.error{color:var(--down);margin:.45rem 0 0;font-size:.95rem;font-weight:600;border-left:3px solid var(--down);padding-left:.55rem}
|
|
input[aria-invalid="true"]{border-color:var(--down);box-shadow:0 0 0 1px var(--down)}
|
|
* { box-sizing: border-box; }
|
|
body {
|
|
margin: 0;
|
|
color: var(--ink);
|
|
font-family: "Source Sans 3", system-ui, sans-serif;
|
|
background: var(--body-bg);
|
|
min-height: 100vh;
|
|
}
|
|
main { max-width: 1000px; margin: 0 auto; padding: 1.25rem 1rem 3.5rem; }
|
|
.hero {
|
|
display: flex;
|
|
gap: 0.85rem;
|
|
align-items: center;
|
|
margin-bottom: 1rem;
|
|
}
|
|
.hero img {
|
|
width: 56px;
|
|
height: 56px;
|
|
border-radius: 50%;
|
|
object-fit: cover;
|
|
border: 1px solid var(--line);
|
|
background: var(--input-bg); filter: var(--logo-filter);
|
|
flex: 0 0 auto;
|
|
}
|
|
.brand {
|
|
font-family: Fraunces, Georgia, serif;
|
|
font-size: clamp(1.9rem, 5vw, 2.5rem);
|
|
font-weight: 700;
|
|
letter-spacing: -0.03em;
|
|
margin: 0;
|
|
line-height: 1;
|
|
}
|
|
.sub { margin: 0.25rem 0 0; color: var(--muted); font-size: 0.95rem; }
|
|
.sub a { color: var(--accent); font-weight: 600; text-decoration: none; }
|
|
.panel {
|
|
background: var(--panel);
|
|
border: 1px solid var(--line);
|
|
border-radius: var(--radius);
|
|
padding: 1rem;
|
|
margin-bottom: 0.85rem;
|
|
box-shadow: var(--shadow);
|
|
}
|
|
label { display: block; font-size: 0.95rem; color: var(--muted); margin-bottom: 0.25rem; font-weight: 600; }
|
|
input, textarea, button {
|
|
font: inherit;
|
|
width: 100%;
|
|
padding: 0.65rem 0.75rem;
|
|
border-radius: 11px;
|
|
border: 1px solid var(--line);
|
|
background: var(--input-bg);
|
|
color: var(--ink);
|
|
}
|
|
textarea { min-height: 2.8rem; resize: vertical; }
|
|
button {
|
|
width: auto;
|
|
cursor: pointer;
|
|
background: var(--accent);
|
|
color: #fff;
|
|
border-color: transparent;
|
|
font-weight: 600;
|
|
}
|
|
button:focus-visible, input:focus-visible, textarea:focus-visible {
|
|
outline: 2px solid var(--wing);
|
|
outline-offset: 2px;
|
|
}
|
|
button.secondary { background: var(--input-bg); color: var(--ink); border-color: var(--line); }
|
|
button.icon {
|
|
width: 2rem; height: 2rem; padding: 0;
|
|
display: inline-grid; place-items: center;
|
|
border-radius: 999px; background: var(--input-bg); color: var(--accent);
|
|
border: 1px solid var(--line); flex: 0 0 auto;
|
|
}
|
|
button.icon.playing { background: var(--accent); color: #fff; }
|
|
button.vote {
|
|
min-width: 2.2rem; padding: 0.3rem 0.45rem;
|
|
background: var(--input-bg); color: var(--ink); border: 1px solid var(--line);
|
|
}
|
|
button.vote.active-up { background: var(--up); color: #fff; border-color: transparent; }
|
|
button.vote.active-down { background: var(--down); color: #fff; border-color: transparent; }
|
|
button.pill {
|
|
padding: 0.2rem 0.55rem;
|
|
border-radius: 999px;
|
|
font-size: 0.8rem;
|
|
letter-spacing: 0.04em;
|
|
text-transform: uppercase;
|
|
background: var(--input-bg);
|
|
color: var(--muted);
|
|
border: 1px solid var(--line);
|
|
}
|
|
button.pill.active { background: var(--ink); color: var(--paper); border-color: transparent; }
|
|
.row { display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: end; }
|
|
.who {
|
|
display: flex; justify-content: space-between; gap: 0.75rem;
|
|
align-items: baseline; flex-wrap: wrap; margin-bottom: 0.75rem;
|
|
color: var(--muted); font-size: 0.92rem;
|
|
}
|
|
.who a { color: var(--accent); font-weight: 600; text-decoration: none; }
|
|
.boards {
|
|
display: flex; gap: 0.85rem; margin-bottom: 0.85rem;
|
|
overflow-x: auto; -webkit-overflow-scrolling: touch;
|
|
scroll-snap-type: x mandatory; overscroll-behavior-x: contain;
|
|
padding-bottom: 0.25rem;
|
|
}
|
|
.boards > section {
|
|
flex: 0 0 auto;
|
|
width: min(18.5rem, calc(100vw - 2.5rem));
|
|
scroll-snap-align: start;
|
|
}
|
|
@media (min-width: 860px) {
|
|
.boards > section { flex: 1 1 0; width: auto; min-width: 16rem; }
|
|
}
|
|
.col-head {
|
|
display: flex; align-items: baseline; justify-content: space-between;
|
|
gap: 0.5rem; margin-bottom: 0.35rem; padding-bottom: 0.45rem;
|
|
border-bottom: 1px solid var(--line);
|
|
}
|
|
.col-head h2 {
|
|
font-family: Fraunces, Georgia, serif;
|
|
font-size: 1.2rem; margin: 0; font-weight: 700;
|
|
}
|
|
.col-head .count { color: var(--muted); font-size: 0.95rem; font-weight: 600; }
|
|
.name-card {
|
|
display: grid;
|
|
grid-template-columns: 2.2rem 1fr auto;
|
|
gap: 0.55rem;
|
|
align-items: start;
|
|
padding: 0.7rem 0.1rem;
|
|
border-top: 1px solid color-mix(in srgb, var(--line) 65%, transparent);
|
|
}
|
|
.name-card:first-of-type { border-top: 0; }
|
|
.score {
|
|
font-family: Fraunces, Georgia, serif;
|
|
font-variant-numeric: tabular-nums;
|
|
font-weight: 700; font-size: 1.2rem; color: var(--rank);
|
|
text-align: center; padding-top: 0.1rem;
|
|
}
|
|
.spelling {
|
|
font-family: Fraunces, Georgia, serif;
|
|
font-size: 1.25rem; margin: 0 0 0.1rem; letter-spacing: -0.02em;
|
|
}
|
|
.meta { color: var(--muted); font-size: 0.95rem; margin: 0 0 0.4rem; }
|
|
.say-row {
|
|
display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
.say {
|
|
font-family: Fraunces, Georgia, serif;
|
|
font-size: 1.05rem; margin: 0; min-width: 3rem;
|
|
}
|
|
.pills { display: flex; gap: 0.25rem; }
|
|
details.more { margin-top: 0.25rem; }
|
|
details.more summary {
|
|
cursor: pointer; color: var(--accent); font-weight: 600; font-size: 0.85rem;
|
|
list-style: none;
|
|
}
|
|
details.more summary::-webkit-details-marker { display: none; }
|
|
.note {
|
|
margin: 0.4rem 0 0; padding: 0.5rem 0.6rem;
|
|
background: color-mix(in srgb, var(--sky) 30%, white);
|
|
border-radius: 10px; color: var(--muted); font-size: 0.86rem;
|
|
}
|
|
.votes { display: flex; flex-direction: column; gap: 0.25rem; }
|
|
.error { color: var(--down); margin: 0.45rem 0 0; }
|
|
.hidden { display: none !important; }
|
|
.empty { color: var(--muted); padding: 0.55rem 0.1rem; font-size: 0.92rem; }
|
|
.compose-head {
|
|
display: flex; flex-wrap: wrap; gap: 0.65rem; align-items: end;
|
|
justify-content: space-between; margin-bottom: 0.75rem;
|
|
}
|
|
.compose-head h2 {
|
|
font-family: Fraunces, Georgia, serif; margin: 0; font-size: 1.15rem;
|
|
}
|
|
.kind-toggle { display: flex; gap: 0.3rem; }
|
|
.kind-toggle button {
|
|
min-width: 5.5rem;
|
|
background: transparent;
|
|
color: var(--ink);
|
|
border: 1px solid var(--line);
|
|
}
|
|
.kind-toggle button.active {
|
|
background: var(--ink);
|
|
color: var(--paper);
|
|
border-color: var(--ink);
|
|
}
|
|
.kind-toggle button:not(.active):hover {
|
|
border-color: var(--accent);
|
|
}
|
|
.grid-3 { display: grid; gap: 0.55rem; }
|
|
@media (min-width: 720px) { .grid-3 { grid-template-columns: repeat(3, 1fr); } }
|
|
.hint { color: var(--muted); font-size: 0.95rem; margin: 0.25rem 0 0.55rem; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<a class="skip" href="#main">Skip to content</a>
|
|
<main id="main">
|
|
<div class="theme-bar" role="group" aria-label="Color theme">
|
|
<span>Theme</span>
|
|
<button type="button" data-theme-toggle="light">Light</button>
|
|
<button type="button" data-theme-toggle="dark">Dark</button>
|
|
<button type="button" data-theme-toggle="system">System</button>
|
|
</div>
|
|
|
|
<header class="hero">
|
|
<img src="/static/logo-mark.png" alt="Stork" width="56" height="56" />
|
|
<div>
|
|
<h1 class="brand">Stork</h1>
|
|
<p class="sub">Classic layout · <a href="/">Paper board</a> · <a href="/logos">Logo ideas</a></p>
|
|
</div>
|
|
</header>
|
|
|
|
<section id="gate" class="panel">
|
|
<label for="invite">Invite code</label>
|
|
<input id="invite" autocomplete="off" />
|
|
<label for="display" style="margin-top:0.65rem">Your name</label>
|
|
<input id="display" autocomplete="nickname" />
|
|
<p class="error hidden" id="gate-error" role="alert" aria-live="polite"></p>
|
|
<div class="row" style="margin-top:0.65rem">
|
|
<button id="enter" type="button">Enter</button>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="board" class="hidden">
|
|
<div class="who">
|
|
<span id="who"></span>
|
|
<a href="#suggest">Suggest a name</a>
|
|
</div>
|
|
|
|
<div class="boards">
|
|
<section class="panel" aria-labelledby="first-heading">
|
|
<div class="col-head">
|
|
<h2 id="first-heading">First</h2>
|
|
<span class="count" id="first-count"></span>
|
|
</div>
|
|
<div id="list-first"></div>
|
|
</section>
|
|
<section class="panel" aria-labelledby="middle-heading">
|
|
<div class="col-head">
|
|
<h2 id="middle-heading">Middle</h2>
|
|
<span class="count" id="middle-count"></span>
|
|
</div>
|
|
<div id="list-middle"></div>
|
|
</section>
|
|
</div>
|
|
|
|
<div class="panel" id="suggest">
|
|
<div class="compose-head">
|
|
<h2>Suggest a name</h2>
|
|
<div class="kind-toggle" role="group" aria-label="Name type">
|
|
<button type="button" class="secondary active" data-kind="c1">First</button>
|
|
<button type="button" class="secondary" data-kind="c2">Middle</button>
|
|
</div>
|
|
</div>
|
|
<label for="spelling">Spelling</label>
|
|
<input id="spelling" required aria-required="true" autocomplete="off" />
|
|
<p class="hint">Pronunciation fields: just the name in that language (e.g. Roze → שושנה).</p>
|
|
<div class="grid-3">
|
|
<div>
|
|
<label>English</label>
|
|
<input data-loc="en" data-field="pronunciation" placeholder="Name" />
|
|
<label style="margin-top:0.4rem">Origin</label>
|
|
<input data-loc="en" data-field="origin" />
|
|
<label style="margin-top:0.4rem">Meaning</label>
|
|
<textarea data-loc="en" data-field="meaning"></textarea>
|
|
</div>
|
|
<div>
|
|
<label>Russian</label>
|
|
<input data-loc="ru" data-field="pronunciation" placeholder="Имя" />
|
|
<label style="margin-top:0.4rem">Origin</label>
|
|
<input data-loc="ru" data-field="origin" />
|
|
<label style="margin-top:0.4rem">Meaning</label>
|
|
<textarea data-loc="ru" data-field="meaning"></textarea>
|
|
</div>
|
|
<div>
|
|
<label>Hebrew</label>
|
|
<input data-loc="he" data-field="pronunciation" placeholder="שם" dir="auto" />
|
|
<label style="margin-top:0.4rem">Origin</label>
|
|
<input data-loc="he" data-field="origin" dir="auto" />
|
|
<label style="margin-top:0.4rem">Meaning</label>
|
|
<textarea data-loc="he" data-field="meaning" dir="auto"></textarea>
|
|
</div>
|
|
</div>
|
|
<p class="error hidden" id="add-error" role="alert" aria-live="polite"></p>
|
|
<div class="row" style="margin-top:0.65rem">
|
|
<button id="add" type="button">Add</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
<script>
|
|
const state = { kind: "c1", c1: [], c2: [], langById: {} };
|
|
const SPEECH_LANG = { en: "en-US", ru: "ru-RU", he: "he-IL" };
|
|
let speakingBtn = null;
|
|
|
|
function params() { return new URLSearchParams(location.search); }
|
|
|
|
async function api(path, opts = {}) {
|
|
const res = await fetch(path, {
|
|
credentials: "include",
|
|
headers: { "Content-Type": "application/json", ...(opts.headers || {}) },
|
|
...opts,
|
|
});
|
|
const text = await res.text();
|
|
let data = null;
|
|
try { data = text ? JSON.parse(text) : null; } catch { data = { detail: text }; }
|
|
if (!res.ok) {
|
|
const detail = (data && (data.detail || data.message)) || res.statusText;
|
|
throw new Error(StorkUI.formatError(detail));
|
|
}
|
|
return data;
|
|
}
|
|
|
|
function escapeHtml(s) {
|
|
return String(s).replace(/[&<>"']/g, (c) => ({
|
|
"&": "&", "<": "<", ">": ">", '"': """, "'": "'"
|
|
})[c]);
|
|
}
|
|
|
|
function stopSpeech() {
|
|
if (window.speechSynthesis) window.speechSynthesis.cancel();
|
|
if (speakingBtn) { speakingBtn.classList.remove("playing"); speakingBtn = null; }
|
|
}
|
|
|
|
function play(lang, text, btn) {
|
|
if (!window.speechSynthesis) {
|
|
alert("Speech not supported in this browser.");
|
|
return;
|
|
}
|
|
const say = (text || "").trim();
|
|
if (!say) return;
|
|
stopSpeech();
|
|
const u = new SpeechSynthesisUtterance(say);
|
|
u.lang = SPEECH_LANG[lang] || "en-US";
|
|
u.rate = 0.92;
|
|
const voices = speechSynthesis.getVoices();
|
|
const match = voices.find((v) => v.lang === u.lang)
|
|
|| voices.find((v) => v.lang && v.lang.startsWith(u.lang.slice(0, 2)));
|
|
if (match) u.voice = match;
|
|
speakingBtn = btn;
|
|
btn.classList.add("playing");
|
|
u.onend = u.onerror = () => { btn.classList.remove("playing"); speakingBtn = null; };
|
|
speechSynthesis.speak(u);
|
|
}
|
|
|
|
function formFor(n, lang) {
|
|
const L = (n.locales && n.locales[lang]) || {};
|
|
return (L.pronunciation || "").trim() || (lang === "en" ? n.spelling : "");
|
|
}
|
|
|
|
function noteFor(n, lang) {
|
|
const L = (n.locales && n.locales[lang]) || {};
|
|
return [L.origin, L.meaning].filter(Boolean).join(" — ");
|
|
}
|
|
|
|
function renderCard(n, kind) {
|
|
const lang = state.langById[n.id] || "en";
|
|
const form = formFor(n, lang);
|
|
const note = noteFor(n, lang);
|
|
const pills = ["en", "ru", "he"].map((l) =>
|
|
`<button type="button" class="pill ${lang === l ? "active" : ""}" data-lang="${l}">${l}</button>`
|
|
).join("");
|
|
return `
|
|
<article class="name-card" data-id="${n.id}" data-kind="${kind}">
|
|
<div class="score">${n.score}</div>
|
|
<div>
|
|
<p class="spelling" dir="auto">${escapeHtml(n.spelling)}</p>
|
|
<p class="meta">${escapeHtml(n.created_by || "someone")} · ${n.vote_count}</p>
|
|
<div class="say-row">
|
|
<div class="pills">${pills}</div>
|
|
<p class="say" dir="auto">${escapeHtml(form || "—")}</p>
|
|
<button type="button" class="icon" data-play
|
|
data-lang="${lang}" data-text="${escapeHtml(form)}"
|
|
aria-label="Play">▶</button>
|
|
</div>
|
|
${note ? `<details class="more"><summary>Meaning</summary><p class="note" dir="auto">${escapeHtml(note)}</p></details>` : ""}
|
|
</div>
|
|
<div class="votes">
|
|
<button type="button" class="vote ${n.my_vote === 1 ? "active-up" : ""}" data-vote="1" aria-label="Upvote">▲</button>
|
|
<button type="button" class="vote ${n.my_vote === -1 ? "active-down" : ""}" data-vote="-1" aria-label="Downvote">▼</button>
|
|
</div>
|
|
</article>`;
|
|
}
|
|
|
|
function bindCard(root, items) {
|
|
root.querySelectorAll("button[data-vote]").forEach((btn) => {
|
|
btn.addEventListener("click", async () => {
|
|
const card = btn.closest(".name-card");
|
|
const id = card.dataset.id;
|
|
let value = Number(btn.dataset.vote);
|
|
const item = items.find((x) => String(x.id) === String(id));
|
|
if (item && item.my_vote === value) value = 0;
|
|
await api(`/api/names/${id}/vote`, { method: "POST", body: JSON.stringify({ value }) });
|
|
await refresh();
|
|
});
|
|
});
|
|
root.querySelectorAll("button[data-lang]").forEach((btn) => {
|
|
btn.addEventListener("click", () => {
|
|
const id = Number(btn.closest(".name-card").dataset.id);
|
|
state.langById[id] = btn.dataset.lang;
|
|
const kind = btn.closest(".name-card").dataset.kind;
|
|
renderList(kind, kind === "c1" ? state.c1 : state.c2);
|
|
});
|
|
});
|
|
root.querySelectorAll("button[data-play]").forEach((btn) => {
|
|
btn.addEventListener("click", () => play(btn.dataset.lang, btn.dataset.text, btn));
|
|
});
|
|
}
|
|
|
|
function renderList(kind, items) {
|
|
const root = document.getElementById(kind === "c1" ? "list-first" : "list-middle");
|
|
const count = document.getElementById(kind === "c1" ? "first-count" : "middle-count");
|
|
count.textContent = String(items.length);
|
|
if (!items.length) {
|
|
root.innerHTML = `<p class="empty">None yet</p>`;
|
|
return;
|
|
}
|
|
root.innerHTML = items.map((n) => renderCard(n, kind)).join("");
|
|
bindCard(root, items);
|
|
}
|
|
|
|
function localePayload() {
|
|
const locales = { en: {}, ru: {}, he: {} };
|
|
document.querySelectorAll("[data-loc][data-field]").forEach((el) => {
|
|
locales[el.dataset.loc][el.dataset.field] = el.value.trim();
|
|
});
|
|
return locales;
|
|
}
|
|
|
|
function clearForm() {
|
|
document.getElementById("spelling").value = "";
|
|
document.querySelectorAll("[data-loc][data-field]").forEach((el) => { el.value = ""; });
|
|
}
|
|
|
|
async function refresh() {
|
|
const first = await api("/api/names?kind=c1");
|
|
state.c1 = first.items || [];
|
|
state.c2 = [];
|
|
renderList("c1", state.c1);
|
|
const midRoot = document.getElementById("list-middle");
|
|
const midCount = document.getElementById("middle-count");
|
|
if (midRoot) midRoot.innerHTML = `<p class="empty">Use the <a href="/">paper board</a> for more columns.</p>`;
|
|
if (midCount) midCount.textContent = "—";
|
|
}
|
|
|
|
async function showBoard(displayName) {
|
|
document.getElementById("gate").classList.add("hidden");
|
|
document.getElementById("board").classList.remove("hidden");
|
|
document.getElementById("who").textContent = displayName;
|
|
if (window.speechSynthesis) speechSynthesis.getVoices();
|
|
await refresh();
|
|
}
|
|
|
|
document.querySelectorAll(".kind-toggle button").forEach((btn) => {
|
|
btn.setAttribute("aria-pressed", btn.classList.contains("active") ? "true" : "false");
|
|
btn.addEventListener("click", () => {
|
|
state.kind = btn.dataset.kind;
|
|
document.querySelectorAll(".kind-toggle button").forEach((b) => {
|
|
const on = b === btn;
|
|
b.classList.toggle("active", on);
|
|
b.setAttribute("aria-pressed", on ? "true" : "false");
|
|
});
|
|
});
|
|
});
|
|
|
|
document.getElementById("enter").addEventListener("click", async () => {
|
|
const err = document.getElementById("gate-error");
|
|
StorkUI.clearError(err);
|
|
try {
|
|
const data = await api("/api/session", {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
invite: document.getElementById("invite").value.trim(),
|
|
display_name: document.getElementById("display").value.trim(),
|
|
}),
|
|
});
|
|
const url = new URL(location.href);
|
|
url.searchParams.delete("invite");
|
|
history.replaceState({}, "", url.pathname + (url.search || ""));
|
|
await showBoard(data.display_name);
|
|
} catch (e) {
|
|
StorkUI.showError(err, e.message);
|
|
}
|
|
});
|
|
|
|
document.getElementById("add").addEventListener("click", async () => {
|
|
const err = document.getElementById("add-error");
|
|
const spellingEl = document.getElementById("spelling");
|
|
StorkUI.clearError(err);
|
|
spellingEl.removeAttribute("aria-invalid");
|
|
const spelling = spellingEl.value.trim();
|
|
if (!spelling) {
|
|
StorkUI.showError(err, "Enter a name spelling");
|
|
spellingEl.setAttribute("aria-invalid", "true");
|
|
spellingEl.focus();
|
|
return;
|
|
}
|
|
try {
|
|
await api("/api/names", {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
kind: state.kind,
|
|
spelling,
|
|
locales: localePayload(),
|
|
}),
|
|
});
|
|
clearForm();
|
|
await refresh();
|
|
} catch (e) {
|
|
StorkUI.showError(err, e.message);
|
|
spellingEl.setAttribute("aria-invalid", "true");
|
|
}
|
|
});
|
|
|
|
StorkUI.initThemeControls();
|
|
if (window.speechSynthesis) speechSynthesis.onvoiceschanged = () => speechSynthesis.getVoices();
|
|
|
|
(async () => {
|
|
const invite = params().get("invite");
|
|
if (invite) document.getElementById("invite").value = invite;
|
|
try {
|
|
const session = await api("/api/session");
|
|
if (session.authenticated) await showBoard(session.display_name);
|
|
} catch (_) {}
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|