diff --git a/web/src/components/CreateTimerModal.tsx b/web/src/components/CreateTimerModal.tsx index c8265e0..90547d7 100644 --- a/web/src/components/CreateTimerModal.tsx +++ b/web/src/components/CreateTimerModal.tsx @@ -47,6 +47,9 @@ export function CreateTimerModal({ isOpen, onClose }: CreateTimerModalProps) { // Event fields const [eventDate, setEventDate] = useState(''); + // Custom pre-warning message + const [customMessage, setCustomMessage] = useState(''); + // Validation errors const [errors, setErrors] = useState>({}); @@ -128,6 +131,7 @@ export function CreateTimerModal({ isOpen, onClose }: CreateTimerModalProps) { urgency, cascade, category: catOrUndef, + customMessage: customMessage || undefined, }); } else if (tab === 'countdown') { const result = countdownSchema.safeParse({ label: label || 'Countdown', hours, minutes, seconds, urgency, cascadePreset }); @@ -144,6 +148,7 @@ export function CreateTimerModal({ isOpen, onClose }: CreateTimerModalProps) { urgency, cascade, category: catOrUndef, + customMessage: customMessage || undefined, }); } else if (tab === 'pomodoro') { const result = pomodoroSchema.safeParse({ label: label || 'Focus Session', workMinutes: workMin, breakMinutes: breakMin, longBreakMinutes: longBreakMin, rounds, urgency }); @@ -177,6 +182,7 @@ export function CreateTimerModal({ isOpen, onClose }: CreateTimerModalProps) { targetTime: target, urgency, category: catOrUndef, + customMessage: customMessage || undefined, }); } @@ -188,6 +194,7 @@ export function CreateTimerModal({ isOpen, onClose }: CreateTimerModalProps) { setSeconds(0); setCategory(''); setEventDate(''); + setCustomMessage(''); setErrors({}); onClose(); }; @@ -511,6 +518,31 @@ export function CreateTimerModal({ isOpen, onClose }: CreateTimerModalProps) { )} + {/* Custom pre-warning message (non-pomodoro) */} + {tab !== 'pomodoro' && ( +
+ + setCustomMessage(e.target.value)} + placeholder='e.g. "Review your agenda" or "Bring your insurance card"' + maxLength={200} + className="w-full px-3 py-2 rounded-lg border text-sm focus:outline-none focus:ring-2" + style={{ + backgroundColor: 'var(--cm-surface-card)', + borderColor: 'var(--cm-border)', + color: 'var(--cm-text-primary)', + }} + /> +

+ Shown in pre-warning notifications instead of auto-generated tips +

+
+ )} + {/* Cascade preset (non-pomodoro) */} {tab !== 'pomodoro' && (
diff --git a/web/src/components/TimerCard.tsx b/web/src/components/TimerCard.tsx index a0982d9..a9b9353 100644 --- a/web/src/components/TimerCard.tsx +++ b/web/src/components/TimerCard.tsx @@ -231,8 +231,13 @@ export function TimerCard({ timer }: TimerCardProps) {

)} - {/* Prep time warning */} - {!isDone && !isFiring && (() => { + {/* Custom message or prep time warning */} + {!isDone && !isFiring && timer.customMessage && ( +

+ 💬 {timer.customMessage} +

+ )} + {!isDone && !isFiring && !timer.customMessage && (() => { const prepConfig = getSuggestedPrepTime(timer.label, timer.category); const minutesUntil = Math.round(remaining / 60_000); if (shouldShowPrepWarning(timer.targetTime, prepConfig, now)) { diff --git a/web/src/lib/timer-engine.ts b/web/src/lib/timer-engine.ts index b48e522..6fa7435 100644 --- a/web/src/lib/timer-engine.ts +++ b/web/src/lib/timer-engine.ts @@ -58,6 +58,7 @@ export interface Timer { tags?: string[]; linkedTimerId?: string | null; recurringTimerId?: string | null; + customMessage?: string | null; } export interface PomodoroConfig { @@ -90,6 +91,7 @@ export interface CreateAlarmParams { cascade?: CascadeConfig; category?: string; description?: string; + customMessage?: string; } export function createAlarm(params: CreateAlarmParams): Timer { @@ -118,6 +120,7 @@ export function createAlarm(params: CreateAlarmParams): Timer { snoozeCount: 0, snoozedUntil: null, category: params.category, + customMessage: params.customMessage ?? null, }; } @@ -128,6 +131,7 @@ export interface CreateCountdownParams { cascade?: CascadeConfig; category?: string; description?: string; + customMessage?: string; } export function createCountdown(params: CreateCountdownParams): Timer { @@ -157,6 +161,7 @@ export function createCountdown(params: CreateCountdownParams): Timer { snoozeCount: 0, snoozedUntil: null, category: params.category, + customMessage: params.customMessage ?? null, }; } @@ -167,6 +172,7 @@ export interface CreateEventParams { cascade?: CascadeConfig; category?: string; description?: string; + customMessage?: string; milestones?: number[]; // days before event to warn (e.g. [30, 7, 1]) } @@ -203,6 +209,7 @@ export function createEvent(params: CreateEventParams): Timer { snoozeCount: 0, snoozedUntil: null, category: params.category, + customMessage: params.customMessage ?? null, }; }