Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ A simple, community-maintained open-source task tracker. Add tasks, check them o
- Tasks persist in `localStorage` by default, with optional GitHub sign-in sync
- GitHub OAuth uses PKCE in the browser and an HTTP-only session cookie
- Keyboard-friendly (more shortcuts coming, see #5)
- Light & dark themes (dark coming, see #1)
- Light & dark themes with persisted user preference
- MIT licensed

## Quick start
Expand Down
2 changes: 2 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
const browserGlobals = {
Blob: "readonly",
crypto: "readonly",
document: "readonly",
fetch: "readonly",
HTMLElement: "readonly",
TextEncoder: "readonly",
URL: "readonly",
URLSearchParams: "readonly",
Expand Down
36 changes: 28 additions & 8 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TaskForge — open-source task tracker</title>
<script>
(() => {
const savedTheme = localStorage.getItem("taskforge.theme");
const prefersDark = window.matchMedia(
"(prefers-color-scheme: dark)",
).matches;
document.documentElement.dataset.theme =
savedTheme || (prefersDark ? "dark" : "light");
})();
</script>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
Expand All @@ -12,14 +22,24 @@
<h1>TaskForge</h1>
<p class="tagline">A tiny task list that lives in your browser.</p>
</div>
<div class="auth-panel" aria-live="polite">
<img id="auth-avatar" class="auth-avatar" hidden />
<span id="auth-status" class="auth-status">
Tasks are stored on this device.
</span>
<button id="auth-action" class="auth-action" type="button">
Sign in with GitHub
<div class="header-actions">
<button
id="theme-toggle"
class="theme-toggle"
type="button"
aria-pressed="false"
>
Dark mode
</button>
<div class="auth-panel" aria-live="polite">
<img id="auth-avatar" class="auth-avatar" hidden />
<span id="auth-status" class="auth-status">
Tasks are stored on this device.
</span>
<button id="auth-action" class="auth-action" type="button">
Sign in with GitHub
</button>
</div>
</div>
</header>

Expand Down
3 changes: 2 additions & 1 deletion server.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ const PORT = Number(process.env.PORT || 8000);
const PUBLIC_DIR = __dirname;
const PUBLIC_ROOT = `${PUBLIC_DIR}${path.sep}`;
const DATA_FILE =
process.env.TASKFORGE_DATA_FILE || path.join(__dirname, ".taskforge-data.json");
process.env.TASKFORGE_DATA_FILE ||
path.join(__dirname, ".taskforge-data.json");
const SESSION_COOKIE = "taskforge_session";
const SESSION_MAX_AGE_SECONDS = 60 * 60 * 24 * 7;

Expand Down
34 changes: 33 additions & 1 deletion src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ const form = document.getElementById("task-form");
const input = document.getElementById("task-input");
const list = document.getElementById("task-list");
const emptyState = document.getElementById("empty-state");
const themeToggle = document.getElementById("theme-toggle");
const themeMedia = window.matchMedia("(prefers-color-scheme: dark)");
const themeStorageKey = "taskforge.theme";
const authStatus = document.getElementById("auth-status");
const authAction = document.getElementById("auth-action");
const authAvatar = document.getElementById("auth-avatar");
Expand Down Expand Up @@ -44,6 +47,21 @@ async function init() {
render();
}

function getSystemTheme() {
return themeMedia.matches ? "dark" : "light";
}

function getSavedTheme() {
return window.localStorage.getItem(themeStorageKey);
}

function applyTheme(theme) {
document.documentElement.dataset.theme = theme;
const isDark = theme === "dark";
themeToggle.textContent = isDark ? "Light mode" : "Dark mode";
themeToggle.setAttribute("aria-pressed", String(isDark));
}

function render() {
list.innerHTML = "";
if (tasks.length === 0) {
Expand Down Expand Up @@ -72,7 +90,7 @@ function render() {
const del = document.createElement("button");
del.type = "button";
del.className = "task-delete";
del.textContent = "";
del.textContent = "x";
del.setAttribute("aria-label", `Delete task: ${task.title}`);
del.addEventListener("click", async () => {
tasks = removeTask(tasks, task.id);
Expand All @@ -85,6 +103,19 @@ function render() {
}
}

themeToggle.addEventListener("click", () => {
const nextTheme =
document.documentElement.dataset.theme === "dark" ? "light" : "dark";
window.localStorage.setItem(themeStorageKey, nextTheme);
applyTheme(nextTheme);
});

themeMedia.addEventListener("change", () => {
if (!getSavedTheme()) {
applyTheme(getSystemTheme());
}
});

form.addEventListener("submit", async (e) => {
e.preventDefault();
const title = input.value.trim();
Expand Down Expand Up @@ -146,4 +177,5 @@ function mergeTasks(localTasks, remoteTasks) {
return [...byId.values()].sort((a, b) => b.createdAt - a.createdAt);
}

applyTheme(getSavedTheme() || getSystemTheme());
init();
3 changes: 2 additions & 1 deletion src/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ export async function startGitHubSignIn() {
const codeVerifier = randomString(64);
const codeChallenge = await pkceChallenge(codeVerifier);
const redirectUri =
config.redirectUri || `${window.location.origin}${window.location.pathname}`;
config.redirectUri ||
`${window.location.origin}${window.location.pathname}`;

window.sessionStorage.setItem(AUTH_STATE_KEY, state);
window.sessionStorage.setItem(CODE_VERIFIER_KEY, codeVerifier);
Expand Down
68 changes: 61 additions & 7 deletions styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,31 @@
--accent: #0969da;
--accent-fg: #ffffff;
--done: #8c959f;
--surface: #f6f8fa;
--danger: #cf222e;
--danger-bg: rgba(207, 34, 46, 0.08);
--warning-bg: #fff8c5;
--warning-border: #d4a72c;
--warning-fg: #7d4e00;
--radius: 6px;
color-scheme: light;
}

:root[data-theme="dark"] {
--bg: #0d1117;
--fg: #f0f6fc;
--muted: #8b949e;
--border: #30363d;
--accent: #58a6ff;
--accent-fg: #0d1117;
--done: #6e7681;
--surface: #161b22;
--danger: #ff7b72;
--danger-bg: rgba(255, 123, 114, 0.12);
--warning-bg: #332b00;
--warning-border: #8a6d1d;
--warning-fg: #f2cc60;
color-scheme: dark;
}

* {
Expand All @@ -15,8 +39,8 @@

body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial,
sans-serif;
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
background: var(--bg);
color: var(--fg);
line-height: 1.5;
Expand All @@ -28,9 +52,17 @@ body {
justify-content: space-between;
gap: 1rem;
padding: 1.25rem;
background: var(--surface);
border-bottom: 1px solid var(--border);
}

.header-actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 0.75rem;
}

.app-header h1 {
margin: 0;
font-size: 1.75rem;
Expand Down Expand Up @@ -89,9 +121,9 @@ body {
.auth-message {
margin: 0 0 1rem;
padding: 0.6rem 0.75rem;
color: #7d4e00;
background: #fff8c5;
border: 1px solid #d4a72c;
color: var(--warning-fg);
background: var(--warning-bg);
border: 1px solid var(--warning-border);
border-radius: var(--radius);
}

Expand Down Expand Up @@ -127,6 +159,21 @@ body {
cursor: pointer;
}

.theme-toggle {
padding: 0.45rem 0.75rem;
font: inherit;
color: var(--fg);
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
cursor: pointer;
white-space: nowrap;
}

.theme-toggle:hover {
border-color: var(--accent);
}

.task-form button:hover {
filter: brightness(1.05);
}
Expand All @@ -147,6 +194,7 @@ body {
padding: 0.6rem 0.75rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--surface);
}

.task-item.done .task-title {
Expand All @@ -171,8 +219,8 @@ body {
}

.task-delete:hover {
color: #cf222e;
background: rgba(207, 34, 46, 0.08);
color: var(--danger);
background: var(--danger-bg);
}

.empty-state {
Expand All @@ -198,6 +246,12 @@ body {
flex-direction: column;
}

.header-actions {
align-items: flex-start;
flex-direction: column;
width: 100%;
}

.auth-panel {
justify-content: flex-start;
min-width: 0;
Expand Down