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:
parent
74a8c9a9a9
commit
948d239867
@ -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": {
|
||||
|
||||
@ -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) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => {
|
||||
setCustomDays(prev =>
|
||||
prev.includes(i) ? (prev.length > 1 ? prev.filter(d => d !== i) : prev) : [...prev, i]
|
||||
);
|
||||
}}
|
||||
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)',
|
||||
fontSize: '0.75rem', fontWeight: 600, cursor: 'pointer',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
{day}
|
||||
</button>
|
||||
))}
|
||||
{(() => {
|
||||
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={dow}
|
||||
onClick={() => {
|
||||
setCustomDays(prev =>
|
||||
prev.includes(dow) ? (prev.length > 1 ? prev.filter(d => d !== dow) : prev) : [...prev, dow]
|
||||
);
|
||||
}}
|
||||
style={{
|
||||
width: '30px', height: '30px', borderRadius: '50%',
|
||||
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',
|
||||
}}
|
||||
>
|
||||
{allDays[dow]}
|
||||
</button>
|
||||
));
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -848,55 +857,87 @@ export default function CalendarEventModal({
|
||||
Cancel
|
||||
</button>
|
||||
)}
|
||||
{!showRecurringEditOptions ? (
|
||||
<button
|
||||
className="weekly-btn-primary"
|
||||
onClick={() => handleSubmit()}
|
||||
disabled={isSaving || isDeleting}
|
||||
style={{
|
||||
padding: '6px 16px', borderRadius: '6px', fontSize: '0.85rem',
|
||||
fontWeight: 600, backgroundColor: '#3b82f6', color: 'white',
|
||||
border: 'none', cursor: 'pointer',
|
||||
opacity: isSaving || isDeleting ? 0.7 : 1
|
||||
}}
|
||||
>
|
||||
{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>
|
||||
))}
|
||||
<button
|
||||
onClick={() => setShowRecurringEditOptions(false)}
|
||||
style={{
|
||||
padding: '4px 6px', fontSize: '0.75rem', color: 'var(--weekly-text-light)',
|
||||
background: 'none', border: 'none', cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
className="weekly-btn-primary"
|
||||
onClick={() => handleSubmit()}
|
||||
disabled={isSaving || isDeleting}
|
||||
style={{
|
||||
padding: '6px 16px', borderRadius: '6px', fontSize: '0.85rem',
|
||||
fontWeight: 600, backgroundColor: '#3b82f6', color: 'white',
|
||||
border: 'none', cursor: 'pointer',
|
||||
opacity: isSaving || isDeleting ? 0.7 : 1
|
||||
}}
|
||||
>
|
||||
{isSaving ? 'Saving...' : 'Save'}
|
||||
</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>
|
||||
))}
|
||||
</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',
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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: 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 }}>
|
||||
background: 'rgba(0,0,0,0.35)', backdropFilter: 'blur(2px)',
|
||||
}} onClick={handleRecurringDragCancel}>
|
||||
<div style={{
|
||||
background: darkMode ? '#1e1e1e' : 'white',
|
||||
color: darkMode ? '#eee' : '#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'}
|
||||
</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>
|
||||
</label>
|
||||
))}
|
||||
</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>
|
||||
<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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user