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:
parent
fbeb4db990
commit
cfa4fb023c
@ -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": {
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user