Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
15 kB · 408 lines
HTML
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Support — NELA Computer Club</title> <!-- NELA Computer Club — donate page. Palette + type match nelacomputer.club/styles.css.
Membership is a $1–15/month sliding scale (NOTAFLOF). The slider builds an OpenCollective donate link on Metagov, per the committee's direction (cf. metagov/projects/rewritela).
⚠ SET THE SLUG: NCC's OpenCollective project doesn't exist yet — when it does, change OC.project below to its slug. Until then these links 404 gracefully on OpenCollective. --> <style> /* dark is canonical (matches nelacomputer.club) */ :root { --bg: #000; --fg: #00d080; /* NCC green body text */ --link: #00c4fd; /* NCC cyan links / boxes */ --link-dim: #006888; }
/* light mode — auto, follows the OS theme. paper palette, deeper green/cyan so the terminal look stays readable on white */ @media (prefers-color-scheme: light) { :root { --bg: #f3f0e6; /* warm paper */ --fg: #0b6b46; /* deep green body text */ --link: #0a6e93; /* deep cyan links / boxes */ --link-dim: #5a7a86; } }
body { margin: 0; padding: 0; background-color: var(--bg); color: var(--fg); font-family: monospace; }
a:link, a:visited { color: var(--link); } h3 a:link, h3 a:visited { color: var(--link-dim); }
h1, h2, h3, h4, h5, h6 { margin: 0; padding: 0; line-height: 1rem; font-size: 1em; font-weight: bold; }
pre { margin: 0; padding: 0; line-height: 1rem; }
h2 { margin-top: 3em; }
h3 { margin-top: 2em; background-color: var(--fg); color: var(--bg); padding: 2px 2px 2px 2px; }
header { color: var(--link); }
.banner { display: block; width: 100%; height: auto; margin-bottom: 1.5em; border: 1px solid var(--link); box-sizing: border-box; }
.container { display: grid; place-items: center; }
.textRegion { width: 100%; max-width: 540px; padding: 10px; box-sizing: border-box; font-family: monospace; }
/* ── boxes (same skeleton as NCC .meetupBox) ───────────────── */ .box { margin-top: 1em; border: 1px solid var(--link); padding: 10px; color: var(--link); }
/* ── membership card (the primary ask) ─────────────────────── */ .member { margin-top: 1em; border: 1px solid var(--fg); padding: 12px; } .member p { margin: 0.4em 0 0 0; }
/* live amount readout */ .readout { margin: 0.6em 0 0.2em 0; font-weight: bold; font-size: 1.6em; line-height: 1.6rem; color: var(--fg); } .readout .per { font-size: 0.6em; }
/* ── the slider — terminal-styled range input ──────────────── */ .slider { -webkit-appearance: none; appearance: none; width: 100%; height: 1.4em; background: transparent; margin: 0.6em 0 0.2em 0; cursor: pointer; } .slider::-webkit-slider-runnable-track { height: 4px; background: var(--fg); } .slider::-moz-range-track { height: 4px; background: var(--fg); } .slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 14px; height: 22px; margin-top: -9px; background: var(--fg); border: 2px solid var(--bg); border-radius: 0; } .slider::-moz-range-thumb { width: 14px; height: 22px; background: var(--fg); border: 2px solid var(--bg); border-radius: 0; } .scale { display: flex; justify-content: space-between; font-size: 0.85em; color: var(--link); }
/* ── the give button — AC terminal-prompt flavor ───────────── */ .give { display: inline-block; margin-top: 0.9em; padding: 6px 10px; border: 1px solid var(--fg); background-color: var(--bg); color: var(--fg); font-family: monospace; font-size: 1em; font-weight: bold; text-decoration: none; cursor: pointer; } .give:hover, .give:focus { background-color: var(--fg); color: var(--bg); outline: none; } .give::before { content: "> "; }
/* blinking terminal cursor */ .cursor { display: inline-block; width: 0.6em; background-color: var(--fg); color: var(--fg); animation: blink 1s steps(1) infinite; } @keyframes blink { 50% { opacity: 0; } } @media (prefers-reduced-motion: reduce) { .cursor { animation: none; } }
.fine { margin-top: 1.4em; }
.colophon { margin-top: 4em; color: var(--link-dim); font-size: 0.85em; } .colophon a:link, .colophon a:visited { color: var(--link-dim); }
@media (max-width: 540px) { h1, h2, h3, h4, h5, p, li { font-size: 1.2em; line-height: 1.2rem; } pre { font-size: 1.2em; line-height: 1.2rem; } } </style></head><body><div class="container"><div class="textRegion">
<picture> <source srcset="banner-light.jpg" media="(prefers-color-scheme: light)" /> <img class="banner" src="banner.jpg" width="1200" height="800" alt="NELA Computer Club — a workbench of salvaged laptops, breadboards, an oscilloscope, and a modular synth, people working together." /> </picture>
<header> <pre>░░░░░░░░░░░░░░░░░░░</pre> <h1>NELA Computer Club</h1> <pre>░░░░░░░░░░░░░░░░░░░</pre><br/>
<pre class="logotype">▛▚ ▌ ▞▔▔▚ ▞▔▔▚▌ ▚ ▌ ▞ ▞▌ ▚ ▌ ▚ ▚▌ ▚▌ ▚▂▂▞ ▚▂▂▞</pre> </header>
<h2 id="hello">Support NCC<span class="cursor"> </span></h2> <pre>▒▒▒▒▒▒▒▒▒▒▒▒</pre>
<!-- Signal sign-in: the club jump box (jump.nelacomputer.club) is an OpenID Connect issuer keyed to Signal accounts. Signing in just greets you by your Signal username — nothing here is gated. --> <div class="box" id="signalBox"> <p id="signalOutView">Already on the club jump box? <a class="give" id="signalBtn" href="#">sign in with signal</a></p> <p id="signalInView" hidden>Signed in as <strong>@<span id="signalName"></span></strong> via Signal. <a href="#" id="signalOff">sign out</a></p> </div>
<p>NCC is something we keep going together. Members chip in monthly to keep the room. No one is turned away for lack of funds.</p>
<h2>Become a member</h2> <pre>▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒</pre>
<div class="member"> <p>Pay what you can, a month. Slide to whatever works for you.</p>
<p class="readout"><span id="amount">$8</span><span class="per"> / month</span></p>
<input class="slider" type="range" id="slider" min="1" max="15" step="1" value="8" aria-label="Monthly membership amount in dollars" /> <div class="scale"><span>$1</span><span>$15</span></div>
<p><a class="give" id="memberBtn" href="#" target="_blank" rel="noopener">become a member</a></p> </div>
<h2>What it pays for</h2> <pre>▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒</pre>
<div class="box"> <ul> <li>The space at <a href="https://plot.place">Plot</a>, in Chinatown.</li> <li>Coffee, and the occasional cable.</li> <li>The legal and logistical bits the board handles.</li> <li>The tools we build to keep NCC alive.</li> </ul> </div>
<p class="fine">Just visiting? Showing up is always free — bring a laptop. You can also <a id="onceBtn" href="#" target="_blank" rel="noopener">give once</a>.</p>
<h3 id="give-time">Or give time.</h3> <p>Teach something. Help someone get unstuck. Bring a project to share, or contribute to the open-source software that quietly runs the world.</p>
<p class="colophon"> ░ a shared, decentralized project — <a href="https://nelacomputer.club">nelacomputer.club</a> </p>
</div><!--.textRegion--></div><!--.container-->
<script> // OpenCollective config. NCC is fiscally sponsored by Metagov, so the // collective lives under metagov as a project (like rewritela). // // ⚠ TO GO LIVE: the collective doesn't exist yet. When it's created, // (1) set `project` to its real slug, (2) flip `live` to true. // Until then `live: false` routes the give buttons to the club site // instead of a dead OpenCollective 404. const OC = { host: "metagov", project: "nela-computer-club", // PLACEHOLDER slug — set the real one live: false, // flip true once the collective exists fallback: "https://nelacomputer.club", // where clicks go until then };
// Build a donate link: monthly memberships prefill amount + interval; // one-time sends people to the open donate flow to choose. While the // collective isn't live, return the safe fallback (no 404s). function ocDonate({ amount, monthly }) { if (!OC.live) return OC.fallback; const base = "https://opencollective.com/" + OC.host + "/projects/" + OC.project + "/donate"; const p = new URLSearchParams(); if (monthly) p.set("interval", "month"); if (amount) p.set("amount", String(amount)); const qs = p.toString(); return qs ? base + "?" + qs : base; }
const slider = document.getElementById("slider"); const amountEl = document.getElementById("amount"); const memberBtn = document.getElementById("memberBtn");
function syncMember() { const v = Number(slider.value); amountEl.textContent = "$" + v; memberBtn.textContent = "become a member · $" + v + "/mo"; memberBtn.href = ocDonate({ amount: v, monthly: true }); } slider.addEventListener("input", syncMember); syncMember();
// One-time: open donate flow, no preset interval. document.getElementById("onceBtn").href = ocDonate({ monthly: false });
// ── Signal sign-in ────────────────────────────────────────────── // OpenID Connect against the club jump box. The authorization code // comes back to this page (the registered redirect URI is the bare // origin) and /api/nela-signin trades it for the profile server-side, // where the client secret lives. We keep only the greeting bits. const SIGNAL = { issuer: "https://jump.nelacomputer.club", clientId: "5653d749cfe76abf", store: "ncc-signal", // localStorage key: { username, uid, at } };
const hello = document.getElementById("hello"); const outView = document.getElementById("signalOutView"); const inView = document.getElementById("signalInView"); const nameEl = document.getElementById("signalName");
function whoami() { try { return JSON.parse(localStorage.getItem(SIGNAL.store)); } catch { return null; } }
function renderSignal() { const me = whoami(); outView.hidden = !!me; inView.hidden = !me; if (me) { nameEl.textContent = me.username; hello.innerHTML = 'Hi, ' + me.username.replace(/[<>&]/g, "") + '!<span class="cursor"> </span>'; } else { hello.innerHTML = 'Support NCC<span class="cursor"> </span>'; } }
document.getElementById("signalBtn").addEventListener("click", (e) => { e.preventDefault(); const state = crypto.randomUUID(); sessionStorage.setItem("ncc-signal-state", state); const p = new URLSearchParams({ response_type: "code", client_id: SIGNAL.clientId, redirect_uri: location.origin, state, }); location.href = SIGNAL.issuer + "/oauth/authorize?" + p; });
document.getElementById("signalOff").addEventListener("click", (e) => { e.preventDefault(); localStorage.removeItem(SIGNAL.store); renderSignal(); });
// Finish the round-trip: ?code=... lands back here after authorize. async function completeSignal() { const params = new URLSearchParams(location.search); const code = params.get("code"); if (code) { const state = sessionStorage.getItem("ncc-signal-state"); sessionStorage.removeItem("ncc-signal-state"); if (params.get("state") === state) { try { const res = await fetch("/api/nela-signin", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ code }), }); if (res.ok) { const { username, uid } = await res.json(); if (username) { localStorage.setItem(SIGNAL.store, JSON.stringify({ username, uid, at: Date.now() })); } console.log("🚪 signed in via signal:", username); } else { console.log("🚪 signal sign-in failed:", res.status); } } catch (err) { console.log("🚪 signal sign-in error:", err); } } else { console.log("🚪 signal sign-in: state mismatch, ignoring code"); } } if (code || params.get("error")) { history.replaceState(null, "", location.pathname); // tidy the URL } renderSignal(); } completeSignal();</script></body></html>