feat: enhance UI with motto, section toggles, and fix calendar colors

This commit is contained in:
mARTin 2026-02-10 11:02:45 +01:00
parent d054144939
commit 7e4ca22e5b
2 changed files with 385 additions and 275 deletions

3
.gitignore vendored
View File

@ -22,3 +22,6 @@ node_modules/
/blob-report/ /blob-report/
/playwright/.cache/ /playwright/.cache/
/playwright/.auth/ /playwright/.auth/
# Next.js
.next/

View File

@ -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;
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' })} {new Date(event.startTime).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })}
</div> </div>
<div className="weekly-calendar-event-title">{event.title}</div> <div className="weekly-calendar-event-title" style={{ color: 'inherit' }}>{event.title}</div>
</div> </div>
))} );
})}
{/* Tasks */} {/* Tasks */}
<ol className="weekly-task-list"> <ol className="weekly-task-list">
@ -1416,15 +1434,22 @@ 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' }}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<span className="all-day-events-title">📆 {t.allDayEvents}</span> <span className="all-day-events-title">📆 {t.allDayEvents}</span>
<span className="all-day-events-count">{allDayEvents.length}</span> <span className="all-day-events-count">{allDayEvents.length}</span>
</div> </div>
<button className="weekly-someday-toggle" style={{ marginLeft: '10px' }}>
{isAllDayExpanded ? '▼' : '▲'}
</button>
</div>
{isAllDayExpanded && (
<div <div
className={`all-day-events-grid cols-${viewDays}`} className={`all-day-events-grid cols-${viewDays}`}
style={{ marginLeft: showTimeGrid ? '50px' : '0' }} style={{ marginLeft: showTimeGrid ? '50px' : '0' }}
@ -1435,7 +1460,26 @@ export default function WeeklyView() {
<div key={date.toISOString()} className="all-day-events-column"> <div key={date.toISOString()} className="all-day-events-column">
{dayEvents.length > 0 ? ( {dayEvents.length > 0 ? (
dayEvents.map(event => ( dayEvents.map(event => (
<div key={event.id} className="all-day-event" title={`${event.calendarTitle}: ${event.title}`}> <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="event-indicator">📅</span>
<span className="all-day-event-title">{event.title}</span> <span className="all-day-event-title">{event.title}</span>
</div> </div>
@ -1447,23 +1491,27 @@ export default function WeeklyView() {
); );
})} })}
</div> </div>
)}
</section> </section>
); );
})()} })()}
{/* Someday Section */} {/* Someday Section */}
{showSomeday && (
<section className={`weekly-someday ${somedayExpanded ? 'expanded' : 'collapsed'}`}> <section className={`weekly-someday ${somedayExpanded ? 'expanded' : 'collapsed'}`}>
<div className="weekly-someday-header"> <div className="weekly-someday-header" onClick={() => setSomedayExpanded(!somedayExpanded)} style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<button <div style={{ display: 'flex', alignItems: 'center' }}>
className="weekly-someday-toggle" <span className="weekly-someday-title" style={{ fontWeight: 'bold' }}>{translations[language]?.someday || translations['en'].someday}</span>
onClick={() => setSomedayExpanded(!somedayExpanded)}
>
{somedayExpanded ? '▼' : '▲'} {translations[language]?.someday || translations['en'].someday}
</button>
<span className="weekly-someday-count" style={{ marginLeft: '12px', fontSize: '0.9rem', color: '#888' }}> <span className="weekly-someday-count" style={{ marginLeft: '12px', fontSize: '0.9rem', color: '#888' }}>
{somedayLists.length} {translations[language]?.lists || translations['en'].lists} {somedayLists.length} {translations[language]?.lists || translations['en'].lists}
</span> </span>
</div> </div>
<button
className="weekly-someday-toggle"
>
{somedayExpanded ? '▼' : '▲'}
</button>
</div>
{somedayExpanded && ( {somedayExpanded && (
<div className="weekly-someday-lists"> <div className="weekly-someday-lists">
@ -1656,6 +1704,7 @@ export default function WeeklyView() {
</div> </div>
)} )}
</section> </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"