feat: redesign calendar event modal for mobile

Compact, icon-driven layout inspired by Google Calendar:
- Icon-row layout with left-aligned icons for each field
- Color accent bar and save button match selected calendar color
- Toggle switch for all-day instead of checkbox
- Compact From/To time inputs
- Tighter spacing throughout, smaller font sizes
- German translations for all labels
- Footer bar with subtle background
- Shorter delete buttons for recurring events (This/Future/All)

v1.74.0

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
mARTin 2026-03-25 11:27:06 +01:00
parent fbeb4db990
commit cfa4fb023c
2 changed files with 390 additions and 357 deletions

View File

@ -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": {

View File

@ -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<number[]>(event?.recurrenceDays || [startDow]);
const [calendarId, setCalendarId] = useState(event?.calendarId || (availableCalendars.length > 0 ? availableCalendars[0].id : ''));
// New fields
const [reminders, setReminders] = useState<Array<{ method: string; minutes: number }>>(
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 (
<div className="weekly-modal-overlay" onClick={onClose}>
<div className="weekly-modal-content" onClick={e => 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 && <div style={{ color: '#ef4444', marginBottom: '6px', fontSize: '0.8rem', textAlign: 'center' }}>{error}</div>}
{/* Color accent bar at top */}
<div style={{ height: '4px', background: calColor }} />
<div style={{ padding: '12px 16px 8px', display: 'flex', flexDirection: 'column', gap: '2px' }}>
{error && <div style={{ color: '#ef4444', fontSize: '0.75rem', textAlign: 'center', padding: '4px', background: 'rgba(239,68,68,0.08)', borderRadius: '6px', marginBottom: '4px' }}>{error}</div>}
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
{/* Title */}
<input
type="text"
value={title}
onChange={e => 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 */}
<input
type="text"
value={location}
onChange={e => 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 */}
<div style={{ ...rowStyle, position: 'relative' }}>
<span style={labelStyle}>Calendar</span>
<div ref={calendarSelectorRef} style={{ position: 'relative', flex: 1, display: 'flex', justifyContent: 'flex-end' }}>
{/* Calendar selector */}
<div style={{ ...iconRow, padding: '4px 0' }}>
<div style={iconCol}>
<div style={{ width: '10px', height: '10px', borderRadius: '50%', background: calColor }} />
</div>
<div ref={calendarSelectorRef} style={{ ...fieldCol, position: 'relative' }}>
<button
type="button"
onClick={() => !event && setIsCalendarSelectorOpen(!isCalendarSelectorOpen)}
disabled={!!event}
style={{
display: 'flex', alignItems: 'center', gap: '6px',
display: 'flex', alignItems: 'center', gap: '4px',
background: 'transparent', border: 'none', fontWeight: 500,
cursor: event ? 'default' : 'pointer', outline: 'none',
color: 'var(--weekly-text)', fontSize: '0.82rem',
padding: '2px 0', maxWidth: '180px', justifyContent: 'flex-end'
color: 'var(--weekly-text)', fontSize: '0.8rem', padding: '2px 0',
}}
>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{selectedCal?.summary || selectedCal?.title || 'Select Calendar'}
{selectedCal?.summary || selectedCal?.title || (language === 'de' ? 'Kalender wählen' : 'Select Calendar')}
</span>
<div style={{
width: '10px', height: '10px', borderRadius: '50%', flexShrink: 0,
backgroundColor: selectedCal?.backgroundColor || selectedCal?.color || '#3b82f6'
}} />
{!event && (isCalendarSelectorOpen ? <ChevronUp size={12} /> : <ChevronDown size={12} />)}
{!event && <ChevronDown size={12} style={{ opacity: 0.5 }} />}
</button>
{isCalendarSelectorOpen && (
<div style={{
position: 'absolute', top: '100%', right: 0, zIndex: 100,
position: 'absolute', top: '100%', left: 0, zIndex: 100,
minWidth: '200px', backgroundColor: 'var(--weekly-bg-popover, #ffffff)',
borderRadius: '8px', boxShadow: '0 4px 15px rgba(0,0,0,0.1)',
borderRadius: '8px', boxShadow: '0 4px 15px rgba(0,0,0,0.12)',
border: '1px solid var(--weekly-border)', marginTop: '2px',
padding: '4px', maxHeight: '200px', overflowY: 'auto'
}}>
@ -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({
<span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{cal.summary || cal.title}
</span>
<span style={{ color: 'var(--weekly-text-light)', opacity: 0.6 }}>
<span style={{ color: 'var(--weekly-text-light)', opacity: 0.5, fontSize: '0.75rem' }}>
{getProviderIcon(cal.provider)}
</span>
</div>
@ -432,92 +431,116 @@ export default function CalendarEventModal({
)}
</div>
</div>
</div>
{/* All Day */}
<div style={rowStyle}>
<span style={labelStyle}>All Day</span>
{/* Divider */}
<div style={{ height: '1px', background: 'var(--weekly-border, #eee)', margin: '0 16px' }} />
{/* Main fields section */}
<div style={{ padding: '4px 16px', display: 'flex', flexDirection: 'column', gap: '0' }}>
{/* All Day toggle */}
<div style={iconRow}>
<div style={iconCol}><Clock size={14} /></div>
<div style={{ ...fieldCol, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<span style={{ fontSize: '0.8rem', color: 'var(--weekly-text)' }}>{language === 'de' ? 'Ganztägig' : 'All day'}</span>
<label style={{ position: 'relative', width: '36px', height: '20px', cursor: 'pointer' }}>
<input
type="checkbox"
checked={allDay}
onChange={e => setAllDay(e.target.checked)}
style={{ width: '16px', height: '16px', cursor: 'pointer' }}
style={{ opacity: 0, width: 0, height: 0 }}
/>
<span style={{
position: 'absolute', inset: 0, borderRadius: '10px',
background: allDay ? '#3b82f6' : 'var(--weekly-border, #ccc)',
transition: 'background 0.2s',
}} />
<span style={{
position: 'absolute', top: '2px', left: allDay ? '18px' : '2px',
width: '16px', height: '16px', borderRadius: '50%',
background: 'white', boxShadow: '0 1px 3px rgba(0,0,0,0.2)',
transition: 'left 0.2s',
}} />
</label>
</div>
</div>
{/* Start / End on same row when not all-day */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '2px', padding: '0 10px' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', minHeight: '30px' }}>
<span style={labelStyle}>Starts</span>
{/* Start & End times - compact inline */}
<div style={{ ...iconRow, padding: '2px 0' }}>
<div style={iconCol} />
<div style={{ ...fieldCol, display: 'flex', flexDirection: 'column', gap: '2px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{ fontSize: '0.75rem', color: 'var(--weekly-text-light)', width: '32px', flexShrink: 0 }}>{language === 'de' ? 'Von' : 'From'}</span>
<input
type={allDay ? "date" : "datetime-local"}
value={allDay ? startDate.toISOString().split('T')[0] : toLocalISOString(startDate)}
onChange={e => handleStartDateChange(e.target.value)}
style={{
padding: '2px 6px', border: 'none', borderRadius: '4px',
flex: 1, padding: '4px 8px', border: 'none', borderRadius: '6px',
background: 'var(--weekly-bg-secondary, #f3f4f6)',
fontSize: '0.8rem', textAlign: 'center',
fontSize: '0.8rem', outline: 'none', color: 'var(--weekly-text)',
minWidth: 0,
}}
/>
</div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', minHeight: '30px' }}>
<span style={labelStyle}>Ends</span>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{ fontSize: '0.75rem', color: 'var(--weekly-text-light)', width: '32px', flexShrink: 0 }}>{language === 'de' ? 'Bis' : 'To'}</span>
<input
type={allDay ? "date" : "datetime-local"}
value={allDay ? endDate.toISOString().split('T')[0] : toLocalISOString(endDate)}
onChange={e => setEndDate(new Date(e.target.value))}
style={{
padding: '2px 6px', border: 'none', borderRadius: '4px',
flex: 1, padding: '4px 8px', border: 'none', borderRadius: '6px',
background: 'var(--weekly-bg-secondary, #f3f4f6)',
fontSize: '0.8rem', textAlign: 'center',
fontSize: '0.8rem', outline: 'none', color: 'var(--weekly-text)',
minWidth: 0,
}}
/>
</div>
</div>
{/* Divider */}
<div style={{ borderTop: '1px solid var(--weekly-border)', margin: '2px 10px' }} />
</div>
{/* Repeat */}
<div style={rowStyle}>
<span style={labelStyle}>Repeat</span>
<select value={recurrence} onChange={e => setRecurrence(e.target.value)} style={selectStyle}>
<option value="none">Never</option>
<option value="daily">Every Day</option>
<option value="weekly">Every Week</option>
<option value="monthly">Every Month</option>
<option value="yearly">Every Year</option>
<option value="custom">Custom...</option>
<div style={iconRow}>
<div style={iconCol}><Repeat size={14} /></div>
<div style={{ ...fieldCol, display: 'flex', alignItems: 'center' }}>
<select value={recurrence} onChange={e => setRecurrence(e.target.value)} style={{ ...inlineSelect, flex: 1 }}>
<option value="none">{language === 'de' ? 'Nie' : 'Never'}</option>
<option value="daily">{language === 'de' ? 'Täglich' : 'Every Day'}</option>
<option value="weekly">{language === 'de' ? 'Wöchentlich' : 'Every Week'}</option>
<option value="monthly">{language === 'de' ? 'Monatlich' : 'Every Month'}</option>
<option value="yearly">{language === 'de' ? 'Jährlich' : 'Every Year'}</option>
<option value="custom">{language === 'de' ? 'Benutzerdefiniert...' : 'Custom...'}</option>
</select>
</div>
</div>
{/* Custom recurrence options */}
{recurrence === 'custom' && (
<>
<div style={{ ...rowStyle, gap: '6px' }}>
<span style={labelStyle}>Every</span>
<div style={{ paddingLeft: '30px', display: 'flex', flexDirection: 'column', gap: '4px', paddingBottom: '4px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<span style={{ fontSize: '0.75rem', color: 'var(--weekly-text-light)' }}>{language === 'de' ? 'Alle' : 'Every'}</span>
<input
type="number" min={1} max={99} value={customInterval}
onChange={e => 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',
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)',
}}
/>
<select value={customUnit} onChange={e => setCustomUnit(e.target.value as any)} style={selectStyle}>
<option value="days">{customInterval === 1 ? 'Day' : 'Days'}</option>
<option value="weeks">{customInterval === 1 ? 'Week' : 'Weeks'}</option>
<option value="months">{customInterval === 1 ? 'Month' : 'Months'}</option>
<option value="years">{customInterval === 1 ? 'Year' : 'Years'}</option>
<select value={customUnit} onChange={e => setCustomUnit(e.target.value as any)} style={inlineSelect}>
<option value="days">{customInterval === 1 ? (language === 'de' ? 'Tag' : 'Day') : (language === 'de' ? 'Tage' : 'Days')}</option>
<option value="weeks">{customInterval === 1 ? (language === 'de' ? 'Woche' : 'Week') : (language === 'de' ? 'Wochen' : 'Weeks')}</option>
<option value="months">{customInterval === 1 ? (language === 'de' ? 'Monat' : 'Month') : (language === 'de' ? 'Monate' : 'Months')}</option>
<option value="years">{customInterval === 1 ? (language === 'de' ? 'Jahr' : 'Year') : (language === 'de' ? 'Jahre' : 'Years')}</option>
</select>
</div>
</div>
{customUnit === 'weeks' && (
<div style={{ ...rowStyle, flexDirection: 'column', alignItems: 'flex-start', gap: '6px' }}>
<span style={labelStyle}>Repeat on</span>
<div style={{ display: 'flex', gap: '4px', paddingLeft: '0' }}>
<div style={{ display: 'flex', gap: '3px', flexWrap: 'wrap' }}>
{(() => {
const allDays = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
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 => (
<button
@ -528,11 +551,11 @@ export default function CalendarEventModal({
);
}}
style={{
width: '30px', height: '30px', borderRadius: '50%',
width: '28px', height: '28px', 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',
fontSize: '0.7rem', fontWeight: 600, cursor: 'pointer',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}
>
@ -541,118 +564,116 @@ export default function CalendarEventModal({
));
})()}
</div>
)}
</div>
)}
</>
)}
{/* Recurrence End - only shown when repeat is set */}
{/* Recurrence End */}
{recurrence !== 'none' && (
<>
<div style={rowStyle}>
<span style={labelStyle}>End</span>
<div style={{ paddingLeft: '30px', display: 'flex', alignItems: 'center', gap: '6px', paddingBottom: '4px' }}>
<span style={{ fontSize: '0.75rem', color: 'var(--weekly-text-light)' }}>{language === 'de' ? 'Ende' : 'End'}</span>
<select
value={recurrenceEndType}
onChange={e => setRecurrenceEndType(e.target.value as 'never' | 'date' | 'count')}
style={selectStyle}
style={inlineSelect}
>
<option value="never">Never</option>
<option value="date">On Date</option>
<option value="count">After...</option>
<option value="never">{language === 'de' ? 'Nie' : 'Never'}</option>
<option value="date">{language === 'de' ? 'Am Datum' : 'On Date'}</option>
<option value="count">{language === 'de' ? 'Nach...' : 'After...'}</option>
</select>
</div>
{recurrenceEndType === 'date' && (
<div style={rowStyle}>
<span style={labelStyle}></span>
<input
type="date"
value={recurrenceEndDateValue}
onChange={e => setRecurrenceEndDateValue(e.target.value)}
style={{
...selectStyle,
fontSize: '0.8rem', border: 'none', borderRadius: '6px',
background: 'var(--weekly-bg-secondary, #f5f5f5)',
borderRadius: '6px',
padding: '4px 8px',
padding: '3px 6px', outline: 'none', color: 'var(--weekly-text)',
}}
/>
</div>
)}
{recurrenceEndType === 'count' && (
<div style={rowStyle}>
<span style={labelStyle}></span>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
<input
type="number"
min={1}
max={999}
value={recurrenceCount}
type="number" min={1} max={999} value={recurrenceCount}
onChange={e => 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',
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)',
}}
/>
<span style={{ fontSize: '0.82rem', color: 'var(--weekly-text)' }}>times</span>
</div>
<span style={{ fontSize: '0.75rem', color: 'var(--weekly-text-light)' }}>
{language === 'de' ? 'mal' : 'times'}
</span>
</div>
)}
</>
</div>
)}
{/* Alert */}
<div style={{ padding: '0 10px' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', minHeight: '32px' }}>
<span style={{ ...labelStyle, display: 'flex', alignItems: 'center', gap: '4px' }}>
<Bell size={12} /> Alert
</span>
{reminders.length === 0 && (
<button onClick={addReminder} style={{ ...selectStyle, color: '#3b82f6', cursor: 'pointer', background: 'none', border: 'none' }}>
Add
</button>
)}
{/* Location */}
<div style={iconRow}>
<div style={iconCol}><MapPin size={14} /></div>
<input
type="text"
value={location}
onChange={e => 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)',
}}
/>
</div>
{reminders.map((reminder, idx) => (
<div key={idx} style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '4px', marginBottom: '2px' }}>
{/* Alert */}
<div style={iconRow}>
<div style={iconCol}><Bell size={14} /></div>
<div style={{ ...fieldCol, display: 'flex', flexDirection: 'column', gap: '2px' }}>
{reminders.length === 0 ? (
<button onClick={addReminder} style={{ ...inlineSelect, color: 'var(--weekly-text-light)', fontSize: '0.8rem', textAlign: 'left', padding: '2px 0' }}>
{language === 'de' ? 'Erinnerung hinzufügen' : 'Add reminder'}
</button>
) : (
reminders.map((reminder, idx) => (
<div key={idx} style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
<select
value={reminder.minutes}
onChange={e => updateReminder(idx, parseInt(e.target.value))}
style={selectStyle}
style={{ ...inlineSelect, flex: 1 }}
>
{REMINDER_OPTIONS.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
<button onClick={() => setReminders(reminders.filter((_, i) => i !== idx))}
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: '1px', color: 'var(--weekly-text-light)' }}>
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: '2px', color: 'var(--weekly-text-light)', opacity: 0.5 }}>
<X size={12} />
</button>
</div>
))}
))
)}
{reminders.length > 0 && reminders.length < 2 && (
<button onClick={addReminder} style={{ fontSize: '0.78rem', color: '#3b82f6', background: 'none', border: 'none', cursor: 'pointer', padding: '1px 0' }}>
+ Add another alert
<button onClick={addReminder} style={{ fontSize: '0.72rem', color: '#3b82f6', background: 'none', border: 'none', cursor: 'pointer', padding: '0', textAlign: 'left' }}>
+ {language === 'de' ? 'Weitere Erinnerung' : 'Add another'}
</button>
)}
</div>
</div>
{/* Status & Visibility side by side */}
<div style={{ display: 'flex', gap: '8px', padding: '0 10px' }}>
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'space-between', minHeight: '32px' }}>
<span style={labelStyle}>Status</span>
<select value={busyStatus} onChange={e => setBusyStatus(e.target.value)} style={selectStyle}>
{/* Status & Visibility chips */}
<div style={iconRow}>
<div style={iconCol}><Activity size={14} /></div>
<div style={{ ...fieldCol, display: 'flex', alignItems: 'center', gap: '6px', flexWrap: 'wrap' }}>
<select value={busyStatus} onChange={e => setBusyStatus(e.target.value)} style={{ ...inlineSelect, fontSize: '0.75rem' }}>
{BUSY_STATUS_OPTIONS.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
</div>
<div style={{ width: '1px', background: 'var(--weekly-border)', margin: '4px 0' }} />
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'space-between', minHeight: '32px' }}>
<span style={labelStyle}>Visibility</span>
<select value={visibility} onChange={e => setVisibility(e.target.value)} style={selectStyle}>
<span style={{ color: 'var(--weekly-border)', fontSize: '0.7rem' }}>|</span>
<select value={visibility} onChange={e => setVisibility(e.target.value)} style={{ ...inlineSelect, fontSize: '0.75rem' }}>
{VISIBILITY_OPTIONS.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
@ -662,45 +683,49 @@ export default function CalendarEventModal({
{/* URL (Conditional) */}
{supportsURL && (
<div style={iconRow}>
<div style={iconCol}><Link2 size={14} /></div>
<input
type="url"
value={url}
onChange={e => 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'
...fieldCol, padding: '2px 0', border: 'none',
background: 'transparent', fontSize: '0.8rem', outline: 'none',
color: 'var(--weekly-text)',
}}
/>
</div>
)}
{/* Expandable: Invitees & Attachments */}
{/* Invitees & Attachments toggle */}
<div style={iconRow}>
<div style={iconCol}><Users size={14} /></div>
<button
onClick={() => setShowMoreOptions(!showMoreOptions)}
style={{
display: 'flex', alignItems: 'center', gap: '4px',
background: 'none', border: 'none', cursor: 'pointer',
color: '#3b82f6', fontSize: '0.8rem', padding: '2px 10px',
color: 'var(--weekly-text-light)', fontSize: '0.8rem', padding: '0',
}}
>
{language === 'de' ? 'Teilnehmer & Anhänge' : 'Invitees & Attachments'}
{showMoreOptions ? <ChevronUp size={12} /> : <ChevronDown size={12} />}
Invitees & Attachments
{attendees.length > 0 && <span style={{ fontSize: '0.7rem', background: 'rgba(59,130,246,0.1)', color: '#3b82f6', padding: '1px 5px', borderRadius: '8px' }}>{attendees.length}</span>}
</button>
</div>
{showMoreOptions && (
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', padding: '0 10px' }}>
<div style={{ paddingLeft: '30px', display: 'flex', flexDirection: 'column', gap: '6px', paddingBottom: '4px' }}>
{/* Attendees */}
<div>
<span style={{ ...labelStyle, display: 'flex', alignItems: 'center', gap: '4px', marginBottom: '4px' }}>
<Users size={12} /> Invitees
</span>
{attendees.map((att, idx) => (
<div key={idx} style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
padding: '3px 6px', marginBottom: '2px',
background: 'var(--weekly-bg-secondary, #f3f4f6)', borderRadius: '4px',
fontSize: '0.78rem',
fontSize: '0.75rem',
}}>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{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)',
}}
/>
<button onClick={addAttendee} style={{
@ -733,18 +759,18 @@ export default function CalendarEventModal({
</div>
</div>
{/* Attachments (Apple/Synology only) */}
{/* Attachments */}
{supportsAttachments && (
<div>
<span style={{ ...labelStyle, display: 'flex', alignItems: 'center', gap: '4px', marginBottom: '4px' }}>
<Paperclip size={12} /> Attachments
<span style={{ fontSize: '0.72rem', color: 'var(--weekly-text-light)', display: 'flex', alignItems: 'center', gap: '4px', marginBottom: '3px' }}>
<Paperclip size={11} /> {language === 'de' ? 'Anhänge' : 'Attachments'}
</span>
{attachments.map((att, idx) => (
<div key={idx} style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
padding: '3px 6px', marginBottom: '2px',
background: 'var(--weekly-bg-secondary, #f3f4f6)', borderRadius: '4px',
fontSize: '0.78rem',
fontSize: '0.75rem',
}}>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{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)',
}}
/>
<button onClick={addAttachment} style={{
@ -779,60 +806,66 @@ export default function CalendarEventModal({
)}
</div>
)}
</div>
{/* Divider */}
<div style={{ height: '1px', background: 'var(--weekly-border, #eee)', margin: '0 16px' }} />
{/* Notes */}
<div style={{ padding: '0 10px' }}>
<div style={{ padding: '6px 16px 8px' }}>
<Suspense fallback={
<textarea
value={description}
onChange={e => setDescription(e.target.value)}
placeholder="Notes"
placeholder={language === 'de' ? 'Notizen' : 'Notes'}
rows={2}
style={{ width: '100%', padding: '4px 0', border: 'none', borderBottom: '1px solid var(--weekly-border)', background: 'transparent', resize: 'none', fontSize: '0.82rem', outline: 'none' }}
style={{ width: '100%', padding: '4px 0', border: 'none', background: 'transparent', resize: 'none', fontSize: '0.8rem', outline: 'none', color: 'var(--weekly-text)' }}
/>
}>
<RichTextEditor
value={description}
onChange={setDescription}
placeholder="Notes"
minHeight="60px"
placeholder={language === 'de' ? 'Notizen' : 'Notes'}
minHeight="50px"
/>
</Suspense>
</div>
</div>
{/* Actions */}
<div style={{ marginTop: '10px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderTop: '1px solid var(--weekly-border)', paddingTop: '10px' }}>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center', flexWrap: 'wrap' }}>
{/* Actions bar */}
<div style={{
padding: '8px 16px', display: 'flex', justifyContent: 'space-between', alignItems: 'center',
borderTop: '1px solid var(--weekly-border)',
background: 'var(--weekly-bg-secondary, #fafafa)',
borderRadius: '0 0 14px 14px',
}}>
<div style={{ display: 'flex', gap: '4px', alignItems: 'center', flexWrap: 'wrap', flex: 1, minWidth: 0 }}>
{event && onDelete && !showRecurringDeleteOptions && (
<button
onClick={() => handleDelete()}
disabled={isSaving || isDeleting}
style={{
padding: '6px 10px', background: 'none', color: '#ef4444',
border: 'none', borderRadius: '6px', fontSize: '0.82rem',
padding: '5px 10px', background: 'none', color: '#ef4444',
border: 'none', borderRadius: '6px', fontSize: '0.8rem',
fontWeight: 500, cursor: 'pointer',
opacity: isSaving || isDeleting ? 0.5 : 1
}}
>
{isDeleting ? 'Deleting...' : isDeleteConfirming ? 'Confirm Delete' : 'Delete'}
{isDeleting ? '...' : isDeleteConfirming ? (language === 'de' ? 'Bestätigen?' : 'Confirm?') : (language === 'de' ? 'Löschen' : 'Delete')}
</button>
)}
{event && onDelete && showRecurringDeleteOptions && (
<div style={{ display: 'flex', gap: '4px', flexWrap: 'wrap', alignItems: 'center' }}>
<span style={{ fontSize: '0.75rem', color: 'var(--weekly-text-light)', marginRight: '2px' }}>Delete:</span>
<div style={{ display: 'flex', gap: '3px', flexWrap: 'wrap', alignItems: 'center' }}>
{[
{ 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 }) => (
<button
key={mode}
onClick={() => handleDelete(mode)}
disabled={isDeleting}
style={{
padding: '4px 8px', fontSize: '0.75rem', fontWeight: 500,
padding: '3px 7px', fontSize: '0.7rem', fontWeight: 500,
color: '#ef4444', background: 'rgba(239,68,68,0.08)',
border: '1px solid rgba(239,68,68,0.2)', borderRadius: '5px',
cursor: 'pointer', opacity: isDeleting ? 0.5 : 1,
@ -843,26 +876,23 @@ export default function CalendarEventModal({
))}
<button
onClick={() => setShowRecurringDeleteOptions(false)}
style={{
padding: '4px 6px', fontSize: '0.75rem', color: 'var(--weekly-text-light)',
background: 'none', border: 'none', cursor: 'pointer',
}}
style={{ padding: '2px', color: 'var(--weekly-text-light)', background: 'none', border: 'none', cursor: 'pointer' }}
>
<X size={14} />
<X size={12} />
</button>
</div>
)}
{!event && (
<button onClick={onClose} style={{ padding: '6px 10px', fontSize: '0.82rem', color: 'var(--weekly-text-light)', border: 'none', background: 'none', cursor: 'pointer' }}>
Cancel
<button onClick={onClose} style={{ padding: '5px 10px', fontSize: '0.8rem', color: 'var(--weekly-text-light)', border: 'none', background: 'none', cursor: 'pointer' }}>
{language === 'de' ? 'Abbrechen' : 'Cancel'}
</button>
)}
</div>
<div style={{ display: 'flex', gap: '6px' }}>
<div style={{ display: 'flex', gap: '6px', flexShrink: 0 }}>
{event && (
<button onClick={onClose} style={{ padding: '6px 10px', fontSize: '0.82rem', color: 'var(--weekly-text-light)', border: 'none', background: 'none', cursor: 'pointer' }}>
Cancel
<button onClick={onClose} style={{ padding: '5px 10px', fontSize: '0.8rem', color: 'var(--weekly-text-light)', border: 'none', background: 'none', cursor: 'pointer' }}>
{language === 'de' ? 'Abbrechen' : 'Cancel'}
</button>
)}
<button
@ -870,13 +900,14 @@ export default function CalendarEventModal({
onClick={() => handleSubmit()}
disabled={isSaving || isDeleting}
style={{
padding: '6px 16px', borderRadius: '6px', fontSize: '0.85rem',
fontWeight: 600, backgroundColor: '#3b82f6', color: 'white',
padding: '6px 18px', borderRadius: '8px', fontSize: '0.82rem',
fontWeight: 600, backgroundColor: calColor, color: 'white',
border: 'none', cursor: 'pointer',
opacity: isSaving || isDeleting ? 0.7 : 1
opacity: isSaving || isDeleting ? 0.7 : 1,
boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
}}
>
{isSaving ? 'Saving...' : 'Save'}
{isSaving ? '...' : (language === 'de' ? 'Speichern' : 'Save')}
</button>
</div>
</div>
@ -890,28 +921,28 @@ export default function CalendarEventModal({
}} onClick={() => setShowRecurringEditOptions(false)}>
<div style={{
background: 'var(--weekly-bg, white)', color: 'var(--weekly-text, #333)',
borderRadius: 12, width: '90%', maxWidth: 420,
borderRadius: 12, width: '90%', maxWidth: 380,
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',
padding: '14px 16px 10px', fontWeight: 700, fontSize: '0.9rem',
borderBottom: `2px solid ${calColor}`,
}}>
{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>
}}><X size={16} /></button>
</div>
<div style={{ padding: '12px 20px' }}>
<div style={{ padding: '8px 16px' }}>
{([
{ 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 => (
<label key={opt.value} style={{
display: 'flex', gap: 12, padding: '10px 4px', cursor: 'pointer',
display: 'flex', gap: 10, padding: '8px 2px', cursor: 'pointer',
borderBottom: '1px solid var(--weekly-border, #eee)',
alignItems: 'flex-start',
}} onClick={() => setRecurringEditMode(opt.value)}>
@ -919,29 +950,31 @@ export default function CalendarEventModal({
type="radio" name="recurringEditMode"
checked={recurringEditMode === opt.value}
onChange={() => setRecurringEditMode(opt.value)}
style={{ marginTop: 3, accentColor: '#3b82f6' }}
style={{ marginTop: 3, accentColor: calColor }}
/>
<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 style={{ fontWeight: 600, fontSize: '0.82rem' }}>{opt.title}</div>
<div style={{ fontSize: '0.72rem', opacity: 0.5, marginTop: 1 }}>{opt.desc}</div>
</div>
</label>
))}
</div>
<div style={{
padding: '12px 20px', display: 'flex', justifyContent: 'flex-end', gap: 8,
padding: '10px 16px', display: 'flex', justifyContent: 'flex-end', gap: 8,
background: 'var(--weekly-bg-secondary, #fafafa)',
}}>
<button onClick={() => setShowRecurringEditOptions(false)} style={{
padding: '8px 16px', fontSize: '0.85rem', fontWeight: 600,
padding: '6px 14px', fontSize: '0.82rem', 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',
padding: '6px 18px', fontSize: '0.82rem', fontWeight: 600,
background: calColor, color: 'white', border: 'none',
borderRadius: 8, cursor: 'pointer',
opacity: isSaving ? 0.6 : 1,
}}>{isSaving ? (language === 'de' ? 'Speichern...' : 'Saving...') : (language === 'de' ? 'Speichern' : 'Save')}</button>
boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
}}>{isSaving ? '...' : (language === 'de' ? 'Speichern' : 'Save')}</button>
</div>
</div>
</div>