Initial psopeeps site import
This commit is contained in:
+614
@@ -0,0 +1,614 @@
|
||||
(() => {
|
||||
const API_BASE = "/api";
|
||||
|
||||
async function api(path, options = {}) {
|
||||
const res = await fetch(`${API_BASE}${path}`, {
|
||||
credentials: "same-origin",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(options.headers || {}),
|
||||
},
|
||||
...options,
|
||||
});
|
||||
|
||||
let data = {};
|
||||
try {
|
||||
data = await res.json();
|
||||
} catch (_) {}
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(data.error || `Request failed: ${res.status}`);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value ?? "").replace(/[&<>"']/g, (ch) => ({
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
'"': """,
|
||||
"'": "'",
|
||||
}[ch]));
|
||||
}
|
||||
|
||||
function replaceText(root, from, to) {
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
||||
const nodes = [];
|
||||
|
||||
while (walker.nextNode()) {
|
||||
if (walker.currentNode.nodeValue.includes(from)) {
|
||||
nodes.push(walker.currentNode);
|
||||
}
|
||||
}
|
||||
|
||||
for (const node of nodes) {
|
||||
node.nodeValue = node.nodeValue.replaceAll(from, to);
|
||||
}
|
||||
}
|
||||
|
||||
function buildLoginForm() {
|
||||
const form = document.createElement("form");
|
||||
form.className = "top-account-form";
|
||||
form.action = "#";
|
||||
form.method = "post";
|
||||
form.setAttribute("aria-label", "Account login or registration");
|
||||
|
||||
let mode = "login";
|
||||
|
||||
function render() {
|
||||
form.classList.toggle("is-register", mode === "register");
|
||||
form.classList.toggle("is-login", mode !== "register");
|
||||
|
||||
if (mode === "register") {
|
||||
form.innerHTML = `
|
||||
<label class="sr-only" for="top-username">Username</label>
|
||||
<input id="top-username" name="username" type="text" placeholder="username" autocomplete="username">
|
||||
|
||||
<label class="sr-only" for="top-email">Email</label>
|
||||
<input id="top-email" name="email" type="email" placeholder="email" autocomplete="email">
|
||||
|
||||
<label class="sr-only" for="top-password">Password</label>
|
||||
<input id="top-password" name="password" type="password" placeholder="password" autocomplete="new-password">
|
||||
|
||||
<div class="top-account-actions">
|
||||
<button class="button-compact" type="submit" data-action="create-account">Create Account</button>
|
||||
<button class="button-compact button-secondary" type="button" data-action="cancel-register">Cancel</button>
|
||||
</div>
|
||||
|
||||
<div class="auth-message" role="status"></div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
form.innerHTML = `
|
||||
<label class="sr-only" for="top-username">Username</label>
|
||||
<input id="top-username" name="username" type="text" placeholder="username" autocomplete="username">
|
||||
|
||||
<label class="sr-only" for="top-password">Password</label>
|
||||
<input id="top-password" name="password" type="password" placeholder="password" autocomplete="current-password">
|
||||
|
||||
<button class="button-compact" type="submit" data-action="login">Login</button>
|
||||
<button class="button-compact button-secondary" type="button" data-action="show-register">Register</button>
|
||||
|
||||
<div class="auth-message" role="status"></div>
|
||||
`;
|
||||
}
|
||||
|
||||
function setMessage(text, kind = "error") {
|
||||
const message = form.querySelector(".auth-message");
|
||||
if (!message) return;
|
||||
|
||||
message.textContent = text || "";
|
||||
message.classList.remove("is-error", "is-ok");
|
||||
message.classList.add(kind === "ok" ? "is-ok" : "is-error");
|
||||
}
|
||||
|
||||
form.addEventListener("click", (event) => {
|
||||
const button = event.target.closest("button[data-action]");
|
||||
if (!button) return;
|
||||
|
||||
const action = button.dataset.action;
|
||||
|
||||
if (action === "show-register") {
|
||||
event.preventDefault();
|
||||
mode = "register";
|
||||
render();
|
||||
form.querySelector('input[name="username"]')?.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === "cancel-register") {
|
||||
event.preventDefault();
|
||||
mode = "login";
|
||||
render();
|
||||
form.querySelector('input[name="username"]')?.focus();
|
||||
}
|
||||
});
|
||||
|
||||
form.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const submitter = event.submitter;
|
||||
const action = submitter?.dataset.action || "login";
|
||||
|
||||
const username = form.querySelector('input[name="username"]')?.value.trim() || "";
|
||||
const password = form.querySelector('input[name="password"]')?.value || "";
|
||||
const email = form.querySelector('input[name="email"]')?.value.trim() || "";
|
||||
|
||||
setMessage("");
|
||||
|
||||
try {
|
||||
if (action === "create-account") {
|
||||
await api("/register", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ username, email, password }),
|
||||
});
|
||||
|
||||
setMessage("Account created. Check your email to verify your account.", "ok");
|
||||
window.setTimeout(() => {
|
||||
window.location.href = "account-ready.html";
|
||||
}, 900);
|
||||
return;
|
||||
}
|
||||
|
||||
await api("/login", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
|
||||
window.location.href = "account-ready.html";
|
||||
} catch (err) {
|
||||
setMessage(err.message);
|
||||
}
|
||||
});
|
||||
|
||||
render();
|
||||
return form;
|
||||
}
|
||||
|
||||
|
||||
function buildSignedInStatus(user) {
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "top-account-status";
|
||||
|
||||
wrap.innerHTML = `
|
||||
<span class="status-dot" aria-hidden="true"></span>
|
||||
<a href="account-ready.html">Signed in as ${escapeHtml(user.username)}</a>
|
||||
<button class="button-compact button-secondary" type="button" data-logout>Logout</button>
|
||||
`;
|
||||
|
||||
wrap.querySelector("[data-logout]").addEventListener("click", async () => {
|
||||
await api("/logout", { method: "POST" });
|
||||
window.location.href = "index.html";
|
||||
});
|
||||
|
||||
return wrap;
|
||||
}
|
||||
|
||||
function renderHeader(user) {
|
||||
const header = document.querySelector(".site-header--accountline");
|
||||
if (!header) return;
|
||||
|
||||
header.querySelectorAll(".top-account-form, .top-account-status").forEach((el) => el.remove());
|
||||
|
||||
if (user) {
|
||||
header.appendChild(buildSignedInStatus(user));
|
||||
} else {
|
||||
header.appendChild(buildLoginForm());
|
||||
}
|
||||
}
|
||||
|
||||
function findCardByHeading(text) {
|
||||
const needle = text.toLowerCase();
|
||||
|
||||
for (const heading of document.querySelectorAll("h1, h2, h3, h4")) {
|
||||
if (!heading.textContent.toLowerCase().includes(needle)) continue;
|
||||
|
||||
const card = heading.closest(".setup-card, .card, section, article, div");
|
||||
if (card) return card;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function findBBCard() {
|
||||
return document.querySelector("[data-bb-card], .setup-card--bb") || findCardByHeading("Blue Burst");
|
||||
}
|
||||
|
||||
function badge(text, kind = "") {
|
||||
const cls = kind ? `badge ${kind}` : "badge";
|
||||
return `<span class="${cls}">${escapeHtml(text)}</span>`;
|
||||
}
|
||||
|
||||
function updateAccountStatusBadges(accountData) {
|
||||
if (!accountData?.bb) return;
|
||||
|
||||
const bb = accountData.bb;
|
||||
let label = "BB PASSWORD NEEDED";
|
||||
let warn = true;
|
||||
|
||||
if (bb.created && bb.ready) {
|
||||
label = "ACCOUNT READY";
|
||||
warn = false;
|
||||
} else if (bb.created || bb.account_id) {
|
||||
label = "BB SYNC PENDING";
|
||||
warn = true;
|
||||
}
|
||||
|
||||
for (const badge of document.querySelectorAll(".status-badges .badge")) {
|
||||
const text = badge.textContent.trim().toUpperCase();
|
||||
|
||||
if (text.includes("BB ")) {
|
||||
badge.textContent = label;
|
||||
badge.classList.toggle("badge--warn", warn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderBBCard(accountData) {
|
||||
const card = findBBCard();
|
||||
if (!card || !accountData?.user) return;
|
||||
|
||||
const user = accountData.user;
|
||||
const bb = accountData.bb || {
|
||||
created: false,
|
||||
ready: false,
|
||||
sync_status: "missing",
|
||||
username: user.username,
|
||||
account_id: null,
|
||||
};
|
||||
|
||||
const bbCreated = !!(bb.created || bb.account_id);
|
||||
const bbReady = !!bb.ready;
|
||||
const bbBadgeLabel = bbReady ? "ACCOUNT READY" : "BB SYNC PENDING";
|
||||
|
||||
if (bbCreated) {
|
||||
card.innerHTML = `
|
||||
<div class="setup-card-header">
|
||||
<div>
|
||||
<p class="eyebrow">Blue Burst</p>
|
||||
<h2>Blue Burst Account</h2>
|
||||
</div>
|
||||
${badge(bbBadgeLabel, bbReady ? "" : "badge--warn")}
|
||||
</div>
|
||||
|
||||
<div class="account-kv">
|
||||
<div>
|
||||
<span>BB username</span>
|
||||
<strong>${escapeHtml(bb.username)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>BB account ID</span>
|
||||
<strong>${escapeHtml(bb.account_id)}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="bb-account-form" data-bb-action="change-password">
|
||||
<p class="muted">Change your Blue Burst login password. This updates the account file, then it needs to sync to the ships.</p>
|
||||
|
||||
<label>
|
||||
New BB password
|
||||
<input name="password" type="password" autocomplete="new-password" maxlength="16" required>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Confirm new BB password
|
||||
<input name="confirm_password" type="password" autocomplete="new-password" maxlength="16" required>
|
||||
</label>
|
||||
|
||||
<button class="button" type="submit">Change Blue Burst Password</button>
|
||||
<div class="bb-message" role="status"></div>
|
||||
</form>
|
||||
`;
|
||||
} else {
|
||||
card.innerHTML = `
|
||||
<div class="setup-card-header">
|
||||
<div>
|
||||
<p class="eyebrow">Blue Burst</p>
|
||||
<h2>Create Blue Burst Account</h2>
|
||||
</div>
|
||||
${badge("BB PASSWORD NEEDED", "badge--warn")}
|
||||
</div>
|
||||
|
||||
<div class="account-kv">
|
||||
<div>
|
||||
<span>BB username</span>
|
||||
<strong>${escapeHtml(user.username)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>BB account ID</span>
|
||||
<strong>created after setup</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="bb-account-form" data-bb-action="create">
|
||||
<p class="muted">Your Blue Burst username will match your website account name.</p>
|
||||
|
||||
<label>
|
||||
BB password
|
||||
<input name="password" type="password" autocomplete="new-password" maxlength="16" required>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Confirm BB password
|
||||
<input name="confirm_password" type="password" autocomplete="new-password" maxlength="16" required>
|
||||
</label>
|
||||
|
||||
<button class="button" type="submit">Create Blue Burst Account</button>
|
||||
<div class="bb-message" role="status"></div>
|
||||
</form>
|
||||
`;
|
||||
}
|
||||
|
||||
const form = card.querySelector(".bb-account-form");
|
||||
if (!form) return;
|
||||
|
||||
form.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const message = form.querySelector(".bb-message");
|
||||
const password = form.querySelector('input[name="password"]').value;
|
||||
const confirmPassword = form.querySelector('input[name="confirm_password"]').value;
|
||||
const action = form.dataset.bbAction;
|
||||
|
||||
message.textContent = "";
|
||||
message.classList.remove("is-error", "is-ok");
|
||||
|
||||
const endpoint = action === "change-password" ? "/bb/change-password" : "/bb/create";
|
||||
|
||||
try {
|
||||
const result = await api(endpoint, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
password,
|
||||
confirm_password: confirmPassword,
|
||||
}),
|
||||
});
|
||||
|
||||
message.textContent = result?.bb?.ready
|
||||
? "Saved and synced."
|
||||
: "Saved. Sync queued; refresh shortly.";
|
||||
message.classList.add("is-ok");
|
||||
|
||||
const fresh = await api("/account");
|
||||
renderBBCard(fresh);
|
||||
} catch (err) {
|
||||
message.textContent = err.message;
|
||||
message.classList.add("is-error");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function renderAccountEmail(accountData) {
|
||||
const hero = document.querySelector(".account-hero-card");
|
||||
const title = document.querySelector("#account-title");
|
||||
if (!hero || !title) return;
|
||||
|
||||
for (const p of Array.from(hero.querySelectorAll("p"))) {
|
||||
if (p.textContent.includes("Manage your Blue Burst login")) {
|
||||
p.remove();
|
||||
}
|
||||
}
|
||||
|
||||
let box = hero.querySelector("#account-email-summary");
|
||||
if (!box) {
|
||||
box = document.createElement("div");
|
||||
box.id = "account-email-summary";
|
||||
box.className = "account-email-summary";
|
||||
title.insertAdjacentElement("afterend", box);
|
||||
}
|
||||
|
||||
const email = accountData?.email || {};
|
||||
const emailText = email.email || "No email address set";
|
||||
const verified = !!email.verified;
|
||||
|
||||
box.innerHTML = `
|
||||
<div class="account-control-line">
|
||||
<span class="account-control-label">Email</span>
|
||||
<strong>${escapeHtml(emailText)}</strong>
|
||||
<span class="account-email-state ${verified ? "is-verified" : "is-pending"}">
|
||||
${verified ? "Verified" : "Verification needed"}
|
||||
</span>
|
||||
<button class="inline-link account-email-update" type="button">update email address</button>
|
||||
<button class="inline-link account-password-update" type="button">change password</button>
|
||||
</div>
|
||||
|
||||
<form class="account-inline-form account-email-form" hidden>
|
||||
<label class="sr-only" for="account-email-input">New email address</label>
|
||||
<input id="account-email-input" name="email" type="email" placeholder="new email address" autocomplete="email">
|
||||
<button class="button-compact" type="submit">Send Verification Email</button>
|
||||
<button class="button-compact button-secondary" type="button" data-email-cancel>Cancel</button>
|
||||
</form>
|
||||
|
||||
<form class="account-inline-form account-password-form" hidden>
|
||||
<label class="sr-only" for="account-current-password">Current password</label>
|
||||
<input id="account-current-password" name="current_password" type="password" placeholder="current password" autocomplete="current-password">
|
||||
|
||||
<label class="sr-only" for="account-new-password">New password</label>
|
||||
<input id="account-new-password" name="password" type="password" placeholder="new password" autocomplete="new-password">
|
||||
|
||||
<label class="sr-only" for="account-confirm-password">Confirm new password</label>
|
||||
<input id="account-confirm-password" name="confirm_password" type="password" placeholder="confirm new password" autocomplete="new-password">
|
||||
|
||||
<button class="button-compact" type="submit">Change Password</button>
|
||||
<button class="button-compact button-secondary" type="button" data-password-cancel>Cancel</button>
|
||||
</form>
|
||||
|
||||
<p class="account-control-message" role="status"></p>
|
||||
`;
|
||||
|
||||
const emailButton = box.querySelector(".account-email-update");
|
||||
const passwordButton = box.querySelector(".account-password-update");
|
||||
const emailForm = box.querySelector(".account-email-form");
|
||||
const passwordForm = box.querySelector(".account-password-form");
|
||||
const emailCancel = box.querySelector("[data-email-cancel]");
|
||||
const passwordCancel = box.querySelector("[data-password-cancel]");
|
||||
const message = box.querySelector(".account-control-message");
|
||||
const emailInput = box.querySelector("#account-email-input");
|
||||
|
||||
function setMessage(text, kind = "") {
|
||||
message.textContent = text || "";
|
||||
message.className = "account-control-message";
|
||||
if (kind) message.classList.add(kind);
|
||||
}
|
||||
|
||||
function hideForms() {
|
||||
emailForm.hidden = true;
|
||||
passwordForm.hidden = true;
|
||||
}
|
||||
|
||||
emailButton.addEventListener("click", () => {
|
||||
hideForms();
|
||||
emailForm.hidden = false;
|
||||
emailInput.value = email.email || "";
|
||||
setMessage("");
|
||||
emailInput.focus();
|
||||
});
|
||||
|
||||
passwordButton.addEventListener("click", () => {
|
||||
hideForms();
|
||||
passwordForm.hidden = false;
|
||||
setMessage("");
|
||||
passwordForm.querySelector('input[name="current_password"]')?.focus();
|
||||
});
|
||||
|
||||
emailCancel.addEventListener("click", () => {
|
||||
emailForm.hidden = true;
|
||||
setMessage("");
|
||||
});
|
||||
|
||||
passwordCancel.addEventListener("click", () => {
|
||||
passwordForm.hidden = true;
|
||||
passwordForm.reset();
|
||||
setMessage("");
|
||||
});
|
||||
|
||||
emailForm.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const newEmail = emailInput.value.trim();
|
||||
setMessage("");
|
||||
|
||||
try {
|
||||
const result = await api("/email/start", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ email: newEmail }),
|
||||
});
|
||||
|
||||
const sent = result?.verification?.sent;
|
||||
const debugLink = result?.verification?.debug_link;
|
||||
|
||||
setMessage(sent ? "Verification email sent. Check your inbox." : "Verification link created.", "is-ok");
|
||||
|
||||
if (debugLink) {
|
||||
const a = document.createElement("a");
|
||||
a.href = debugLink;
|
||||
a.target = "_blank";
|
||||
a.rel = "noopener noreferrer";
|
||||
a.textContent = " Open verification link";
|
||||
message.appendChild(a);
|
||||
}
|
||||
|
||||
const updated = await api("/account");
|
||||
renderAccountEmail(updated);
|
||||
} catch (err) {
|
||||
setMessage(err.message || String(err), "is-error");
|
||||
}
|
||||
});
|
||||
|
||||
passwordForm.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const currentPassword = passwordForm.querySelector('input[name="current_password"]').value;
|
||||
const newPassword = passwordForm.querySelector('input[name="password"]').value;
|
||||
const confirmPassword = passwordForm.querySelector('input[name="confirm_password"]').value;
|
||||
|
||||
setMessage("");
|
||||
|
||||
try {
|
||||
await api("/password/change", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
current_password: currentPassword,
|
||||
password: newPassword,
|
||||
confirm_password: confirmPassword,
|
||||
}),
|
||||
});
|
||||
|
||||
passwordForm.reset();
|
||||
passwordForm.hidden = true;
|
||||
setMessage("Password changed.", "is-ok");
|
||||
} catch (err) {
|
||||
setMessage(err.message || String(err), "is-error");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function renderAccountPage(user, accountData) {
|
||||
const main = document.querySelector(".account-layout");
|
||||
if (!main) return;
|
||||
|
||||
if (!user) {
|
||||
main.innerHTML = `
|
||||
<section class="card account-hero-card" aria-labelledby="account-title">
|
||||
<div>
|
||||
<p class="eyebrow">Account Dashboard</p>
|
||||
<h1 id="account-title">Sign in or register</h1>
|
||||
<p>Use the account form at the top right to create or open your PSO Peeps website account.</p>
|
||||
</div>
|
||||
<div class="status-badges" aria-label="Account setup status">
|
||||
<span class="badge badge--warn">Not signed in</span>
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
replaceText(document.body, "chuudoku", user.username);
|
||||
|
||||
const title = document.querySelector("#account-title");
|
||||
if (title) title.textContent = user.username;
|
||||
|
||||
renderAccountEmail(accountData);
|
||||
updateAccountStatusBadges(accountData);
|
||||
renderBBCard(accountData);
|
||||
}
|
||||
|
||||
async function boot() {
|
||||
let user = null;
|
||||
let accountData = null;
|
||||
|
||||
try {
|
||||
const me = await api("/me");
|
||||
if (me.authenticated) {
|
||||
user = me.user;
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
if (user) {
|
||||
try {
|
||||
accountData = await api("/account");
|
||||
} catch (_) {
|
||||
accountData = {
|
||||
authenticated: true,
|
||||
user,
|
||||
bb: {
|
||||
ready: false,
|
||||
username: user.username,
|
||||
account_id: null,
|
||||
},
|
||||
v2_v3_keys: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
renderHeader(user);
|
||||
renderAccountPage(user, accountData);
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", boot);
|
||||
})();
|
||||
Reference in New Issue
Block a user