From d909830fcda8e266b58938386aa5eaaa08439ad7 Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Fri, 27 Feb 2026 22:31:20 -0800 Subject: [PATCH] feat(web): snooze suggestion card on Dashboard, event countdown large display MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Dashboard: adaptive snooze suggestion cards (dismissable, max 2 shown, pattern-based) - TimerCard: event countdown type gets large days display with milestone progress bar - Progress bar color-codes: accent → warning (70%) → danger (90%) - Shows days + hours remaining with target date - Updated roadmap: marked snooze suggestion card and event visual as completed - 373 tests across 16 files, tsc clean --- docs/roadmap.md | 4 +-- web/src/components/Dashboard.tsx | 40 +++++++++++++++++++++++++++ web/src/components/TimerCard.tsx | 47 +++++++++++++++++++++++++++++++- 3 files changed, 88 insertions(+), 3 deletions(-) diff --git a/docs/roadmap.md b/docs/roadmap.md index 48d92c4..6ddf78f 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -381,7 +381,7 @@ ChronoMind ships in **5 phases over ~6 months**, from web MVP to full cross-plat - [x] After 5+ data points: "You always snooze your morning alarm 3 times. Set it 15 minutes later?" - [x] Label normalization for pattern grouping (e.g. "Meeting with Bob" → "meeting with") - [x] checkForSnoozeSuggestion() for real-time suggestions on timer creation - - [ ] Suggestion shown as dismissable card on dashboard + - [x] Suggestion shown as dismissable card on dashboard - [ ] Accept → auto-adjusts recurring timer - [x] Unit tests (22 tests) @@ -390,7 +390,7 @@ ChronoMind ships in **5 phases over ~6 months**, from web MVP to full cross-plat - [x] Timer type `event` in data model + createEvent factory + store addEvent - [x] Event tab in CreateTimerModal with date picker + days-remaining preview - [x] Use cases: "132 days until wedding", "47 days until vacation" - - [ ] Visual: large countdown display with milestone progress bar + - [x] Visual: large days display with milestone progress bar (color-coded: accent → warning → danger) - [x] **Timer export / import (local backup) — `lib/export.ts` + History page** - [x] "Export timers" → JSON download of all timers diff --git a/web/src/components/Dashboard.tsx b/web/src/components/Dashboard.tsx index f49a66c..9b6521a 100644 --- a/web/src/components/Dashboard.tsx +++ b/web/src/components/Dashboard.tsx @@ -17,12 +17,16 @@ import Link from 'next/link'; import { FeedbackButton } from './FeedbackButton'; import { InstallPrompt } from './InstallPrompt'; import { useTheme } from '@/lib/use-theme'; +import { getSnoozeSuggestions } from '@/lib/adaptive-snooze'; +import type { SnoozeSuggestion } from '@/lib/adaptive-snooze'; export function Dashboard() { const [isCreateOpen, setIsCreateOpen] = useState(false); const [showShortcuts, setShowShortcuts] = useState(false); const [mounted, setMounted] = useState(false); const [filterCategory, setFilterCategory] = useState(null); + const [dismissedSuggestions, setDismissedSuggestions] = useState>(new Set()); + const [snoozeSuggestions, setSnoozeSuggestions] = useState([]); const timers = useTimerStore((s) => s.timers); const now = useTimerStore((s) => s.now); const { pause, resume } = useTimerStore(); @@ -56,6 +60,7 @@ export function Dashboard() { useEffect(() => { setMounted(true); requestNotificationPermission(); + setSnoozeSuggestions(getSnoozeSuggestions()); }, []); if (!mounted) { @@ -267,6 +272,41 @@ export function Dashboard() { ))} + {/* Snooze suggestions */} + {snoozeSuggestions.filter((s) => !dismissedSuggestions.has(s.labelPattern)).length > 0 && ( +
+ {snoozeSuggestions + .filter((s) => !dismissedSuggestions.has(s.labelPattern)) + .slice(0, 2) + .map((suggestion) => ( +
+
+

+ Snooze pattern detected +

+

+ {suggestion.message} +

+
+ +
+ ))} +
+ )} + {/* Active timers */} {activeTimers.length > 0 ? (
diff --git a/web/src/components/TimerCard.tsx b/web/src/components/TimerCard.tsx index 3767467..1e4de66 100644 --- a/web/src/components/TimerCard.tsx +++ b/web/src/components/TimerCard.tsx @@ -130,7 +130,52 @@ export function TimerCard({ timer }: TimerCardProps) { {/* Countdown / Time */} - {!isDone && ( + {!isDone && timer.type === 'event' && ( +
+ {/* Large days display for event countdowns */} + {(() => { + const days = Math.floor(remaining / 86_400_000); + const hours = Math.floor((remaining % 86_400_000) / 3_600_000); + const totalDays = timer.duration ? Math.ceil(timer.duration / 86_400_000) : days; + const progress = totalDays > 0 ? Math.max(0, Math.min(1, 1 - days / totalDays)) : 0; + return ( + <> +
+ + {days} + + + day{days !== 1 ? 's' : ''} + + + {hours}h remaining + +
+ {/* Milestone progress bar */} +
+
0.9 ? 'var(--cm-danger)' : progress > 0.7 ? 'var(--cm-warning)' : 'var(--cm-accent)', + }} + /> +
+
+ Created + + {new Date(timer.targetTime).toLocaleDateString()} + +
+ + ); + })()} +
+ )} + {!isDone && timer.type !== 'event' && (