From 911163c633bb0a029ee267e11db4e1d01f69edef Mon Sep 17 00:00:00 2001 From: djg Date: Mon, 21 Sep 2026 22:38:56 +0000 Subject: [PATCH] initial import: static site + CI pipeline --- .forgejo/workflows/deploy.yml | 32 +++++ Dockerfile | 4 + deploy.sh | 18 +++ nginx.conf | 28 ++++ site/index.html | 141 ++++++++++++++++++ site/robots.txt | 4 + site/sitemap.xml | 4 + site/static/app.js | 264 ++++++++++++++++++++++++++++++++++ site/static/favicon.svg | 6 + site/static/style.css | 132 +++++++++++++++++ 10 files changed, 633 insertions(+) create mode 100644 .forgejo/workflows/deploy.yml create mode 100644 Dockerfile create mode 100755 deploy.sh create mode 100644 nginx.conf create mode 100644 site/index.html create mode 100644 site/robots.txt create mode 100644 site/sitemap.xml create mode 100644 site/static/app.js create mode 100644 site/static/favicon.svg create mode 100644 site/static/style.css diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml new file mode 100644 index 0000000..a786b60 --- /dev/null +++ b/.forgejo/workflows/deploy.yml @@ -0,0 +1,32 @@ +name: Build and Deploy +on: + push: + branches: [main] + +jobs: + deploy: + runs-on: self-hosted + env: + KUBECONFIG: /home/opc/.kube/config + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build image + run: | + docker build \ + -t git.ch4t.buzz/buzzparty/myadhd:${{ github.sha }} \ + -t git.ch4t.buzz/buzzparty/myadhd:latest \ + . + + - name: Push to registry + run: | + docker push git.ch4t.buzz/buzzparty/myadhd:${{ github.sha }} + docker push git.ch4t.buzz/buzzparty/myadhd:latest + + - name: Roll out to k8s + run: | + kubectl set image deployment/static-adhd \ + nginx=git.ch4t.buzz/buzzparty/myadhd:${{ github.sha }} \ + -n default + kubectl rollout status deployment/static-adhd -n default --timeout=120s diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9f207d7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,4 @@ +FROM nginx:alpine +COPY site/ /usr/share/nginx/html/ +COPY nginx.conf /etc/nginx/nginx.conf +EXPOSE 80 diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..caab93b --- /dev/null +++ b/deploy.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# Build (nothing to build yet — site is static) + sync docroot in place. +set -euo pipefail +cd "$(dirname "$0")" +BASE=/home/opc/zai-home-base + +mkdir -p "$BASE/sites/myadhd.dev" +rsync -a --delete site/ "$BASE/sites/myadhd.dev/" + +echo "==> verifying through Traefik (origin)" +sleep 1 +code=$(curl -sk -o /dev/null -w '%{http_code}' --resolve myadhd.dev:443:127.0.0.1 https://myadhd.dev/ --max-time 10) +echo " https://myadhd.dev -> $code (origin)" +for asset in /static/style.css /static/app.js /static/favicon.svg /robots.txt /sitemap.xml; do + out=$(curl -sk -o /dev/null -w '%{http_code} %{content_type}' --resolve myadhd.dev:443:127.0.0.1 "https://myadhd.dev$asset" --max-time 10) + echo " $asset -> $out" +done +echo "==> deploy complete" diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..22f3212 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,28 @@ +worker_processes 1; +error_log /dev/stderr warn; +pid /tmp/nginx.pid; +events { worker_connections 1024; } +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + access_log /dev/stdout; + sendfile on; + gzip on; + gzip_types text/plain text/css application/javascript application/json application/xml image/svg+xml; + gzip_min_length 1024; + server { + listen 80; + root /usr/share/nginx/html; + index index.html; + error_page 404 /404.html; + add_header X-Content-Type-Options nosniff always; + add_header Referrer-Policy strict-origin-when-cross-origin always; + location ~* \.(css|js|svg|png|jpg|ico|woff2?)$ { + expires 7d; + add_header Cache-Control "public"; + } + location / { + try_files $uri $uri/ $uri/index.html =404; + } + } +} diff --git a/site/index.html b/site/index.html new file mode 100644 index 0000000..0aa30c7 --- /dev/null +++ b/site/index.html @@ -0,0 +1,141 @@ + + + + + +myadhd.dev — ADHD-friendly tools for focus, tasks and momentum + + + + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + +
+ + + + + + diff --git a/site/robots.txt b/site/robots.txt new file mode 100644 index 0000000..60c1325 --- /dev/null +++ b/site/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://myadhd.dev/sitemap.xml diff --git a/site/sitemap.xml b/site/sitemap.xml new file mode 100644 index 0000000..add0956 --- /dev/null +++ b/site/sitemap.xml @@ -0,0 +1,4 @@ + + + https://myadhd.dev/weekly1.0 + diff --git a/site/static/app.js b/site/static/app.js new file mode 100644 index 0000000..375c243 --- /dev/null +++ b/site/static/app.js @@ -0,0 +1,264 @@ +/* myadhd.dev — client-side tools. No accounts, no network; everything in localStorage. */ +(function () { + 'use strict'; + var $ = function (s, r) { return (r || document).querySelector(s); }; + var $$ = function (s, r) { return Array.prototype.slice.call((r || document).querySelectorAll(s)); }; + function store(k, v) { + if (v === undefined) { try { return JSON.parse(localStorage.getItem('myadhd.' + k)); } catch (e) { return null; } } + localStorage.setItem('myadhd.' + k, JSON.stringify(v)); + } + function today() { return new Date().toISOString().slice(0, 10); } + + /* ---------- tabs ---------- */ + function showTab(name) { + $$('.tool').forEach(function (t) { t.hidden = (t.id !== name); }); + $$('.tab').forEach(function (t) { t.classList.toggle('on', t.dataset.tab === name); }); + if (!location.hash.slice(1)) {} // keep url in sync without loops + try { history.replaceState(null, '', '#' + name); } catch (e) {} + } + $$('.tab').forEach(function (t) { + t.addEventListener('click', function (e) { e.preventDefault(); showTab(t.dataset.tab); }); + }); + var initial = location.hash.slice(1); + showTab(['breaker', 'timer', 'dopamine', 'now'].indexOf(initial) !== -1 ? initial : 'breaker'); + + /* ---------- task breaker ---------- */ + var breakerKey = 'breaker'; + function breakerTemplates(task) { + return [ + { t: 'Write down what “done” looks like for: ' + task + '.', mins: 2, done: false }, + { t: 'Gather everything you need for “' + task + '” in one spot.', mins: 5, done: false }, + { t: 'Do the ugliest possible first version of “' + task + '”. No polish allowed.', mins: 5, done: false }, + { t: 'Set a 10-minute timer and start “' + task + '”. Starting is the whole job.', mins: 10, done: false }, + { t: 'Momentum check: keep going, or park it with a note for next time.', mins: 2, done: false }, + { t: 'Close the loop: tidy up and write down the very next step.', mins: 2, done: false } + ]; + } + function breakerRender() { + var b = store(breakerKey); + $('#breaker-out').hidden = !b; + if (!b) return; + var ul = $('#breaker-steps'); + ul.innerHTML = ''; + b.steps.forEach(function (s, i) { + var li = document.createElement('li'); + li.className = s.done ? 'done' : ''; + var tick = document.createElement('button'); + tick.className = 'tick'; tick.type = 'button'; + tick.setAttribute('aria-label', 'toggle done'); + tick.addEventListener('click', function () { + s.done = !s.done; store(breakerKey, b); breakerRender(); + }); + var txt = document.createElement('span'); + txt.className = 'txt'; txt.contentEditable = 'true'; txt.textContent = s.t; + txt.addEventListener('blur', function () { s.t = txt.textContent.trim(); store(breakerKey, b); }); + var mins = document.createElement('span'); + mins.className = 'mins'; mins.textContent = s.mins ? s.mins + ' min' : ''; + li.appendChild(tick); li.appendChild(txt); li.appendChild(mins); + ul.appendChild(li); + }); + var done = b.steps.filter(function (s) { return s.done; }).length; + $('#breaker-progress').style.width = b.steps.length ? (100 * done / b.steps.length) + '%' : '0'; + $('#breaker-count').textContent = done + ' of ' + b.steps.length + ' steps done' + + (done === b.steps.length && b.steps.length ? ' — look at you. 🎉' : ''); + } + $('#breaker-form').addEventListener('submit', function (e) { + e.preventDefault(); + var task = $('#breaker-input').value.trim(); + if (!task) return; + store(breakerKey, { task: task, steps: breakerTemplates(task) }); + breakerRender(); + }); + $('#breaker-add').addEventListener('click', function () { + var b = store(breakerKey); if (!b) return; + b.steps.push({ t: 'Next step…', mins: 0, done: false }); + store(breakerKey, b); breakerRender(); + }); + $('#breaker-reset').addEventListener('click', function () { + localStorage.removeItem('myadhd.' + breakerKey); + $('#breaker-input').value = ''; + breakerRender(); + }); + breakerRender(); + + /* ---------- focus timer ---------- */ + var CIRC = 628.3; + var timer = { total: 25 * 60, left: 25 * 60, running: false, phase: 'focus', iv: null }; + function timerPaint() { + var m = Math.floor(timer.left / 60), s = timer.left % 60; + $('#timer-time').textContent = (m < 10 ? '0' : '') + m + ':' + (s < 10 ? '0' : '') + s; + $('#ring-fg').style.strokeDashoffset = CIRC * (1 - timer.left / timer.total); + var ph = $('#timer-phase'); + ph.textContent = timer.running ? (timer.phase === 'focus' ? 'focus — you got this' : 'break — actually rest') : ph.textContent; + ph.className = 'timer-phase' + (timer.phase === 'break' ? ' break' : ''); + } + function beep() { + try { + var ctx = new (window.AudioContext || window.webkitAudioContext)(); + [[660, 0], [880, 0.18]].forEach(function (n) { + var o = ctx.createOscillator(), g = ctx.createGain(); + o.frequency.value = n[0]; o.type = 'sine'; + g.gain.setValueAtTime(0.001, ctx.currentTime + n[1]); + g.gain.exponentialRampToValueAtTime(0.12, ctx.currentTime + n[1] + 0.02); + g.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + n[1] + 0.4); + o.connect(g); g.connect(ctx.destination); + o.start(ctx.currentTime + n[1]); o.stop(ctx.currentTime + n[1] + 0.45); + }); + } catch (e) {} + } + function timerStop() { clearInterval(timer.iv); timer.iv = null; timer.running = false; } + $('#timer-start').addEventListener('click', function () { + if (timer.running) { timerStop(); $('#timer-start').textContent = 'resume'; timerPaint(); return; } + timer.running = true; + $('#timer-start').textContent = 'pause'; + timer.iv = setInterval(function () { + timer.left--; + if (timer.left <= 0) { + timerStop(); beep(); + if (timer.phase === 'focus') { + store('sessions', (store('sessions') || 0) + 1); + timer.phase = 'break'; + $('#timer-note').textContent = 'Focus round done (' + store('sessions') + ' total). Stand up. Drink water. Actually take the break.'; + } else { + timer.phase = 'focus'; + $('#timer-note').textContent = 'Break over. Pick a length and go again — or stop here, that is allowed too.'; + } + timer.total = timer.left = (timer.phase === 'break' ? 5 : 25) * 60; + $('#timer-start').textContent = 'start'; + } + timerPaint(); + }, 1000); + timerPaint(); + }); + $('#timer-reset').addEventListener('click', function () { + timerStop(); timer.left = timer.total; $('#timer-start').textContent = 'start'; + timerPaint(); + }); + $$('.preset').forEach(function (p) { + p.addEventListener('click', function () { + timerStop(); + timer.total = timer.left = (+p.dataset.min) * 60; + timer.phase = p.classList.contains('break') ? 'break' : 'focus'; + $('#timer-start').textContent = 'start'; + timerPaint(); + }); + }); + timerPaint(); + + /* ---------- dopamine menu ---------- */ + var MENU = { + 5: [ + 'Put on one song and tidy one single surface.', + 'Drink a big glass of water like it is a magic potion.', + 'Step outside for two minutes and name five sounds you hear.', + 'Stretch your arms and neck slowly — like a cat waking up.', + 'Text someone one true compliment. No context, no apology.', + 'Draw the nearest object as badly as you possibly can.', + 'Open the window and take ten slow breaths.', + 'Rearrange exactly three things on your desk.', + 'Do ten squats or one ridiculous dance move.', + 'Write down three things you can see right now that you like.' + ], + 15: [ + 'Walk once around the block — phone stays home or in pocket.', + 'Build the fanciest snack possible from what you already have.', + 'Write a short thank-you note to your future self.', + 'Doodle your current mood as a weather report.', + 'Do a ten-minute follow-along stretch video.', + 'Clean one drawer. One. Set a timer so it cannot grow.', + 'Read five pages of any book you own.', + 'Water (or rescue) one plant.', + 'Have a shower, but treat it like a whole concert.', + 'Learn three words in a language you do not speak.' + ], + 30: [ + 'Cook something new with a video recipe playing.', + 'Go for a 25-minute walk with one podcast episode.', + 'Call someone you like. With your voice. Wild, we know.', + 'Build something with zero rules: LEGO, blanket fort, collage.', + 'Brain-dump everything in your head onto paper for ten minutes.', + 'Deep-clean the one object that quietly bothers you most.', + 'Follow a 30-minute workout video at whatever intensity you like.', + 'Walk or bike to a spot nearby you have never actually been.', + 'Pack a small comfort bag for your worst brain days.', + 'Ten minutes journaling, ten minutes stretching, ten minutes staring.' + ], + 60: [ + 'Long walk or bike ride somewhere with trees.', + 'One-hour hobby sprint — make something terrible and be proud.', + 'Bake something from scratch. Eat evidence.', + 'One-room tidy: music loud, timer rules, no mercy.', + 'Wandering trip (thrift store, library) with a $10 budget.', + 'Digital reset: close twenty tabs, clear the inbox as far as you can.', + 'Follow a full workout or yoga class at home.', + 'Meal-prep one lunch so tomorrow-you says thanks.', + 'Take a notebook to a café or library and people-watch or sketch.', + 'Watch a documentary about something absurd and become an expert.' + ] + }; + var lastPick = null; + function menuPick(mins) { + var pool = MENU[mins].filter(function (x) { return x !== lastPick; }); + var item = pool[Math.floor(Math.random() * pool.length)]; + lastPick = item; + $('#menu-text').textContent = item; + $('#menu-item').hidden = false; + $('#menu-actions').hidden = false; + } + $$('.menu-pick').forEach(function (b) { + b.addEventListener('click', function () { menuPick(+b.dataset.min); }); + }); + $('#menu-reroll').addEventListener('click', function () { + var mins = $$('.menu-pick').filter(function (b) { return b.classList.contains('on'); })[0]; + menuPick(mins ? +mins.dataset.min : 5); + }); + function paintStreak() { + var s = store('streak'); + $('#menu-streak').innerHTML = s && s.streak ? + 'Streak: ' + s.streak + ' day' + (s.streak > 1 ? 's' : '') + ' of healthy dopamine. ' + + (s.last === today() ? 'Today is already logged.' : '') : ''; + } + $('#menu-done').addEventListener('click', function () { + var s = store('streak') || { last: '', streak: 0 }; + if (s.last !== today()) { + var y = new Date(Date.now() - 86400000).toISOString().slice(0, 10); + s.streak = (s.last === y) ? s.streak + 1 : 1; + s.last = today(); + store('streak', s); + } + $('#menu-item').hidden = true; + $('#menu-actions').hidden = true; + paintStreak(); + }); + paintStreak(); + + /* ---------- one thing ---------- */ + var nowSaved = store('now'); + if (nowSaved && nowSaved.text) { + $('#now-text').textContent = nowSaved.text; + $('#now-form').hidden = true; + $('#now-view').hidden = false; + } + $('#now-form').addEventListener('submit', function (e) { + e.preventDefault(); + var t = $('#now-input').value.trim(); + if (!t) return; + store('now', { text: t }); + $('#now-text').textContent = t; + $('#now-form').hidden = true; + $('#now-view').hidden = false; + }); + $('#now-edit').addEventListener('click', function () { + $('#now-view').hidden = true; + $('#now-form').hidden = false; + $('#now-input').value = ''; + $('#now-input').focus(); + }); + $('#now-done').addEventListener('click', function () { + localStorage.removeItem('myadhd.now'); + $('#now-view').hidden = true; + $('#now-form').hidden = false; + $('#now-input').value = ''; + $('#now-input').focus(); + }); +})(); diff --git a/site/static/favicon.svg b/site/static/favicon.svg new file mode 100644 index 0000000..0c97572 --- /dev/null +++ b/site/static/favicon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/site/static/style.css b/site/static/style.css new file mode 100644 index 0000000..bdc255f --- /dev/null +++ b/site/static/style.css @@ -0,0 +1,132 @@ +/* myadhd.dev — calm, low-stimulation dark theme */ +* { box-sizing: border-box; margin: 0; padding: 0; } +:root { + --bg: #14141c; + --bg2: #1b1b26; + --card: #1e1e2a; + --line: #2c2c3c; + --text: #e6e6f0; + --dim: #9c9cb4; + --accent: #a78bfa; + --accent-dim: #6d5aa8; + --good: #7dd3a0; + --radius: 18px; +} +body { + font-family: ui-rounded, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + background: var(--bg); color: var(--text); + min-height: 100vh; line-height: 1.6; +} +main { max-width: 760px; margin: 0 auto; padding: 0 20px 60px; } + +/* header */ +.top { + max-width: 760px; margin: 0 auto; padding: 22px 20px 6px; + display: flex; flex-direction: column; gap: 14px; +} +.logo { font-size: 1.5rem; font-weight: 800; color: var(--text); text-decoration: none; letter-spacing: -0.5px; } +.logo span { color: var(--accent); } +.tabs { display: flex; gap: 8px; flex-wrap: wrap; } +.tab { + color: var(--dim); text-decoration: none; font-weight: 600; font-size: .95rem; + padding: 8px 16px; border-radius: 999px; border: 1.5px solid var(--line); background: var(--bg2); +} +.tab:hover { color: var(--text); } +.tab.on { background: var(--accent-dim); border-color: var(--accent-dim); color: #fff; } + +/* tools */ +.tool-head { margin: 34px 0 18px; } +h1 { font-size: 1.9rem; letter-spacing: -0.5px; } +.sub { color: var(--dim); margin-top: 6px; max-width: 40rem; } +.card { + background: var(--card); border: 1.5px solid var(--line); + border-radius: var(--radius); padding: 24px; +} +.center { text-align: center; } + +.row { display: flex; gap: 10px; flex-wrap: wrap; } +.row.center { justify-content: center; } +.row.gap { margin-top: 18px; justify-content: center; } +input[type="text"] { + flex: 1 1 240px; font: inherit; color: var(--text); + background: var(--bg2); border: 1.5px solid var(--line); border-radius: 12px; + padding: 12px 16px; outline: none; +} +input[type="text"]:focus { border-color: var(--accent-dim); } + +.btn { + font: inherit; font-weight: 700; cursor: pointer; color: #fff; + background: var(--accent-dim); border: none; border-radius: 12px; padding: 12px 22px; +} +.btn:hover { filter: brightness(1.15); } +.btn.ghost { background: transparent; border: 1.5px solid var(--line); color: var(--dim); } +.btn.ghost:hover { color: var(--text); border-color: var(--accent-dim); } + +.chip { + font: inherit; font-size: .9rem; font-weight: 600; cursor: pointer; + color: var(--dim); background: var(--bg2); border: 1.5px solid var(--line); + border-radius: 999px; padding: 7px 15px; +} +.chip:hover, .chip.on { color: var(--text); border-color: var(--accent-dim); } +.presets { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; margin-bottom: 22px; } + +.tiny { color: var(--dim); font-size: .85rem; margin-top: 14px; } + +/* breaker */ +.progress-wrap { height: 8px; background: var(--bg2); border-radius: 999px; overflow: hidden; margin: 18px 0 6px; } +.progress { height: 100%; width: 0; background: var(--accent); border-radius: 999px; transition: width .3s ease; } +.steps { list-style: none; margin-top: 14px; display: flex; flex-direction: column; gap: 8px; } +.steps li { + display: flex; align-items: flex-start; gap: 12px; + background: var(--bg2); border: 1.5px solid var(--line); border-radius: 12px; padding: 12px 14px; +} +.steps li .tick { + flex: 0 0 auto; width: 22px; height: 22px; margin-top: 2px; cursor: pointer; + border: 2px solid var(--accent-dim); border-radius: 7px; background: transparent; +} +.steps li.done .tick { background: var(--good); border-color: var(--good); } +.steps li.done .txt { color: var(--dim); text-decoration: line-through; } +.steps li .txt { flex: 1; outline: none; } +.steps li .txt:focus { background: var(--bg); border-radius: 6px; } +.steps li .mins { flex: 0 0 auto; color: var(--accent); font-weight: 700; font-size: .85rem; margin-top: 3px; } + +/* timer */ +.timer-wrap { position: relative; width: 240px; margin: 10px auto; } +.ring { width: 240px; height: 240px; transform: rotate(-90deg); } +.ring-bg { fill: none; stroke: var(--bg2); stroke-width: 10; } +.ring-fg { + fill: none; stroke: var(--accent); stroke-width: 10; stroke-linecap: round; + stroke-dasharray: 628.3; stroke-dashoffset: 0; +} +.timer-readout { + position: absolute; inset: 0; display: flex; flex-direction: column; + align-items: center; justify-content: center; +} +.timer-time { font-size: 2.6rem; font-weight: 800; letter-spacing: 1px; font-variant-numeric: tabular-nums; } +.timer-phase { color: var(--dim); font-size: .9rem; font-weight: 600; } +.timer-phase.break { color: var(--good); } + +/* dopamine menu */ +.menu-item { + background: var(--bg2); border: 1.5px dashed var(--accent-dim); border-radius: 14px; + padding: 26px 22px; margin: 8px auto; max-width: 34rem; +} +.menu-text { font-size: 1.15rem; font-weight: 600; } +#menu-streak { margin-top: 16px; } +.streak-num { color: var(--accent); font-weight: 800; } + +/* one thing */ +.now-label { color: var(--dim); text-transform: uppercase; letter-spacing: 2px; font-size: .8rem; margin-top: 8px; } +.now-text { font-size: 1.5rem; font-weight: 700; margin: 12px 0 20px; } + +/* footer */ +.foot { + max-width: 760px; margin: 40px auto 0; padding: 24px 20px 40px; + border-top: 1.5px solid var(--line); color: var(--dim); font-size: .92rem; +} +.foot .tiny { margin-top: 6px; } + +@media (max-width: 560px) { + h1 { font-size: 1.55rem; } + .timer-wrap, .ring { width: 200px; height: 200px; } +}