feat(web): add custom pre-warning messages per timer

- Add customMessage field to Timer interface + all Create*Params
- Custom message input in CreateTimerModal (optional, non-pomodoro)
- TimerCard shows custom message, falls back to auto-generated prep tips
- 373/373 tests pass, 0 type errors
This commit is contained in:
saravanakumardb1 2026-02-28 13:37:42 -08:00
parent 4e1a22f869
commit d3b55a2135
3 changed files with 46 additions and 2 deletions

View File

@ -47,6 +47,9 @@ export function CreateTimerModal({ isOpen, onClose }: CreateTimerModalProps) {
// Event fields // Event fields
const [eventDate, setEventDate] = useState(''); const [eventDate, setEventDate] = useState('');
// Custom pre-warning message
const [customMessage, setCustomMessage] = useState('');
// Validation errors // Validation errors
const [errors, setErrors] = useState<Record<string, string>>({}); const [errors, setErrors] = useState<Record<string, string>>({});
@ -128,6 +131,7 @@ export function CreateTimerModal({ isOpen, onClose }: CreateTimerModalProps) {
urgency, urgency,
cascade, cascade,
category: catOrUndef, category: catOrUndef,
customMessage: customMessage || undefined,
}); });
} else if (tab === 'countdown') { } else if (tab === 'countdown') {
const result = countdownSchema.safeParse({ label: label || 'Countdown', hours, minutes, seconds, urgency, cascadePreset }); const result = countdownSchema.safeParse({ label: label || 'Countdown', hours, minutes, seconds, urgency, cascadePreset });
@ -144,6 +148,7 @@ export function CreateTimerModal({ isOpen, onClose }: CreateTimerModalProps) {
urgency, urgency,
cascade, cascade,
category: catOrUndef, category: catOrUndef,
customMessage: customMessage || undefined,
}); });
} else if (tab === 'pomodoro') { } else if (tab === 'pomodoro') {
const result = pomodoroSchema.safeParse({ label: label || 'Focus Session', workMinutes: workMin, breakMinutes: breakMin, longBreakMinutes: longBreakMin, rounds, urgency }); 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, targetTime: target,
urgency, urgency,
category: catOrUndef, category: catOrUndef,
customMessage: customMessage || undefined,
}); });
} }
@ -188,6 +194,7 @@ export function CreateTimerModal({ isOpen, onClose }: CreateTimerModalProps) {
setSeconds(0); setSeconds(0);
setCategory(''); setCategory('');
setEventDate(''); setEventDate('');
setCustomMessage('');
setErrors({}); setErrors({});
onClose(); onClose();
}; };
@ -511,6 +518,31 @@ export function CreateTimerModal({ isOpen, onClose }: CreateTimerModalProps) {
</div> </div>
)} )}
{/* Custom pre-warning message (non-pomodoro) */}
{tab !== 'pomodoro' && (
<div>
<label className="block text-sm font-medium mb-1" style={{ color: 'var(--cm-text-secondary)' }}>
Custom Warning Message <span className="text-xs font-normal" style={{ color: 'var(--cm-text-tertiary)' }}>(optional)</span>
</label>
<input
type="text"
value={customMessage}
onChange={(e) => 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)',
}}
/>
<p className="text-xs mt-1" style={{ color: 'var(--cm-text-tertiary)' }}>
Shown in pre-warning notifications instead of auto-generated tips
</p>
</div>
)}
{/* Cascade preset (non-pomodoro) */} {/* Cascade preset (non-pomodoro) */}
{tab !== 'pomodoro' && ( {tab !== 'pomodoro' && (
<div> <div>

View File

@ -231,8 +231,13 @@ export function TimerCard({ timer }: TimerCardProps) {
</p> </p>
)} )}
{/* Prep time warning */} {/* Custom message or prep time warning */}
{!isDone && !isFiring && (() => { {!isDone && !isFiring && timer.customMessage && (
<p className="text-xs mb-3 flex items-center gap-1 font-medium" style={{ color: 'var(--cm-accent-secondary)' }}>
💬 {timer.customMessage}
</p>
)}
{!isDone && !isFiring && !timer.customMessage && (() => {
const prepConfig = getSuggestedPrepTime(timer.label, timer.category); const prepConfig = getSuggestedPrepTime(timer.label, timer.category);
const minutesUntil = Math.round(remaining / 60_000); const minutesUntil = Math.round(remaining / 60_000);
if (shouldShowPrepWarning(timer.targetTime, prepConfig, now)) { if (shouldShowPrepWarning(timer.targetTime, prepConfig, now)) {

View File

@ -58,6 +58,7 @@ export interface Timer {
tags?: string[]; tags?: string[];
linkedTimerId?: string | null; linkedTimerId?: string | null;
recurringTimerId?: string | null; recurringTimerId?: string | null;
customMessage?: string | null;
} }
export interface PomodoroConfig { export interface PomodoroConfig {
@ -90,6 +91,7 @@ export interface CreateAlarmParams {
cascade?: CascadeConfig; cascade?: CascadeConfig;
category?: string; category?: string;
description?: string; description?: string;
customMessage?: string;
} }
export function createAlarm(params: CreateAlarmParams): Timer { export function createAlarm(params: CreateAlarmParams): Timer {
@ -118,6 +120,7 @@ export function createAlarm(params: CreateAlarmParams): Timer {
snoozeCount: 0, snoozeCount: 0,
snoozedUntil: null, snoozedUntil: null,
category: params.category, category: params.category,
customMessage: params.customMessage ?? null,
}; };
} }
@ -128,6 +131,7 @@ export interface CreateCountdownParams {
cascade?: CascadeConfig; cascade?: CascadeConfig;
category?: string; category?: string;
description?: string; description?: string;
customMessage?: string;
} }
export function createCountdown(params: CreateCountdownParams): Timer { export function createCountdown(params: CreateCountdownParams): Timer {
@ -157,6 +161,7 @@ export function createCountdown(params: CreateCountdownParams): Timer {
snoozeCount: 0, snoozeCount: 0,
snoozedUntil: null, snoozedUntil: null,
category: params.category, category: params.category,
customMessage: params.customMessage ?? null,
}; };
} }
@ -167,6 +172,7 @@ export interface CreateEventParams {
cascade?: CascadeConfig; cascade?: CascadeConfig;
category?: string; category?: string;
description?: string; description?: string;
customMessage?: string;
milestones?: number[]; // days before event to warn (e.g. [30, 7, 1]) milestones?: number[]; // days before event to warn (e.g. [30, 7, 1])
} }
@ -203,6 +209,7 @@ export function createEvent(params: CreateEventParams): Timer {
snoozeCount: 0, snoozeCount: 0,
snoozedUntil: null, snoozedUntil: null,
category: params.category, category: params.category,
customMessage: params.customMessage ?? null,
}; };
} }