initial import: static site + CI pipeline
Some checks are pending
Build and Deploy / deploy (push) Waiting to run
Some checks are pending
Build and Deploy / deploy (push) Waiting to run
This commit is contained in:
commit
911163c633
10 changed files with 633 additions and 0 deletions
32
.forgejo/workflows/deploy.yml
Normal file
32
.forgejo/workflows/deploy.yml
Normal file
|
|
@ -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
|
||||
4
Dockerfile
Normal file
4
Dockerfile
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
FROM nginx:alpine
|
||||
COPY site/ /usr/share/nginx/html/
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
EXPOSE 80
|
||||
18
deploy.sh
Executable file
18
deploy.sh
Executable file
|
|
@ -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"
|
||||
28
nginx.conf
Normal file
28
nginx.conf
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
141
site/index.html
Normal file
141
site/index.html
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>myadhd.dev — ADHD-friendly tools for focus, tasks and momentum</title>
|
||||
<meta name="description" content="Free, ADHD-friendly tools: task breaker, focus timer with time-blindness support, a dopamine menu, and a one-thing mode. No signup, everything stays in your browser.">
|
||||
<meta name="theme-color" content="#14141c">
|
||||
<link rel="canonical" href="https://myadhd.dev/">
|
||||
<link rel="icon" href="/static/favicon.svg" type="image/svg+xml">
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<meta property="og:title" content="myadhd.dev — ADHD-friendly tools">
|
||||
<meta property="og:description" content="Break down scary tasks, tame time blindness, and build momentum. Free, no signup, data stays in your browser.">
|
||||
<meta property="og:type" content="website">
|
||||
</head>
|
||||
<body>
|
||||
<header class="top">
|
||||
<a class="logo" href="/">myadhd<span>.dev</span></a>
|
||||
<nav class="tabs" role="tablist">
|
||||
<a href="#breaker" data-tab="breaker" class="tab">Break it down</a>
|
||||
<a href="#timer" data-tab="timer" class="tab">Focus timer</a>
|
||||
<a href="#dopamine" data-tab="dopamine" class="tab">Dopamine menu</a>
|
||||
<a href="#now" data-tab="now" class="tab">One thing</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
|
||||
<!-- ============ TASK BREAKER ============ -->
|
||||
<section id="breaker" class="tool" hidden>
|
||||
<div class="tool-head">
|
||||
<h1>Break it down</h1>
|
||||
<p class="sub">Big scary tasks are just 5–10 small boring ones wearing a trench coat. Name the monster; we'll slice it.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<form id="breaker-form" class="row">
|
||||
<input id="breaker-input" type="text" placeholder="The big scary task… e.g. 'do my taxes'" autocomplete="off">
|
||||
<button class="btn" type="submit">Break it down</button>
|
||||
</form>
|
||||
<div id="breaker-out" hidden>
|
||||
<div class="progress-wrap"><div id="breaker-progress" class="progress"></div></div>
|
||||
<p class="tiny" id="breaker-count"></p>
|
||||
<ul id="breaker-steps" class="steps"></ul>
|
||||
<div class="row gap">
|
||||
<button class="btn ghost" id="breaker-add" type="button">+ add step</button>
|
||||
<button class="btn ghost" id="breaker-reset" type="button">start over</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="tiny">Steps are editable — click any of them to rewrite it your way. Everything saves in this browser.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ FOCUS TIMER ============ -->
|
||||
<section id="timer" class="tool" hidden>
|
||||
<div class="tool-head">
|
||||
<h1>Focus timer</h1>
|
||||
<p class="sub">A timer for time-blind brains: you always see how much time is left, how much is gone, and what phase you're in.</p>
|
||||
</div>
|
||||
<div class="card center">
|
||||
<div class="presets">
|
||||
<button class="chip preset" data-min="10">10 min</button>
|
||||
<button class="chip preset" data-min="25">25 min</button>
|
||||
<button class="chip preset" data-min="45">45 min</button>
|
||||
<button class="chip preset break" data-min="5">5 min break</button>
|
||||
<button class="chip preset break" data-min="15">15 min break</button>
|
||||
</div>
|
||||
<div class="timer-wrap">
|
||||
<svg viewBox="0 0 220 220" class="ring" aria-hidden="true">
|
||||
<circle class="ring-bg" cx="110" cy="110" r="100"/>
|
||||
<circle id="ring-fg" class="ring-fg" cx="110" cy="110" r="100"/>
|
||||
</svg>
|
||||
<div class="timer-readout">
|
||||
<div id="timer-time" class="timer-time">25:00</div>
|
||||
<div id="timer-phase" class="timer-phase">pick a length</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row center gap">
|
||||
<button class="btn" id="timer-start">start</button>
|
||||
<button class="btn ghost" id="timer-reset">reset</button>
|
||||
</div>
|
||||
<p class="tiny" id="timer-note">Tip: 25 minutes is a suggestion, not a law. The best length is the one you'll actually start.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ DOPAMINE MENU ============ -->
|
||||
<section id="dopamine" class="tool" hidden>
|
||||
<div class="tool-head">
|
||||
<h1>Dopamine menu</h1>
|
||||
<p class="sub">Quick, healthy mood hits so your brain stops hijacking you for worse ones. Pick how long you have.</p>
|
||||
</div>
|
||||
<div class="card center">
|
||||
<div class="presets">
|
||||
<button class="chip menu-pick" data-min="5">5 min</button>
|
||||
<button class="chip menu-pick" data-min="15">15 min</button>
|
||||
<button class="chip menu-pick" data-min="30">30 min</button>
|
||||
<button class="chip menu-pick" data-min="60">60 min</button>
|
||||
</div>
|
||||
<div id="menu-item" class="menu-item" hidden>
|
||||
<p id="menu-text" class="menu-text"></p>
|
||||
</div>
|
||||
<div class="row center gap" id="menu-actions" hidden>
|
||||
<button class="btn ghost" id="menu-reroll">something else</button>
|
||||
<button class="btn" id="menu-done">did it ✓</button>
|
||||
</div>
|
||||
<p class="tiny" id="menu-streak"></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ ONE THING ============ -->
|
||||
<section id="now" class="tool" hidden>
|
||||
<div class="tool-head">
|
||||
<h1>One thing</h1>
|
||||
<p class="sub">Not the whole task. Not the project. The next physical action your hands can actually do.</p>
|
||||
</div>
|
||||
<div class="card center">
|
||||
<form id="now-form" class="row">
|
||||
<input id="now-input" type="text" placeholder="The next physical action… e.g. 'open the tax folder'" autocomplete="off">
|
||||
<button class="btn" type="submit">lock it in</button>
|
||||
</form>
|
||||
<div id="now-view" hidden>
|
||||
<p class="now-label">the one thing:</p>
|
||||
<p id="now-text" class="now-text"></p>
|
||||
<div class="row center gap">
|
||||
<button class="btn" id="now-done">done ✓</button>
|
||||
<button class="btn ghost" id="now-edit">change</button>
|
||||
</div>
|
||||
<p class="tiny">If it still feels too big, start with the 2-minute version: just open the thing and look at it.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<footer class="foot">
|
||||
<p><strong>myadhd.dev</strong> — free tools, no signup, no tracking. Everything you type or save lives only in this browser (localStorage). Clear your browser data and it's gone.</p>
|
||||
<p class="tiny">Not medical advice. Just friendly scaffolding for a brain that works differently.</p>
|
||||
</footer>
|
||||
|
||||
<script src="/static/app.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
4
site/robots.txt
Normal file
4
site/robots.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://myadhd.dev/sitemap.xml
|
||||
4
site/sitemap.xml
Normal file
4
site/sitemap.xml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url><loc>https://myadhd.dev/</loc><changefreq>weekly</changefreq><priority>1.0</priority></url>
|
||||
</urlset>
|
||||
264
site/static/app.js
Normal file
264
site/static/app.js
Normal file
|
|
@ -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: <span class="streak-num">' + s.streak + ' day' + (s.streak > 1 ? 's' : '') + '</span> 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();
|
||||
});
|
||||
})();
|
||||
6
site/static/favicon.svg
Normal file
6
site/static/favicon.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<rect x="2" y="2" width="60" height="60" rx="14" fill="#a78bfa" stroke="#2c2c3c" stroke-width="4"/>
|
||||
<path d="M18 40c0-10 6-16 14-16s14 6 14 16z" fill="#14141c"/>
|
||||
<circle cx="27" cy="34" r="3" fill="#a78bfa"/>
|
||||
<circle cx="37" cy="34" r="3" fill="#a78bfa"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 332 B |
132
site/static/style.css
Normal file
132
site/static/style.css
Normal file
|
|
@ -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; }
|
||||
}
|
||||
Loading…
Reference in a new issue