340 lines
22 KiB
HTML
340 lines
22 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>The Button</title>
|
|
<meta name="description" content="A web game of patience and nerve. Press the button before the 24-hour countdown runs out, and score based on how long you waited.">
|
|
<meta property="og:title" content="The Button">
|
|
<meta property="og:type" content="website">
|
|
<meta property="og:description" content="A web game of patience and nerve. Press the button before the 24-hour countdown runs out, and score based on how long you waited.">
|
|
<meta property="og:image" content="/poster.png">
|
|
<meta property="og:site_name" content="The Button">
|
|
<meta property="og:locale" content="en_US">
|
|
<link rel="stylesheet" href="/style.css">
|
|
<link id="zone-favicon" rel="icon" type="image/svg+xml" sizes="any" href="/favicon-green.svg">
|
|
</head>
|
|
<body>
|
|
<main>
|
|
<div class="game-panel">
|
|
<p class="eyebrow game-eyebrow">A SHARED EXPERIMENT</p>
|
|
<h1>THE BUTTON</h1>
|
|
<p id="season-subtitle" class="season-subtitle">Loading season…</p>
|
|
<p class="game-tagline">One clock. Everyone's nerve.</p>
|
|
|
|
<section id="timer-section">
|
|
<div id="countdown" class="zone-green">--:--:--.--</div>
|
|
<div id="zone-label"></div>
|
|
<div id="zone-bar"><div id="zone-marker"></div></div>
|
|
<p id="history-global-summary" aria-live="polite"></p>
|
|
<p id="presence-count" class="presence-count" role="status">Checking who's here…</p>
|
|
<p id="season-label" class="season-label"></p>
|
|
</section>
|
|
|
|
<section id="button-section">
|
|
<button id="the-button" class="zone-green" disabled>PRESS</button>
|
|
<p class="button-caption">Every press buys everyone more time.</p>
|
|
<p id="press-result" aria-live="polite"></p>
|
|
</section>
|
|
</div>
|
|
|
|
<div class="section-browser">
|
|
<div class="browser-heading"><span class="eyebrow">MAKE YOURSELF AT HOME</span><span class="site-badge">The Button / 01</span></div>
|
|
<nav id="section-nav" aria-label="Game sections">
|
|
<button type="button" data-section="account-section" aria-controls="account-section" aria-expanded="false">Log in / Join</button>
|
|
<button type="button" data-section="leaderboard-section" aria-controls="leaderboard-section" aria-expanded="false">Leaderboard</button>
|
|
<button type="button" data-section="seasons-section" aria-controls="seasons-section" aria-expanded="false">Seasons</button>
|
|
<button type="button" data-section="chat-section" aria-controls="chat-section" aria-expanded="false">Chat</button>
|
|
<button type="button" data-section="alerts-section" aria-controls="alerts-section" aria-expanded="false">Alerts</button>
|
|
<button type="button" data-section="admin-section" aria-controls="admin-section" aria-expanded="false" hidden>Admin</button>
|
|
<button type="button" data-section="stats-section" aria-controls="stats-section" aria-expanded="false" hidden>Final stats</button>
|
|
<button type="button" data-section="rules" aria-controls="rules" aria-expanded="false">Rules</button>
|
|
<button type="button" data-section="share-controls" aria-controls="share-controls" aria-expanded="false" hidden>Share</button>
|
|
<button type="button" data-section="more" aria-controls="more" aria-expanded="false">More</button>
|
|
<button type="button" data-section="donate" aria-controls="donate" aria-expanded="false">Donate</button>
|
|
</nav>
|
|
<div id="section-hint" class="welcome-card">
|
|
<span class="welcome-mark" aria-hidden="true">◎</span>
|
|
<p class="eyebrow">PATIENCE IS THE GAME</p>
|
|
<h2>How long can<br>we keep this going?</h2>
|
|
<p>A single countdown, shared by everyone. Wait for more points, or press to keep the game alive.</p>
|
|
<div class="welcome-notes"><span>One shared clock</span><span>Six scoring zones</span><span>No second chances</span></div>
|
|
<p class="welcome-hint">Choose a section above to join in, compare scores, or say hello.</p>
|
|
</div>
|
|
|
|
<section id="admin-section" data-panel hidden aria-labelledby="admin-heading">
|
|
<header class="panel-header"><div class="header-topline"><p class="eyebrow">KEEP THE EXPERIMENT WELCOMING</p><span class="site-badge">Administrator</span></div>
|
|
<h2 id="admin-heading">Control room.</h2>
|
|
<p class="panel-description">Manage player access and keep the conversation on track. Every action is recorded.</p>
|
|
</header>
|
|
<div class="panel-body">
|
|
<div class="form-actions"><button id="admin-refresh" type="button">Refresh dashboard</button></div>
|
|
<p id="admin-status" role="status" class="fine-print"></p>
|
|
<p id="admin-error" role="status" class="error"></p>
|
|
<div id="admin-overview" class="score-grid"></div>
|
|
<h3 class="subheading">Players</h3>
|
|
<label class="field" for="admin-player-filter"><span>Filter by player name</span><input id="admin-player-filter" type="search" placeholder="Find a player" autocomplete="off"></label>
|
|
<p class="fine-print">Role changes and bans revoke the player's sessions. You cannot remove your own access or the last active administrator. Delete account removes credentials and anonymizes the player; historical scores stay.</p>
|
|
<ul id="admin-players" class="admin-list" aria-label="Player administration"></ul>
|
|
<h3 class="subheading">Recent chat</h3>
|
|
<p class="fine-print">Latest 100 visible messages, including automatic announcements. Removal is permanent; it does not undo presses or milestone unlocks.</p>
|
|
<ul id="admin-messages" class="admin-list" aria-label="Chat moderation"></ul>
|
|
<h3 class="subheading">Recent admin actions</h3>
|
|
<p class="fine-print">Most recent 50 actions. IDs are internal account/message identifiers; no passwords or tokens are recorded.</p>
|
|
<div class="table-scroll"><table id="admin-audit" aria-label="Administrator audit log"></table></div>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="seasons-section" data-panel hidden aria-labelledby="seasons-heading">
|
|
<header class="panel-header"><p class="eyebrow">A NEW CHAPTER, FOUR TIMES A YEAR</p>
|
|
<h2 id="seasons-heading">The seasonal leaderboard.</h2>
|
|
<p class="panel-description">New scores. Familiar faces. Every equinox and solstice brings a fresh start.</p>
|
|
</header>
|
|
<div class="panel-body">
|
|
<div id="season-current" class="rival-card"></div>
|
|
<h3 class="subheading">Season champions</h3>
|
|
<p class="fine-print">Starts at 00:00 UTC on the astronomical equinox/solstice date. Joint top scorers share the win. Only players who pressed can win, including zero-point presses.</p>
|
|
<p id="seasons-status" role="status">Loading seasons…</p>
|
|
<div class="table-scroll"><table id="season-winners" aria-label="Season winners and durations"></table></div>
|
|
<div id="season-standings"></div>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="alerts-section" data-panel hidden aria-labelledby="alerts-heading">
|
|
<header class="panel-header"><p class="eyebrow">WAIT ON YOUR TERMS</p>
|
|
<h2 id="alerts-heading">Don't miss your moment.</h2>
|
|
<p class="panel-description">Choose a zone. We'll give you a nudge when the clock gets there.</p>
|
|
</header>
|
|
<div class="panel-body">
|
|
<form id="alerts-form">
|
|
<label class="alert-option"><input id="alerts-enabled" type="checkbox"> Enable zone alerts</label>
|
|
<label class="field" for="alerts-zone"><span>Notify me on entering</span><select id="alerts-zone" required><option value="blue">Blue zone</option><option value="yellow">Yellow zone</option><option value="red">Red zone</option><option value="black">Black zone</option><option value="white">White zone</option></select></label>
|
|
<label class="alert-option"><input id="alerts-sound" type="checkbox"> Play a gentle chime</label>
|
|
<label class="alert-option"><input id="alerts-notification" type="checkbox"> Show a browser notification</label>
|
|
<div class="form-actions"><button class="primary-button" id="alerts-save" type="submit">Save alerts</button><button id="alerts-test" type="button">Test chime</button></div>
|
|
</form>
|
|
<p id="alerts-status" role="status" class="fine-print">Alerts are off by default.</p>
|
|
<p id="alerts-error" class="error" role="status"></p>
|
|
<div class="alert-note"><h3 class="subheading">A heads-up, not a guarantee</h3><p>Keep this page open. Alerts follow server updates, usually every five seconds. Background tabs may be delayed or suspended, and very short zones can be missed. Alerts do not work after the page is closed.</p><p>Opening the page, saving settings, or someone resetting the clock won't trigger an alert. Each selected zone alerts once per countdown. Preferences stay in this browser; no account is required.</p><p>Sound may need a click on Test chime after reopening the page. Browser notifications require permission and a supported secure browser (HTTPS or localhost).</p></div>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="share-controls" data-panel hidden>
|
|
<header class="panel-header">
|
|
<p class="eyebrow">A MOMENT WORTH SHARING</p>
|
|
<h2>Share your press</h2>
|
|
<p class="panel-description">A close call? A perfect moment? Tell someone about it.</p>
|
|
</header>
|
|
<div class="panel-body">
|
|
<div class="share-links">
|
|
<a id="share-facebook" class="share-link" target="_blank" rel="noopener noreferrer">Facebook</a>
|
|
<a id="share-x" class="share-link" target="_blank" rel="noopener noreferrer">X</a>
|
|
<a id="share-bluesky" class="share-link" target="_blank" rel="noopener noreferrer">Bluesky</a>
|
|
</div>
|
|
<label for="mastodon-instance">Mastodon instance</label>
|
|
<div class="share-links">
|
|
<input id="mastodon-instance" type="url" value="https://mastodon.social" aria-label="Mastodon instance URL">
|
|
<a id="share-mastodon" class="share-link" target="_blank" rel="noopener noreferrer">Share on Mastodon</a>
|
|
</div>
|
|
<p id="share-error" class="error" aria-live="polite"></p>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="account-section" data-panel hidden>
|
|
<div id="register-section" hidden>
|
|
<header class="panel-header">
|
|
<p class="eyebrow">YOUR NEXT MOVE</p>
|
|
<h2>Take your place.</h2>
|
|
<p class="panel-description">Choose a name. Trust your timing. Every press is part of the story.</p>
|
|
</header>
|
|
<div class="panel-body">
|
|
<form id="register-form">
|
|
<label class="field" for="name-input"><span>Player name</span>
|
|
<input id="name-input" maxlength="24" placeholder="Your name" required autocomplete="username">
|
|
</label>
|
|
<label class="field" for="password-input"><span>Password</span>
|
|
<input id="password-input" type="password" minlength="8" maxlength="128" placeholder="Password (8+ chars)" required autocomplete="current-password">
|
|
</label>
|
|
<div class="form-actions"><button class="primary-button" type="submit" value="login">Log in <span aria-hidden="true">→</span></button>
|
|
<button type="submit" value="register">Sign up</button>
|
|
</div>
|
|
</form>
|
|
<p id="register-error" class="error" role="status"></p>
|
|
<p class="fine-print">Already playing? Log in with the same name and password to pick up where you left off.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="me-section" hidden>
|
|
<header class="panel-header score-header">
|
|
<div><p class="eyebrow">YOUR PART IN THE EXPERIMENT</p>
|
|
<h2 id="my-name"></h2>
|
|
<p class="panel-description">Good things come to those who wait. Sometimes.</p></div>
|
|
<button id="logout-button" type="button">Log out</button>
|
|
</header>
|
|
<div class="panel-body">
|
|
<p id="my-subscribed" hidden>You're on the mailing list. Thanks!</p>
|
|
<div class="presence-preference">
|
|
<label for="presence-opt-in"><input id="presence-opt-in" type="checkbox" aria-describedby="presence-help"> Show me online</label>
|
|
<p id="presence-help">On by default. Uncheck to hide your online status and chat presence. Only visible tabs count; no activity history is saved.</p>
|
|
<p id="presence-error" class="error" role="status"></p>
|
|
</div>
|
|
<div class="score-grid">
|
|
<div class="metric-card"><span>Total points</span><strong id="my-points">0</strong><small>Every moment counts</small></div>
|
|
<div class="metric-card"><span>Button presses</span><strong id="my-presses">0</strong><small>Times you kept us going</small></div>
|
|
</div>
|
|
<h3 class="subheading">Your personal bests</h3>
|
|
<div class="score-grid personal-records">
|
|
<div class="metric-card"><span>Closest call</span><strong id="my-closest-call">—</strong><small id="my-closest-detail">Your first press starts the story.</small></div>
|
|
<div class="metric-card"><span>Highest-scoring press</span><strong id="my-best-press">—</strong><small id="my-best-detail">Wait for your moment.</small></div>
|
|
</div>
|
|
<div class="rival-card" aria-labelledby="rival-heading">
|
|
<div class="header-topline"><p id="rival-heading" class="eyebrow">YOUR NEXT CHALLENGE</p><span id="my-position" class="site-badge"></span></div>
|
|
<h3 id="my-rival-title">Finding your place…</h3>
|
|
<p id="my-rival-description"></p>
|
|
<p class="fine-print">Positions follow the leaderboard: total points, then player name for ties. Any positive points gain puts you ahead of an equal score.</p>
|
|
</div>
|
|
<div class="header-topline achievements-heading"><h3 id="achievements-heading" class="subheading">Your achievements</h3><span id="my-achievement-count" class="site-badge">Loading badges…</span></div>
|
|
<p class="fine-print">Earned through timing, not volume. Your existing history counts. No bonus points.</p>
|
|
<ul id="my-achievements" class="achievement-list" aria-labelledby="achievements-heading"></ul>
|
|
<h3 class="subheading">Your scoring zones</h3>
|
|
<div class="table-scroll"><table id="my-zones" class="zone-table"></table></div>
|
|
<details>
|
|
<summary>Your press history</summary>
|
|
<div class="table-scroll"><table id="my-history"></table></div>
|
|
</details>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="leaderboard-section" data-panel hidden aria-labelledby="leaderboard-heading">
|
|
<header class="panel-header">
|
|
<div class="header-topline"><p class="eyebrow">THE LONG GAME</p><span class="site-badge">Updates every 5s</span></div>
|
|
<h2 id="leaderboard-heading">Leaderboard</h2>
|
|
<p class="panel-description">This season's scores. A little patience. A little nerve. See who's making every second count.</p>
|
|
</header>
|
|
<div class="panel-body">
|
|
<p id="leaderboard-status" role="status">Loading scores…</p>
|
|
<div class="leaderboard-scroll" tabindex="0" role="region" aria-label="Player leaderboard">
|
|
<table id="leaderboard" aria-label="Player rankings" hidden></table>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="chat-section" data-panel hidden aria-labelledby="chat-heading">
|
|
<header class="chat-header">
|
|
<div>
|
|
<p class="chat-eyebrow">THE PLAYER LOUNGE</p>
|
|
<h2 id="chat-heading">A little company<br>while you wait.</h2>
|
|
<p class="chat-description">Talk strategy. Share a close call. Say hello.</p>
|
|
</div>
|
|
<span class="chat-badge">Public room</span>
|
|
</header>
|
|
<div class="chat-presence" aria-label="Players viewing chat">
|
|
<p id="chat-presence-summary">Who's in the lounge</p>
|
|
<ul id="chat-presence-names" aria-label="Online chat players"></ul>
|
|
</div>
|
|
<div class="chat-feed">
|
|
<div class="chat-feed-label"><span>Conversation</span><span>Latest 100 messages</span></div>
|
|
<ol id="chat-messages" aria-label="Chat messages" tabindex="0"></ol>
|
|
<p id="chat-status" role="status">Loading messages…</p>
|
|
</div>
|
|
<div class="chat-composer">
|
|
<p id="chat-login">Pull up a seat. Log in or join the game to say hello.</p>
|
|
<form id="chat-form" hidden>
|
|
<label for="chat-input">Add to the conversation</label>
|
|
<div class="chat-input-row">
|
|
<input id="chat-input" maxlength="200" placeholder="What's on your mind? Try :smile:" required autocomplete="off" aria-describedby="chat-help" role="combobox" aria-autocomplete="list" aria-controls="emoji-suggestions" aria-expanded="false">
|
|
<button id="chat-send" type="submit">Send <span aria-hidden="true">↑</span></button>
|
|
</div>
|
|
<ul id="emoji-suggestions" class="emoji-suggestions" role="listbox" aria-label="Emoji suggestions" hidden></ul>
|
|
</form>
|
|
<p id="chat-help">Type : to find an emoji. Use ↑/↓ to choose, Enter or Tab to insert, Esc to dismiss. 200 characters max · 2 seconds between messages.</p>
|
|
<p id="chat-error" class="error" role="status"></p>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="stats-section" data-panel hidden>
|
|
<header class="panel-header"><p class="eyebrow">THE FINAL CHAPTER</p>
|
|
<h2>Every second had a story.</h2>
|
|
<p class="panel-description">This season's clock has stopped. Here's how we got here. A new game starts next season.</p></header>
|
|
<div class="panel-body">
|
|
<div id="stats-summary"></div>
|
|
<h3>Presses per zone</h3>
|
|
<div class="table-scroll"><table id="stats-zones" class="zone-table"></table></div>
|
|
<h3>Leaderboard</h3>
|
|
<div class="table-scroll"><table id="stats-leaderboard"></table></div>
|
|
<details>
|
|
<summary>Full press log</summary>
|
|
<div class="table-scroll"><table id="stats-log"></table></div>
|
|
</details>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="rules" data-panel hidden>
|
|
<header class="panel-header"><p class="eyebrow">SIMPLE RULES. DIFFICULT DECISIONS.</p>
|
|
<h2>It's all in the timing.</h2>
|
|
<p class="panel-description">Everyone shares the same clock. What you do with it is up to you.</p></header>
|
|
<div class="panel-body">
|
|
<ol class="rules-list">
|
|
<li><div><h3>Keep the clock alive</h3><p>Every press resets the countdown for everyone.</p></div></li>
|
|
<li><div><h3>Wait for your moment</h3><p>The lower the countdown, the more you score. Each zone awards a fixed number of points per press.</p></div></li>
|
|
<li><div><h3>Don't wait too long</h3><p>If the countdown reaches zero, this season's game ends. The next equinox or solstice starts a new countdown and resets scores. Accounts and chat stay.</p></div></li>
|
|
</ol>
|
|
<h3 class="subheading">Six zones. Increasing stakes.</h3>
|
|
<div id="rules-zones" class="zone-guide"></div>
|
|
<p class="fine-print">Player scores are always public. Detailed global stats are revealed when the game ends.</p>
|
|
<p class="fine-print">Inspired by the original <a href="https://en.wikipedia.org/wiki/The_Button_(Reddit)">Reddit "The Button"</a> experiment.</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="more" data-panel hidden>
|
|
<header class="panel-header"><p class="eyebrow">BEYOND THE BUTTON</p>
|
|
<h2>Stay a little longer.</h2>
|
|
<p class="panel-description">Small experiments, open-source projects, and the person behind them.</p></header>
|
|
<div class="panel-body">
|
|
<p>Visit <a href="https://www.eom.dev/">Eric's homepage</a> for contact information, social media, and links to his other projects.</p>
|
|
<a class="feature-link" href="https://www.eom.dev/"><span><strong>Meet the maker</strong><small>More projects, ideas, and ways to get in touch.</small></span><span aria-hidden="true">↗</span></a>
|
|
|
|
<section id="mailing-list" hidden>
|
|
<h3>Mailing list</h3>
|
|
<p>Join the mailing list to receive updates on Eric's projects:</p>
|
|
<form id="subscribe-form">
|
|
<label class="field" for="sub-first"><span>First name</span>
|
|
<input id="sub-first" maxlength="50" placeholder="First name" required autocomplete="given-name">
|
|
</label><label class="field" for="sub-last"><span>Last name <small>(optional)</small></span>
|
|
<input id="sub-last" maxlength="50" placeholder="Last name (optional)" autocomplete="family-name">
|
|
</label><label class="field" for="sub-email"><span>Email address</span>
|
|
<input id="sub-email" type="email" maxlength="254" placeholder="Email" required autocomplete="email">
|
|
</label><label class="field" for="sub-phone"><span>Phone <small>(optional)</small></span>
|
|
<input id="sub-phone" type="tel" maxlength="25" placeholder="Phone (optional)" autocomplete="tel">
|
|
</label><button class="primary-button" type="submit">Keep me in the loop <span aria-hidden="true">→</span></button>
|
|
</form>
|
|
<p id="subscribe-result" aria-live="polite"></p>
|
|
</section>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="donate" data-panel hidden>
|
|
<header class="panel-header"><p class="eyebrow">FUEL THE NEXT EXPERIMENT</p>
|
|
<h2>Help keep ideas alive.</h2>
|
|
<p class="panel-description">Independent projects, made for everyone. A little support goes a long way.</p></header>
|
|
<div class="panel-body">
|
|
<p>Help support my <a href="https://gitea.eom.dev/">free and open source projects</a> by making a donation through Stripe:</p>
|
|
<script async
|
|
src="https://js.stripe.com/v3/buy-button.js">
|
|
</script>
|
|
|
|
<stripe-buy-button
|
|
buy-button-id="buy_btn_1US18mC5KPXqTvVL4BJNK1MU"
|
|
publishable-key="pk_live_51RXqTsC5KPXqTvVLWl5bzCxoaZivWceeLGMNURUmxzQMwaM5pUUl4VabywrjCQvgKopGFvUBqPLxRMF3eALsiDst00tR59vRhN"
|
|
>
|
|
</stripe-buy-button>
|
|
<p class="fine-print">Thank you for supporting independent, open-source work.</p>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</main>
|
|
<script src="/app.js"></script>
|
|
</body>
|
|
</html>
|