Engagement features

This commit is contained in:
2026-10-21 13:10:25 -04:00
parent f4c10c9d7d
commit f9afa8ccbe
6 changed files with 405 additions and 4 deletions
+98
View File
@@ -12,6 +12,73 @@ let currentShareText = "";
let selectedSection = null;
let chatSignature = null;
let chatLoading = false;
const PRESENCE_KEY = "thebutton-share-presence";
let sharePresence = localStorage.getItem(PRESENCE_KEY) === "true";
let presenceTab = sessionStorage.getItem("thebutton-presence-tab");
if (!presenceTab) {
presenceTab = Array.from(crypto.getRandomValues(new Uint8Array(16)), (n) => n.toString(16).padStart(2, "0")).join("");
sessionStorage.setItem("thebutton-presence-tab", presenceTab);
}
let presenceQueue = Promise.resolve();
let lastPresenceUpdate = null;
function renderPresence(data) {
const count = data.online_count;
const countText = `${count.toLocaleString()} ${count === 1 ? "player" : "players"} watching`;
if ($("presence-count").textContent !== countText) $("presence-count").textContent = countText;
$("chat-presence-summary").textContent = data.chat_players.length
? `${data.chat_players.length} ${data.chat_players.length === 1 ? "player" : "players"} in the lounge`
: "No players sharing chat presence yet";
const list = $("chat-presence-names");
// Player names are text, never HTML.
if (list.dataset.names !== JSON.stringify(data.chat_players)) {
list.replaceChildren(...data.chat_players.map((name) => el("li", name)));
list.dataset.names = JSON.stringify(data.chat_players);
}
}
function syncPresence() {
presenceQueue = presenceQueue.then(async () => {
const active = !!token && sharePresence && !document.hidden;
const inChat = active && selectedSection === "chat-section";
const changed = !lastPresenceUpdate || lastPresenceUpdate.token !== token
|| lastPresenceUpdate.active !== active || lastPresenceUpdate.inChat !== inChat;
const due = active && (!lastPresenceUpdate || performance.now() - lastPresenceUpdate.at >= 15000);
if (token && (changed || due) && (active || lastPresenceUpdate?.active)) {
const data = await api("POST", "/api/presence", { tab_id: presenceTab, active, in_chat: inChat });
lastPresenceUpdate = { token, active, inChat, at: performance.now() };
$("presence-error").textContent = "";
renderPresence(data);
} else if (!document.hidden) {
renderPresence(await api("GET", "/api/presence"));
}
}).catch((error) => {
$("presence-count").textContent = "Online status temporarily unavailable";
if (token && sharePresence) $("presence-error").textContent = "Couldn't update online status. Retrying automatically.";
if (error.status === 401) {
token = null;
localStorage.removeItem(TOKEN_KEY);
lastPresenceUpdate = null;
updateButton();
}
});
return presenceQueue;
}
$("presence-opt-in").addEventListener("change", () => {
sharePresence = $("presence-opt-in").checked;
localStorage.setItem(PRESENCE_KEY, String(sharePresence));
syncPresence();
});
document.addEventListener("visibilitychange", syncPresence);
window.addEventListener("storage", (event) => {
if (event.key === PRESENCE_KEY || event.key === TOKEN_KEY) {
sharePresence = localStorage.getItem(PRESENCE_KEY) === "true";
token = localStorage.getItem(TOKEN_KEY);
updateButton();
syncPresence();
}
});
function renderSections() {
const available = {
@@ -38,6 +105,7 @@ function renderSections() {
$("chat-form").hidden = !token;
$("chat-login").hidden = !!token;
$("section-hint").hidden = selectedSection !== null;
$("presence-opt-in").checked = sharePresence;
}
$("section-nav").addEventListener("click", (event) => {
@@ -48,6 +116,7 @@ $("section-nav").addEventListener("click", (event) => {
if (selectedSection === "account-section") loadMe();
if (selectedSection === "leaderboard-section") loadLeaderboard();
if (selectedSection === "chat-section") loadChat();
syncPresence();
});
async function api(method, path, body) {
@@ -190,8 +259,10 @@ async function refreshState() {
if (first) buildStaticUi();
if (game.ended && !statsLoaded) loadStats();
updateButton();
await syncPresence();
await loadLeaderboard();
if (selectedSection === "chat-section") await loadChat();
if (selectedSection === "account-section") await loadMe();
}
function updateButton() {
@@ -225,6 +296,31 @@ async function loadMe() {
$("my-name").textContent = me.name;
$("my-points").textContent = me.total_points.toLocaleString();
$("my-presses").textContent = me.press_count;
const closest = me.personal_bests.closest_call;
const best = me.personal_bests.highest_scoring_press;
$("my-closest-call").textContent = closest ? formatPrecise(closest.remaining_ms) : "--";
$("my-closest-detail").textContent = closest
? `${closest.zone} zone / ${new Date(closest.at).toLocaleDateString()}`
: "Your first press starts the story.";
$("my-best-press").textContent = best ? `${best.points.toLocaleString()} pts` : "--";
$("my-best-detail").textContent = best
? `${best.zone} zone / ${formatPrecise(best.remaining_ms)} left`
: "No presses yet. Wait for your moment.";
const ranking = me.ranking;
$("my-position").textContent = `#${ranking.position} of ${ranking.total_players}`;
const rival = ranking.rival;
if (rival) {
$("my-rival-title").textContent = rival.points_to_tie === 0 ? `Tied with ${rival.name}` : `Next up: ${rival.name}`;
$("my-rival-description").textContent = rival.points_to_tie === 0
? `You're both on ${me.total_points.toLocaleString()} points. Gain points to move ahead.`
: `${rival.points_to_tie.toLocaleString()} more points to match their ${rival.total_points.toLocaleString()}; ${rival.points_to_pass.toLocaleString()} to beat it.`;
} else {
$("my-rival-title").textContent = ranking.total_players === 1 ? "The first to take a seat." : ranking.tied_for_lead ? "Sharing the lead." : "You're setting the pace.";
$("my-rival-description").textContent = ranking.total_players === 1
? "You're the only player so far. Your next challenge is your own best." : ranking.tied_for_lead
? "You're first alphabetically among equal top scores. Gain points to take a clear lead."
: "You're at the top of the leaderboard. Make your next press count.";
}
subscribed = me.subscribed;
updateButton();
fillTable($("my-zones"), ["Zone", "Points each", "Your presses"], zoneRows(me.zone_counts));
@@ -394,6 +490,7 @@ $("register-form").addEventListener("submit", async (ev) => {
});
token = r.token;
localStorage.setItem(TOKEN_KEY, token);
syncPresence();
$("password-input").value = "";
updateButton();
await loadMe();
@@ -412,6 +509,7 @@ $("logout-button").addEventListener("click", async () => {
$("press-result").textContent = "";
$("subscribe-result").textContent = "";
updateButton();
syncPresence();
});
$("subscribe-form").addEventListener("submit", async (ev) => {