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:
parent
4e1a22f869
commit
d3b55a2135
@ -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>
|
||||||
|
|||||||
@ -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)) {
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user