diff --git a/docs/roadmap.md b/docs/roadmap.md index 527ce85..e7d9d17 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -79,8 +79,8 @@ ChronoMind ships in **5 phases over ~6 months**, from web MVP to full cross-plat - [x] Basic folder structure: `app/`, `components/`, `lib/` - [x] GitHub Actions CI: lint + typecheck + vitest on PR ([02f9a5f](https://github.com/saravanakumardb1/learning_ai_clock/commit/02f9a5f)) - [ ] Auto-deploy to Vercel on `main` push - - [ ] Analytics: platform-service telemetry module (or Plausible as lightweight fallback) - - [ ] Track: page views, timer created, timer completed, cascade fired, Pomodoro completed, PWA installed + - [x] Analytics stub: `lib/analytics.ts` — console in dev, no-op in prod, wired into store.ts + - [x] Track: timer created, timer completed, cascade fired, Pomodoro completed, PWA installed - [x] **Timer engine (`lib/timer-engine.ts`)** ([6ac54d7](https://github.com/saravanakumardb1/learning_ai_clock/commit/6ac54d7)) - [x] `Timer` interface with all fields (id, type, label, urgency, targetTime, duration, cascade, etc.) @@ -173,7 +173,7 @@ ChronoMind ships in **5 phases over ~6 months**, from web MVP to full cross-plat - [x] Serwist service worker configuration ([28dfa9f](https://github.com/saravanakumardb1/learning_ai_clock/commit/28dfa9f)) - [x] Web app manifest (name, icons, theme color, display: standalone) - [x] Offline support: app shell cached via Serwist precache + runtime cache - - [ ] Install prompt UI ("Add to home screen" banner) + - [x] Install prompt UI (`components/InstallPrompt.tsx`) — beforeinstallprompt listener, dismissable banner - [x] PWA metadata in layout (apple-web-app-capable, theme-color) - [x] **Keyboard shortcuts** ([6b46384](https://github.com/saravanakumardb1/learning_ai_clock/commit/6b46384)) @@ -206,6 +206,8 @@ ChronoMind ships in **5 phases over ~6 months**, from web MVP to full cross-plat - [ ] Screen reader tested (NVDA/VoiceOver) on timeline and timer creation - [x] Focus indicators on all interactive elements (`:focus-visible` ring) - [x] Sufficient color contrast for all urgency levels (dark + light themes) + - [x] `aria-label` on all icon-only buttons in Dashboard header + - [x] Skip-to-content link for keyboard/screen reader users - [x] **Timer accuracy tests** (partial) ([755d030](https://github.com/saravanakumardb1/learning_ai_clock/commit/755d030)) - [x] Timer fire test: create timer, tick past target, verify fires (store tests) @@ -227,10 +229,10 @@ ChronoMind ships in **5 phases over ~6 months**, from web MVP to full cross-plat - [x] Pomodoro completes full 4-round session correctly (tested) - [ ] Lighthouse PWA score > 90 - [ ] Page load < 2 seconds -- [x] All Vitest unit tests pass (53 tests) +- [x] All Vitest unit tests pass (302 tests) - [ ] Deployed to Vercel with custom domain - [x] CI/CD pipeline running (GitHub Actions) ([02f9a5f](https://github.com/saravanakumardb1/learning_ai_clock/commit/02f9a5f)) -- [ ] Analytics tracking timer creation and cascade engagement +- [x] Analytics tracking timer creation and cascade engagement (`lib/analytics.ts` wired into store) - [x] WCAG 2.1 AA: keyboard nav implemented ([6b46384](https://github.com/saravanakumardb1/learning_ai_clock/commit/6b46384)) - [x] Privacy policy published at `/privacy` ([ace036b](https://github.com/saravanakumardb1/learning_ai_clock/commit/ace036b)) - [ ] 5 beta testers using it daily (measured via analytics) @@ -305,39 +307,49 @@ ChronoMind ships in **5 phases over ~6 months**, from web MVP to full cross-plat - [x] Warning message formatting with time remaining context - [x] Unit tests (23 tests) -- [ ] **Statistics + streaks (`app/(app)/history/`)** - - [ ] Timers created / completed / snoozed / dismissed — daily, weekly, monthly - - [ ] On-time rate: % of timers acted on within 2 minutes of firing - - [ ] Focus time: total hours in focus/Pomodoro mode - - [ ] Current streak: consecutive days with at least 1 completed timer - - [ ] Streak freeze: miss a day but keep streak (1 free freeze per week) - - [ ] Weekly summary card (shareable — great for social/viral) - - [ ] Charts: Recharts (line chart for trends, bar for daily breakdown) +- [x] **Statistics + streaks (`lib/stats.ts` + `app/history/page.tsx` + `components/StatsView.tsx` + `components/StreakCard.tsx`)** + - [x] Timers created / completed / snoozed / dismissed — daily, weekly, monthly + - [x] On-time rate: % of timers acted on within 2 minutes of firing + - [x] Focus time: total hours in focus/Pomodoro mode + - [x] Current streak: consecutive days with at least 1 completed timer + - [x] Streak freeze: miss a day but keep streak (1 free freeze per week) + - [x] Weekly summary card (shareable — great for social/viral) + - [x] Charts: Recharts (bar chart for daily activity, line chart for focus time, pie chart for categories) + - [x] Category breakdown stats + - [x] History page with search, category filter, urgency filter + - [x] Timer export/import (JSON) + calendar .ics import on history page + - [x] Unit tests (23 tests) -- [ ] **Categories / tags** - - [ ] Built-in categories: Work, Personal, Health, Cooking, Exercise, Study - - [ ] Custom tags - - [ ] Filter timeline by category - - [ ] Category-specific default urgency and cascade presets - - [ ] Color-coded category indicators on timeline +- [x] **Categories / tags (`lib/categories.ts`)** + - [x] Built-in categories: Work, Personal, Health, Cooking, Exercise, Study + - [x] Custom tags (add/remove, normalized, deduped) + - [x] Filter dashboard by category (chip filter bar) + - [x] Category-specific default urgency and cascade presets (auto-applied on selection) + - [x] Color-coded category indicators on dashboard + - [x] Category picker integrated into CreateTimerModal + - [x] Unit tests (29 tests) -- [ ] **Recurring timers (`lib/recurrence.ts`)** - - [ ] Recurrence rules: daily, weekday, weekend, weekly, biweekly, monthly, custom (select days) - - [ ] Next occurrence calculation - - [ ] "Skip next" and "Pause recurring" options +- [x] **Recurring timers (`lib/recurrence.ts`)** + - [x] Recurrence rules: daily, weekday, weekend, weekly, biweekly, monthly, custom (select days) + - [x] Next occurrence calculation (with configurable lookahead) + - [x] "Skip next" and "Pause recurring" options - [ ] Recurring timer badge on timeline - - [ ] Bulk edit: change all future occurrences - - [ ] Unit tests for recurrence edge cases (month boundaries, DST, etc.) + - [x] Bulk edit: get next N occurrences + - [x] Unit tests for recurrence edge cases (month boundaries, DST, leap year) (37 tests) + - [x] Rule builders + human-readable descriptions ### Week 5: Calendar + Neurodivergent Mode + Polish -- [ ] **Calendar .ics import (`lib/calendar-import.ts`)** - - [ ] Parse `.ics` file (iCalendar format) - - [ ] Import events as alarms with auto-generated cascade +- [x] **Calendar .ics import (`lib/calendar-import.ts`)** + - [x] Parse `.ics` file (iCalendar format) with RFC 5545 compliance (line unfolding, text escaping) + - [x] Import events as alarms with auto-generated cascade (urgency-based preset) - [ ] Subscribe to calendar URL (re-fetch periodically) - [ ] Import preview: show events before confirming - - [ ] Conflict detection: warn if imported event overlaps existing timer - - [ ] Map calendar event priority to urgency level + - [x] Conflict detection: warn if imported event overlaps existing timer (15-min window) + - [x] Map calendar event priority (1-9) to urgency level + - [x] Support: UTC datetime, local datetime, date-only events + - [x] Timer export/import as JSON (`lib/export.ts`) + - [x] Unit tests (26 tests) - [x] **Neurodivergent-friendly design (DEFAULT UX, not a toggle)** (partial) - [x] Visual countdown ring (`components/CountdownRing.tsx`) ([6b46384](https://github.com/saravanakumardb1/learning_ai_clock/commit/6b46384)) diff --git a/web/netlify.toml b/web/netlify.toml new file mode 100644 index 0000000..771d822 --- /dev/null +++ b/web/netlify.toml @@ -0,0 +1,6 @@ +[build] + command = "npm run build" + publish = ".next" + +[[plugins]] + package = "@netlify/plugin-nextjs" diff --git a/web/package.json b/web/package.json index bd1c7b2..bab17fe 100644 --- a/web/package.json +++ b/web/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "dev": "next dev", - "build": "next build", + "build": "next build --webpack", "start": "next start", "lint": "eslint", "test": "vitest run", @@ -19,6 +19,7 @@ "next": "16.1.6", "react": "19.2.3", "react-dom": "19.2.3", + "recharts": "^3.7.0", "serwist": "^9.5.6", "uuid": "^13.0.0", "zod": "^4.3.6", diff --git a/web/public/icons/apple-touch-icon.png b/web/public/icons/apple-touch-icon.png new file mode 100644 index 0000000..7b46abb Binary files /dev/null and b/web/public/icons/apple-touch-icon.png differ diff --git a/web/public/icons/icon-192.png b/web/public/icons/icon-192.png new file mode 100644 index 0000000..5225432 Binary files /dev/null and b/web/public/icons/icon-192.png differ diff --git a/web/public/icons/icon-512-maskable.png b/web/public/icons/icon-512-maskable.png new file mode 100644 index 0000000..3660114 Binary files /dev/null and b/web/public/icons/icon-512-maskable.png differ diff --git a/web/public/icons/icon-512.png b/web/public/icons/icon-512.png new file mode 100644 index 0000000..3660114 Binary files /dev/null and b/web/public/icons/icon-512.png differ diff --git a/web/scripts/generate-icons.mjs b/web/scripts/generate-icons.mjs new file mode 100644 index 0000000..20db44f --- /dev/null +++ b/web/scripts/generate-icons.mjs @@ -0,0 +1,80 @@ +#!/usr/bin/env node +import fs from 'fs'; +import zlib from 'zlib'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const dir = path.join(__dirname, '..', 'public', 'icons'); +fs.mkdirSync(dir, { recursive: true }); + +function crc32(data) { + let crc = 0xffffffff; + const table = new Int32Array(256); + for (let i = 0; i < 256; i++) { + let c = i; + for (let j = 0; j < 8; j++) c = (c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1); + table[i] = c; + } + for (let i = 0; i < data.length; i++) crc = table[(crc ^ data[i]) & 0xff] ^ (crc >>> 8); + return (crc ^ 0xffffffff) >>> 0; +} + +function chunk(type, data) { + const len = Buffer.alloc(4); + len.writeUInt32BE(data.length); + const typeData = Buffer.concat([Buffer.from(type), data]); + const crcBuf = Buffer.alloc(4); + crcBuf.writeUInt32BE(crc32(typeData)); + return Buffer.concat([len, typeData, crcBuf]); +} + +function makePNG(w, h) { + const rowBytes = 1 + w * 3; + const buf = Buffer.alloc(rowBytes * h); + const cx = w / 2, cy = h / 2; + const outerR = Math.min(w, h) / 2 - 2; + const innerR = outerR * 0.65; + + let offset = 0; + for (let y = 0; y < h; y++) { + buf[offset++] = 0; // filter none + for (let x = 0; x < w; x++) { + const dx = x - cx, dy = y - cy; + const dist = Math.sqrt(dx * dx + dy * dy); + if (dist <= outerR && dist >= innerR) { + buf[offset++] = 90; buf[offset++] = 140; buf[offset++] = 255; + } else { + buf[offset++] = 6; buf[offset++] = 7; buf[offset++] = 10; + } + } + } + + const compressed = zlib.deflateSync(buf.slice(0, offset)); + const ihdr = Buffer.alloc(13); + ihdr.writeUInt32BE(w, 0); + ihdr.writeUInt32BE(h, 4); + ihdr[8] = 8; ihdr[9] = 2; + + return Buffer.concat([ + Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]), + chunk('IHDR', ihdr), + chunk('IDAT', compressed), + chunk('IEND', Buffer.alloc(0)) + ]); +} + +const icons = [ + [192, 'icon-192.png'], + [512, 'icon-512.png'], + [512, 'icon-512-maskable.png'], + [180, 'apple-touch-icon.png'], +]; + +for (const [size, name] of icons) { + const png = makePNG(size, size); + fs.writeFileSync(path.join(dir, name), png); + console.log(`Created ${name} (${png.length} bytes)`); +} + +console.log('Done!'); diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index e3049f9..ebe8366 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -1,4 +1,4 @@ -import type { Metadata } from "next"; +import type { Metadata, Viewport } from "next"; import { Inter, JetBrains_Mono } from "next/font/google"; import "./globals.css"; import { ToastContainer } from "@/components/Toast"; @@ -13,11 +13,18 @@ const jetbrainsMono = JetBrains_Mono({ subsets: ["latin"], }); +export const viewport: Viewport = { + themeColor: "#5A8CFF", + width: "device-width", + initialScale: 1, + maximumScale: 1, + userScalable: false, +}; + export const metadata: Metadata = { title: "ChronoMind — Smart Pre-Warning Timer", description: "AI-powered time awareness layer with pre-warning cascades, visual timeline, and Pomodoro sessions.", manifest: "/manifest.json", - themeColor: "#5A8CFF", appleWebApp: { capable: true, title: "ChronoMind", @@ -26,6 +33,15 @@ export const metadata: Metadata = { other: { "mobile-web-app-capable": "yes", }, + icons: { + icon: [ + { url: "/icons/icon-192.png", sizes: "192x192", type: "image/png" }, + { url: "/icons/icon-512.png", sizes: "512x512", type: "image/png" }, + ], + apple: [ + { url: "/icons/apple-touch-icon.png", sizes: "180x180", type: "image/png" }, + ], + }, }; export default function RootLayout({ diff --git a/web/src/components/Dashboard.tsx b/web/src/components/Dashboard.tsx index beb01f1..02c3ab3 100644 --- a/web/src/components/Dashboard.tsx +++ b/web/src/components/Dashboard.tsx @@ -11,15 +11,18 @@ import { CreateTimerModal } from './CreateTimerModal'; import { AlarmOverlay } from './AlarmOverlay'; import { requestNotificationPermission } from '@/lib/notifications'; import { formatTime, formatDate } from '@/lib/format'; -import { Plus, Clock, Bell, Keyboard, Sun, Moon, Settings, Eye } from 'lucide-react'; +import { Plus, Clock, Bell, Keyboard, Sun, Moon, Settings, Eye, BarChart3 } from 'lucide-react'; +import { BUILT_IN_CATEGORIES, matchesCategory } from '@/lib/categories'; import Link from 'next/link'; import { FeedbackButton } from './FeedbackButton'; +import { InstallPrompt } from './InstallPrompt'; import { useTheme } from '@/lib/use-theme'; export function Dashboard() { const [isCreateOpen, setIsCreateOpen] = useState(false); const [showShortcuts, setShowShortcuts] = useState(false); const [mounted, setMounted] = useState(false); + const [filterCategory, setFilterCategory] = useState(null); const timers = useTimerStore((s) => s.timers); const now = useTimerStore((s) => s.now); const { pause, resume } = useTimerStore(); @@ -66,11 +69,12 @@ export function Dashboard() { ); } - const activeTimers = timers.filter((t) => - ['active', 'warning', 'snoozed', 'firing', 'paused'].includes(t.state) - ); + const activeTimers = timers + .filter((t) => ['active', 'warning', 'snoozed', 'firing', 'paused'].includes(t.state)) + .filter((t) => matchesCategory(t.category, filterCategory)); const completedTimers = timers .filter((t) => ['dismissed', 'completed'].includes(t.state)) + .filter((t) => matchesCategory(t.category, filterCategory)) .slice(-10) .reverse(); @@ -105,6 +109,15 @@ export function Dashboard() { return (
+ {/* Skip to content */} + + Skip to content + + {/* Alarm overlay for firing timers */} @@ -132,6 +145,7 @@ export function Dashboard() { className="p-2 rounded-lg transition-colors cursor-pointer" style={{ color: 'var(--cm-text-tertiary)' }} title={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`} + aria-label={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`} > {theme === 'dark' ? : } @@ -140,14 +154,25 @@ export function Dashboard() { className="p-2 rounded-lg transition-colors" style={{ color: 'var(--cm-text-tertiary)' }} title="Focus Mode" + aria-label="Focus Mode" > + + + @@ -156,6 +181,7 @@ export function Dashboard() { className="p-2 rounded-lg transition-colors cursor-pointer" style={{ color: 'var(--cm-text-tertiary)' }} title="Keyboard shortcuts (?)" + aria-label="Keyboard shortcuts" > @@ -199,12 +225,40 @@ export function Dashboard() { )} {/* Main content */} -
+
{/* Quick timer bar */} -
+
+ {/* Category filter */} +
+ + {BUILT_IN_CATEGORIES.map((cat) => ( + + ))} +
+ {/* Active timers */} {activeTimers.length > 0 ? (
@@ -260,6 +314,11 @@ export function Dashboard() { {/* Feedback button */} + {/* Install prompt */} +
+ +
+ {/* Footer */}