feat: enhance UI with motto, section toggles, and fix calendar colors
This commit is contained in:
parent
d054144939
commit
7e4ca22e5b
3
.gitignore
vendored
3
.gitignore
vendored
@ -22,3 +22,6 @@ node_modules/
|
|||||||
/blob-report/
|
/blob-report/
|
||||||
/playwright/.cache/
|
/playwright/.cache/
|
||||||
/playwright/.auth/
|
/playwright/.auth/
|
||||||
|
|
||||||
|
# Next.js
|
||||||
|
.next/
|
||||||
|
|||||||
@ -85,7 +85,10 @@ const translations: Record<string, any> = {
|
|||||||
signOut: 'Sign Out',
|
signOut: 'Sign Out',
|
||||||
startHour: 'Start of Day',
|
startHour: 'Start of Day',
|
||||||
endHour: 'End of Day',
|
endHour: 'End of Day',
|
||||||
weekAbbr: 'W'
|
weekAbbr: 'W',
|
||||||
|
mottoOfWeek: 'Motto of the Week',
|
||||||
|
showSomeday: 'Show Someday Section',
|
||||||
|
showAllDay: 'Show All-Day Section'
|
||||||
},
|
},
|
||||||
de: {
|
de: {
|
||||||
settings: 'Einstellungen',
|
settings: 'Einstellungen',
|
||||||
@ -129,7 +132,10 @@ const translations: Record<string, any> = {
|
|||||||
signOut: 'Abmelden',
|
signOut: 'Abmelden',
|
||||||
startHour: 'Tagesbeginn',
|
startHour: 'Tagesbeginn',
|
||||||
endHour: 'Tagesende',
|
endHour: 'Tagesende',
|
||||||
weekAbbr: 'KW '
|
weekAbbr: 'KW',
|
||||||
|
mottoOfWeek: 'Motto der Woche',
|
||||||
|
showSomeday: 'Irgendwann-Bereich anzeigen',
|
||||||
|
showAllDay: 'Ganztägige Ereignisse anzeigen'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -181,6 +187,28 @@ function getWeekNumber(date: Date): number {
|
|||||||
return Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
|
return Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if an event is an all-day event
|
||||||
|
// Defined outside component to avoid stale closure issues in useCallbacks
|
||||||
|
const isAllDayEvent = (event: CalendarEvent): boolean => {
|
||||||
|
if (!event.startTime) return false;
|
||||||
|
|
||||||
|
// Date-only format (YYYY-MM-DD)
|
||||||
|
if (!event.startTime.includes('T')) return true;
|
||||||
|
|
||||||
|
const start = new Date(event.startTime);
|
||||||
|
const end = new Date(event.endTime);
|
||||||
|
const durationHours = (end.getTime() - start.getTime()) / (1000 * 60 * 60);
|
||||||
|
|
||||||
|
// Check if strictly midnight to midnight in local time
|
||||||
|
const isLocalMidnight = start.getHours() === 0 && start.getMinutes() === 0;
|
||||||
|
|
||||||
|
// Check if UTC midnight (common for API-converted date strings)
|
||||||
|
const isUTCMidnight = start.getUTCHours() === 0 && start.getUTCMinutes() === 0;
|
||||||
|
|
||||||
|
// If it's effectively 24h+ and starts at midnight (local or UTC), treat as all-day
|
||||||
|
return durationHours >= 24 && (isLocalMidnight || isUTCMidnight);
|
||||||
|
};
|
||||||
|
|
||||||
// Main Component
|
// Main Component
|
||||||
export default function WeeklyView() {
|
export default function WeeklyView() {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
@ -191,6 +219,7 @@ export default function WeeklyView() {
|
|||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [darkMode, setDarkMode] = useState(false);
|
const [darkMode, setDarkMode] = useState(false);
|
||||||
const [somedayExpanded, setSomedayExpanded] = useState(true);
|
const [somedayExpanded, setSomedayExpanded] = useState(true);
|
||||||
|
const [isAllDayExpanded, setIsAllDayExpanded] = useState(true);
|
||||||
const [somedayLists, setSomedayLists] = useState<SomedayList[]>([]);
|
const [somedayLists, setSomedayLists] = useState<SomedayList[]>([]);
|
||||||
const [editingTaskId, setEditingTaskId] = useState<string | null>(null);
|
const [editingTaskId, setEditingTaskId] = useState<string | null>(null);
|
||||||
const [rollMenuTaskId, setRollMenuTaskId] = useState<string | null>(null);
|
const [rollMenuTaskId, setRollMenuTaskId] = useState<string | null>(null);
|
||||||
@ -215,6 +244,9 @@ export default function WeeklyView() {
|
|||||||
|
|
||||||
const [startHour, setStartHour] = useState(6);
|
const [startHour, setStartHour] = useState(6);
|
||||||
const [endHour, setEndHour] = useState(22);
|
const [endHour, setEndHour] = useState(22);
|
||||||
|
const [showSomeday, setShowSomeday] = useState(true);
|
||||||
|
const [showAllDay, setShowAllDay] = useState(true);
|
||||||
|
const [motto, setMotto] = useState('');
|
||||||
|
|
||||||
// Translation helper
|
// Translation helper
|
||||||
const t = translations[language] || translations['en'];
|
const t = translations[language] || translations['en'];
|
||||||
@ -303,6 +335,10 @@ export default function WeeklyView() {
|
|||||||
}
|
}
|
||||||
}, [session]);
|
}, [session]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Refetch calendar events when week changes
|
// Refetch calendar events when week changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (session) {
|
if (session) {
|
||||||
@ -487,6 +523,9 @@ export default function WeeklyView() {
|
|||||||
// Get calendar events for a specific date
|
// Get calendar events for a specific date
|
||||||
const getEventsForDate = useCallback((date: Date): CalendarEvent[] => {
|
const getEventsForDate = useCallback((date: Date): CalendarEvent[] => {
|
||||||
return calendarEvents.filter(event => {
|
return calendarEvents.filter(event => {
|
||||||
|
// Skip all-day events (handled separately)
|
||||||
|
if (isAllDayEvent(event)) return false;
|
||||||
|
|
||||||
const eventDate = new Date(event.startTime);
|
const eventDate = new Date(event.startTime);
|
||||||
return isSameDay(eventDate, date);
|
return isSameDay(eventDate, date);
|
||||||
});
|
});
|
||||||
@ -496,7 +535,12 @@ export default function WeeklyView() {
|
|||||||
const getEventsForSlot = useCallback((date: Date, slot: string): CalendarEvent[] => {
|
const getEventsForSlot = useCallback((date: Date, slot: string): CalendarEvent[] => {
|
||||||
return calendarEvents.filter(event => {
|
return calendarEvents.filter(event => {
|
||||||
// Skip all-day events (handled separately)
|
// Skip all-day events (handled separately)
|
||||||
if (isAllDayEvent(event)) return false;
|
const isAllDay = isAllDayEvent(event);
|
||||||
|
if (isAllDay) return false;
|
||||||
|
|
||||||
|
if (event.title.includes('Valentinstag')) {
|
||||||
|
// Debug removed
|
||||||
|
}
|
||||||
|
|
||||||
const eventDate = new Date(event.startTime);
|
const eventDate = new Date(event.startTime);
|
||||||
if (!isSameDay(eventDate, date)) return false;
|
if (!isSameDay(eventDate, date)) return false;
|
||||||
@ -515,25 +559,6 @@ export default function WeeklyView() {
|
|||||||
});
|
});
|
||||||
}, [calendarEvents, cellDuration]);
|
}, [calendarEvents, cellDuration]);
|
||||||
|
|
||||||
// Check if an event is an all-day event
|
|
||||||
const isAllDayEvent = (event: CalendarEvent): boolean => {
|
|
||||||
// All-day events have date format without time (e.g., "2026-02-03")
|
|
||||||
// or the startTime/endTime difference is exactly 24 hours starting at midnight
|
|
||||||
if (!event.startTime) return false;
|
|
||||||
|
|
||||||
// Check if it's a date-only format (no 'T' in the string)
|
|
||||||
if (!event.startTime.includes('T')) return true;
|
|
||||||
|
|
||||||
// Check if it starts at midnight and ends at midnight next day
|
|
||||||
const start = new Date(event.startTime);
|
|
||||||
const end = new Date(event.endTime);
|
|
||||||
const isStartMidnight = start.getHours() === 0 && start.getMinutes() === 0;
|
|
||||||
const isEndMidnight = end.getHours() === 0 && end.getMinutes() === 0;
|
|
||||||
const duration = (end.getTime() - start.getTime()) / (1000 * 60 * 60);
|
|
||||||
|
|
||||||
return isStartMidnight && isEndMidnight && duration >= 24;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Calculate event duration in pixels for proper height display
|
// Calculate event duration in pixels for proper height display
|
||||||
const getEventDuration = (event: CalendarEvent): number => {
|
const getEventDuration = (event: CalendarEvent): number => {
|
||||||
if (isAllDayEvent(event)) return 0; // All-day events handled separately
|
if (isAllDayEvent(event)) return 0; // All-day events handled separately
|
||||||
@ -1007,6 +1032,7 @@ export default function WeeklyView() {
|
|||||||
{/* Year/Week and Navigation */}
|
{/* Year/Week and Navigation */}
|
||||||
<div className="weekly-week-number">
|
<div className="weekly-week-number">
|
||||||
{currentWeekStart.getFullYear()} ({t.weekAbbr}{getWeekNumber(currentWeekStart).toString().padStart(2, '0')})
|
{currentWeekStart.getFullYear()} ({t.weekAbbr}{getWeekNumber(currentWeekStart).toString().padStart(2, '0')})
|
||||||
|
{motto && <span className="weekly-motto" style={{ marginLeft: '1rem', fontStyle: 'italic', fontWeight: 'normal', fontSize: '0.9em', opacity: 0.8 }}>— "{motto}"</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@ -1329,25 +1355,7 @@ export default function WeeklyView() {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{/* All Day Events Section */}
|
{/* All Day Events Section */}
|
||||||
<div className="weekly-all-day-section">
|
|
||||||
<div className="weekly-all-day-label">{t.allDayEvents}</div>
|
|
||||||
<div className="weekly-all-day-events">
|
|
||||||
{/* ... existing all day events rendering ... */}
|
|
||||||
{getAllDayEventsForDate(date).map(event => (
|
|
||||||
<div
|
|
||||||
key={event.id}
|
|
||||||
className="weekly-all-day-event"
|
|
||||||
style={{
|
|
||||||
backgroundColor: event.calendarColor || '#3b82f6',
|
|
||||||
color: 'white'
|
|
||||||
}}
|
|
||||||
title={`${event.title} (${event.calendarTitle || 'Calendar'})`}
|
|
||||||
>
|
|
||||||
{event.title}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* Untimed Tasks List below grid */}
|
{/* Untimed Tasks List below grid */}
|
||||||
<div className="weekly-task-list" style={{ marginTop: '1rem', borderTop: '1px solid #eee', paddingTop: '0.5rem' }}>
|
<div className="weekly-task-list" style={{ marginTop: '1rem', borderTop: '1px solid #eee', paddingTop: '0.5rem' }}>
|
||||||
{/* Filter for untimed tasks */}
|
{/* Filter for untimed tasks */}
|
||||||
@ -1373,14 +1381,24 @@ export default function WeeklyView() {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* Calendar Events */}
|
{/* Calendar Events */}
|
||||||
{getEventsForDate(date).map(event => (
|
{getEventsForDate(date).map(event => {
|
||||||
<div key={event.id} className="weekly-calendar-event">
|
const eventColor = event.calendarColor || '#009a9a';
|
||||||
<div className="weekly-calendar-event-time">
|
const bgColor = eventColor.startsWith('#') ? `${eventColor}20` : eventColor;
|
||||||
{new Date(event.startTime).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })}
|
const borderColor = eventColor.startsWith('#') ? eventColor : 'var(--weekly-teal)';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={event.id} className="weekly-calendar-event" style={{
|
||||||
|
backgroundColor: bgColor,
|
||||||
|
borderLeftColor: borderColor,
|
||||||
|
color: borderColor
|
||||||
|
}}>
|
||||||
|
<div className="weekly-calendar-event-time" style={{ color: 'inherit', opacity: 0.8 }}>
|
||||||
|
{new Date(event.startTime).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })}
|
||||||
|
</div>
|
||||||
|
<div className="weekly-calendar-event-title" style={{ color: 'inherit' }}>{event.title}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="weekly-calendar-event-title">{event.title}</div>
|
);
|
||||||
</div>
|
})}
|
||||||
))}
|
|
||||||
|
|
||||||
{/* Tasks */}
|
{/* Tasks */}
|
||||||
<ol className="weekly-task-list">
|
<ol className="weekly-task-list">
|
||||||
@ -1416,246 +1434,277 @@ export default function WeeklyView() {
|
|||||||
|
|
||||||
{/* All-Day Events Section */}
|
{/* All-Day Events Section */}
|
||||||
{(() => {
|
{(() => {
|
||||||
|
if (!showAllDay) return null;
|
||||||
const allDayEvents = calendarEvents.filter(event => isAllDayEvent(event));
|
const allDayEvents = calendarEvents.filter(event => isAllDayEvent(event));
|
||||||
if (allDayEvents.length === 0) return null;
|
if (allDayEvents.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="all-day-events-section">
|
<section className={`all-day-events-section ${isAllDayExpanded ? 'expanded' : 'collapsed'}`}>
|
||||||
<div className="all-day-events-header">
|
<div className="all-day-events-header" onClick={() => setIsAllDayExpanded(!isAllDayExpanded)} style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
<span className="all-day-events-title">📆 {t.allDayEvents}</span>
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
<span className="all-day-events-count">{allDayEvents.length}</span>
|
<span className="all-day-events-title">📆 {t.allDayEvents}</span>
|
||||||
</div>
|
<span className="all-day-events-count">{allDayEvents.length}</span>
|
||||||
<div
|
</div>
|
||||||
className={`all-day-events-grid cols-${viewDays}`}
|
<button className="weekly-someday-toggle" style={{ marginLeft: '10px' }}>
|
||||||
style={{ marginLeft: showTimeGrid ? '50px' : '0' }}
|
{isAllDayExpanded ? '▼' : '▲'}
|
||||||
>
|
</button>
|
||||||
{getVisibleDays().map((date) => {
|
|
||||||
const dayEvents = getAllDayEventsForDate(date);
|
|
||||||
return (
|
|
||||||
<div key={date.toISOString()} className="all-day-events-column">
|
|
||||||
{dayEvents.length > 0 ? (
|
|
||||||
dayEvents.map(event => (
|
|
||||||
<div key={event.id} className="all-day-event" title={`${event.calendarTitle}: ${event.title}`}>
|
|
||||||
<span className="event-indicator">📅</span>
|
|
||||||
<span className="all-day-event-title">{event.title}</span>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div className="all-day-empty"></div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
|
{isAllDayExpanded && (
|
||||||
|
<div
|
||||||
|
className={`all-day-events-grid cols-${viewDays}`}
|
||||||
|
style={{ marginLeft: showTimeGrid ? '50px' : '0' }}
|
||||||
|
>
|
||||||
|
{getVisibleDays().map((date) => {
|
||||||
|
const dayEvents = getAllDayEventsForDate(date);
|
||||||
|
return (
|
||||||
|
<div key={date.toISOString()} className="all-day-events-column">
|
||||||
|
{dayEvents.length > 0 ? (
|
||||||
|
dayEvents.map(event => (
|
||||||
|
<div
|
||||||
|
key={event.id}
|
||||||
|
className="all-day-event"
|
||||||
|
title={`${event.calendarTitle}: ${event.title}`}
|
||||||
|
style={{
|
||||||
|
backgroundColor: event.calendarColor || '#3b82f6',
|
||||||
|
color: 'white',
|
||||||
|
borderLeft: 'none',
|
||||||
|
padding: '2px 4px',
|
||||||
|
borderRadius: '3px',
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
marginBottom: '2px',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '4px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="event-indicator">📅</span>
|
||||||
|
<span className="all-day-event-title">{event.title}</span>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="all-day-empty"></div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
{/* Someday Section */}
|
{/* Someday Section */}
|
||||||
<section className={`weekly-someday ${somedayExpanded ? 'expanded' : 'collapsed'}`}>
|
{showSomeday && (
|
||||||
<div className="weekly-someday-header">
|
<section className={`weekly-someday ${somedayExpanded ? 'expanded' : 'collapsed'}`}>
|
||||||
<button
|
<div className="weekly-someday-header" onClick={() => setSomedayExpanded(!somedayExpanded)} style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
className="weekly-someday-toggle"
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
onClick={() => setSomedayExpanded(!somedayExpanded)}
|
<span className="weekly-someday-title" style={{ fontWeight: 'bold' }}>{translations[language]?.someday || translations['en'].someday}</span>
|
||||||
>
|
<span className="weekly-someday-count" style={{ marginLeft: '12px', fontSize: '0.9rem', color: '#888' }}>
|
||||||
{somedayExpanded ? '▼' : '▲'} {translations[language]?.someday || translations['en'].someday}
|
{somedayLists.length} {translations[language]?.lists || translations['en'].lists}
|
||||||
</button>
|
</span>
|
||||||
<span className="weekly-someday-count" style={{ marginLeft: '12px', fontSize: '0.9rem', color: '#888' }}>
|
</div>
|
||||||
{somedayLists.length} {translations[language]?.lists || translations['en'].lists}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{somedayExpanded && (
|
|
||||||
<div className="weekly-someday-lists">
|
|
||||||
{somedayLists.map(list => (
|
|
||||||
<div
|
|
||||||
key={list.id}
|
|
||||||
className="weekly-someday-list"
|
|
||||||
draggable
|
|
||||||
onDragStart={(e) => {
|
|
||||||
e.dataTransfer.setData('text/plain', list.id);
|
|
||||||
e.dataTransfer.effectAllowed = 'move';
|
|
||||||
}}
|
|
||||||
onDragOver={(e) => {
|
|
||||||
e.preventDefault(); // Allow drop
|
|
||||||
e.dataTransfer.dropEffect = 'move';
|
|
||||||
}}
|
|
||||||
onDrop={async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const draggedId = e.dataTransfer.getData('text/plain');
|
|
||||||
if (draggedId === list.id) return;
|
|
||||||
|
|
||||||
// Check if we are dropping a list onto another list (not a task)
|
|
||||||
// We might need to distinguish between task drag and list drag.
|
|
||||||
// We might need to distinguish between task drag and list drag.
|
|
||||||
// Task drag usually has JSON data or specific format.
|
|
||||||
// Let's assume list drag for now if we can validation.
|
|
||||||
// Actually, standardizing on a type prefix is safer.
|
|
||||||
// But since tasks use 'json' usually or custom, let's try.
|
|
||||||
|
|
||||||
// Reorder logic
|
|
||||||
const draggedIndex = somedayLists.findIndex(l => l.id === draggedId);
|
|
||||||
const targetIndex = somedayLists.findIndex(l => l.id === list.id);
|
|
||||||
|
|
||||||
if (draggedIndex === -1 || targetIndex === -1) return; // Not a list drag
|
|
||||||
|
|
||||||
const newLists = [...somedayLists];
|
|
||||||
const [draggedItem] = newLists.splice(draggedIndex, 1);
|
|
||||||
newLists.splice(targetIndex, 0, draggedItem);
|
|
||||||
|
|
||||||
setSomedayLists(newLists);
|
|
||||||
|
|
||||||
// Persist order
|
|
||||||
const orderUpdates = newLists.map((l, index) => ({ id: l.id, order: index }));
|
|
||||||
try {
|
|
||||||
await fetch('/api/someday-lists', {
|
|
||||||
method: 'PATCH',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(orderUpdates)
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to update list order', err);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'grab' }} title="Drag to reorder">
|
|
||||||
{/* Editable Title */}
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
defaultValue={list.title}
|
|
||||||
className="weekly-someday-list-title-input"
|
|
||||||
onBlur={async (e) => {
|
|
||||||
const newTitle = e.target.value.trim();
|
|
||||||
if (newTitle && newTitle !== list.title) {
|
|
||||||
try {
|
|
||||||
await fetch('/api/someday-lists', {
|
|
||||||
method: 'PATCH',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ id: list.id, title: newTitle }),
|
|
||||||
});
|
|
||||||
setSomedayLists(prev => prev.map(l => l.id === list.id ? { ...l, title: newTitle } : l));
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
e.target.value = list.title;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter') e.currentTarget.blur();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={async (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
if (confirm('Delete this list?')) {
|
|
||||||
try {
|
|
||||||
await fetch(`/api/someday-lists?id=${list.id}`, { method: 'DELETE' });
|
|
||||||
setSomedayLists(prev => prev.filter(l => l.id !== list.id));
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
style={{ border: 'none', background: 'none', cursor: 'pointer', fontSize: '1rem', color: '#ccc', marginLeft: 'auto' }}
|
|
||||||
title="Delete List"
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<ol className="weekly-task-list">
|
|
||||||
{list.tasks.map(task => (
|
|
||||||
<TaskItem
|
|
||||||
key={task.id}
|
|
||||||
task={task}
|
|
||||||
isEditing={editingTaskId === task.id}
|
|
||||||
onToggle={() => toggleTask(task.id)}
|
|
||||||
onEdit={() => setEditingTaskId(task.id)}
|
|
||||||
onUpdate={(title) => updateTask(task.id, title)}
|
|
||||||
onDelete={() => deleteTask(task.id)}
|
|
||||||
onNotes={() => setSelectedTaskForNotes(task)}
|
|
||||||
onRollToggle={() => toggleTaskRolling(task.id)}
|
|
||||||
onDragStart={handleDragStart}
|
|
||||||
onDragEnd={handleDragEnd}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ol>
|
|
||||||
{/* Add Task Input */}
|
|
||||||
<TaskInput
|
|
||||||
onAddTask={async (title) => {
|
|
||||||
// Create task in this list
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/tasks', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ title, somedayListId: list.id }),
|
|
||||||
});
|
|
||||||
if (res.ok) {
|
|
||||||
const data = await res.json();
|
|
||||||
const newTask = { ...data.task, createdAt: new Date(data.task.createdAt), updatedAt: new Date(data.task.updatedAt) };
|
|
||||||
setSomedayLists(prev => prev.map(l =>
|
|
||||||
l.id === list.id ? { ...l, tasks: [...l.tasks, newTask] } : l
|
|
||||||
));
|
|
||||||
// Also update main tasks state if needed, though they are segregated.
|
|
||||||
// Ideally strictly segregated.
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onDragOver={(e) => e.preventDefault()}
|
|
||||||
onDrop={() => { }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{/* New List Input */}
|
|
||||||
{isAddingSomedayList && (
|
|
||||||
<div className="weekly-someday-list" style={{ minWidth: '220px' }}>
|
|
||||||
<input
|
|
||||||
autoFocus
|
|
||||||
type="text"
|
|
||||||
placeholder="LIST NAME..."
|
|
||||||
value={newSomedayListName}
|
|
||||||
onChange={(e) => setNewSomedayListName(e.target.value)}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter') saveSomedayList();
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
setIsAddingSomedayList(false);
|
|
||||||
setNewSomedayListName('');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onBlur={() => {
|
|
||||||
if (!newSomedayListName.trim()) setIsAddingSomedayList(false);
|
|
||||||
else saveSomedayList();
|
|
||||||
}}
|
|
||||||
className="weekly-someday-list-title-input"
|
|
||||||
style={{ borderBottom: '1px solid #d12028' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<button
|
<button
|
||||||
className="weekly-someday-add"
|
className="weekly-someday-toggle"
|
||||||
onClick={handleStartAddSomedayList}
|
|
||||||
style={{
|
|
||||||
minWidth: '40px',
|
|
||||||
height: '40px',
|
|
||||||
borderRadius: '50%',
|
|
||||||
border: '1px dashed #ccc',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
cursor: 'pointer',
|
|
||||||
background: 'transparent',
|
|
||||||
color: '#888',
|
|
||||||
fontSize: '1.2rem',
|
|
||||||
marginLeft: '16px',
|
|
||||||
alignSelf: 'flex-start'
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
+
|
{somedayExpanded ? '▼' : '▲'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</section>
|
{somedayExpanded && (
|
||||||
|
<div className="weekly-someday-lists">
|
||||||
|
{somedayLists.map(list => (
|
||||||
|
<div
|
||||||
|
key={list.id}
|
||||||
|
className="weekly-someday-list"
|
||||||
|
draggable
|
||||||
|
onDragStart={(e) => {
|
||||||
|
e.dataTransfer.setData('text/plain', list.id);
|
||||||
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
|
}}
|
||||||
|
onDragOver={(e) => {
|
||||||
|
e.preventDefault(); // Allow drop
|
||||||
|
e.dataTransfer.dropEffect = 'move';
|
||||||
|
}}
|
||||||
|
onDrop={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const draggedId = e.dataTransfer.getData('text/plain');
|
||||||
|
if (draggedId === list.id) return;
|
||||||
|
|
||||||
|
// Check if we are dropping a list onto another list (not a task)
|
||||||
|
// We might need to distinguish between task drag and list drag.
|
||||||
|
// We might need to distinguish between task drag and list drag.
|
||||||
|
// Task drag usually has JSON data or specific format.
|
||||||
|
// Let's assume list drag for now if we can validation.
|
||||||
|
// Actually, standardizing on a type prefix is safer.
|
||||||
|
// But since tasks use 'json' usually or custom, let's try.
|
||||||
|
|
||||||
|
// Reorder logic
|
||||||
|
const draggedIndex = somedayLists.findIndex(l => l.id === draggedId);
|
||||||
|
const targetIndex = somedayLists.findIndex(l => l.id === list.id);
|
||||||
|
|
||||||
|
if (draggedIndex === -1 || targetIndex === -1) return; // Not a list drag
|
||||||
|
|
||||||
|
const newLists = [...somedayLists];
|
||||||
|
const [draggedItem] = newLists.splice(draggedIndex, 1);
|
||||||
|
newLists.splice(targetIndex, 0, draggedItem);
|
||||||
|
|
||||||
|
setSomedayLists(newLists);
|
||||||
|
|
||||||
|
// Persist order
|
||||||
|
const orderUpdates = newLists.map((l, index) => ({ id: l.id, order: index }));
|
||||||
|
try {
|
||||||
|
await fetch('/api/someday-lists', {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(orderUpdates)
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to update list order', err);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'grab' }} title="Drag to reorder">
|
||||||
|
{/* Editable Title */}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
defaultValue={list.title}
|
||||||
|
className="weekly-someday-list-title-input"
|
||||||
|
onBlur={async (e) => {
|
||||||
|
const newTitle = e.target.value.trim();
|
||||||
|
if (newTitle && newTitle !== list.title) {
|
||||||
|
try {
|
||||||
|
await fetch('/api/someday-lists', {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ id: list.id, title: newTitle }),
|
||||||
|
});
|
||||||
|
setSomedayLists(prev => prev.map(l => l.id === list.id ? { ...l, title: newTitle } : l));
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
e.target.value = list.title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') e.currentTarget.blur();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (confirm('Delete this list?')) {
|
||||||
|
try {
|
||||||
|
await fetch(`/api/someday-lists?id=${list.id}`, { method: 'DELETE' });
|
||||||
|
setSomedayLists(prev => prev.filter(l => l.id !== list.id));
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={{ border: 'none', background: 'none', cursor: 'pointer', fontSize: '1rem', color: '#ccc', marginLeft: 'auto' }}
|
||||||
|
title="Delete List"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<ol className="weekly-task-list">
|
||||||
|
{list.tasks.map(task => (
|
||||||
|
<TaskItem
|
||||||
|
key={task.id}
|
||||||
|
task={task}
|
||||||
|
isEditing={editingTaskId === task.id}
|
||||||
|
onToggle={() => toggleTask(task.id)}
|
||||||
|
onEdit={() => setEditingTaskId(task.id)}
|
||||||
|
onUpdate={(title) => updateTask(task.id, title)}
|
||||||
|
onDelete={() => deleteTask(task.id)}
|
||||||
|
onNotes={() => setSelectedTaskForNotes(task)}
|
||||||
|
onRollToggle={() => toggleTaskRolling(task.id)}
|
||||||
|
onDragStart={handleDragStart}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
{/* Add Task Input */}
|
||||||
|
<TaskInput
|
||||||
|
onAddTask={async (title) => {
|
||||||
|
// Create task in this list
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/tasks', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ title, somedayListId: list.id }),
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
const newTask = { ...data.task, createdAt: new Date(data.task.createdAt), updatedAt: new Date(data.task.updatedAt) };
|
||||||
|
setSomedayLists(prev => prev.map(l =>
|
||||||
|
l.id === list.id ? { ...l, tasks: [...l.tasks, newTask] } : l
|
||||||
|
));
|
||||||
|
// Also update main tasks state if needed, though they are segregated.
|
||||||
|
// Ideally strictly segregated.
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onDragOver={(e) => e.preventDefault()}
|
||||||
|
onDrop={() => { }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{/* New List Input */}
|
||||||
|
{isAddingSomedayList && (
|
||||||
|
<div className="weekly-someday-list" style={{ minWidth: '220px' }}>
|
||||||
|
<input
|
||||||
|
autoFocus
|
||||||
|
type="text"
|
||||||
|
placeholder="LIST NAME..."
|
||||||
|
value={newSomedayListName}
|
||||||
|
onChange={(e) => setNewSomedayListName(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') saveSomedayList();
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
setIsAddingSomedayList(false);
|
||||||
|
setNewSomedayListName('');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
if (!newSomedayListName.trim()) setIsAddingSomedayList(false);
|
||||||
|
else saveSomedayList();
|
||||||
|
}}
|
||||||
|
className="weekly-someday-list-title-input"
|
||||||
|
style={{ borderBottom: '1px solid #d12028' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
className="weekly-someday-add"
|
||||||
|
onClick={handleStartAddSomedayList}
|
||||||
|
style={{
|
||||||
|
minWidth: '40px',
|
||||||
|
height: '40px',
|
||||||
|
borderRadius: '50%',
|
||||||
|
border: '1px dashed #ccc',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
cursor: 'pointer',
|
||||||
|
background: 'transparent',
|
||||||
|
color: '#888',
|
||||||
|
fontSize: '1.2rem',
|
||||||
|
marginLeft: '16px',
|
||||||
|
alignSelf: 'flex-start'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<footer className="weekly-footer">
|
<footer className="weekly-footer">
|
||||||
@ -1696,6 +1745,12 @@ export default function WeeklyView() {
|
|||||||
setCellDuration={setCellDuration}
|
setCellDuration={setCellDuration}
|
||||||
viewStyle={viewStyle}
|
viewStyle={viewStyle}
|
||||||
setViewStyle={setViewStyle}
|
setViewStyle={setViewStyle}
|
||||||
|
showSomeday={showSomeday}
|
||||||
|
setShowSomeday={setShowSomeday}
|
||||||
|
showAllDay={showAllDay}
|
||||||
|
setShowAllDay={setShowAllDay}
|
||||||
|
motto={motto}
|
||||||
|
setMotto={setMotto}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -2046,6 +2101,12 @@ interface SettingsModalProps {
|
|||||||
setCellDuration: (duration: CellDuration) => void;
|
setCellDuration: (duration: CellDuration) => void;
|
||||||
viewStyle: 'grid' | 'list';
|
viewStyle: 'grid' | 'list';
|
||||||
setViewStyle: (style: 'grid' | 'list') => void;
|
setViewStyle: (style: 'grid' | 'list') => void;
|
||||||
|
showSomeday: boolean;
|
||||||
|
setShowSomeday: (show: boolean) => void;
|
||||||
|
showAllDay: boolean;
|
||||||
|
setShowAllDay: (show: boolean) => void;
|
||||||
|
motto: string;
|
||||||
|
setMotto: (motto: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SettingsModal({
|
function SettingsModal({
|
||||||
@ -2056,7 +2117,13 @@ function SettingsModal({
|
|||||||
cellDuration,
|
cellDuration,
|
||||||
setCellDuration,
|
setCellDuration,
|
||||||
viewStyle,
|
viewStyle,
|
||||||
setViewStyle
|
setViewStyle,
|
||||||
|
showSomeday,
|
||||||
|
setShowSomeday,
|
||||||
|
showAllDay,
|
||||||
|
setShowAllDay,
|
||||||
|
motto,
|
||||||
|
setMotto
|
||||||
}: SettingsModalProps) {
|
}: SettingsModalProps) {
|
||||||
const [activeTab, setActiveTab] = useState<'general' | 'calendar' | 'account'>('general');
|
const [activeTab, setActiveTab] = useState<'general' | 'calendar' | 'account'>('general');
|
||||||
const [connections, setConnections] = useState<any[]>([]);
|
const [connections, setConnections] = useState<any[]>([]);
|
||||||
@ -2275,6 +2342,46 @@ function SettingsModal({
|
|||||||
<div className="weekly-settings-content">
|
<div className="weekly-settings-content">
|
||||||
{activeTab === 'general' ? (
|
{activeTab === 'general' ? (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
|
||||||
|
{/* Motto */}
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', fontSize: '0.9rem', fontWeight: 600, marginBottom: '4px' }}>{t.mottoOfWeek}</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={motto}
|
||||||
|
onChange={(e) => setMotto(e.target.value)}
|
||||||
|
className="weekly-input"
|
||||||
|
placeholder={t.mottoOfWeek}
|
||||||
|
style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Visibility Toggles */}
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="showSomeday"
|
||||||
|
checked={showSomeday}
|
||||||
|
onChange={e => setShowSomeday(e.target.checked)}
|
||||||
|
style={{ width: '16px', height: '16px' }}
|
||||||
|
/>
|
||||||
|
<label htmlFor="showSomeday" style={{ fontSize: '0.9rem', fontWeight: 600 }}>
|
||||||
|
{t.showSomeday}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="showAllDay"
|
||||||
|
checked={showAllDay}
|
||||||
|
onChange={e => setShowAllDay(e.target.checked)}
|
||||||
|
style={{ width: '16px', height: '16px' }}
|
||||||
|
/>
|
||||||
|
<label htmlFor="showAllDay" style={{ fontSize: '0.9rem', fontWeight: 600 }}>
|
||||||
|
{t.showAllDay}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user