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", "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", "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

@ -1,5 +1,5 @@
import React, { useState, useEffect, lazy, Suspense, useRef } from 'react'; 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 { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faGoogle, faApple, faMicrosoft } from '@fortawesome/free-brands-svg-icons'; import { faGoogle, faApple, faMicrosoft } from '@fortawesome/free-brands-svg-icons';
import { faServer } from '@fortawesome/free-solid-svg-icons'; import { faServer } from '@fortawesome/free-solid-svg-icons';
@ -83,14 +83,12 @@ export default function CalendarEventModal({
event?.recurrenceEndDate || '' event?.recurrenceEndDate || ''
); );
const [recurrenceCount, setRecurrenceCount] = useState(event?.recurrenceCount || 10); const [recurrenceCount, setRecurrenceCount] = useState(event?.recurrenceCount || 10);
// Custom recurrence fields
const [customInterval, setCustomInterval] = useState(event?.recurrenceInterval || 1); const [customInterval, setCustomInterval] = useState(event?.recurrenceInterval || 1);
const [customUnit, setCustomUnit] = useState<'days' | 'weeks' | 'months' | 'years'>(event?.recurrenceUnit || 'weeks'); 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 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 [customDays, setCustomDays] = useState<number[]>(event?.recurrenceDays || [startDow]);
const [calendarId, setCalendarId] = useState(event?.calendarId || (availableCalendars.length > 0 ? availableCalendars[0].id : '')); const [calendarId, setCalendarId] = useState(event?.calendarId || (availableCalendars.length > 0 ? availableCalendars[0].id : ''));
// New fields
const [reminders, setReminders] = useState<Array<{ method: string; minutes: number }>>( const [reminders, setReminders] = useState<Array<{ method: string; minutes: number }>>(
event?.reminders || [{ method: 'display', minutes: 15 }] event?.reminders || [{ method: 'display', minutes: 15 }]
); );
@ -201,19 +199,18 @@ export default function CalendarEventModal({
const handleSubmit = async (editMode?: string) => { const handleSubmit = async (editMode?: string) => {
if (!title.trim()) { if (!title.trim()) {
setError('Title is required'); setError(language === 'de' ? 'Titel erforderlich' : 'Title is required');
return; return;
} }
if (!calendarId) { if (!calendarId) {
setError('Please select a calendar'); setError(language === 'de' ? 'Bitte Kalender auswählen' : 'Please select a calendar');
return; return;
} }
if (endDate <= startDate) { 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; return;
} }
// For existing recurring events, show options first
if (event?.id && event?.isRecurring && !editMode && !showRecurringEditOptions) { if (event?.id && event?.isRecurring && !editMode && !showRecurringEditOptions) {
setShowRecurringEditOptions(true); setShowRecurringEditOptions(true);
return; return;
@ -240,13 +237,11 @@ export default function CalendarEventModal({
const handleDelete = async (mode?: string) => { const handleDelete = async (mode?: string) => {
if (!event?.id || !onDelete) return; if (!event?.id || !onDelete) return;
// For recurring events, show options first
if (event.isRecurring && !mode && !showRecurringDeleteOptions) { if (event.isRecurring && !mode && !showRecurringDeleteOptions) {
setShowRecurringDeleteOptions(true); setShowRecurringDeleteOptions(true);
return; return;
} }
// For non-recurring events, use confirm flow
if (!event.isRecurring && !isDeleteConfirming) { if (!event.isRecurring && !isDeleteConfirming) {
setIsDeleteConfirming(true); setIsDeleteConfirming(true);
setTimeout(() => setIsDeleteConfirming(false), 3000); setTimeout(() => setIsDeleteConfirming(false), 3000);
@ -317,91 +312,95 @@ export default function CalendarEventModal({
setReminders([...reminders, { method: 'display', minutes: 15 }]); setReminders([...reminders, { method: 'display', minutes: 15 }]);
}; };
const rowStyle: React.CSSProperties = { // Compact icon-row style used throughout
display: 'flex', alignItems: 'center', justifyContent: 'space-between', const iconRow: React.CSSProperties = {
padding: '0 10px', minHeight: '32px', display: 'flex', alignItems: 'center', gap: '10px',
padding: '6px 0', minHeight: '28px',
}; };
const labelStyle: React.CSSProperties = { const iconCol: React.CSSProperties = {
fontSize: '0.82rem', color: 'var(--weekly-text-light)', fontWeight: 500, width: '20px', display: 'flex', alignItems: 'center', justifyContent: 'center',
color: 'var(--weekly-text-light)', flexShrink: 0, opacity: 0.6,
}; };
const selectStyle: React.CSSProperties = { const fieldCol: React.CSSProperties = {
background: 'transparent', border: 'none', textAlign: 'right' as const, flex: 1, minWidth: 0,
fontSize: '0.82rem', cursor: 'pointer', outline: 'none', color: 'var(--weekly-text)',
}; };
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 ( return (
<div className="weekly-modal-overlay" onClick={onClose}> <div className="weekly-modal-overlay" onClick={onClose}>
<div className="weekly-modal-content" onClick={e => e.stopPropagation()} style={{ <div className="weekly-modal-content" onClick={e => e.stopPropagation()} style={{
maxWidth: '400px', maxWidth: '380px',
width: 'calc(100vw - 16px)', width: 'calc(100vw - 16px)',
padding: '12px', padding: '0',
borderRadius: '10px', borderRadius: '14px',
boxShadow: '0 10px 25px rgba(0,0,0,0.15)', boxShadow: '0 12px 40px rgba(0,0,0,0.18)',
border: '1px solid var(--weekly-border)', border: 'none',
maxHeight: '85vh', maxHeight: '90vh',
overflowY: 'auto', 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 */} {/* Title */}
<input <input
type="text" type="text"
value={title} value={title}
onChange={e => setTitle(e.target.value)} onChange={e => setTitle(e.target.value)}
placeholder="New Event" placeholder={language === 'de' ? 'Titel hinzufügen' : 'Add title'}
autoFocus autoFocus
style={{ style={{
width: '100%', padding: '6px 10px', fontSize: '1.05rem', fontWeight: 600, width: '100%', padding: '4px 0', fontSize: '1.1rem', fontWeight: 600,
border: 'none', borderBottom: '1px solid var(--weekly-border)', border: 'none', borderBottom: `2px solid ${calColor}`,
borderRadius: '0', background: 'transparent', outline: 'none', background: 'transparent', outline: 'none',
color: 'var(--weekly-text)',
}} }}
/> />
{/* Location */} {/* Calendar selector */}
<input <div style={{ ...iconRow, padding: '4px 0' }}>
type="text" <div style={iconCol}>
value={location} <div style={{ width: '10px', height: '10px', borderRadius: '50%', background: calColor }} />
onChange={e => setLocation(e.target.value)} </div>
placeholder="Location or Video Call" <div ref={calendarSelectorRef} style={{ ...fieldCol, position: 'relative' }}>
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' }}>
<button <button
type="button" type="button"
onClick={() => !event && setIsCalendarSelectorOpen(!isCalendarSelectorOpen)} onClick={() => !event && setIsCalendarSelectorOpen(!isCalendarSelectorOpen)}
disabled={!!event} disabled={!!event}
style={{ style={{
display: 'flex', alignItems: 'center', gap: '6px', display: 'flex', alignItems: 'center', gap: '4px',
background: 'transparent', border: 'none', fontWeight: 500, background: 'transparent', border: 'none', fontWeight: 500,
cursor: event ? 'default' : 'pointer', outline: 'none', cursor: event ? 'default' : 'pointer', outline: 'none',
color: 'var(--weekly-text)', fontSize: '0.82rem', color: 'var(--weekly-text)', fontSize: '0.8rem', padding: '2px 0',
padding: '2px 0', maxWidth: '180px', justifyContent: 'flex-end'
}} }}
> >
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}> <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> </span>
<div style={{ {!event && <ChevronDown size={12} style={{ opacity: 0.5 }} />}
width: '10px', height: '10px', borderRadius: '50%', flexShrink: 0,
backgroundColor: selectedCal?.backgroundColor || selectedCal?.color || '#3b82f6'
}} />
{!event && (isCalendarSelectorOpen ? <ChevronUp size={12} /> : <ChevronDown size={12} />)}
</button> </button>
{isCalendarSelectorOpen && ( {isCalendarSelectorOpen && (
<div style={{ <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)', 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', border: '1px solid var(--weekly-border)', marginTop: '2px',
padding: '4px', maxHeight: '200px', overflowY: 'auto' padding: '4px', maxHeight: '200px', overflowY: 'auto'
}}> }}>
@ -411,10 +410,10 @@ export default function CalendarEventModal({
onClick={() => { setCalendarId(cal.id); setIsCalendarSelectorOpen(false); }} onClick={() => { setCalendarId(cal.id); setIsCalendarSelectorOpen(false); }}
style={{ style={{
display: 'flex', alignItems: 'center', gap: '8px', display: 'flex', alignItems: 'center', gap: '8px',
padding: '6px 10px', borderRadius: '5px', cursor: 'pointer', padding: '6px 8px', borderRadius: '5px', cursor: 'pointer',
fontSize: '0.82rem', color: 'var(--weekly-text)', fontSize: '0.8rem', color: 'var(--weekly-text)',
backgroundColor: calendarId === cal.id ? 'var(--weekly-selection, rgba(59, 130, 246, 0.1))' : 'transparent', 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))'} 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'} 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' }}> <span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{cal.summary || cal.title} {cal.summary || cal.title}
</span> </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)} {getProviderIcon(cal.provider)}
</span> </span>
</div> </div>
@ -432,92 +431,116 @@ export default function CalendarEventModal({
)} )}
</div> </div>
</div> </div>
</div>
{/* All Day */} {/* Divider */}
<div style={rowStyle}> <div style={{ height: '1px', background: 'var(--weekly-border, #eee)', margin: '0 16px' }} />
<span style={labelStyle}>All Day</span>
{/* 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 <input
type="checkbox" type="checkbox"
checked={allDay} checked={allDay}
onChange={e => setAllDay(e.target.checked)} 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> </div>
{/* Start / End on same row when not all-day */} {/* Start & End times - compact inline */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '2px', padding: '0 10px' }}> <div style={{ ...iconRow, padding: '2px 0' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', minHeight: '30px' }}> <div style={iconCol} />
<span style={labelStyle}>Starts</span> <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 <input
type={allDay ? "date" : "datetime-local"} type={allDay ? "date" : "datetime-local"}
value={allDay ? startDate.toISOString().split('T')[0] : toLocalISOString(startDate)} value={allDay ? startDate.toISOString().split('T')[0] : toLocalISOString(startDate)}
onChange={e => handleStartDateChange(e.target.value)} onChange={e => handleStartDateChange(e.target.value)}
style={{ style={{
padding: '2px 6px', border: 'none', borderRadius: '4px', flex: 1, padding: '4px 8px', border: 'none', borderRadius: '6px',
background: 'var(--weekly-bg-secondary, #f3f4f6)', background: 'var(--weekly-bg-secondary, #f3f4f6)',
fontSize: '0.8rem', textAlign: 'center', fontSize: '0.8rem', outline: 'none', color: 'var(--weekly-text)',
minWidth: 0,
}} }}
/> />
</div> </div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', minHeight: '30px' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={labelStyle}>Ends</span> <span style={{ fontSize: '0.75rem', color: 'var(--weekly-text-light)', width: '32px', flexShrink: 0 }}>{language === 'de' ? 'Bis' : 'To'}</span>
<input <input
type={allDay ? "date" : "datetime-local"} type={allDay ? "date" : "datetime-local"}
value={allDay ? endDate.toISOString().split('T')[0] : toLocalISOString(endDate)} value={allDay ? endDate.toISOString().split('T')[0] : toLocalISOString(endDate)}
onChange={e => setEndDate(new Date(e.target.value))} onChange={e => setEndDate(new Date(e.target.value))}
style={{ style={{
padding: '2px 6px', border: 'none', borderRadius: '4px', flex: 1, padding: '4px 8px', border: 'none', borderRadius: '6px',
background: 'var(--weekly-bg-secondary, #f3f4f6)', background: 'var(--weekly-bg-secondary, #f3f4f6)',
fontSize: '0.8rem', textAlign: 'center', fontSize: '0.8rem', outline: 'none', color: 'var(--weekly-text)',
minWidth: 0,
}} }}
/> />
</div> </div>
</div> </div>
</div>
{/* Divider */}
<div style={{ borderTop: '1px solid var(--weekly-border)', margin: '2px 10px' }} />
{/* Repeat */} {/* Repeat */}
<div style={rowStyle}> <div style={iconRow}>
<span style={labelStyle}>Repeat</span> <div style={iconCol}><Repeat size={14} /></div>
<select value={recurrence} onChange={e => setRecurrence(e.target.value)} style={selectStyle}> <div style={{ ...fieldCol, display: 'flex', alignItems: 'center' }}>
<option value="none">Never</option> <select value={recurrence} onChange={e => setRecurrence(e.target.value)} style={{ ...inlineSelect, flex: 1 }}>
<option value="daily">Every Day</option> <option value="none">{language === 'de' ? 'Nie' : 'Never'}</option>
<option value="weekly">Every Week</option> <option value="daily">{language === 'de' ? 'Täglich' : 'Every Day'}</option>
<option value="monthly">Every Month</option> <option value="weekly">{language === 'de' ? 'Wöchentlich' : 'Every Week'}</option>
<option value="yearly">Every Year</option> <option value="monthly">{language === 'de' ? 'Monatlich' : 'Every Month'}</option>
<option value="custom">Custom...</option> <option value="yearly">{language === 'de' ? 'Jährlich' : 'Every Year'}</option>
<option value="custom">{language === 'de' ? 'Benutzerdefiniert...' : 'Custom...'}</option>
</select> </select>
</div> </div>
</div>
{/* Custom recurrence options */} {/* Custom recurrence options */}
{recurrence === 'custom' && ( {recurrence === 'custom' && (
<> <div style={{ paddingLeft: '30px', display: 'flex', flexDirection: 'column', gap: '4px', paddingBottom: '4px' }}>
<div style={{ ...rowStyle, gap: '6px' }}>
<span style={labelStyle}>Every</span>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<span style={{ fontSize: '0.75rem', color: 'var(--weekly-text-light)' }}>{language === 'de' ? 'Alle' : 'Every'}</span>
<input <input
type="number" min={1} max={99} value={customInterval} type="number" min={1} max={99} value={customInterval}
onChange={e => setCustomInterval(Math.max(1, parseInt(e.target.value) || 1))} onChange={e => setCustomInterval(Math.max(1, parseInt(e.target.value) || 1))}
style={{ style={{
...selectStyle, width: '45px', textAlign: 'center', width: '40px', textAlign: 'center', fontSize: '0.8rem',
background: 'var(--weekly-bg-secondary, #f5f5f5)', borderRadius: '6px', padding: '4px 6px', 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}> <select value={customUnit} onChange={e => setCustomUnit(e.target.value as any)} style={inlineSelect}>
<option value="days">{customInterval === 1 ? 'Day' : 'Days'}</option> <option value="days">{customInterval === 1 ? (language === 'de' ? 'Tag' : 'Day') : (language === 'de' ? 'Tage' : 'Days')}</option>
<option value="weeks">{customInterval === 1 ? 'Week' : 'Weeks'}</option> <option value="weeks">{customInterval === 1 ? (language === 'de' ? 'Woche' : 'Week') : (language === 'de' ? 'Wochen' : 'Weeks')}</option>
<option value="months">{customInterval === 1 ? 'Month' : 'Months'}</option> <option value="months">{customInterval === 1 ? (language === 'de' ? 'Monat' : 'Month') : (language === 'de' ? 'Monate' : 'Months')}</option>
<option value="years">{customInterval === 1 ? 'Year' : 'Years'}</option> <option value="years">{customInterval === 1 ? (language === 'de' ? 'Jahr' : 'Year') : (language === 'de' ? 'Jahre' : 'Years')}</option>
</select> </select>
</div> </div>
</div>
{customUnit === 'weeks' && ( {customUnit === 'weeks' && (
<div style={{ ...rowStyle, flexDirection: 'column', alignItems: 'flex-start', gap: '6px' }}> <div style={{ display: 'flex', gap: '3px', flexWrap: 'wrap' }}>
<span style={labelStyle}>Repeat on</span>
<div style={{ display: 'flex', gap: '4px', paddingLeft: '0' }}>
{(() => { {(() => {
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); const dayIndices = Array.from({ length: 7 }, (_, i) => (i + weekStartDay) % 7);
return dayIndices.map(dow => ( return dayIndices.map(dow => (
<button <button
@ -528,11 +551,11 @@ export default function CalendarEventModal({
); );
}} }}
style={{ 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)', border: customDays.includes(dow) ? '2px solid #3b82f6' : '1px solid var(--weekly-border, #ddd)',
backgroundColor: customDays.includes(dow) ? '#3b82f6' : 'transparent', backgroundColor: customDays.includes(dow) ? '#3b82f6' : 'transparent',
color: customDays.includes(dow) ? 'white' : 'var(--weekly-text)', 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', display: 'flex', alignItems: 'center', justifyContent: 'center',
}} }}
> >
@ -541,118 +564,116 @@ export default function CalendarEventModal({
)); ));
})()} })()}
</div> </div>
)}
</div> </div>
)} )}
</>
)}
{/* Recurrence End - only shown when repeat is set */} {/* Recurrence End */}
{recurrence !== 'none' && ( {recurrence !== 'none' && (
<> <div style={{ paddingLeft: '30px', display: 'flex', alignItems: 'center', gap: '6px', paddingBottom: '4px' }}>
<div style={rowStyle}> <span style={{ fontSize: '0.75rem', color: 'var(--weekly-text-light)' }}>{language === 'de' ? 'Ende' : 'End'}</span>
<span style={labelStyle}>End</span>
<select <select
value={recurrenceEndType} value={recurrenceEndType}
onChange={e => setRecurrenceEndType(e.target.value as 'never' | 'date' | 'count')} onChange={e => setRecurrenceEndType(e.target.value as 'never' | 'date' | 'count')}
style={selectStyle} style={inlineSelect}
> >
<option value="never">Never</option> <option value="never">{language === 'de' ? 'Nie' : 'Never'}</option>
<option value="date">On Date</option> <option value="date">{language === 'de' ? 'Am Datum' : 'On Date'}</option>
<option value="count">After...</option> <option value="count">{language === 'de' ? 'Nach...' : 'After...'}</option>
</select> </select>
</div>
{recurrenceEndType === 'date' && ( {recurrenceEndType === 'date' && (
<div style={rowStyle}>
<span style={labelStyle}></span>
<input <input
type="date" type="date"
value={recurrenceEndDateValue} value={recurrenceEndDateValue}
onChange={e => setRecurrenceEndDateValue(e.target.value)} onChange={e => setRecurrenceEndDateValue(e.target.value)}
style={{ style={{
...selectStyle, fontSize: '0.8rem', border: 'none', borderRadius: '6px',
background: 'var(--weekly-bg-secondary, #f5f5f5)', background: 'var(--weekly-bg-secondary, #f5f5f5)',
borderRadius: '6px', padding: '3px 6px', outline: 'none', color: 'var(--weekly-text)',
padding: '4px 8px',
}} }}
/> />
</div>
)} )}
{recurrenceEndType === 'count' && ( {recurrenceEndType === 'count' && (
<div style={rowStyle}> <div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
<span style={labelStyle}></span>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<input <input
type="number" type="number" min={1} max={999} value={recurrenceCount}
min={1}
max={999}
value={recurrenceCount}
onChange={e => setRecurrenceCount(Math.max(1, parseInt(e.target.value) || 1))} onChange={e => setRecurrenceCount(Math.max(1, parseInt(e.target.value) || 1))}
style={{ style={{
...selectStyle, width: '44px', textAlign: 'center', fontSize: '0.8rem',
width: '50px', background: 'var(--weekly-bg-secondary, #f5f5f5)', borderRadius: '6px',
background: 'var(--weekly-bg-secondary, #f5f5f5)', padding: '3px 4px', border: 'none', outline: 'none', color: 'var(--weekly-text)',
borderRadius: '6px',
padding: '4px 8px',
textAlign: 'center',
}} }}
/> />
<span style={{ fontSize: '0.82rem', color: 'var(--weekly-text)' }}>times</span> <span style={{ fontSize: '0.75rem', color: 'var(--weekly-text-light)' }}>
</div> {language === 'de' ? 'mal' : 'times'}
</span>
</div> </div>
)} )}
</> </div>
)} )}
{/* Alert */} {/* Location */}
<div style={{ padding: '0 10px' }}> <div style={iconRow}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', minHeight: '32px' }}> <div style={iconCol}><MapPin size={14} /></div>
<span style={{ ...labelStyle, display: 'flex', alignItems: 'center', gap: '4px' }}> <input
<Bell size={12} /> Alert type="text"
</span> value={location}
{reminders.length === 0 && ( onChange={e => setLocation(e.target.value)}
<button onClick={addReminder} style={{ ...selectStyle, color: '#3b82f6', cursor: 'pointer', background: 'none', border: 'none' }}> placeholder={language === 'de' ? 'Ort hinzufügen' : 'Add location'}
Add style={{
</button> ...fieldCol, padding: '2px 0', border: 'none',
)} background: 'transparent', outline: 'none', fontSize: '0.8rem',
color: 'var(--weekly-text)',
}}
/>
</div> </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 <select
value={reminder.minutes} value={reminder.minutes}
onChange={e => updateReminder(idx, parseInt(e.target.value))} onChange={e => updateReminder(idx, parseInt(e.target.value))}
style={selectStyle} style={{ ...inlineSelect, flex: 1 }}
> >
{REMINDER_OPTIONS.map(opt => ( {REMINDER_OPTIONS.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option> <option key={opt.value} value={opt.value}>{opt.label}</option>
))} ))}
</select> </select>
<button onClick={() => setReminders(reminders.filter((_, i) => i !== idx))} <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} /> <X size={12} />
</button> </button>
</div> </div>
))} ))
)}
{reminders.length > 0 && reminders.length < 2 && ( {reminders.length > 0 && reminders.length < 2 && (
<button onClick={addReminder} style={{ fontSize: '0.78rem', color: '#3b82f6', background: 'none', border: 'none', cursor: 'pointer', padding: '1px 0' }}> <button onClick={addReminder} style={{ fontSize: '0.72rem', color: '#3b82f6', background: 'none', border: 'none', cursor: 'pointer', padding: '0', textAlign: 'left' }}>
+ Add another alert + {language === 'de' ? 'Weitere Erinnerung' : 'Add another'}
</button> </button>
)} )}
</div> </div>
</div>
{/* Status & Visibility side by side */} {/* Status & Visibility chips */}
<div style={{ display: 'flex', gap: '8px', padding: '0 10px' }}> <div style={iconRow}>
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'space-between', minHeight: '32px' }}> <div style={iconCol}><Activity size={14} /></div>
<span style={labelStyle}>Status</span> <div style={{ ...fieldCol, display: 'flex', alignItems: 'center', gap: '6px', flexWrap: 'wrap' }}>
<select value={busyStatus} onChange={e => setBusyStatus(e.target.value)} style={selectStyle}> <select value={busyStatus} onChange={e => setBusyStatus(e.target.value)} style={{ ...inlineSelect, fontSize: '0.75rem' }}>
{BUSY_STATUS_OPTIONS.map(opt => ( {BUSY_STATUS_OPTIONS.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option> <option key={opt.value} value={opt.value}>{opt.label}</option>
))} ))}
</select> </select>
</div> <span style={{ color: 'var(--weekly-border)', fontSize: '0.7rem' }}>|</span>
<div style={{ width: '1px', background: 'var(--weekly-border)', margin: '4px 0' }} /> <select value={visibility} onChange={e => setVisibility(e.target.value)} style={{ ...inlineSelect, fontSize: '0.75rem' }}>
<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}>
{VISIBILITY_OPTIONS.map(opt => ( {VISIBILITY_OPTIONS.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option> <option key={opt.value} value={opt.value}>{opt.label}</option>
))} ))}
@ -662,45 +683,49 @@ export default function CalendarEventModal({
{/* URL (Conditional) */} {/* URL (Conditional) */}
{supportsURL && ( {supportsURL && (
<div style={iconRow}>
<div style={iconCol}><Link2 size={14} /></div>
<input <input
type="url" type="url"
value={url} value={url}
onChange={e => setUrl(e.target.value)} onChange={e => setUrl(e.target.value)}
placeholder="URL" placeholder="URL"
style={{ style={{
width: '100%', padding: '4px 10px', border: 'none', ...fieldCol, padding: '2px 0', border: 'none',
borderBottom: '1px solid var(--weekly-border)', background: 'transparent', fontSize: '0.8rem', outline: 'none',
background: 'transparent', fontSize: '0.82rem', outline: 'none' color: 'var(--weekly-text)',
}} }}
/> />
</div>
)} )}
{/* Expandable: Invitees & Attachments */} {/* Invitees & Attachments toggle */}
<div style={iconRow}>
<div style={iconCol}><Users size={14} /></div>
<button <button
onClick={() => setShowMoreOptions(!showMoreOptions)} onClick={() => setShowMoreOptions(!showMoreOptions)}
style={{ style={{
display: 'flex', alignItems: 'center', gap: '4px', display: 'flex', alignItems: 'center', gap: '4px',
background: 'none', border: 'none', cursor: 'pointer', 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} />} {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> </button>
</div>
{showMoreOptions && ( {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 */} {/* Attendees */}
<div> <div>
<span style={{ ...labelStyle, display: 'flex', alignItems: 'center', gap: '4px', marginBottom: '4px' }}>
<Users size={12} /> Invitees
</span>
{attendees.map((att, idx) => ( {attendees.map((att, idx) => (
<div key={idx} style={{ <div key={idx} style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between', display: 'flex', alignItems: 'center', justifyContent: 'space-between',
padding: '3px 6px', marginBottom: '2px', padding: '3px 6px', marginBottom: '2px',
background: 'var(--weekly-bg-secondary, #f3f4f6)', borderRadius: '4px', background: 'var(--weekly-bg-secondary, #f3f4f6)', borderRadius: '4px',
fontSize: '0.78rem', fontSize: '0.75rem',
}}> }}>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}> <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{att.displayName ? `${att.displayName} (${att.email})` : att.email} {att.displayName ? `${att.displayName} (${att.email})` : att.email}
@ -716,12 +741,13 @@ export default function CalendarEventModal({
type="email" type="email"
value={newAttendeeEmail} value={newAttendeeEmail}
onChange={e => setNewAttendeeEmail(e.target.value)} onChange={e => setNewAttendeeEmail(e.target.value)}
placeholder="Add email address" placeholder={language === 'de' ? 'E-Mail-Adresse' : 'Email address'}
onKeyDown={e => e.key === 'Enter' && addAttendee()} onKeyDown={e => e.key === 'Enter' && addAttendee()}
style={{ style={{
flex: 1, padding: '4px 6px', border: 'none', flex: 1, padding: '3px 6px', border: 'none',
borderBottom: '1px solid var(--weekly-border)', 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={{ <button onClick={addAttendee} style={{
@ -733,18 +759,18 @@ export default function CalendarEventModal({
</div> </div>
</div> </div>
{/* Attachments (Apple/Synology only) */} {/* Attachments */}
{supportsAttachments && ( {supportsAttachments && (
<div> <div>
<span style={{ ...labelStyle, display: 'flex', alignItems: 'center', gap: '4px', marginBottom: '4px' }}> <span style={{ fontSize: '0.72rem', color: 'var(--weekly-text-light)', display: 'flex', alignItems: 'center', gap: '4px', marginBottom: '3px' }}>
<Paperclip size={12} /> Attachments <Paperclip size={11} /> {language === 'de' ? 'Anhänge' : 'Attachments'}
</span> </span>
{attachments.map((att, idx) => ( {attachments.map((att, idx) => (
<div key={idx} style={{ <div key={idx} style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between', display: 'flex', alignItems: 'center', justifyContent: 'space-between',
padding: '3px 6px', marginBottom: '2px', padding: '3px 6px', marginBottom: '2px',
background: 'var(--weekly-bg-secondary, #f3f4f6)', borderRadius: '4px', background: 'var(--weekly-bg-secondary, #f3f4f6)', borderRadius: '4px',
fontSize: '0.78rem', fontSize: '0.75rem',
}}> }}>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}> <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{att.title || att.url} {att.title || att.url}
@ -760,12 +786,13 @@ export default function CalendarEventModal({
type="url" type="url"
value={newAttachmentUrl} value={newAttachmentUrl}
onChange={e => setNewAttachmentUrl(e.target.value)} onChange={e => setNewAttachmentUrl(e.target.value)}
placeholder="Add attachment URL" placeholder="URL"
onKeyDown={e => e.key === 'Enter' && addAttachment()} onKeyDown={e => e.key === 'Enter' && addAttachment()}
style={{ style={{
flex: 1, padding: '4px 6px', border: 'none', flex: 1, padding: '3px 6px', border: 'none',
borderBottom: '1px solid var(--weekly-border)', 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={{ <button onClick={addAttachment} style={{
@ -779,60 +806,66 @@ export default function CalendarEventModal({
)} )}
</div> </div>
)} )}
</div>
{/* Divider */}
<div style={{ height: '1px', background: 'var(--weekly-border, #eee)', margin: '0 16px' }} />
{/* Notes */} {/* Notes */}
<div style={{ padding: '0 10px' }}> <div style={{ padding: '6px 16px 8px' }}>
<Suspense fallback={ <Suspense fallback={
<textarea <textarea
value={description} value={description}
onChange={e => setDescription(e.target.value)} onChange={e => setDescription(e.target.value)}
placeholder="Notes" placeholder={language === 'de' ? 'Notizen' : 'Notes'}
rows={2} 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 <RichTextEditor
value={description} value={description}
onChange={setDescription} onChange={setDescription}
placeholder="Notes" placeholder={language === 'de' ? 'Notizen' : 'Notes'}
minHeight="60px" minHeight="50px"
/> />
</Suspense> </Suspense>
</div> </div>
</div>
{/* Actions */} {/* Actions bar */}
<div style={{ marginTop: '10px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderTop: '1px solid var(--weekly-border)', paddingTop: '10px' }}> <div style={{
<div style={{ display: 'flex', gap: '8px', alignItems: 'center', flexWrap: 'wrap' }}> 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 && ( {event && onDelete && !showRecurringDeleteOptions && (
<button <button
onClick={() => handleDelete()} onClick={() => handleDelete()}
disabled={isSaving || isDeleting} disabled={isSaving || isDeleting}
style={{ style={{
padding: '6px 10px', background: 'none', color: '#ef4444', padding: '5px 10px', background: 'none', color: '#ef4444',
border: 'none', borderRadius: '6px', fontSize: '0.82rem', border: 'none', borderRadius: '6px', fontSize: '0.8rem',
fontWeight: 500, cursor: 'pointer', fontWeight: 500, cursor: 'pointer',
opacity: isSaving || isDeleting ? 0.5 : 1 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> </button>
)} )}
{event && onDelete && showRecurringDeleteOptions && ( {event && onDelete && showRecurringDeleteOptions && (
<div style={{ display: 'flex', gap: '4px', flexWrap: 'wrap', alignItems: 'center' }}> <div style={{ display: 'flex', gap: '3px', flexWrap: 'wrap', alignItems: 'center' }}>
<span style={{ fontSize: '0.75rem', color: 'var(--weekly-text-light)', marginRight: '2px' }}>Delete:</span>
{[ {[
{ mode: 'this', label: 'This event' }, { mode: 'this', label: language === 'de' ? 'Dieses' : 'This' },
{ mode: 'past', label: 'This & past' }, { mode: 'future', label: language === 'de' ? 'Künftige' : 'Future' },
{ mode: 'future', label: 'This & future' }, { mode: 'all', label: language === 'de' ? 'Alle' : 'All' },
{ mode: 'all', label: 'All events' },
].map(({ mode, label }) => ( ].map(({ mode, label }) => (
<button <button
key={mode} key={mode}
onClick={() => handleDelete(mode)} onClick={() => handleDelete(mode)}
disabled={isDeleting} disabled={isDeleting}
style={{ 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)', color: '#ef4444', background: 'rgba(239,68,68,0.08)',
border: '1px solid rgba(239,68,68,0.2)', borderRadius: '5px', border: '1px solid rgba(239,68,68,0.2)', borderRadius: '5px',
cursor: 'pointer', opacity: isDeleting ? 0.5 : 1, cursor: 'pointer', opacity: isDeleting ? 0.5 : 1,
@ -843,26 +876,23 @@ export default function CalendarEventModal({
))} ))}
<button <button
onClick={() => setShowRecurringDeleteOptions(false)} onClick={() => setShowRecurringDeleteOptions(false)}
style={{ style={{ padding: '2px', color: 'var(--weekly-text-light)', background: 'none', border: 'none', cursor: 'pointer' }}
padding: '4px 6px', fontSize: '0.75rem', color: 'var(--weekly-text-light)',
background: 'none', border: 'none', cursor: 'pointer',
}}
> >
<X size={14} /> <X size={12} />
</button> </button>
</div> </div>
)} )}
{!event && ( {!event && (
<button onClick={onClose} style={{ padding: '6px 10px', fontSize: '0.82rem', 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' }}>
Cancel {language === 'de' ? 'Abbrechen' : 'Cancel'}
</button> </button>
)} )}
</div> </div>
<div style={{ display: 'flex', gap: '6px' }}> <div style={{ display: 'flex', gap: '6px', flexShrink: 0 }}>
{event && ( {event && (
<button onClick={onClose} style={{ padding: '6px 10px', fontSize: '0.82rem', 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' }}>
Cancel {language === 'de' ? 'Abbrechen' : 'Cancel'}
</button> </button>
)} )}
<button <button
@ -870,13 +900,14 @@ export default function CalendarEventModal({
onClick={() => handleSubmit()} onClick={() => handleSubmit()}
disabled={isSaving || isDeleting} disabled={isSaving || isDeleting}
style={{ style={{
padding: '6px 16px', borderRadius: '6px', fontSize: '0.85rem', padding: '6px 18px', borderRadius: '8px', fontSize: '0.82rem',
fontWeight: 600, backgroundColor: '#3b82f6', color: 'white', fontWeight: 600, backgroundColor: calColor, color: 'white',
border: 'none', cursor: 'pointer', 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> </button>
</div> </div>
</div> </div>
@ -890,28 +921,28 @@ export default function CalendarEventModal({
}} onClick={() => setShowRecurringEditOptions(false)}> }} onClick={() => setShowRecurringEditOptions(false)}>
<div style={{ <div style={{
background: 'var(--weekly-bg, white)', color: 'var(--weekly-text, #333)', 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)', boxShadow: '0 12px 40px rgba(0,0,0,0.25)',
overflow: 'hidden', overflow: 'hidden',
}} onClick={e => e.stopPropagation()}> }} onClick={e => e.stopPropagation()}>
<div style={{ <div style={{
padding: '16px 20px 12px', fontWeight: 700, fontSize: '0.95rem', padding: '14px 16px 10px', fontWeight: 700, fontSize: '0.9rem',
borderBottom: '2px solid #3b82f6', borderBottom: `2px solid ${calColor}`,
}}> }}>
{language === 'de' ? 'Wiederkehrendes Ereignis bearbeiten' : 'Edit recurring event'} {language === 'de' ? 'Wiederkehrendes Ereignis bearbeiten' : 'Edit recurring event'}
<button onClick={() => setShowRecurringEditOptions(false)} style={{ <button onClick={() => setShowRecurringEditOptions(false)} style={{
float: 'right', background: 'none', border: 'none', cursor: 'pointer', float: 'right', background: 'none', border: 'none', cursor: 'pointer',
color: 'var(--weekly-text-light)', padding: 0, color: 'var(--weekly-text-light)', padding: 0,
}}><X size={18} /></button> }}><X size={16} /></button>
</div> </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: '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 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: '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.' }, { 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 => ( ]).map(opt => (
<label key={opt.value} style={{ <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)', borderBottom: '1px solid var(--weekly-border, #eee)',
alignItems: 'flex-start', alignItems: 'flex-start',
}} onClick={() => setRecurringEditMode(opt.value)}> }} onClick={() => setRecurringEditMode(opt.value)}>
@ -919,29 +950,31 @@ export default function CalendarEventModal({
type="radio" name="recurringEditMode" type="radio" name="recurringEditMode"
checked={recurringEditMode === opt.value} checked={recurringEditMode === opt.value}
onChange={() => setRecurringEditMode(opt.value)} onChange={() => setRecurringEditMode(opt.value)}
style={{ marginTop: 3, accentColor: '#3b82f6' }} style={{ marginTop: 3, accentColor: calColor }}
/> />
<div> <div>
<div style={{ fontWeight: 600, fontSize: '0.88rem' }}>{opt.title}</div> <div style={{ fontWeight: 600, fontSize: '0.82rem' }}>{opt.title}</div>
<div style={{ fontSize: '0.78rem', opacity: 0.6, marginTop: 2 }}>{opt.desc}</div> <div style={{ fontSize: '0.72rem', opacity: 0.5, marginTop: 1 }}>{opt.desc}</div>
</div> </div>
</label> </label>
))} ))}
</div> </div>
<div style={{ <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={{ <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', background: 'none', border: 'none', cursor: 'pointer',
color: 'var(--weekly-text-light)', color: 'var(--weekly-text-light)',
}}>{language === 'de' ? 'Abbrechen' : 'Cancel'}</button> }}>{language === 'de' ? 'Abbrechen' : 'Cancel'}</button>
<button onClick={() => handleSubmit(recurringEditMode)} disabled={isSaving} style={{ <button onClick={() => handleSubmit(recurringEditMode)} disabled={isSaving} style={{
padding: '8px 20px', fontSize: '0.85rem', fontWeight: 600, padding: '6px 18px', fontSize: '0.82rem', fontWeight: 600,
background: '#3b82f6', color: 'white', border: 'none', background: calColor, color: 'white', border: 'none',
borderRadius: 8, cursor: 'pointer', borderRadius: 8, cursor: 'pointer',
opacity: isSaving ? 0.6 : 1, 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> </div>
</div> </div>