diff --git a/.gitignore b/.gitignore index 7021a8a..69e3928 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,6 @@ node_modules/ /blob-report/ /playwright/.cache/ /playwright/.auth/ + +# Next.js +.next/ diff --git a/src/components/WeeklyView.tsx b/src/components/WeeklyView.tsx index 0f36463..60088c0 100644 --- a/src/components/WeeklyView.tsx +++ b/src/components/WeeklyView.tsx @@ -85,7 +85,10 @@ const translations: Record = { signOut: 'Sign Out', startHour: 'Start 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: { settings: 'Einstellungen', @@ -129,7 +132,10 @@ const translations: Record = { signOut: 'Abmelden', startHour: 'Tagesbeginn', 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); } +// 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 export default function WeeklyView() { const { data: session } = useSession(); @@ -191,6 +219,7 @@ export default function WeeklyView() { const [isLoading, setIsLoading] = useState(true); const [darkMode, setDarkMode] = useState(false); const [somedayExpanded, setSomedayExpanded] = useState(true); + const [isAllDayExpanded, setIsAllDayExpanded] = useState(true); const [somedayLists, setSomedayLists] = useState([]); const [editingTaskId, setEditingTaskId] = useState(null); const [rollMenuTaskId, setRollMenuTaskId] = useState(null); @@ -215,6 +244,9 @@ export default function WeeklyView() { const [startHour, setStartHour] = useState(6); const [endHour, setEndHour] = useState(22); + const [showSomeday, setShowSomeday] = useState(true); + const [showAllDay, setShowAllDay] = useState(true); + const [motto, setMotto] = useState(''); // Translation helper const t = translations[language] || translations['en']; @@ -303,6 +335,10 @@ export default function WeeklyView() { } }, [session]); + + + + // Refetch calendar events when week changes useEffect(() => { if (session) { @@ -487,6 +523,9 @@ export default function WeeklyView() { // Get calendar events for a specific date const getEventsForDate = useCallback((date: Date): CalendarEvent[] => { return calendarEvents.filter(event => { + // Skip all-day events (handled separately) + if (isAllDayEvent(event)) return false; + const eventDate = new Date(event.startTime); return isSameDay(eventDate, date); }); @@ -496,7 +535,12 @@ export default function WeeklyView() { const getEventsForSlot = useCallback((date: Date, slot: string): CalendarEvent[] => { return calendarEvents.filter(event => { // 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); if (!isSameDay(eventDate, date)) return false; @@ -515,25 +559,6 @@ export default function WeeklyView() { }); }, [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 const getEventDuration = (event: CalendarEvent): number => { if (isAllDayEvent(event)) return 0; // All-day events handled separately @@ -1007,6 +1032,7 @@ export default function WeeklyView() { {/* Year/Week and Navigation */}
{currentWeekStart.getFullYear()} ({t.weekAbbr}{getWeekNumber(currentWeekStart).toString().padStart(2, '0')}) + {motto && — "{motto}"}
@@ -1329,25 +1355,7 @@ export default function WeeklyView() { ); })} {/* All Day Events Section */} -
-
{t.allDayEvents}
-
- {/* ... existing all day events rendering ... */} - {getAllDayEventsForDate(date).map(event => ( -
- {event.title} -
- ))} -
-
+ {/* Untimed Tasks List below grid */}
{/* Filter for untimed tasks */} @@ -1373,14 +1381,24 @@ export default function WeeklyView() { ) : ( <> {/* Calendar Events */} - {getEventsForDate(date).map(event => ( -
-
- {new Date(event.startTime).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })} + {getEventsForDate(date).map(event => { + const eventColor = event.calendarColor || '#009a9a'; + const bgColor = eventColor.startsWith('#') ? `${eventColor}20` : eventColor; + const borderColor = eventColor.startsWith('#') ? eventColor : 'var(--weekly-teal)'; + + return ( +
+
+ {new Date(event.startTime).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })} +
+
{event.title}
-
{event.title}
-
- ))} + ); + })} {/* Tasks */}
    @@ -1416,246 +1434,277 @@ export default function WeeklyView() { {/* All-Day Events Section */} {(() => { + if (!showAllDay) return null; const allDayEvents = calendarEvents.filter(event => isAllDayEvent(event)); if (allDayEvents.length === 0) return null; return ( -
    -
    - 📆 {t.allDayEvents} - {allDayEvents.length} -
    -
    - {getVisibleDays().map((date) => { - const dayEvents = getAllDayEventsForDate(date); - return ( -
    - {dayEvents.length > 0 ? ( - dayEvents.map(event => ( -
    - 📅 - {event.title} -
    - )) - ) : ( -
    - )} -
    - ); - })} +
    +
    setIsAllDayExpanded(!isAllDayExpanded)} style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}> +
    + 📆 {t.allDayEvents} + {allDayEvents.length} +
    +
    + {isAllDayExpanded && ( +
    + {getVisibleDays().map((date) => { + const dayEvents = getAllDayEventsForDate(date); + return ( +
    + {dayEvents.length > 0 ? ( + dayEvents.map(event => ( +
    + 📅 + {event.title} +
    + )) + ) : ( +
    + )} +
    + ); + })} +
    + )}
    ); })()} {/* Someday Section */} -
    -
    - - - {somedayLists.length} {translations[language]?.lists || translations['en'].lists} - -
    - - {somedayExpanded && ( -
    - {somedayLists.map(list => ( -
    { - 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); - } - }} - > -
    - {/* Editable Title */} - { - 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(); - }} - /> - -
    -
      - {list.tasks.map(task => ( - 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} - /> - ))} -
    - {/* Add Task Input */} - { - // 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={() => { }} - /> -
    - ))} - {/* New List Input */} - {isAddingSomedayList && ( -
    - 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' }} - /> -
    - )} + {showSomeday && ( +
    +
    setSomedayExpanded(!somedayExpanded)} style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}> +
    + {translations[language]?.someday || translations['en'].someday} + + {somedayLists.length} {translations[language]?.lists || translations['en'].lists} + +
    - )} -
    + + {somedayExpanded && ( +
    + {somedayLists.map(list => ( +
    { + 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); + } + }} + > +
    + {/* Editable Title */} + { + 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(); + }} + /> + +
    +
      + {list.tasks.map(task => ( + 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} + /> + ))} +
    + {/* Add Task Input */} + { + // 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={() => { }} + /> +
    + ))} + {/* New List Input */} + {isAddingSomedayList && ( +
    + 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' }} + /> +
    + )} + +
    + )} +
    + )} {/* Footer */}