From e6b97fcbf09958813b121f6f26de19c2a4366369 Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Fri, 27 Feb 2026 21:57:43 -0800 Subject: [PATCH] =?UTF-8?q?feat(deploy):=20Phase=201=20polish=20=E2=80=94?= =?UTF-8?q?=20analytics,=20install=20prompt,=20a11y,=20PWA=20icons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/roadmap.md | 72 ++++++++++------- web/netlify.toml | 6 ++ web/package.json | 3 +- web/public/icons/apple-touch-icon.png | Bin 0 -> 1193 bytes web/public/icons/icon-192.png | Bin 0 -> 1303 bytes web/public/icons/icon-512-maskable.png | Bin 0 -> 6538 bytes web/public/icons/icon-512.png | Bin 0 -> 6538 bytes web/scripts/generate-icons.mjs | 80 +++++++++++++++++++ web/src/app/layout.tsx | 20 ++++- web/src/components/Dashboard.tsx | 71 +++++++++++++++-- web/src/components/InstallPrompt.tsx | 105 +++++++++++++++++++++++++ web/src/lib/analytics.ts | 44 +++++++++++ web/src/lib/store.ts | 9 +++ 13 files changed, 371 insertions(+), 39 deletions(-) create mode 100644 web/netlify.toml create mode 100644 web/public/icons/apple-touch-icon.png create mode 100644 web/public/icons/icon-192.png create mode 100644 web/public/icons/icon-512-maskable.png create mode 100644 web/public/icons/icon-512.png create mode 100644 web/scripts/generate-icons.mjs create mode 100644 web/src/components/InstallPrompt.tsx create mode 100644 web/src/lib/analytics.ts 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 0000000000000000000000000000000000000000..7b46abb90d41cd03c621f96ee724af9b0f669542 GIT binary patch literal 1193 zcmV;a1XlZrP)psM>VZ)zx?`}*?;ye{so3j9RKR~ zKmLq=1E(!G{=WLP#b0!wu{bW*u+YjV9EU-ShfQBYx&RL~3xv z@48S)0gm_~*AhYFh#zk`5h#xM!6ZV&5kHbdh&bYh;S|3G2uJ)F%;I;+04TdBffZ7i*UrZZfrh|_{8mv#Sve2MgwugH=WcR9PufqR*xgT;OvTV#JAf}C64%N zdn&^b-)mbnIO0p~tN=%Rqs^gl#Mdz>j`%M2JsghsBK9#5j`$Wg?HK>ItdCtdeC%l) z@f8k$*!lK%am3#rF|lNaw{XN?9yc+2lQ}rzEaolqI*$08nJgwv^dgS6scUt1(0vxn2o*im!7LNFB2yq7g0te?qR|T1! zf+KzpT7>0)h6CbvXN2()IO3OJ7T-UQBYp#NP#nsD6g-4D=;3fE16ArA;wWexj`-qT zQE;T3S{BETN6FxbZ{3(UY&VYh%DshgoabpA9G|Xp8b^HJ_QYZ0up%7FsOZ?4#L<^H zY!i<7rjrtfVQ}2~d*Ud<@z1fuQQ0UQ@ipfq4ikro!^9yDafm}4;$SueM|{VriNnNU z;;=p(_x+4G8gYETntwTpJM@=B9F2*?#9=#e=%Y)9IO-FJiNnNU;t+>8#32rS06TLm zaa491$K5{}A9PY@E+vl2rs2>>g$!|YCk}(*&NhpYH=Jt{;2;~wxHBG#8D6nhdww34&4cls!Qn zhd!|TBXH=0IsVUZ5b&;zusj8aK7g}-frIm*d*Tev!l4h;+F#?KeevLEI}>s2^Z^|C zAsoz4pA>UmE{;7uu-iV0gZzf-$g9$E%=Q7F^a&jN$8^OXvjfL4A}upMhok6W*op>x z8b{ajXmw53hoh88e6d3~AWv-}5?|@%tzN{zc{7^VdL0Mq@iZdwH7;3Z4h{&B z_yQCh%q=2H;_tmS=egO{tM%O}#zv9?qcj0(;#Q3*Oz|p?rye_%v zWjOS~>8gWXi9;WRF1+T&IP@__#w|GXF+;{(IP@_<#*H}i(Jtd&9Qvr1aXSuu^vXDa zLm#Cw4&jhU9gEXA^wA~bG!A_f$q3+(NCk@|4t?-tBys2iE+dRXB3Kp_9Qt6&px}@Q zjRg*eL^v#x@x>P5Xi*a1 zXElzG#VHH`@v%`jJ|>9w8;j#pYIvb7I38@t)gMe8_P_lF6)1Gu!dD!q00000NkvXX Hu0mjfkG3-U literal 0 HcmV?d00001 diff --git a/web/public/icons/icon-192.png b/web/public/icons/icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..522543261016e7fc1d2f7941e0774b2d0775cbbe GIT binary patch literal 1303 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 literal 0 HcmV?d00001 diff --git a/web/public/icons/icon-512.png b/web/public/icons/icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..36601149998aa299a3e60ee747dc9d08db6ac3a1 GIT binary patch literal 6538 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 literal 0 HcmV?d00001 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 */}