feat: redesign recurring edit dialogs with Google Calendar style and i18n

- Redesigned drag/resize and modal recurring edit dialogs with radio buttons
  (This event / This and following / All events) with descriptions
- Added German/English i18n to both recurring edit dialogs
- Repeat-on day buttons now respect weekStartDay setting (Mon/Sun start)
- Added language prop to CalendarEventModal

v1.70.0

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
mARTin 2026-03-24 16:40:33 +01:00
parent 74a8c9a9a9
commit 948d239867
3 changed files with 162 additions and 116 deletions

View File

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

View File

@ -40,6 +40,8 @@ interface CalendarEventModalProps {
initialDate?: Date;
initialStartTime?: string;
connections: any[];
weekStartDay?: number; // 0=Sunday, 1=Monday
language?: string;
onClose: () => void;
onSave: (eventData: any) => Promise<void>;
onDelete?: (eventId: string, calendarId: string, deleteMode?: string) => Promise<void>;
@ -50,6 +52,8 @@ export default function CalendarEventModal({
initialDate,
initialStartTime,
connections,
weekStartDay = 0,
language = 'en',
onClose,
onSave,
onDelete
@ -223,6 +227,7 @@ export default function CalendarEventModal({
const [isDeleteConfirming, setIsDeleteConfirming] = useState(false);
const [showRecurringDeleteOptions, setShowRecurringDeleteOptions] = useState(false);
const [showRecurringEditOptions, setShowRecurringEditOptions] = useState(false);
const [recurringEditMode, setRecurringEditMode] = useState<'this' | 'future' | 'all'>('this');
const handleDelete = async (mode?: string) => {
if (!event?.id || !onDelete) return;
@ -503,26 +508,30 @@ export default function CalendarEventModal({
<div style={{ ...rowStyle, flexDirection: 'column', alignItems: 'flex-start', gap: '6px' }}>
<span style={labelStyle}>Repeat on</span>
<div style={{ display: 'flex', gap: '4px', paddingLeft: '0' }}>
{['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((day, i) => (
{(() => {
const allDays = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
const dayIndices = Array.from({ length: 7 }, (_, i) => (i + weekStartDay) % 7);
return dayIndices.map(dow => (
<button
key={i}
key={dow}
onClick={() => {
setCustomDays(prev =>
prev.includes(i) ? (prev.length > 1 ? prev.filter(d => d !== i) : prev) : [...prev, i]
prev.includes(dow) ? (prev.length > 1 ? prev.filter(d => d !== dow) : prev) : [...prev, dow]
);
}}
style={{
width: '30px', height: '30px', borderRadius: '50%',
border: customDays.includes(i) ? '2px solid #3b82f6' : '1px solid var(--weekly-border, #ddd)',
backgroundColor: customDays.includes(i) ? '#3b82f6' : 'transparent',
color: customDays.includes(i) ? 'white' : 'var(--weekly-text)',
border: customDays.includes(dow) ? '2px solid #3b82f6' : '1px solid var(--weekly-border, #ddd)',
backgroundColor: customDays.includes(dow) ? '#3b82f6' : 'transparent',
color: customDays.includes(dow) ? 'white' : 'var(--weekly-text)',
fontSize: '0.75rem', fontWeight: 600, cursor: 'pointer',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}
>
{day}
{allDays[dow]}
</button>
))}
));
})()}
</div>
</div>
)}
@ -848,7 +857,6 @@ export default function CalendarEventModal({
Cancel
</button>
)}
{!showRecurringEditOptions ? (
<button
className="weekly-btn-primary"
onClick={() => handleSubmit()}
@ -862,41 +870,74 @@ export default function CalendarEventModal({
>
{isSaving ? 'Saving...' : 'Save'}
</button>
) : (
<div style={{ display: 'flex', gap: '4px', alignItems: 'center' }}>
<span style={{ fontSize: '0.75rem', color: 'var(--weekly-text-light)', marginRight: '2px' }}>Save:</span>
{[
{ mode: 'this', label: 'This event' },
{ mode: 'all', label: 'All events' },
].map(({ mode, label }) => (
<button
key={mode}
onClick={() => handleSubmit(mode)}
disabled={isSaving}
style={{
padding: '4px 10px', fontSize: '0.78rem', fontWeight: 600,
color: 'white', background: mode === 'this' ? '#3b82f6' : '#6366f1',
border: 'none', borderRadius: '5px',
cursor: 'pointer', opacity: isSaving ? 0.5 : 1,
}}
>
{isSaving ? '...' : label}
</button>
</div>
</div>
</div>
{/* Recurring edit mode overlay */}
{showRecurringEditOptions && (
<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={() => setShowRecurringEditOptions(false)}>
<div style={{
background: 'var(--weekly-bg, white)', color: 'var(--weekly-text, #333)',
borderRadius: 12, width: '90%', maxWidth: 420,
boxShadow: '0 12px 40px rgba(0,0,0,0.25)',
overflow: 'hidden',
}} onClick={e => e.stopPropagation()}>
<div style={{
padding: '16px 20px 12px', fontWeight: 700, fontSize: '0.95rem',
borderBottom: '2px solid #3b82f6',
}}>
{language === 'de' ? 'Wiederkehrendes Ereignis bearbeiten' : 'Edit recurring event'}
<button onClick={() => setShowRecurringEditOptions(false)} style={{
float: 'right', background: 'none', border: 'none', cursor: 'pointer',
color: 'var(--weekly-text-light)', padding: 0,
}}><X size={18} /></button>
</div>
<div style={{ padding: '12px 20px' }}>
{([
{ value: 'this' as const, title: language === 'de' ? 'Nur dieses Ereignis' : 'This event', desc: language === 'de' ? 'Alle anderen Ereignisse der Serie bleiben unverändert.' : 'All other events in the series stay the same.' },
{ value: 'future' as const, title: language === 'de' ? 'Dieses und folgende Ereignisse' : 'This and following events', desc: language === 'de' ? 'Dieses und alle zukünftigen Ereignisse der Serie werden geändert.' : 'This and all future events in the series will be changed.' },
{ value: 'all' as const, title: language === 'de' ? 'Alle Ereignisse' : 'All events', desc: language === 'de' ? 'Alle Ereignisse der Serie werden geändert.' : 'All events in the series will be changed.' },
]).map(opt => (
<label key={opt.value} style={{
display: 'flex', gap: 12, padding: '10px 4px', cursor: 'pointer',
borderBottom: '1px solid var(--weekly-border, #eee)',
alignItems: 'flex-start',
}} onClick={() => setRecurringEditMode(opt.value)}>
<input
type="radio" name="recurringEditMode"
checked={recurringEditMode === opt.value}
onChange={() => setRecurringEditMode(opt.value)}
style={{ marginTop: 3, accentColor: '#3b82f6' }}
/>
<div>
<div style={{ fontWeight: 600, fontSize: '0.88rem' }}>{opt.title}</div>
<div style={{ fontSize: '0.78rem', opacity: 0.6, marginTop: 2 }}>{opt.desc}</div>
</div>
</label>
))}
<button
onClick={() => setShowRecurringEditOptions(false)}
style={{
padding: '4px 6px', fontSize: '0.75rem', color: 'var(--weekly-text-light)',
</div>
<div style={{
padding: '12px 20px', display: 'flex', justifyContent: 'flex-end', gap: 8,
}}>
<button onClick={() => setShowRecurringEditOptions(false)} style={{
padding: '8px 16px', fontSize: '0.85rem', fontWeight: 600,
background: 'none', border: 'none', cursor: 'pointer',
}}
>
<X size={14} />
</button>
color: 'var(--weekly-text-light)',
}}>{language === 'de' ? 'Abbrechen' : 'Cancel'}</button>
<button onClick={() => handleSubmit(recurringEditMode)} disabled={isSaving} style={{
padding: '8px 20px', fontSize: '0.85rem', fontWeight: 600,
background: '#3b82f6', color: 'white', border: 'none',
borderRadius: 8, cursor: 'pointer',
opacity: isSaving ? 0.6 : 1,
}}>{isSaving ? (language === 'de' ? 'Speichern...' : 'Saving...') : (language === 'de' ? 'Speichern' : 'Save')}</button>
</div>
</div>
</div>
)}
</div>
</div>
</div>
</div>
);
}

View File

@ -2247,6 +2247,7 @@ export default function WeeklyView() {
originalStartTime: string;
originalEndTime: string;
} | null>(null);
const [recurringDragEditMode, setRecurringDragEditMode] = useState<'this' | 'future' | 'all'>('this');
// Dark Mode Persistence & Class Toggle
const [mounted, setMounted] = useState(false);
@ -3820,6 +3821,7 @@ export default function WeeklyView() {
// Check if this is a recurring event — if so, ask before saving
const draggedEvent = calendarEvents.find(e => e.id === eventDragState.eventId);
if (draggedEvent?.isRecurring) {
setRecurringDragEditMode('this');
setPendingRecurringDrag({
eventId: eventDragState.eventId,
calendarId: eventDragState.calendarId || '',
@ -3870,7 +3872,7 @@ export default function WeeklyView() {
}, [eventDragState, effectiveCellDuration, calendarEvents]);
// Handle recurring event drag confirm (this/all)
const handleRecurringDragConfirm = async (editMode: 'this' | 'all') => {
const handleRecurringDragConfirm = async (editMode: 'this' | 'future' | 'all') => {
if (!pendingRecurringDrag) return;
const { eventId, calendarId, recurringEventId, startTime, endTime, originalStartTime, originalEndTime } = pendingRecurringDrag;
try {
@ -9153,6 +9155,8 @@ export default function WeeklyView() {
initialDate={calendarEventModal.initialDate}
initialStartTime={calendarEventModal.initialStartTime}
connections={connections}
weekStartDay={weekStartDay}
language={language}
onClose={() =>
setCalendarEventModal({ ...calendarEventModal, isOpen: false })
}
@ -9166,57 +9170,58 @@ export default function WeeklyView() {
<div style={{
position: 'fixed', inset: 0, zIndex: 2000,
display: 'flex', justifyContent: 'center', alignItems: 'center',
background: 'rgba(0,0,0,0.3)', backdropFilter: 'blur(2px)',
}}
onClick={handleRecurringDragCancel}
>
<div
style={{
background: 'rgba(0,0,0,0.35)', backdropFilter: 'blur(2px)',
}} onClick={handleRecurringDragCancel}>
<div style={{
background: darkMode ? '#1e1e1e' : 'white',
color: darkMode ? '#eee' : '#333',
borderRadius: 12, padding: '20px 24px',
boxShadow: '0 8px 30px rgba(0,0,0,0.25)',
minWidth: 260, textAlign: 'center',
}}
onClick={e => e.stopPropagation()}
>
<div style={{ fontSize: '0.9rem', fontWeight: 600, marginBottom: 4 }}>
borderRadius: 12, width: '90%', maxWidth: 420,
boxShadow: '0 12px 40px rgba(0,0,0,0.25)',
overflow: 'hidden',
}} onClick={e => e.stopPropagation()}>
<div style={{
padding: '16px 20px 12px', fontWeight: 700, fontSize: '0.95rem',
borderBottom: '2px solid #3b82f6',
}}>
{language === 'de' ? 'Wiederkehrendes Ereignis bearbeiten' : 'Edit recurring event'}
</div>
<div style={{ fontSize: '0.78rem', opacity: 0.7, marginBottom: 16 }}>
{language === 'de' ? 'Änderung anwenden auf:' : 'Apply change to:'}
<div style={{ padding: '12px 20px' }}>
{([
{ value: 'this' as const, title: language === 'de' ? 'Nur dieses Ereignis' : 'This event', desc: language === 'de' ? 'Alle anderen Ereignisse der Serie bleiben unverändert.' : 'All other events in the series stay the same.' },
{ value: 'future' as const, title: language === 'de' ? 'Dieses und folgende Ereignisse' : 'This and following events', desc: language === 'de' ? 'Dieses und alle zukünftigen Ereignisse der Serie werden geändert.' : 'This and all future events in the series will be changed.' },
{ value: 'all' as const, title: language === 'de' ? 'Alle Ereignisse' : 'All events', desc: language === 'de' ? 'Alle Ereignisse der Serie werden geändert.' : 'All events in the series will be changed.' },
]).map(opt => (
<label key={opt.value} style={{
display: 'flex', gap: 12, padding: '10px 4px', cursor: 'pointer',
borderBottom: `1px solid ${darkMode ? '#333' : '#eee'}`,
alignItems: 'flex-start',
}} onClick={() => setRecurringDragEditMode(opt.value)}>
<input
type="radio" name="recurringDragEditMode"
checked={recurringDragEditMode === opt.value}
onChange={() => setRecurringDragEditMode(opt.value)}
style={{ marginTop: 3, accentColor: '#3b82f6' }}
/>
<div>
<div style={{ fontWeight: 600, fontSize: '0.88rem' }}>{opt.title}</div>
<div style={{ fontSize: '0.78rem', opacity: 0.6, marginTop: 2 }}>{opt.desc}</div>
</div>
<div style={{ display: 'flex', gap: 8, justifyContent: 'center' }}>
<button
onClick={() => handleRecurringDragConfirm('this')}
style={{
padding: '8px 16px', fontSize: '0.82rem', fontWeight: 600,
color: 'white', background: '#3b82f6',
border: 'none', borderRadius: 8, cursor: 'pointer',
}}
>
{language === 'de' ? 'Nur dieses' : 'This event'}
</button>
<button
onClick={() => handleRecurringDragConfirm('all')}
style={{
padding: '8px 16px', fontSize: '0.82rem', fontWeight: 600,
color: 'white', background: '#6366f1',
border: 'none', borderRadius: 8, cursor: 'pointer',
}}
>
{language === 'de' ? 'Alle Ereignisse' : 'All events'}
</button>
<button
onClick={handleRecurringDragCancel}
style={{
padding: '8px 16px', fontSize: '0.82rem', fontWeight: 500,
color: darkMode ? '#aaa' : '#666', background: darkMode ? '#333' : '#f0f0f0',
border: 'none', borderRadius: 8, cursor: 'pointer',
}}
>
{language === 'de' ? 'Abbrechen' : 'Cancel'}
</button>
</label>
))}
</div>
<div style={{
padding: '12px 20px', display: 'flex', justifyContent: 'flex-end', gap: 8,
}}>
<button onClick={handleRecurringDragCancel} style={{
padding: '8px 16px', fontSize: '0.85rem', fontWeight: 600,
background: 'none', border: 'none', cursor: 'pointer',
color: darkMode ? '#aaa' : '#666',
}}>{language === 'de' ? 'Abbrechen' : 'Cancel'}</button>
<button onClick={() => handleRecurringDragConfirm(recurringDragEditMode)} style={{
padding: '8px 20px', fontSize: '0.85rem', fontWeight: 600,
background: '#3b82f6', color: 'white', border: 'none',
borderRadius: 8, cursor: 'pointer',
}}>{language === 'de' ? 'Speichern' : 'Save'}</button>
</div>
</div>
</div>