fix: align time column header, calendar events, all-day events, and swap view icons
- Fix time-column-header height to dynamically match weekly-day-header - Fix all-day events column alignment (width 50px → 55px to match time column) - Fix calendar events overflowing weekday columns (remove negative left/right) - Swap Simple/Calendar view icons in header switcher - Uncomment headerCustomFormat in API select for instant toggle - Add response error checking in updateTaskFields for kanban persistence - Add font/view dependencies to header height ResizeObserver v1.33.2 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
58de23a18f
commit
ebfe911b81
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "my-weekly-todo-list",
|
||||
"version": "1.33.1",
|
||||
"version": "1.33.2",
|
||||
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@ -79,6 +79,7 @@ model User {
|
||||
dateAlignment String @default("center")
|
||||
dateVerticalAlign String? @default("middle")
|
||||
headerDisplay String? @default("kw")
|
||||
headerCustomFormat String? @default("KW WW | YYYY")
|
||||
hourLabelFormat String @default("short")
|
||||
showSubHourSlots Boolean @default(true)
|
||||
allDayPosition String @default("above")
|
||||
|
||||
@ -46,6 +46,7 @@ export async function GET(request: NextRequest) {
|
||||
mobileDateLayout: true,
|
||||
dateVerticalAlign: true,
|
||||
headerDisplay: true,
|
||||
headerCustomFormat: true,
|
||||
headlineFont: true,
|
||||
headlineFontSize: true,
|
||||
headlineFontWeight: true,
|
||||
@ -130,7 +131,7 @@ export async function PATCH(request: NextRequest) {
|
||||
weekdayColor, dateColor, taskColor, todayHighlightColor,
|
||||
pastDayColor, goalFallbackType, goalDefaultSentence,
|
||||
goalFontFamily, goalFontSize, goalFontWeight, goalScope,
|
||||
dateLayout, mobileDateLayout, dateVerticalAlign, headerDisplay,
|
||||
dateLayout, mobileDateLayout, dateVerticalAlign, headerDisplay, headerCustomFormat,
|
||||
hourLabelFormat, showSubHourSlots, allDayPosition,
|
||||
cwFontFamily, cwFontSize, cwFontWeight, cwColor,
|
||||
yearFontFamily, yearFontSize, yearFontWeight, yearColor,
|
||||
@ -196,6 +197,7 @@ export async function PATCH(request: NextRequest) {
|
||||
...(mobileDateLayout !== undefined && { mobileDateLayout }),
|
||||
...(dateVerticalAlign !== undefined && { dateVerticalAlign }),
|
||||
...(headerDisplay !== undefined && { headerDisplay }),
|
||||
...(headerCustomFormat !== undefined && { headerCustomFormat }),
|
||||
...(hourLabelFormat !== undefined && { hourLabelFormat }),
|
||||
...(showSubHourSlots !== undefined && { showSubHourSlots }),
|
||||
...(allDayPosition !== undefined && { allDayPosition }),
|
||||
@ -289,6 +291,7 @@ export async function PATCH(request: NextRequest) {
|
||||
mobileDateLayout: true,
|
||||
dateVerticalAlign: true,
|
||||
headerDisplay: true,
|
||||
headerCustomFormat: true,
|
||||
cwFontFamily: true,
|
||||
cwFontSize: true,
|
||||
cwFontWeight: true,
|
||||
|
||||
@ -2324,15 +2324,15 @@ h3 {
|
||||
|
||||
/* Side Navigation Arrows (hover overlays) */
|
||||
.side-nav {
|
||||
position: sticky;
|
||||
top: 50px; /* Below header height if needed, or 0 */
|
||||
height: 0; /* Let it be a point of origin */
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.35rem 0.15rem;
|
||||
gap: 0.1rem;
|
||||
padding: 2px;
|
||||
gap: 2px;
|
||||
z-index: 30;
|
||||
pointer-events: none; /* Let clicks pass through to grid except buttons */
|
||||
}
|
||||
@ -2352,6 +2352,7 @@ h3 {
|
||||
|
||||
.side-nav-left {
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.side-nav-right {
|
||||
@ -2372,43 +2373,50 @@ h3 {
|
||||
}
|
||||
|
||||
.side-nav-btn:hover {
|
||||
background: var(--weekly-hover-bg, rgba(0, 0, 0, 0.06));
|
||||
background: var(--weekly-bg); /* Use background over hover to hide header text */
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
color: var(--weekly-text, #333);
|
||||
}
|
||||
|
||||
/* Time Column (Hours) */
|
||||
.time-column {
|
||||
width: 50px;
|
||||
width: 55px; /* Slightly wider for labels */
|
||||
flex-shrink: 0;
|
||||
border-right: 1px solid var(--weekly-border);
|
||||
background: var(--weekly-bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* Flush to left */
|
||||
margin-left: -1px;
|
||||
}
|
||||
|
||||
.time-column-header {
|
||||
padding: 1rem 0.5rem 0.75rem;
|
||||
min-height: 50px !important;
|
||||
padding: 1rem 1rem 0.75rem; /* Match weekly-day-header padding */
|
||||
min-height: 50px; /* Match weekly-day-header min-height */
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.time-column-slots {
|
||||
flex: 1;
|
||||
overflow-y: visible; /* Unify with parent scroll */
|
||||
position: relative;
|
||||
padding-top: 0.4rem;
|
||||
padding-top: 0; /* Flush with header */
|
||||
}
|
||||
|
||||
|
||||
.time-slot-label {
|
||||
display: flex;
|
||||
align-items: flex-start; /* Aligns to top so translateY(-50%) centers on the line */
|
||||
align-items: flex-start;
|
||||
justify-content: flex-end;
|
||||
padding: 0 0.5rem;
|
||||
font-size: 0.65rem;
|
||||
color: var(--weekly-text-light);
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.time-slot-label.hour-start {
|
||||
@ -2444,7 +2452,7 @@ h3 {
|
||||
flex: 1;
|
||||
overflow: visible; /* Changed from auto to unify scroll */
|
||||
overflow-x: visible;
|
||||
padding-top: 0.4rem; /* Aligned with .time-column-slots */
|
||||
padding-top: 0; /* Flush with header */
|
||||
}
|
||||
|
||||
.time-slot {
|
||||
@ -2549,8 +2557,9 @@ h3 {
|
||||
border-radius: 0 4px 4px 0;
|
||||
margin: 1px 0;
|
||||
overflow: hidden;
|
||||
left: -10px;
|
||||
right: -15px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.time-slot-event .event-title-row {
|
||||
@ -2666,6 +2675,7 @@ h3 {
|
||||
min-height: 28px;
|
||||
border-right: 1px solid var(--weekly-border);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE/Edge */
|
||||
}
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
import React, { useState, useEffect, lazy, Suspense } from 'react';
|
||||
import React, { useState, useEffect, lazy, Suspense, useRef } from 'react';
|
||||
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faGoogle, faApple, faMicrosoft } from '@fortawesome/free-brands-svg-icons';
|
||||
import { faServer } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
const RichTextEditor = lazy(() => import('./RichTextEditor'));
|
||||
|
||||
@ -78,6 +82,31 @@ export default function CalendarEventModal({
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [isCalendarSelectorOpen, setIsCalendarSelectorOpen] = useState(false);
|
||||
const calendarSelectorRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Close dropdown on outside click
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (calendarSelectorRef.current && !calendarSelectorRef.current.contains(event.target as Node)) {
|
||||
setIsCalendarSelectorOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const selectedCal = availableCalendars.find((c: any) => c.id === calendarId);
|
||||
const supportsURL = selectedCal && (selectedCal.provider !== 'google' && selectedCal.provider !== 'outlook');
|
||||
const getProviderIcon = (provider: string) => {
|
||||
switch (provider) {
|
||||
case 'google': return <FontAwesomeIcon icon={faGoogle} style={{ opacity: 0.8 }} />;
|
||||
case 'apple': return <FontAwesomeIcon icon={faApple} style={{ opacity: 0.8 }} />;
|
||||
case 'outlook': return <FontAwesomeIcon icon={faMicrosoft} style={{ opacity: 0.8 }} />;
|
||||
case 'synology': return <FontAwesomeIcon icon={faServer} style={{ opacity: 0.8, fontSize: '0.8rem' }} />;
|
||||
default: return null;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!title.trim()) {
|
||||
@ -155,186 +184,313 @@ export default function CalendarEventModal({
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className="weekly-modal-overlay" onClick={onClose}>
|
||||
<div className="weekly-modal-content" onClick={e => e.stopPropagation()} style={{ maxWidth: '500px' }}>
|
||||
<h3 style={{ marginBottom: '1.5rem' }}>
|
||||
{event ? 'Edit Event' : 'New Event'}
|
||||
</h3>
|
||||
<div className="weekly-modal-content" onClick={e => e.stopPropagation()} style={{
|
||||
maxWidth: '450px',
|
||||
padding: '20px',
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0 10px 25px rgba(0,0,0,0.15)',
|
||||
border: '1px solid var(--weekly-border)'
|
||||
}}>
|
||||
{error && <div style={{ color: '#ef4444', marginBottom: '1rem', fontSize: '0.85rem', textAlign: 'center' }}>{error}</div>}
|
||||
|
||||
{error && <div style={{ color: 'red', marginBottom: '1rem' }}>{error}</div>}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
{/* Title Input */}
|
||||
<input
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={e => setTitle(e.target.value)}
|
||||
placeholder="New Event"
|
||||
autoFocus
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px 16px',
|
||||
fontSize: '1.25rem',
|
||||
fontWeight: 600,
|
||||
border: 'none',
|
||||
borderBottom: '1px solid var(--weekly-border)',
|
||||
borderRadius: '0',
|
||||
background: 'transparent',
|
||||
outline: 'none',
|
||||
marginBottom: '4px'
|
||||
}}
|
||||
/>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '15px' }}>
|
||||
{/* Location Input */}
|
||||
<input
|
||||
type="text"
|
||||
value={location}
|
||||
onChange={e => setLocation(e.target.value)}
|
||||
placeholder="Location or Video Call"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 16px',
|
||||
border: 'none',
|
||||
borderBottom: '1px solid var(--weekly-border)',
|
||||
borderRadius: '0',
|
||||
background: 'transparent',
|
||||
outline: 'none',
|
||||
fontSize: '0.95rem'
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Title */}
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '5px', fontSize: '0.9rem', color: '#666' }}>Title</label>
|
||||
{/* Calendar row: Label + Custom Selector */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '4px 16px', position: 'relative' }}>
|
||||
<span style={{ fontSize: '0.9rem', color: 'var(--weekly-text-light)', fontWeight: 500 }}>Calendar</span>
|
||||
|
||||
<div ref={calendarSelectorRef} style={{ position: 'relative', flex: 1, display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => !event && setIsCalendarSelectorOpen(!isCalendarSelectorOpen)}
|
||||
disabled={!!event}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
fontWeight: 500,
|
||||
cursor: event ? 'default' : 'pointer',
|
||||
outline: 'none',
|
||||
color: 'var(--weekly-text)',
|
||||
fontSize: '0.95rem',
|
||||
padding: '4px 0',
|
||||
maxWidth: '200px',
|
||||
justifyContent: 'flex-end'
|
||||
}}
|
||||
>
|
||||
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{selectedCal?.summary || selectedCal?.title || 'Select Calendar'}
|
||||
</span>
|
||||
<div style={{
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: selectedCal?.backgroundColor || selectedCal?.color || '#3b82f6'
|
||||
}} />
|
||||
{!event && (isCalendarSelectorOpen ? <ChevronUp size={14} /> : <ChevronDown size={14} />)}
|
||||
</button>
|
||||
|
||||
{isCalendarSelectorOpen && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: '100%',
|
||||
right: 0,
|
||||
zIndex: 100,
|
||||
minWidth: '220px',
|
||||
backgroundColor: 'var(--weekly-bg-popover, #ffffff)',
|
||||
borderRadius: '10px',
|
||||
boxShadow: '0 4px 15px rgba(0,0,0,0.1)',
|
||||
border: '1px solid var(--weekly-border)',
|
||||
marginTop: '5px',
|
||||
padding: '6px',
|
||||
maxHeight: '250px',
|
||||
overflowY: 'auto'
|
||||
}}>
|
||||
{availableCalendars.map((cal: any) => (
|
||||
<div
|
||||
key={cal.id}
|
||||
onClick={() => {
|
||||
setCalendarId(cal.id);
|
||||
setIsCalendarSelectorOpen(false);
|
||||
}}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '6px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.9rem',
|
||||
color: 'var(--weekly-text)',
|
||||
backgroundColor: calendarId === cal.id ? 'var(--weekly-selection, rgba(59, 130, 246, 0.1))' : 'transparent',
|
||||
transition: 'background 0.2s',
|
||||
textAlign: 'left'
|
||||
}}
|
||||
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'}
|
||||
>
|
||||
<div style={{ width: '10px', height: '10px', borderRadius: '50%', backgroundColor: cal.backgroundColor || cal.color || '#3b82f6', flexShrink: 0 }} />
|
||||
<span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{cal.summary || cal.title}
|
||||
</span>
|
||||
<span style={{ color: 'var(--weekly-text-light)', opacity: 0.6 }}>
|
||||
{getProviderIcon(cal.provider)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* All Day row */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '4px 16px' }}>
|
||||
<span style={{ fontSize: '0.9rem', color: 'var(--weekly-text-light)', fontWeight: 500 }}>All Day</span>
|
||||
<input
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={e => setTitle(e.target.value)}
|
||||
placeholder="Event Title"
|
||||
autoFocus
|
||||
style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px', fontSize: '1rem' }}
|
||||
type="checkbox"
|
||||
checked={allDay}
|
||||
onChange={e => setAllDay(e.target.checked)}
|
||||
style={{ width: '18px', height: '18px', cursor: 'pointer' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Calendar Selection */}
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '5px', fontSize: '0.9rem', color: '#666' }}>Calendar</label>
|
||||
<select
|
||||
value={calendarId}
|
||||
onChange={e => setCalendarId(e.target.value)}
|
||||
disabled={!!event} // Usually can't move events between calendars easily in basic implementation
|
||||
style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px' }}
|
||||
>
|
||||
{availableCalendars.length === 0 && <option value="">No editable calendars</option>}
|
||||
{availableCalendars.map((cal: any) => (
|
||||
<option key={cal.id} value={cal.id}>
|
||||
{cal.summary || cal.title} ({cal.providerName})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* All Day Toggle */}
|
||||
<div>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer', fontSize: '0.9rem', color: '#666' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={allDay}
|
||||
onChange={e => setAllDay(e.target.checked)}
|
||||
/>
|
||||
All Day
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Date/Time */}
|
||||
<div style={{ display: 'flex', gap: '15px' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<label style={{ display: 'block', marginBottom: '5px', fontSize: '0.9rem', color: '#666' }}>Start</label>
|
||||
{/* Date/Time rows */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', padding: '0 16px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<span style={{ fontSize: '0.9rem', color: 'var(--weekly-text-light)', fontWeight: 500 }}>Starts</span>
|
||||
<input
|
||||
type={allDay ? "date" : "datetime-local"}
|
||||
value={allDay ? startDate.toISOString().split('T')[0] : toLocalISOString(startDate)}
|
||||
onChange={e => handleStartDateChange(e.target.value)}
|
||||
style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px' }}
|
||||
style={{
|
||||
padding: '4px 8px',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
background: 'var(--weekly-bg-secondary, #f3f4f6)',
|
||||
fontSize: '0.9rem',
|
||||
textAlign: 'center'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<label style={{ display: 'block', marginBottom: '5px', fontSize: '0.9rem', color: '#666' }}>End</label>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<span style={{ fontSize: '0.9rem', color: 'var(--weekly-text-light)', fontWeight: 500 }}>Ends</span>
|
||||
<input
|
||||
type={allDay ? "date" : "datetime-local"}
|
||||
value={allDay ? endDate.toISOString().split('T')[0] : toLocalISOString(endDate)}
|
||||
onChange={e => setEndDate(new Date(e.target.value))}
|
||||
style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px' }}
|
||||
style={{
|
||||
padding: '4px 8px',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
background: 'var(--weekly-bg-secondary, #f3f4f6)',
|
||||
fontSize: '0.9rem',
|
||||
textAlign: 'center'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recurrence */}
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '5px', fontSize: '0.9rem', color: '#666' }}>Repeat</label>
|
||||
<select
|
||||
value={recurrence}
|
||||
onChange={e => setRecurrence(e.target.value)}
|
||||
style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px' }}
|
||||
>
|
||||
<option value="none">Never</option>
|
||||
<option value="daily">Every Day</option>
|
||||
<option value="weekly">Every Week</option>
|
||||
<option value="biweekly">Every 2 Weeks</option>
|
||||
<option value="monthly">Every Month</option>
|
||||
<option value="yearly">Every Year</option>
|
||||
</select>
|
||||
</div>
|
||||
{/* Meta Fields Group */}
|
||||
<div style={{ padding: '8px 16px', display: 'flex', flexDirection: 'column', gap: '12px', marginTop: '4px' }}>
|
||||
{/* Recurrence */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<span style={{ fontSize: '0.9rem', color: 'var(--weekly-text-light)', fontWeight: 500 }}>Repeat</span>
|
||||
<select
|
||||
value={recurrence}
|
||||
onChange={e => setRecurrence(e.target.value)}
|
||||
style={{
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
textAlign: 'right',
|
||||
fontSize: '0.9rem',
|
||||
cursor: 'pointer',
|
||||
outline: 'none',
|
||||
color: 'var(--weekly-text)'
|
||||
}}
|
||||
>
|
||||
<option value="none">Never</option>
|
||||
<option value="daily">Every Day</option>
|
||||
<option value="weekly">Every Week</option>
|
||||
<option value="monthly">Every Month</option>
|
||||
<option value="yearly">Every Year</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Location */}
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '5px', fontSize: '0.9rem', color: '#666' }}>Location</label>
|
||||
<input
|
||||
type="text"
|
||||
value={location}
|
||||
onChange={e => setLocation(e.target.value)}
|
||||
placeholder="Location (optional)"
|
||||
style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px' }}
|
||||
/>
|
||||
</div>
|
||||
{/* URL (Conditional) */}
|
||||
{supportsURL && (
|
||||
<input
|
||||
type="url"
|
||||
value={url}
|
||||
onChange={e => setUrl(e.target.value)}
|
||||
placeholder="URL"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '6px 0',
|
||||
border: 'none',
|
||||
borderBottom: '1px solid var(--weekly-border)',
|
||||
background: 'transparent',
|
||||
fontSize: '0.9rem',
|
||||
outline: 'none'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* URL */}
|
||||
{(() => {
|
||||
const selectedCal = availableCalendars.find((c: any) => c.id === calendarId);
|
||||
const isUrlDisabled = selectedCal && (selectedCal.provider === 'google' || selectedCal.provider === 'outlook');
|
||||
return (
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '5px', fontSize: '0.9rem', color: '#666' }}>URL</label>
|
||||
<input
|
||||
type="url"
|
||||
value={isUrlDisabled ? '' : url}
|
||||
onChange={e => setUrl(e.target.value)}
|
||||
placeholder={isUrlDisabled ? 'Not supported by this calendar provider' : 'https://...'}
|
||||
disabled={!!isUrlDisabled}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
opacity: isUrlDisabled ? 0.5 : 1,
|
||||
background: isUrlDisabled ? '#f5f5f5' : undefined,
|
||||
cursor: isUrlDisabled ? 'not-allowed' : undefined,
|
||||
}}
|
||||
{/* Notes */}
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
<Suspense fallback={
|
||||
<textarea
|
||||
value={description}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
placeholder="Notes"
|
||||
rows={3}
|
||||
style={{ width: '100%', padding: '8px 0', border: 'none', borderBottom: '1px solid var(--weekly-border)', background: 'transparent', resize: 'none', fontSize: '0.9rem', outline: 'none' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Description - Rich text */}
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '5px', fontSize: '0.9rem', color: '#666' }}>Notes</label>
|
||||
<Suspense fallback={
|
||||
<textarea
|
||||
value={description}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
placeholder="Notes..."
|
||||
rows={3}
|
||||
style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px', resize: 'vertical' }}
|
||||
/>
|
||||
}>
|
||||
<RichTextEditor
|
||||
value={description}
|
||||
onChange={setDescription}
|
||||
placeholder="Notes..."
|
||||
minHeight="120px"
|
||||
/>
|
||||
</Suspense>
|
||||
}>
|
||||
<RichTextEditor
|
||||
value={description}
|
||||
onChange={setDescription}
|
||||
placeholder="Notes"
|
||||
minHeight="80px"
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="weekly-modal-actions" style={{ marginTop: '2rem', display: 'flex', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<div className="weekly-modal-actions" style={{ marginTop: '20px', display: 'flex', justifyContent: 'space-between', borderTop: '1px solid var(--weekly-border)', paddingTop: '15px' }}>
|
||||
<div style={{ display: 'flex', gap: '12px', alignItems: 'center' }}>
|
||||
{event && onDelete && (
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
disabled={isSaving || isDeleting}
|
||||
style={{
|
||||
padding: '10px 20px',
|
||||
background: isDeleteConfirming ? '#d32f2f' : 'transparent',
|
||||
color: isDeleteConfirming ? 'white' : '#d32f2f',
|
||||
border: '1px solid #d32f2f',
|
||||
borderRadius: '4px',
|
||||
fontSize: '1rem',
|
||||
padding: '8px 12px',
|
||||
background: 'none',
|
||||
color: '#ef4444',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
fontSize: '0.9rem',
|
||||
fontWeight: 500,
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
width: isDeleteConfirming ? 'auto' : 'initial' // Expand if needed
|
||||
opacity: isSaving || isDeleting ? 0.5 : 1
|
||||
}}
|
||||
>
|
||||
{isDeleting ? 'Deleting...' : isDeleteConfirming ? 'Confirm Delete' : 'Delete'}
|
||||
</button>
|
||||
)}
|
||||
{!event && (
|
||||
<button className="weekly-btn-ghost" onClick={onClose} style={{ padding: '8px 12px', fontSize: '0.9rem', color: 'var(--weekly-text-light)', border: 'none', background: 'none', cursor: 'pointer' }}>
|
||||
Cancel
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '10px' }}>
|
||||
<button className="weekly-btn weekly-btn-secondary" onClick={onClose} disabled={isSaving || isDeleting}>Cancel</button>
|
||||
<button className="weekly-btn weekly-btn-primary" onClick={handleSubmit} disabled={isSaving || isDeleting}>
|
||||
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
{event && (
|
||||
<button className="weekly-btn-ghost" onClick={onClose} style={{ padding: '8px 12px', fontSize: '0.9rem', color: 'var(--weekly-text-light)', border: 'none', background: 'none', cursor: 'pointer' }}>
|
||||
Cancel
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="weekly-btn-primary"
|
||||
onClick={handleSubmit}
|
||||
disabled={isSaving || isDeleting}
|
||||
style={{
|
||||
padding: '8px 20px',
|
||||
borderRadius: '8px',
|
||||
fontSize: '0.95rem',
|
||||
fontWeight: 600,
|
||||
backgroundColor: '#3b82f6',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
opacity: isSaving || isDeleting ? 0.7 : 1
|
||||
}}
|
||||
>
|
||||
{isSaving ? 'Saving...' : 'Save'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user