diff --git a/package.json b/package.json index a5da876..5ddd343 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "my-weekly-todo-list", - "version": "1.73.3", + "version": "1.74.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": { diff --git a/src/components/CalendarEventModal.tsx b/src/components/CalendarEventModal.tsx index ac959d4..ac1d0f8 100644 --- a/src/components/CalendarEventModal.tsx +++ b/src/components/CalendarEventModal.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, lazy, Suspense, useRef } from 'react'; -import { ChevronDown, ChevronUp, Plus, X, Bell, Users, Paperclip } from 'lucide-react'; +import { ChevronDown, ChevronUp, Plus, X, Bell, Users, Paperclip, MapPin, Calendar, Clock, Repeat, Link2, Eye, Activity } from 'lucide-react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faGoogle, faApple, faMicrosoft } from '@fortawesome/free-brands-svg-icons'; import { faServer } from '@fortawesome/free-solid-svg-icons'; @@ -83,14 +83,12 @@ export default function CalendarEventModal({ event?.recurrenceEndDate || '' ); const [recurrenceCount, setRecurrenceCount] = useState(event?.recurrenceCount || 10); - // Custom recurrence fields const [customInterval, setCustomInterval] = useState(event?.recurrenceInterval || 1); const [customUnit, setCustomUnit] = useState<'days' | 'weeks' | 'months' | 'years'>(event?.recurrenceUnit || 'weeks'); const startDow = (() => { const d = event?.start?.dateTime ? new Date(event.start.dateTime) : new Date(); return d.getDay(); })(); const [customDays, setCustomDays] = useState(event?.recurrenceDays || [startDow]); const [calendarId, setCalendarId] = useState(event?.calendarId || (availableCalendars.length > 0 ? availableCalendars[0].id : '')); - // New fields const [reminders, setReminders] = useState>( event?.reminders || [{ method: 'display', minutes: 15 }] ); @@ -201,19 +199,18 @@ export default function CalendarEventModal({ const handleSubmit = async (editMode?: string) => { if (!title.trim()) { - setError('Title is required'); + setError(language === 'de' ? 'Titel erforderlich' : 'Title is required'); return; } if (!calendarId) { - setError('Please select a calendar'); + setError(language === 'de' ? 'Bitte Kalender auswählen' : 'Please select a calendar'); return; } if (endDate <= startDate) { - setError('End time must be after start time'); + setError(language === 'de' ? 'Ende muss nach Start liegen' : 'End time must be after start time'); return; } - // For existing recurring events, show options first if (event?.id && event?.isRecurring && !editMode && !showRecurringEditOptions) { setShowRecurringEditOptions(true); return; @@ -240,13 +237,11 @@ export default function CalendarEventModal({ const handleDelete = async (mode?: string) => { if (!event?.id || !onDelete) return; - // For recurring events, show options first if (event.isRecurring && !mode && !showRecurringDeleteOptions) { setShowRecurringDeleteOptions(true); return; } - // For non-recurring events, use confirm flow if (!event.isRecurring && !isDeleteConfirming) { setIsDeleteConfirming(true); setTimeout(() => setIsDeleteConfirming(false), 3000); @@ -317,91 +312,95 @@ export default function CalendarEventModal({ setReminders([...reminders, { method: 'display', minutes: 15 }]); }; - const rowStyle: React.CSSProperties = { - display: 'flex', alignItems: 'center', justifyContent: 'space-between', - padding: '0 10px', minHeight: '32px', + // Compact icon-row style used throughout + const iconRow: React.CSSProperties = { + display: 'flex', alignItems: 'center', gap: '10px', + padding: '6px 0', minHeight: '28px', }; - const labelStyle: React.CSSProperties = { - fontSize: '0.82rem', color: 'var(--weekly-text-light)', fontWeight: 500, + const iconCol: React.CSSProperties = { + width: '20px', display: 'flex', alignItems: 'center', justifyContent: 'center', + color: 'var(--weekly-text-light)', flexShrink: 0, opacity: 0.6, }; - const selectStyle: React.CSSProperties = { - background: 'transparent', border: 'none', textAlign: 'right' as const, - fontSize: '0.82rem', cursor: 'pointer', outline: 'none', color: 'var(--weekly-text)', + const fieldCol: React.CSSProperties = { + flex: 1, minWidth: 0, }; + const inlineSelect: React.CSSProperties = { + background: 'transparent', border: 'none', + fontSize: '0.8rem', cursor: 'pointer', outline: 'none', + color: 'var(--weekly-text)', padding: '2px 0', + }; + const chipBtn = (active: boolean): React.CSSProperties => ({ + padding: '3px 10px', borderRadius: '14px', fontSize: '0.72rem', fontWeight: 500, + border: active ? '1.5px solid #3b82f6' : '1px solid var(--weekly-border, #ddd)', + background: active ? 'rgba(59,130,246,0.1)' : 'transparent', + color: active ? '#3b82f6' : 'var(--weekly-text)', + cursor: 'pointer', transition: 'all 0.15s', + }); + + const calColor = selectedCal?.backgroundColor || selectedCal?.color || '#3b82f6'; return (
e.stopPropagation()} style={{ - maxWidth: '400px', + maxWidth: '380px', width: 'calc(100vw - 16px)', - padding: '12px', - borderRadius: '10px', - boxShadow: '0 10px 25px rgba(0,0,0,0.15)', - border: '1px solid var(--weekly-border)', - maxHeight: '85vh', + padding: '0', + borderRadius: '14px', + boxShadow: '0 12px 40px rgba(0,0,0,0.18)', + border: 'none', + maxHeight: '90vh', overflowY: 'auto', + overflow: 'hidden', }}> - {error &&
{error}
} + {/* Color accent bar at top */} +
+ +
+ {error &&
{error}
} -
{/* Title */} setTitle(e.target.value)} - placeholder="New Event" + placeholder={language === 'de' ? 'Titel hinzufügen' : 'Add title'} autoFocus style={{ - width: '100%', padding: '6px 10px', fontSize: '1.05rem', fontWeight: 600, - border: 'none', borderBottom: '1px solid var(--weekly-border)', - borderRadius: '0', background: 'transparent', outline: 'none', + width: '100%', padding: '4px 0', fontSize: '1.1rem', fontWeight: 600, + border: 'none', borderBottom: `2px solid ${calColor}`, + background: 'transparent', outline: 'none', + color: 'var(--weekly-text)', }} /> - {/* Location */} - setLocation(e.target.value)} - placeholder="Location or Video Call" - style={{ - width: '100%', padding: '5px 10px', border: 'none', - borderBottom: '1px solid var(--weekly-border)', - borderRadius: '0', background: 'transparent', outline: 'none', fontSize: '0.82rem', - }} - /> - - {/* Calendar row */} -
- Calendar -
+ {/* Calendar selector */} +
+
+
+
+
{isCalendarSelectorOpen && (
@@ -411,10 +410,10 @@ export default function CalendarEventModal({ onClick={() => { setCalendarId(cal.id); setIsCalendarSelectorOpen(false); }} style={{ display: 'flex', alignItems: 'center', gap: '8px', - padding: '6px 10px', borderRadius: '5px', cursor: 'pointer', - fontSize: '0.82rem', color: 'var(--weekly-text)', + padding: '6px 8px', borderRadius: '5px', cursor: 'pointer', + fontSize: '0.8rem', color: 'var(--weekly-text)', backgroundColor: calendarId === cal.id ? 'var(--weekly-selection, rgba(59, 130, 246, 0.1))' : 'transparent', - transition: 'background 0.2s', textAlign: 'left' + transition: 'background 0.2s', }} onMouseEnter={(e) => e.currentTarget.style.backgroundColor = 'var(--weekly-hover, rgba(0,0,0,0.05))'} onMouseLeave={(e) => e.currentTarget.style.backgroundColor = calendarId === cal.id ? 'var(--weekly-selection, rgba(59, 130, 246, 0.1))' : 'transparent'} @@ -423,7 +422,7 @@ export default function CalendarEventModal({ {cal.summary || cal.title} - + {getProviderIcon(cal.provider)}
@@ -432,227 +431,249 @@ export default function CalendarEventModal({ )}
+
- {/* All Day */} -
- All Day - setAllDay(e.target.checked)} - style={{ width: '16px', height: '16px', cursor: 'pointer' }} - /> -
+ {/* Divider */} +
- {/* Start / End on same row when not all-day */} -
-
- Starts - handleStartDateChange(e.target.value)} - style={{ - padding: '2px 6px', border: 'none', borderRadius: '4px', - background: 'var(--weekly-bg-secondary, #f3f4f6)', - fontSize: '0.8rem', textAlign: 'center', - }} - /> -
-
- Ends - setEndDate(new Date(e.target.value))} - style={{ - padding: '2px 6px', border: 'none', borderRadius: '4px', - background: 'var(--weekly-bg-secondary, #f3f4f6)', - fontSize: '0.8rem', textAlign: 'center', - }} - /> + {/* Main fields section */} +
+ + {/* All Day toggle */} +
+
+
+ {language === 'de' ? 'Ganztägig' : 'All day'} +
- {/* Divider */} -
+ {/* Start & End times - compact inline */} +
+
+
+
+ {language === 'de' ? 'Von' : 'From'} + handleStartDateChange(e.target.value)} + style={{ + flex: 1, padding: '4px 8px', border: 'none', borderRadius: '6px', + background: 'var(--weekly-bg-secondary, #f3f4f6)', + fontSize: '0.8rem', outline: 'none', color: 'var(--weekly-text)', + minWidth: 0, + }} + /> +
+
+ {language === 'de' ? 'Bis' : 'To'} + setEndDate(new Date(e.target.value))} + style={{ + flex: 1, padding: '4px 8px', border: 'none', borderRadius: '6px', + background: 'var(--weekly-bg-secondary, #f3f4f6)', + fontSize: '0.8rem', outline: 'none', color: 'var(--weekly-text)', + minWidth: 0, + }} + /> +
+
+
{/* Repeat */} -
- Repeat - +
+
+
+ +
{/* Custom recurrence options */} {recurrence === 'custom' && ( - <> -
- Every -
- setCustomInterval(Math.max(1, parseInt(e.target.value) || 1))} - style={{ - ...selectStyle, width: '45px', textAlign: 'center', - background: 'var(--weekly-bg-secondary, #f5f5f5)', borderRadius: '6px', padding: '4px 6px', - }} - /> - -
-
- {customUnit === 'weeks' && ( -
- Repeat on -
- {(() => { - const allDays = ['S', 'M', 'T', 'W', 'T', 'F', 'S']; - const dayIndices = Array.from({ length: 7 }, (_, i) => (i + weekStartDay) % 7); - return dayIndices.map(dow => ( - - )); - })()} -
-
- )} - - )} - - {/* Recurrence End - only shown when repeat is set */} - {recurrence !== 'none' && ( - <> -
- End - setCustomInterval(Math.max(1, parseInt(e.target.value) || 1))} + style={{ + width: '40px', textAlign: 'center', fontSize: '0.8rem', + background: 'var(--weekly-bg-secondary, #f5f5f5)', borderRadius: '6px', + padding: '3px 4px', border: 'none', outline: 'none', color: 'var(--weekly-text)', + }} + /> +
- {recurrenceEndType === 'date' && ( -
- - setRecurrenceEndDateValue(e.target.value)} - style={{ - ...selectStyle, - background: 'var(--weekly-bg-secondary, #f5f5f5)', - borderRadius: '6px', - padding: '4px 8px', - }} - /> + {customUnit === 'weeks' && ( +
+ {(() => { + const allDays = language === 'de' ? ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'] : ['S', 'M', 'T', 'W', 'T', 'F', 'S']; + const dayIndices = Array.from({ length: 7 }, (_, i) => (i + weekStartDay) % 7); + return dayIndices.map(dow => ( + + )); + })()}
)} - {recurrenceEndType === 'count' && ( -
- -
- setRecurrenceCount(Math.max(1, parseInt(e.target.value) || 1))} - style={{ - ...selectStyle, - width: '50px', - background: 'var(--weekly-bg-secondary, #f5f5f5)', - borderRadius: '6px', - padding: '4px 8px', - textAlign: 'center', - }} - /> - times -
-
- )} - +
)} + {/* Recurrence End */} + {recurrence !== 'none' && ( +
+ {language === 'de' ? 'Ende' : 'End'} + + {recurrenceEndType === 'date' && ( + setRecurrenceEndDateValue(e.target.value)} + style={{ + fontSize: '0.8rem', border: 'none', borderRadius: '6px', + background: 'var(--weekly-bg-secondary, #f5f5f5)', + padding: '3px 6px', outline: 'none', color: 'var(--weekly-text)', + }} + /> + )} + {recurrenceEndType === 'count' && ( +
+ setRecurrenceCount(Math.max(1, parseInt(e.target.value) || 1))} + style={{ + width: '44px', textAlign: 'center', fontSize: '0.8rem', + background: 'var(--weekly-bg-secondary, #f5f5f5)', borderRadius: '6px', + padding: '3px 4px', border: 'none', outline: 'none', color: 'var(--weekly-text)', + }} + /> + + {language === 'de' ? 'mal' : 'times'} + +
+ )} +
+ )} + + {/* Location */} +
+
+ setLocation(e.target.value)} + placeholder={language === 'de' ? 'Ort hinzufügen' : 'Add location'} + style={{ + ...fieldCol, padding: '2px 0', border: 'none', + background: 'transparent', outline: 'none', fontSize: '0.8rem', + color: 'var(--weekly-text)', + }} + /> +
+ {/* Alert */} -
-
- - Alert - - {reminders.length === 0 && ( - + ) : ( + reminders.map((reminder, idx) => ( +
+ + +
+ )) + )} + {reminders.length > 0 && reminders.length < 2 && ( + )}
- {reminders.map((reminder, idx) => ( -
- - -
- ))} - {reminders.length > 0 && reminders.length < 2 && ( - - )}
- {/* Status & Visibility side by side */} -
-
- Status - setBusyStatus(e.target.value)} style={{ ...inlineSelect, fontSize: '0.75rem' }}> {BUSY_STATUS_OPTIONS.map(opt => ( ))} -
-
-
- Visibility - setVisibility(e.target.value)} style={{ ...inlineSelect, fontSize: '0.75rem' }}> {VISIBILITY_OPTIONS.map(opt => ( ))} @@ -662,45 +683,49 @@ export default function CalendarEventModal({ {/* URL (Conditional) */} {supportsURL && ( - setUrl(e.target.value)} - placeholder="URL" - style={{ - width: '100%', padding: '4px 10px', border: 'none', - borderBottom: '1px solid var(--weekly-border)', - background: 'transparent', fontSize: '0.82rem', outline: 'none' - }} - /> +
+
+ setUrl(e.target.value)} + placeholder="URL" + style={{ + ...fieldCol, padding: '2px 0', border: 'none', + background: 'transparent', fontSize: '0.8rem', outline: 'none', + color: 'var(--weekly-text)', + }} + /> +
)} - {/* Expandable: Invitees & Attachments */} - + {/* Invitees & Attachments toggle */} +
+
+ +
{showMoreOptions && ( -
+
{/* Attendees */}
- - Invitees - {attendees.map((att, idx) => (
{att.displayName ? `${att.displayName} (${att.email})` : att.email} @@ -716,12 +741,13 @@ export default function CalendarEventModal({ type="email" value={newAttendeeEmail} onChange={e => setNewAttendeeEmail(e.target.value)} - placeholder="Add email address" + placeholder={language === 'de' ? 'E-Mail-Adresse' : 'Email address'} onKeyDown={e => e.key === 'Enter' && addAttendee()} style={{ - flex: 1, padding: '4px 6px', border: 'none', + flex: 1, padding: '3px 6px', border: 'none', borderBottom: '1px solid var(--weekly-border)', - background: 'transparent', fontSize: '0.78rem', outline: 'none', + background: 'transparent', fontSize: '0.75rem', outline: 'none', + color: 'var(--weekly-text)', }} />
- {/* Attachments (Apple/Synology only) */} + {/* Attachments */} {supportsAttachments && (
- - Attachments + + {language === 'de' ? 'Anhänge' : 'Attachments'} {attachments.map((att, idx) => (
{att.title || att.url} @@ -760,12 +786,13 @@ export default function CalendarEventModal({ type="url" value={newAttachmentUrl} onChange={e => setNewAttachmentUrl(e.target.value)} - placeholder="Add attachment URL" + placeholder="URL" onKeyDown={e => e.key === 'Enter' && addAttachment()} style={{ - flex: 1, padding: '4px 6px', border: 'none', + flex: 1, padding: '3px 6px', border: 'none', borderBottom: '1px solid var(--weekly-border)', - background: 'transparent', fontSize: '0.78rem', outline: 'none', + background: 'transparent', fontSize: '0.75rem', outline: 'none', + color: 'var(--weekly-text)', }} />
- {/* Actions */} -
-
+ {/* Divider */} +
+ + {/* Notes */} +
+ setDescription(e.target.value)} + placeholder={language === 'de' ? 'Notizen' : 'Notes'} + rows={2} + style={{ width: '100%', padding: '4px 0', border: 'none', background: 'transparent', resize: 'none', fontSize: '0.8rem', outline: 'none', color: 'var(--weekly-text)' }} + /> + }> + + +
+ + {/* Actions bar */} +
+
{event && onDelete && !showRecurringDeleteOptions && ( )} {event && onDelete && showRecurringDeleteOptions && ( -
- Delete: +
{[ - { mode: 'this', label: 'This event' }, - { mode: 'past', label: 'This & past' }, - { mode: 'future', label: 'This & future' }, - { mode: 'all', label: 'All events' }, + { mode: 'this', label: language === 'de' ? 'Dieses' : 'This' }, + { mode: 'future', label: language === 'de' ? 'Künftige' : 'Future' }, + { mode: 'all', label: language === 'de' ? 'Alle' : 'All' }, ].map(({ mode, label }) => (
)} {!event && ( - )}
-
+
{event && ( - )}
@@ -890,28 +921,28 @@ export default function CalendarEventModal({ }} onClick={() => setShowRecurringEditOptions(false)}>
e.stopPropagation()}>
{language === 'de' ? 'Wiederkehrendes Ereignis bearbeiten' : 'Edit recurring event'} + }}>
-
+
{([ - { 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: 'this' as const, title: language === 'de' ? 'Nur dieses Ereignis' : 'This event', desc: language === 'de' ? 'Alle anderen bleiben unverändert.' : '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 geändert.' : 'This and all future events 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 => ( ))}
+ boxShadow: '0 1px 3px rgba(0,0,0,0.1)', + }}>{isSaving ? '...' : (language === 'de' ? 'Speichern' : 'Save')}