fix: redesign recurring delete as overlay with loading indicator

Replace inline delete buttons (Dieses/Künftige/Alle) with a proper
overlay dialog matching the recurring edit overlay style. Shows animated
dots on the active delete option while processing. Other options dim
during deletion.

v1.75.7

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
mARTin 2026-03-26 12:09:20 +01:00
parent de3b4a103e
commit 51301bc497
2 changed files with 76 additions and 29 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "my-weekly-todo-list", "name": "my-weekly-todo-list",
"version": "1.75.6", "version": "1.75.7",
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view", "description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

View File

@ -161,6 +161,7 @@ export default function CalendarEventModal({
const [allDay, setAllDay] = useState(!!event?.allDay); const [allDay, setAllDay] = useState(!!event?.allDay);
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [isDeleting, setIsDeleting] = useState(false); const [isDeleting, setIsDeleting] = useState(false);
const [deletingMode, setDeletingMode] = useState<string | null>(null);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [isCalendarSelectorOpen, setIsCalendarSelectorOpen] = useState(false); const [isCalendarSelectorOpen, setIsCalendarSelectorOpen] = useState(false);
const calendarSelectorRef = useRef<HTMLDivElement>(null); const calendarSelectorRef = useRef<HTMLDivElement>(null);
@ -269,12 +270,14 @@ export default function CalendarEventModal({
} }
setIsDeleting(true); setIsDeleting(true);
setDeletingMode(mode || 'all');
try { try {
await onDelete(event.id, event.calendarId, mode || 'all'); await onDelete(event.id, event.calendarId, mode || 'all');
onClose(); onClose();
} catch (err: any) { } catch (err: any) {
setError(err.message || 'Failed to delete event'); setError(err.message || 'Failed to delete event');
setIsDeleting(false); setIsDeleting(false);
setDeletingMode(null);
setIsDeleteConfirming(false); setIsDeleteConfirming(false);
setShowRecurringDeleteOptions(false); setShowRecurringDeleteOptions(false);
} }
@ -874,34 +877,21 @@ export default function CalendarEventModal({
{isDeleting ? '...' : isDeleteConfirming ? (language === 'de' ? 'Bestätigen?' : 'Confirm?') : (language === 'de' ? 'Löschen' : 'Delete')} {isDeleting ? '...' : isDeleteConfirming ? (language === 'de' ? 'Bestätigen?' : 'Confirm?') : (language === 'de' ? 'Löschen' : 'Delete')}
</button> </button>
)} )}
{event && onDelete && showRecurringDeleteOptions && ( {event && onDelete && showRecurringDeleteOptions && !isDeleting && (
<div style={{ display: 'flex', gap: '3px', flexWrap: 'wrap', alignItems: 'center' }}> <button
{[ onClick={() => setShowRecurringDeleteOptions(false)}
{ mode: 'this', label: language === 'de' ? 'Dieses' : 'This' }, style={{
{ mode: 'future', label: language === 'de' ? 'Künftige' : 'Future' }, padding: '5px 10px', background: 'none', color: 'var(--weekly-text-light)',
{ mode: 'all', label: language === 'de' ? 'Alle' : 'All' }, border: 'none', fontSize: '0.8rem', cursor: 'pointer',
].map(({ mode, label }) => ( }}
<button >
key={mode} {language === 'de' ? 'Abbrechen' : 'Cancel'}
onClick={() => handleDelete(mode)} </button>
disabled={isDeleting} )}
style={{ {event && onDelete && isDeleting && (
padding: '3px 7px', fontSize: '0.7rem', fontWeight: 500, <span style={{ fontSize: '0.8rem', color: '#ef4444', fontWeight: 500, padding: '5px 10px' }}>
color: '#ef4444', background: 'rgba(239,68,68,0.08)', {language === 'de' ? 'Lösche' : 'Deleting'}<AnimatedDots />
border: '1px solid rgba(239,68,68,0.2)', borderRadius: '5px', </span>
cursor: 'pointer', opacity: isDeleting ? 0.5 : 1,
}}
>
{label}
</button>
))}
<button
onClick={() => setShowRecurringDeleteOptions(false)}
style={{ padding: '2px', color: 'var(--weekly-text-light)', background: 'none', border: 'none', cursor: 'pointer' }}
>
<X size={12} />
</button>
</div>
)} )}
{!event && ( {!event && (
<button onClick={onClose} style={{ padding: '5px 10px', fontSize: '0.8rem', color: 'var(--weekly-text-light)', border: 'none', background: 'none', cursor: 'pointer' }}> <button onClick={onClose} style={{ padding: '5px 10px', fontSize: '0.8rem', color: 'var(--weekly-text-light)', border: 'none', background: 'none', cursor: 'pointer' }}>
@ -1000,6 +990,63 @@ export default function CalendarEventModal({
</div> </div>
</div> </div>
)} )}
{/* Recurring delete mode overlay */}
{showRecurringDeleteOptions && (
<div style={{
position: 'fixed', inset: 0, zIndex: 3000,
display: 'flex', justifyContent: 'center', alignItems: 'center',
background: 'rgba(0,0,0,0.35)', backdropFilter: 'blur(2px)',
}} onClick={() => !isDeleting && setShowRecurringDeleteOptions(false)}>
<div style={{
background: 'var(--weekly-bg, white)', color: 'var(--weekly-text, #333)',
borderRadius: 12, width: '90%', maxWidth: 340,
boxShadow: '0 12px 40px rgba(0,0,0,0.25)',
overflow: 'hidden',
}} onClick={e => e.stopPropagation()}>
<div style={{
padding: '14px 16px 10px', fontWeight: 700, fontSize: '0.9rem',
borderBottom: '2px solid #ef4444',
}}>
{language === 'de' ? 'Wiederkehrendes Ereignis löschen' : 'Delete recurring event'}
{!isDeleting && <button onClick={() => setShowRecurringDeleteOptions(false)} style={{
float: 'right', background: 'none', border: 'none', cursor: 'pointer',
color: 'var(--weekly-text-light)', padding: 0,
}}><X size={16} /></button>}
</div>
<div style={{ padding: '4px 0' }}>
{([
{ value: 'this' as const, title: language === 'de' ? 'Nur dieses Ereignis' : 'This event only', desc: language === 'de' ? 'Alle anderen bleiben erhalten.' : 'All other events stay the same.' },
{ value: 'future' as const, title: language === 'de' ? 'Dieses und folgende' : 'This and following', desc: language === 'de' ? 'Dieses und alle zukünftigen werden gelöscht.' : 'This and all future events will be deleted.' },
{ value: 'all' as const, title: language === 'de' ? 'Alle Ereignisse' : 'All events', desc: language === 'de' ? 'Die gesamte Serie wird gelöscht.' : 'The entire series will be deleted.' },
]).map(opt => (
<button
key={opt.value}
onClick={() => handleDelete(opt.value)}
disabled={isDeleting}
style={{
display: 'flex', gap: 10, padding: '10px 16px', cursor: 'pointer',
width: '100%', textAlign: 'left', background: 'none',
border: 'none', borderBottom: '1px solid var(--weekly-border, #eee)',
color: 'var(--weekly-text, #333)',
opacity: isDeleting && deletingMode !== opt.value ? 0.4 : 1,
alignItems: 'center',
}}
>
<div style={{ flex: 1 }}>
<div style={{ fontWeight: 600, fontSize: '0.82rem', color: '#ef4444' }}>{opt.title}</div>
<div style={{ fontSize: '0.72rem', opacity: 0.5, marginTop: 1 }}>{opt.desc}</div>
</div>
{isDeleting && deletingMode === opt.value && (
<span style={{ fontSize: '0.8rem', color: '#ef4444', fontWeight: 600 }}>
<AnimatedDots />
</span>
)}
</button>
))}
</div>
</div>
</div>
)}
</div> </div>
); );
} }