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",
|
"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",
|
"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": {
|
||||||
|
|||||||
@ -79,6 +79,7 @@ model User {
|
|||||||
dateAlignment String @default("center")
|
dateAlignment String @default("center")
|
||||||
dateVerticalAlign String? @default("middle")
|
dateVerticalAlign String? @default("middle")
|
||||||
headerDisplay String? @default("kw")
|
headerDisplay String? @default("kw")
|
||||||
|
headerCustomFormat String? @default("KW WW | YYYY")
|
||||||
hourLabelFormat String @default("short")
|
hourLabelFormat String @default("short")
|
||||||
showSubHourSlots Boolean @default(true)
|
showSubHourSlots Boolean @default(true)
|
||||||
allDayPosition String @default("above")
|
allDayPosition String @default("above")
|
||||||
|
|||||||
@ -46,6 +46,7 @@ export async function GET(request: NextRequest) {
|
|||||||
mobileDateLayout: true,
|
mobileDateLayout: true,
|
||||||
dateVerticalAlign: true,
|
dateVerticalAlign: true,
|
||||||
headerDisplay: true,
|
headerDisplay: true,
|
||||||
|
headerCustomFormat: true,
|
||||||
headlineFont: true,
|
headlineFont: true,
|
||||||
headlineFontSize: true,
|
headlineFontSize: true,
|
||||||
headlineFontWeight: true,
|
headlineFontWeight: true,
|
||||||
@ -130,7 +131,7 @@ export async function PATCH(request: NextRequest) {
|
|||||||
weekdayColor, dateColor, taskColor, todayHighlightColor,
|
weekdayColor, dateColor, taskColor, todayHighlightColor,
|
||||||
pastDayColor, goalFallbackType, goalDefaultSentence,
|
pastDayColor, goalFallbackType, goalDefaultSentence,
|
||||||
goalFontFamily, goalFontSize, goalFontWeight, goalScope,
|
goalFontFamily, goalFontSize, goalFontWeight, goalScope,
|
||||||
dateLayout, mobileDateLayout, dateVerticalAlign, headerDisplay,
|
dateLayout, mobileDateLayout, dateVerticalAlign, headerDisplay, headerCustomFormat,
|
||||||
hourLabelFormat, showSubHourSlots, allDayPosition,
|
hourLabelFormat, showSubHourSlots, allDayPosition,
|
||||||
cwFontFamily, cwFontSize, cwFontWeight, cwColor,
|
cwFontFamily, cwFontSize, cwFontWeight, cwColor,
|
||||||
yearFontFamily, yearFontSize, yearFontWeight, yearColor,
|
yearFontFamily, yearFontSize, yearFontWeight, yearColor,
|
||||||
@ -196,6 +197,7 @@ export async function PATCH(request: NextRequest) {
|
|||||||
...(mobileDateLayout !== undefined && { mobileDateLayout }),
|
...(mobileDateLayout !== undefined && { mobileDateLayout }),
|
||||||
...(dateVerticalAlign !== undefined && { dateVerticalAlign }),
|
...(dateVerticalAlign !== undefined && { dateVerticalAlign }),
|
||||||
...(headerDisplay !== undefined && { headerDisplay }),
|
...(headerDisplay !== undefined && { headerDisplay }),
|
||||||
|
...(headerCustomFormat !== undefined && { headerCustomFormat }),
|
||||||
...(hourLabelFormat !== undefined && { hourLabelFormat }),
|
...(hourLabelFormat !== undefined && { hourLabelFormat }),
|
||||||
...(showSubHourSlots !== undefined && { showSubHourSlots }),
|
...(showSubHourSlots !== undefined && { showSubHourSlots }),
|
||||||
...(allDayPosition !== undefined && { allDayPosition }),
|
...(allDayPosition !== undefined && { allDayPosition }),
|
||||||
@ -289,6 +291,7 @@ export async function PATCH(request: NextRequest) {
|
|||||||
mobileDateLayout: true,
|
mobileDateLayout: true,
|
||||||
dateVerticalAlign: true,
|
dateVerticalAlign: true,
|
||||||
headerDisplay: true,
|
headerDisplay: true,
|
||||||
|
headerCustomFormat: true,
|
||||||
cwFontFamily: true,
|
cwFontFamily: true,
|
||||||
cwFontSize: true,
|
cwFontSize: true,
|
||||||
cwFontWeight: true,
|
cwFontWeight: true,
|
||||||
|
|||||||
@ -2324,15 +2324,15 @@ h3 {
|
|||||||
|
|
||||||
/* Side Navigation Arrows (hover overlays) */
|
/* Side Navigation Arrows (hover overlays) */
|
||||||
.side-nav {
|
.side-nav {
|
||||||
position: sticky;
|
position: absolute;
|
||||||
top: 50px; /* Below header height if needed, or 0 */
|
top: 0;
|
||||||
height: 0; /* Let it be a point of origin */
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 0.35rem 0.15rem;
|
padding: 2px;
|
||||||
gap: 0.1rem;
|
gap: 2px;
|
||||||
z-index: 30;
|
z-index: 30;
|
||||||
pointer-events: none; /* Let clicks pass through to grid except buttons */
|
pointer-events: none; /* Let clicks pass through to grid except buttons */
|
||||||
}
|
}
|
||||||
@ -2352,6 +2352,7 @@ h3 {
|
|||||||
|
|
||||||
.side-nav-left {
|
.side-nav-left {
|
||||||
left: 0;
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.side-nav-right {
|
.side-nav-right {
|
||||||
@ -2372,43 +2373,50 @@ h3 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.side-nav-btn:hover {
|
.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);
|
color: var(--weekly-text, #333);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Time Column (Hours) */
|
/* Time Column (Hours) */
|
||||||
.time-column {
|
.time-column {
|
||||||
width: 50px;
|
width: 55px; /* Slightly wider for labels */
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
border-right: 1px solid var(--weekly-border);
|
border-right: 1px solid var(--weekly-border);
|
||||||
background: var(--weekly-bg);
|
background: var(--weekly-bg);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
/* Flush to left */
|
||||||
|
margin-left: -1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-column-header {
|
.time-column-header {
|
||||||
padding: 1rem 0.5rem 0.75rem;
|
padding: 1rem 1rem 0.75rem; /* Match weekly-day-header padding */
|
||||||
min-height: 50px !important;
|
min-height: 50px; /* Match weekly-day-header min-height */
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-column-slots {
|
.time-column-slots {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: visible; /* Unify with parent scroll */
|
overflow-y: visible; /* Unify with parent scroll */
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-top: 0.4rem;
|
padding-top: 0; /* Flush with header */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.time-slot-label {
|
.time-slot-label {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start; /* Aligns to top so translateY(-50%) centers on the line */
|
align-items: flex-start;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
padding: 0 0.5rem;
|
padding: 0 0.5rem;
|
||||||
font-size: 0.65rem;
|
font-size: 0.65rem;
|
||||||
color: var(--weekly-text-light);
|
color: var(--weekly-text-light);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-slot-label.hour-start {
|
.time-slot-label.hour-start {
|
||||||
@ -2444,7 +2452,7 @@ h3 {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: visible; /* Changed from auto to unify scroll */
|
overflow: visible; /* Changed from auto to unify scroll */
|
||||||
overflow-x: visible;
|
overflow-x: visible;
|
||||||
padding-top: 0.4rem; /* Aligned with .time-column-slots */
|
padding-top: 0; /* Flush with header */
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-slot {
|
.time-slot {
|
||||||
@ -2549,8 +2557,9 @@ h3 {
|
|||||||
border-radius: 0 4px 4px 0;
|
border-radius: 0 4px 4px 0;
|
||||||
margin: 1px 0;
|
margin: 1px 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
left: -10px;
|
left: 0;
|
||||||
right: -15px;
|
right: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-slot-event .event-title-row {
|
.time-slot-event .event-title-row {
|
||||||
@ -2666,6 +2675,7 @@ h3 {
|
|||||||
min-height: 28px;
|
min-height: 28px;
|
||||||
border-right: 1px solid var(--weekly-border);
|
border-right: 1px solid var(--weekly-border);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
scrollbar-width: none; /* Firefox */
|
scrollbar-width: none; /* Firefox */
|
||||||
-ms-overflow-style: none; /* IE/Edge */
|
-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'));
|
const RichTextEditor = lazy(() => import('./RichTextEditor'));
|
||||||
|
|
||||||
@ -78,6 +82,31 @@ export default function CalendarEventModal({
|
|||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const [isDeleting, setIsDeleting] = useState(false);
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
const [error, setError] = useState('');
|
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 () => {
|
const handleSubmit = async () => {
|
||||||
if (!title.trim()) {
|
if (!title.trim()) {
|
||||||
@ -155,186 +184,313 @@ export default function CalendarEventModal({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
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={{ maxWidth: '500px' }}>
|
<div className="weekly-modal-content" onClick={e => e.stopPropagation()} style={{
|
||||||
<h3 style={{ marginBottom: '1.5rem' }}>
|
maxWidth: '450px',
|
||||||
{event ? 'Edit Event' : 'New Event'}
|
padding: '20px',
|
||||||
</h3>
|
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 */}
|
{/* Calendar row: Label + Custom Selector */}
|
||||||
<div>
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '4px 16px', position: 'relative' }}>
|
||||||
<label style={{ display: 'block', marginBottom: '5px', fontSize: '0.9rem', color: '#666' }}>Title</label>
|
<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
|
<input
|
||||||
type="text"
|
type="checkbox"
|
||||||
value={title}
|
checked={allDay}
|
||||||
onChange={e => setTitle(e.target.value)}
|
onChange={e => setAllDay(e.target.checked)}
|
||||||
placeholder="Event Title"
|
style={{ width: '18px', height: '18px', cursor: 'pointer' }}
|
||||||
autoFocus
|
|
||||||
style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px', fontSize: '1rem' }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Calendar Selection */}
|
{/* Date/Time rows */}
|
||||||
<div>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', padding: '0 16px' }}>
|
||||||
<label style={{ display: 'block', marginBottom: '5px', fontSize: '0.9rem', color: '#666' }}>Calendar</label>
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
<select
|
<span style={{ fontSize: '0.9rem', color: 'var(--weekly-text-light)', fontWeight: 500 }}>Starts</span>
|
||||||
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>
|
|
||||||
<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={{ 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>
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
<label style={{ display: 'block', marginBottom: '5px', fontSize: '0.9rem', color: '#666' }}>End</label>
|
<span style={{ fontSize: '0.9rem', color: 'var(--weekly-text-light)', fontWeight: 500 }}>Ends</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={{ 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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Recurrence */}
|
{/* Meta Fields Group */}
|
||||||
<div>
|
<div style={{ padding: '8px 16px', display: 'flex', flexDirection: 'column', gap: '12px', marginTop: '4px' }}>
|
||||||
<label style={{ display: 'block', marginBottom: '5px', fontSize: '0.9rem', color: '#666' }}>Repeat</label>
|
{/* Recurrence */}
|
||||||
<select
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
value={recurrence}
|
<span style={{ fontSize: '0.9rem', color: 'var(--weekly-text-light)', fontWeight: 500 }}>Repeat</span>
|
||||||
onChange={e => setRecurrence(e.target.value)}
|
<select
|
||||||
style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px' }}
|
value={recurrence}
|
||||||
>
|
onChange={e => setRecurrence(e.target.value)}
|
||||||
<option value="none">Never</option>
|
style={{
|
||||||
<option value="daily">Every Day</option>
|
background: 'transparent',
|
||||||
<option value="weekly">Every Week</option>
|
border: 'none',
|
||||||
<option value="biweekly">Every 2 Weeks</option>
|
textAlign: 'right',
|
||||||
<option value="monthly">Every Month</option>
|
fontSize: '0.9rem',
|
||||||
<option value="yearly">Every Year</option>
|
cursor: 'pointer',
|
||||||
</select>
|
outline: 'none',
|
||||||
</div>
|
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 */}
|
{/* URL (Conditional) */}
|
||||||
<div>
|
{supportsURL && (
|
||||||
<label style={{ display: 'block', marginBottom: '5px', fontSize: '0.9rem', color: '#666' }}>Location</label>
|
<input
|
||||||
<input
|
type="url"
|
||||||
type="text"
|
value={url}
|
||||||
value={location}
|
onChange={e => setUrl(e.target.value)}
|
||||||
onChange={e => setLocation(e.target.value)}
|
placeholder="URL"
|
||||||
placeholder="Location (optional)"
|
style={{
|
||||||
style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px' }}
|
width: '100%',
|
||||||
/>
|
padding: '6px 0',
|
||||||
</div>
|
border: 'none',
|
||||||
|
borderBottom: '1px solid var(--weekly-border)',
|
||||||
|
background: 'transparent',
|
||||||
|
fontSize: '0.9rem',
|
||||||
|
outline: 'none'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* URL */}
|
{/* Notes */}
|
||||||
{(() => {
|
<div style={{ marginTop: '4px' }}>
|
||||||
const selectedCal = availableCalendars.find((c: any) => c.id === calendarId);
|
<Suspense fallback={
|
||||||
const isUrlDisabled = selectedCal && (selectedCal.provider === 'google' || selectedCal.provider === 'outlook');
|
<textarea
|
||||||
return (
|
value={description}
|
||||||
<div>
|
onChange={e => setDescription(e.target.value)}
|
||||||
<label style={{ display: 'block', marginBottom: '5px', fontSize: '0.9rem', color: '#666' }}>URL</label>
|
placeholder="Notes"
|
||||||
<input
|
rows={3}
|
||||||
type="url"
|
style={{ width: '100%', padding: '8px 0', border: 'none', borderBottom: '1px solid var(--weekly-border)', background: 'transparent', resize: 'none', fontSize: '0.9rem', outline: 'none' }}
|
||||||
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,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
}>
|
||||||
);
|
<RichTextEditor
|
||||||
})()}
|
value={description}
|
||||||
|
onChange={setDescription}
|
||||||
{/* Description - Rich text */}
|
placeholder="Notes"
|
||||||
<div>
|
minHeight="80px"
|
||||||
<label style={{ display: 'block', marginBottom: '5px', fontSize: '0.9rem', color: '#666' }}>Notes</label>
|
/>
|
||||||
<Suspense fallback={
|
</Suspense>
|
||||||
<textarea
|
</div>
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="weekly-modal-actions" style={{ marginTop: '2rem', display: 'flex', justifyContent: 'space-between' }}>
|
<div className="weekly-modal-actions" style={{ marginTop: '20px', display: 'flex', justifyContent: 'space-between', borderTop: '1px solid var(--weekly-border)', paddingTop: '15px' }}>
|
||||||
<div>
|
<div style={{ display: 'flex', gap: '12px', alignItems: 'center' }}>
|
||||||
{event && onDelete && (
|
{event && onDelete && (
|
||||||
<button
|
<button
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
disabled={isSaving || isDeleting}
|
disabled={isSaving || isDeleting}
|
||||||
style={{
|
style={{
|
||||||
padding: '10px 20px',
|
padding: '8px 12px',
|
||||||
background: isDeleteConfirming ? '#d32f2f' : 'transparent',
|
background: 'none',
|
||||||
color: isDeleteConfirming ? 'white' : '#d32f2f',
|
color: '#ef4444',
|
||||||
border: '1px solid #d32f2f',
|
border: 'none',
|
||||||
borderRadius: '4px',
|
borderRadius: '6px',
|
||||||
fontSize: '1rem',
|
fontSize: '0.9rem',
|
||||||
|
fontWeight: 500,
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
transition: 'all 0.2s',
|
transition: 'all 0.2s',
|
||||||
width: isDeleteConfirming ? 'auto' : 'initial' // Expand if needed
|
opacity: isSaving || isDeleting ? 0.5 : 1
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isDeleting ? 'Deleting...' : isDeleteConfirming ? 'Confirm Delete' : 'Delete'}
|
{isDeleting ? 'Deleting...' : isDeleteConfirming ? 'Confirm Delete' : 'Delete'}
|
||||||
</button>
|
</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>
|
||||||
<div style={{ display: 'flex', gap: '10px' }}>
|
|
||||||
<button className="weekly-btn weekly-btn-secondary" onClick={onClose} disabled={isSaving || isDeleting}>Cancel</button>
|
<div style={{ display: 'flex', gap: '8px' }}>
|
||||||
<button className="weekly-btn weekly-btn-primary" onClick={handleSubmit} disabled={isSaving || isDeleting}>
|
{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'}
|
{isSaving ? 'Saving...' : 'Save'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user