commit 8f1bf141911151aa6191563bfe41dfa887bcce62 Author: Armands Vagalis Date: Sun Apr 12 18:55:00 2026 +0300 First commit diff --git a/.env b/.env new file mode 100644 index 0000000..7e74194 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +MONGODB_URI=mongodb+srv://saturn:taCexQWvuSGKg8hCbh%25@mycluster.my1emqo.mongodb.net/?appName=MyCluster +MONGODB_DB=dailyform diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..229052c --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +MONGODB_URI=mongodb+srv://:@.mongodb.net/?retryWrites=true&w=majority +MONGODB_DB=dailyform diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..aada888 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM python:3.12-slim +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY . . +EXPOSE 5000 +CMD ["python", "app.py"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..995e700 --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ +# Daily Form + +Shared daily check-in form. Answers are global (no per-session state). Auto-snapshots to MongoDB at 10:00 AM Riga time. + +## Setup + +### 1. MongoDB Atlas (free) + +1. Go to https://cloud.mongodb.com → create account → create a free M0 cluster +2. Create a database user (Database Access → Add New User) +3. Allow access from anywhere (Network Access → Add IP Address → `0.0.0.0/0`) +4. Get connection string (Connect → Drivers) — looks like: + `mongodb+srv://user:pass@cluster.mongodb.net/` + +### 2. Configure + +```bash +cp .env.example .env +# Edit .env and paste your MongoDB URI +``` + +### 3. Run on saturn + +```bash +# Copy files to saturn +scp -r . armandins@saturn:/opt/dailyform + +# SSH in +ssh armandins@saturn +cd /opt/dailyform + +docker compose up -d +``` + +Then add to Nginx Proxy Manager pointing to port 5000. + +### 4. Editing questions + +Edit `questions.yaml` and restart the container: + +```bash +docker compose restart +``` + +The `questions.yaml` is mounted as a read-only volume so you edit it on the host, no rebuild needed. + +## Question types + +| Type | Description | Extra fields | +|---|---|---| +| `yesno` | Yes / No buttons | — | +| `scale` | Slider | `min`, `max`, `step` | +| `number` | Number input | `min`, `max` | +| `duration` | H : MM input (stored as minutes) | — | +| `counter` | Big − / + buttons | `min`, `max`, `step` | +| `text` | Single line | — | +| `textarea` | Multi-line | — | +| `choice` | Single select pills | `options: [...]` | +| `multichoice` | Multi select pills | `options: [...]` | + +## Snapshots + +Snapshots are stored in the `snapshots` MongoDB collection. View last 30: + +``` +GET /api/snapshots +``` + +Or browse directly in MongoDB Atlas UI → Collections → `dailyform` → `snapshots`. diff --git a/app.py b/app.py new file mode 100644 index 0000000..26e1035 --- /dev/null +++ b/app.py @@ -0,0 +1,131 @@ +import os +import yaml +from datetime import datetime, timedelta +from zoneinfo import ZoneInfo +from flask import Flask, jsonify, request, render_template, Response +from pymongo import MongoClient +from apscheduler.schedulers.background import BackgroundScheduler +from dotenv import load_dotenv + +load_dotenv() + +app = Flask(__name__) +RIGA = ZoneInfo("Europe/Riga") + +# MongoDB +client = MongoClient(os.environ["MONGODB_URI"]) +db = client[os.environ.get("MONGODB_DB", "dailyform")] +answers_col = db["current_answers"] +snapshots_col = db["snapshots"] + +def load_config(): + with open("questions.yaml", "r") as f: + return yaml.safe_load(f) + +def get_questions(): + return load_config().get("questions", []) + +def get_current_period(): + now = datetime.now(RIGA) + today_10 = now.replace(hour=10, minute=0, second=0, microsecond=0) + if now < today_10: + start = today_10 - timedelta(days=1) + end = today_10 + else: + start = today_10 + end = today_10 + timedelta(days=1) + return {"start": start.isoformat(), "end": end.isoformat()} + +def take_snapshot(): + now = datetime.now(RIGA) + doc = answers_col.find_one({}, {"_id": 0}) or {} + date_str = now.strftime("%Y-%m-%d") + period = get_current_period() + for_date = datetime.fromisoformat(period["start"]).strftime("%Y-%m-%d") + snapshots_col.insert_one({ + "timestamp": now.isoformat(), + "date": date_str, + "for_date": for_date, + "answers": doc, + }) + answers_col.replace_one({}, {}, upsert=True) + app.logger.info(f"Snapshot taken: {date_str}, for_date: {for_date}") + +scheduler = BackgroundScheduler(timezone=RIGA) +scheduler.add_job(take_snapshot, "cron", hour=10, minute=0) +scheduler.start() + +@app.route("/") +def index(): + return render_template("index.html", questions=get_questions(), period=get_current_period()) + +@app.route("/history") +def history(): + return render_template("history.html", questions=get_questions()) + +@app.route("/api/answers", methods=["GET"]) +def get_answers(): + doc = answers_col.find_one({}, {"_id": 0}) or {} + return jsonify(doc) + +@app.route("/api/answers", methods=["POST"]) +def save_answers(): + data = request.get_json() + if not isinstance(data, dict): + return jsonify({"error": "invalid"}), 400 + answers_col.replace_one({}, data, upsert=True) + return jsonify({"ok": True}) + +@app.route("/api/snapshots") +def get_snapshots(): + limit = int(request.args.get("limit", 365)) + docs = list(snapshots_col.find({}, {"_id": 0}).sort("timestamp", -1).limit(limit)) + return jsonify(docs) + +@app.route("/api/snapshots//md") +def snapshot_as_md(date_str): + snap = snapshots_col.find_one({"date": date_str}, {"_id": 0}) + if not snap: + return "Not found", 404 + questions = get_questions() + answers = snap.get("answers", {}) + + lines = [f"# Daily Check-in — {date_str}", ""] + lines.append(f"_Snapshot: {snap.get('timestamp', '?')}_") + if answers.get("__done__"): + done_at = answers.get("__done_at__", "") + try: + done_at = datetime.fromisoformat(done_at).strftime("%H:%M") + except Exception: + pass + lines.append(f"_Marked as filled at: {done_at}_") + lines += ["", "---", ""] + + for q in questions: + val = answers.get(q["id"]) + lines.append(f"## {q['label']}") + lines.append("") + if val is None: + lines.append("_(no answer)_") + elif q["type"] == "yesno": + lines.append("Yes" if val else "No") + elif q["type"] in ("duration", "time"): + h, m = divmod(int(val), 60) + lines.append(f"{h:02d}:{m:02d}") + elif q["type"] == "multichoice": + lines.append(", ".join(val) if isinstance(val, list) else str(val)) + else: + lines.append(str(val)) + lines.append("") + + md = "\n".join(lines) + return Response(md, mimetype="text/markdown", + headers={"Content-Disposition": f"attachment; filename=checkin-{date_str}.md"}) + +@app.route("/api/snapshot/now", methods=["POST"]) +def snapshot_now(): + take_snapshot() + return jsonify({"ok": True}) + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=5000, debug=False) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..291e068 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,9 @@ +services: + app: + build: . + restart: unless-stopped + ports: + - "5000:5000" + env_file: .env + volumes: + - ./questions.yaml:/app/questions.yaml:ro diff --git a/questions.yaml b/questions.yaml new file mode 100644 index 0000000..95a068d --- /dev/null +++ b/questions.yaml @@ -0,0 +1,50 @@ +questions: + - id: mood + label: Overall mood today + type: scale + min: 1 + max: 10 + step: 1 + + - id: slept_well + label: Slept well last night? + type: yesno + + - id: wake_time + label: Wake-up time + type: time + + - id: focus_hours + label: Hours of deep focus + type: duration + + - id: energy + label: Energy level + type: counter + min: 0 + max: 10 + step: 1 + + - id: tasks_done + label: Tasks completed + type: number + min: 0 + max: 100 + + - id: primary_activity + label: Primary activity today + type: choice + options: + - Work + - Study + - Rest + - Exercise + - Social + + - id: wins + label: What went well? + type: textarea + + - id: notes + label: Anything else? + type: text diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8014004 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +flask==3.1.0 +pymongo==4.10.1 +pyyaml==6.0.2 +apscheduler==3.10.4 +python-dotenv==1.0.1 diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..e943502 --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,566 @@ +/* macOS Mojave — SF Pro feel, blue accent, light/dark */ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&display=swap'); + +:root { + --accent: #0a84ff; + --accent-hover: #409cff; + --accent-dim: rgba(10,132,255,0.15); + --accent-dim2: rgba(10,132,255,0.08); + --radius-sm: 6px; + --radius: 10px; + --radius-lg: 14px; + --font: -apple-system, 'SF Pro Text', 'Inter', sans-serif; + --font-display: -apple-system, 'SF Pro Display', 'Inter', sans-serif; + --transition: 0.18s ease; +} + +/* Dark (default) */ +:root, [data-theme="dark"] { + --bg: #1c1c1e; + --bg2: #2c2c2e; + --bg3: #3a3a3c; + --surface: #2c2c2e; + --surface2: #3a3a3c; + --border: rgba(255,255,255,0.1); + --border-active: rgba(10,132,255,0.6); + --text: #ffffff; + --text2: rgba(255,255,255,0.6); + --text3: rgba(255,255,255,0.3); + --shadow: 0 2px 16px rgba(0,0,0,0.4); + --shadow-sm: 0 1px 4px rgba(0,0,0,0.3); + --success-bg: rgba(48,209,88,0.15); + --success: #30d158; + --danger: #ff453a; + --danger-bg: rgba(255,69,58,0.15); +} + +[data-theme="light"] { + --bg: #f2f2f7; + --bg2: #ffffff; + --bg3: #e5e5ea; + --surface: #ffffff; + --surface2: #f2f2f7; + --border: rgba(0,0,0,0.1); + --border-active: rgba(10,132,255,0.5); + --text: #000000; + --text2: rgba(0,0,0,0.55); + --text3: rgba(0,0,0,0.25); + --shadow: 0 2px 16px rgba(0,0,0,0.1); + --shadow-sm: 0 1px 4px rgba(0,0,0,0.08); + --success-bg: rgba(48,209,88,0.12); + --success: #28a745; + --danger: #ff3b30; + --danger-bg: rgba(255,59,48,0.12); +} + +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +html { font-size: 15px; } + +body { + background: var(--bg); + color: var(--text); + font-family: var(--font); + font-weight: 400; + min-height: 100vh; + -webkit-font-smoothing: antialiased; + transition: background var(--transition), color var(--transition); +} + +/* ── TOPBAR ── */ +.topbar { + position: sticky; + top: 0; + z-index: 100; + backdrop-filter: blur(20px) saturate(180%); + -webkit-backdrop-filter: blur(20px) saturate(180%); + background: rgba(28,28,30,0.8); + border-bottom: 1px solid var(--border); + padding: 0 24px; + height: 52px; + display: flex; + align-items: center; + gap: 16px; +} +[data-theme="light"] .topbar { + background: rgba(242,242,247,0.85); +} + +.topbar-title { + font-family: var(--font-display); + font-size: 15px; + font-weight: 600; + letter-spacing: -0.02em; + flex: 1; +} + +.topbar-nav { + display: flex; + gap: 4px; +} + +.topbar-nav a { + padding: 5px 12px; + border-radius: var(--radius-sm); + font-size: 13px; + font-weight: 500; + color: var(--text2); + text-decoration: none; + transition: all var(--transition); +} +.topbar-nav a:hover { background: var(--surface2); color: var(--text); } +.topbar-nav a.active { background: var(--accent-dim); color: var(--accent); } + +/* Theme toggle */ +.theme-toggle { + width: 32px; height: 32px; + border-radius: var(--radius-sm); + border: 1px solid var(--border); + background: var(--surface2); + color: var(--text2); + cursor: pointer; + display: flex; align-items: center; justify-content: center; + font-size: 15px; + transition: all var(--transition); + flex-shrink: 0; +} +.theme-toggle:hover { color: var(--text); background: var(--bg3); } + +/* ── MAIN LAYOUT ── */ +.page { max-width: 640px; margin: 0 auto; padding: 32px 24px 96px; } +.page-wide { max-width: 900px; margin: 0 auto; padding: 32px 24px 96px; } + +/* ── PERIOD BANNER ── */ +.period-banner { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 14px 18px; + margin-bottom: 24px; + display: flex; + align-items: center; + gap: 12px; + box-shadow: var(--shadow-sm); +} +.period-icon { font-size: 18px; flex-shrink: 0; } +.period-text { font-size: 13px; color: var(--text2); line-height: 1.5; } +.period-text strong { color: var(--text); font-weight: 500; } +.period-progress { + margin-left: auto; + flex-shrink: 0; + text-align: right; +} +.period-pct { + font-size: 13px; + font-weight: 600; + color: var(--accent); +} +.period-bar-wrap { + width: 80px; + height: 3px; + background: var(--bg3); + border-radius: 2px; + margin-top: 4px; + overflow: hidden; +} +.period-bar { + height: 100%; + background: var(--accent); + border-radius: 2px; + transition: width 0.5s ease; +} + +/* ── STATUS DOT ── */ +#status { + font-size: 12px; + color: var(--text3); + transition: color var(--transition); + display: flex; + align-items: center; + gap: 5px; +} +#status::before { + content: ''; + width: 6px; height: 6px; + border-radius: 50%; + background: var(--text3); + display: inline-block; + transition: background var(--transition); +} +#status.saving { color: var(--accent); } +#status.saving::before { background: var(--accent); } +#status.saved { color: var(--success); } +#status.saved::before { background: var(--success); } +#status.error { color: var(--danger); } +#status.error::before { background: var(--danger); } + +/* ── CARD ── */ +.card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 20px; + box-shadow: var(--shadow-sm); + transition: border-color var(--transition); +} +.card:focus-within { border-color: var(--border-active); } + +/* ── QUESTION ── */ +.question { margin-bottom: 16px; } +.question-label { + font-size: 12px; + font-weight: 500; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--text2); + margin-bottom: 10px; +} + +/* ── YESNO ── */ +.yesno { display: flex; gap: 8px; } +.yesno button { + flex: 1; padding: 10px 16px; + background: var(--surface2); + border: 1px solid var(--border); + border-radius: var(--radius); + color: var(--text2); + font-family: var(--font); + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all var(--transition); +} +.yesno button:hover { background: var(--bg3); color: var(--text); } +.yesno button.active-yes { background: var(--accent-dim); border-color: var(--accent); color: var(--accent); } +.yesno button.active-no { background: var(--danger-bg); border-color: var(--danger); color: var(--danger); } + +/* ── SCALE ── */ +.scale-wrap { display: flex; flex-direction: column; gap: 10px; } +.scale-top { display: flex; align-items: baseline; justify-content: space-between; } +.scale-value { font-size: 32px; font-weight: 600; color: var(--accent); letter-spacing: -0.03em; font-family: var(--font-display); } +.scale-range { font-size: 12px; color: var(--text3); } +input[type=range] { + -webkit-appearance: none; + width: 100%; height: 4px; + background: var(--bg3); + border-radius: 2px; + outline: none; +} +input[type=range]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 20px; height: 20px; + border-radius: 50%; + background: var(--accent); + cursor: pointer; + box-shadow: 0 0 0 4px var(--accent-dim), var(--shadow-sm); +} + +/* ── NUMBER ── */ +input[type=number] { + background: var(--surface2); + border: 1px solid var(--border); + border-radius: var(--radius); + color: var(--text); + font-family: var(--font); + font-size: 18px; + font-weight: 500; + padding: 10px 14px; + width: 140px; + outline: none; + transition: border-color var(--transition); + -moz-appearance: textfield; +} +input[type=number]::-webkit-outer-spin-button, +input[type=number]::-webkit-inner-spin-button { -webkit-appearance: none; } +input[type=number]:focus { border-color: var(--accent); } + +/* ── DURATION / TIME ── */ +.duration-wrap, .time-wrap { + display: flex; align-items: center; gap: 6px; +} +.duration-wrap input[type=number], +.time-wrap input[type=number] { + width: 76px; text-align: center; +} +.time-sep { + font-size: 22px; font-weight: 600; + color: var(--text3); line-height: 1; + margin-bottom: 2px; +} + +/* ── COUNTER ── */ +.counter-wrap { display: flex; align-items: center; gap: 0; } +.counter-btn { + width: 52px; height: 52px; + background: var(--surface2); + border: 1px solid var(--border); + color: var(--text); + font-size: 22px; + cursor: pointer; + transition: all var(--transition); + font-family: var(--font); + display: flex; align-items: center; justify-content: center; + font-weight: 300; + user-select: none; +} +.counter-btn:first-child { border-radius: var(--radius) 0 0 var(--radius); } +.counter-btn:last-child { border-radius: 0 var(--radius) var(--radius) 0; } +.counter-btn:hover { background: var(--bg3); } +.counter-btn:active { background: var(--accent-dim); color: var(--accent); } +.counter-val { + min-width: 68px; + text-align: center; + font-size: 28px; + font-weight: 600; + color: var(--accent); + font-family: var(--font-display); + letter-spacing: -0.02em; + border-top: 1px solid var(--border); + border-bottom: 1px solid var(--border); + padding: 9px 0; +} + +/* ── TEXT / TEXTAREA ── */ +input[type=text], textarea { + background: var(--surface2); + border: 1px solid var(--border); + border-radius: var(--radius); + color: var(--text); + font-family: var(--font); + font-size: 14px; + padding: 10px 14px; + width: 100%; + outline: none; + transition: border-color var(--transition); +} +input[type=text]:focus, textarea:focus { border-color: var(--accent); } +textarea { min-height: 88px; resize: vertical; line-height: 1.5; } +::placeholder { color: var(--text3); } + +/* ── CHOICE / MULTICHOICE ── */ +.choice-wrap, .multichoice-wrap { display: flex; flex-wrap: wrap; gap: 8px; } +.choice-btn { + padding: 7px 15px; + background: var(--surface2); + border: 1px solid var(--border); + border-radius: 100px; + color: var(--text2); + font-family: var(--font); + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: all var(--transition); +} +.choice-btn:hover { background: var(--bg3); color: var(--text); } +.choice-btn.active { background: var(--accent-dim); border-color: var(--accent); color: var(--accent); } + +/* ── DONE CHECKBOX ── */ +.done-card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 18px 20px; + margin-top: 8px; + display: flex; + align-items: center; + gap: 14px; + cursor: pointer; + transition: all var(--transition); + box-shadow: var(--shadow-sm); +} +.done-card:hover { border-color: var(--border-active); } +.done-card.checked { + background: var(--success-bg); + border-color: var(--success); +} +.done-checkbox { + width: 22px; height: 22px; + border-radius: 50%; + border: 2px solid var(--border-active); + display: flex; align-items: center; justify-content: center; + flex-shrink: 0; + transition: all var(--transition); + font-size: 13px; +} +.done-card.checked .done-checkbox { + background: var(--success); + border-color: var(--success); + color: white; +} +.done-label { font-size: 14px; font-weight: 500; color: var(--text); } +.done-sub { font-size: 12px; color: var(--text2); margin-top: 2px; } + +/* ── CALENDAR PAGE ── */ +.cal-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 24px; +} +.cal-header h2 { + font-size: 22px; + font-weight: 700; + letter-spacing: -0.03em; + font-family: var(--font-display); + flex: 1; +} +.cal-nav-btn { + width: 32px; height: 32px; + border-radius: var(--radius-sm); + border: 1px solid var(--border); + background: var(--surface); + color: var(--text2); + cursor: pointer; + font-size: 14px; + display: flex; align-items: center; justify-content: center; + transition: all var(--transition); +} +.cal-nav-btn:hover { background: var(--bg3); color: var(--text); } + +.cal-grid { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 6px; +} +.cal-dow { + text-align: center; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.05em; + text-transform: uppercase; + color: var(--text3); + padding: 4px 0 8px; +} +.cal-cell { + aspect-ratio: 1; + border-radius: var(--radius); + border: 1px solid var(--border); + background: var(--surface); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: 14px; + font-weight: 500; + color: var(--text2); + cursor: default; + transition: all var(--transition); + position: relative; + gap: 3px; +} +.cal-cell.empty { background: transparent; border-color: transparent; } +.cal-cell.has-snapshot { + background: var(--accent-dim2); + border-color: var(--accent-dim); + color: var(--text); + cursor: pointer; +} +.cal-cell.has-snapshot:hover { border-color: var(--accent); background: var(--accent-dim); } +.cal-cell.done { background: var(--success-bg); border-color: var(--success); } +.cal-cell.done .cal-day-num { color: var(--success); } +.cal-cell.today { border-color: var(--accent); } +.cal-cell.today .cal-day-num { color: var(--accent); font-weight: 700; } +.cal-day-num { font-size: 15px; line-height: 1; } +.cal-dot { width: 5px; height: 5px; border-radius: 50%; background: var(--accent); } +.cal-done-dot { width: 5px; height: 5px; border-radius: 50%; background: var(--success); } + +/* ── SNAPSHOT DRAWER ── */ +.snapshot-panel { + position: fixed; + top: 0; right: -480px; + width: 460px; + height: 100vh; + background: var(--surface); + border-left: 1px solid var(--border); + box-shadow: -8px 0 32px rgba(0,0,0,0.3); + z-index: 200; + transition: right 0.3s cubic-bezier(0.4,0,0.2,1); + display: flex; + flex-direction: column; + overflow: hidden; +} +.snapshot-panel.open { right: 0; } +.snapshot-panel-header { + padding: 20px 20px 16px; + border-bottom: 1px solid var(--border); + display: flex; + align-items: center; + gap: 12px; + flex-shrink: 0; +} +.snapshot-panel-title { font-size: 16px; font-weight: 600; flex: 1; } +.snapshot-panel-body { flex: 1; overflow-y: auto; padding: 20px; } +.panel-close-btn { + width: 28px; height: 28px; + border-radius: var(--radius-sm); + border: 1px solid var(--border); + background: var(--surface2); + color: var(--text2); + cursor: pointer; + font-size: 16px; + display: flex; align-items: center; justify-content: center; + transition: all var(--transition); +} +.panel-close-btn:hover { background: var(--bg3); color: var(--text); } + +.snapshot-qa { margin-bottom: 18px; } +.snapshot-q { font-size: 11px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text3); margin-bottom: 4px; } +.snapshot-a { font-size: 14px; color: var(--text); } + +.download-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 14px; + background: var(--accent-dim); + border: 1px solid var(--accent); + border-radius: var(--radius); + color: var(--accent); + font-size: 13px; + font-weight: 500; + cursor: pointer; + text-decoration: none; + transition: all var(--transition); + margin-bottom: 20px; +} +.download-btn:hover { background: var(--accent); color: white; } + +.overlay { + display: none; + position: fixed; + inset: 0; + background: rgba(0,0,0,0.4); + z-index: 199; + backdrop-filter: blur(2px); +} +.overlay.visible { display: block; } + +/* ── UTILS ── */ +.section-title { + font-size: 12px; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--text3); + margin-bottom: 12px; + margin-top: 28px; +} +.section-title:first-child { margin-top: 0; } + +/* Early submit button */ +.early-submit-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 14px; + background: var(--danger-bg); + border: 1px solid var(--danger); + border-radius: var(--radius); + color: var(--danger); + font-size: 13px; + font-weight: 500; + cursor: pointer; + font-family: var(--font); + transition: all var(--transition); +} +.early-submit-btn:hover { background: var(--danger); color: white; } diff --git a/static/js/theme.js b/static/js/theme.js new file mode 100644 index 0000000..b802c7a --- /dev/null +++ b/static/js/theme.js @@ -0,0 +1,22 @@ +// Theme toggle — persisted to localStorage +(function() { + const saved = localStorage.getItem('theme') || 'dark'; + document.documentElement.setAttribute('data-theme', saved); +})(); + +function initThemeToggle() { + const btn = document.getElementById('theme-toggle'); + if (!btn) return; + function update() { + const t = document.documentElement.getAttribute('data-theme'); + btn.textContent = t === 'dark' ? '☀️' : '🌙'; + } + update(); + btn.addEventListener('click', () => { + const current = document.documentElement.getAttribute('data-theme'); + const next = current === 'dark' ? 'light' : 'dark'; + document.documentElement.setAttribute('data-theme', next); + localStorage.setItem('theme', next); + update(); + }); +} diff --git a/templates/history.html b/templates/history.html new file mode 100644 index 0000000..2e78526 --- /dev/null +++ b/templates/history.html @@ -0,0 +1,198 @@ + + + + + +History — Daily Check-in + + + + + + + +
+ +
+ +

+ +
+ +
+ +
+ + +
+ + +
+
+ + +
+
+

Select a day to view answers.

+
+
+ + + + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..adb0425 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,390 @@ + + + + + +Daily Check-in + + + + + + + +
+ + +
+ 🕐 +
Loading period…
+
+
+
+
+
+ + +
+ + +
+ ⬇ Download as .md + +
+ + + + + +
+
+
+
Mark as fully filled
+
Not marked yet
+
+
+ +
+ + + +