feat: Improve rolling tasks logic, UI polish, and fix profile settings persistence

- Relocated 'Now Do This' button and adjusted header alignment
- Hid scrollbars in All-Day Events section for cleaner UI
- Fixed 'Failed to update profile' error and ensured settings persistence
- Improved rolling task logic with collision detection and auto-reschedule
- Cleaned up duplicate code and resolved lint errors
- Updated documentation (task.md, walkthrough.md, implementation_plan.md)
This commit is contained in:
mARTin 2026-02-16 09:49:00 +01:00
parent b97230dcb4
commit 72e62ead4a
4 changed files with 686 additions and 462 deletions

View File

@ -39,6 +39,7 @@ model User {
showSchedule Boolean @default(true) showSchedule Boolean @default(true)
cellDuration Int @default(30) cellDuration Int @default(30)
viewStyle String @default("grid") viewStyle String @default("grid")
viewDays Int @default(7)
fontSize String @default("M") // "S", "M", "L" fontSize String @default("M") // "S", "M", "L"
headlineFont String @default("Inter") headlineFont String @default("Inter")
headlineFontSize String? @default("1.25rem") headlineFontSize String? @default("1.25rem")

View File

@ -32,6 +32,7 @@ export async function GET(request: NextRequest) {
showSchedule: true, showSchedule: true,
cellDuration: true, cellDuration: true,
viewStyle: true, viewStyle: true,
viewDays: true,
fontSize: true, fontSize: true,
headlineFont: true, headlineFont: true,
headlineFontSize: true, headlineFontSize: true,
@ -83,7 +84,7 @@ export async function PATCH(request: NextRequest) {
language, dateFormat, timeFormat, startHour, endHour, language, dateFormat, timeFormat, startHour, endHour,
showNextTask, calendarEditMode, focusTimerDuration, focusBreakDuration, showNextTask, calendarEditMode, focusTimerDuration, focusBreakDuration,
showTimeGrid, showSomeday, showAllDayEvents, showSchedule, showTimeGrid, showSomeday, showAllDayEvents, showSchedule,
cellDuration, viewStyle, fontSize, cellDuration, viewStyle, viewDays, fontSize,
headlineFont, headlineFontSize, headlineFontWeight, headlineFont, headlineFontSize, headlineFontWeight,
dateFontFamily, dateFontSize, dateFontWeight, dateFontFamily, dateFontSize, dateFontWeight,
timeTaskFontFamily, timeTaskFontSize, timeTaskFontWeight, timeTaskFontFamily, timeTaskFontSize, timeTaskFontWeight,
@ -114,6 +115,7 @@ export async function PATCH(request: NextRequest) {
...(showSchedule !== undefined && { showSchedule }), ...(showSchedule !== undefined && { showSchedule }),
...(cellDuration !== undefined && !isNaN(cellDuration) && { cellDuration }), ...(cellDuration !== undefined && !isNaN(cellDuration) && { cellDuration }),
...(viewStyle !== undefined && { viewStyle }), ...(viewStyle !== undefined && { viewStyle }),
...(viewDays !== undefined && !isNaN(viewDays) && { viewDays }),
...(fontSize !== undefined && { fontSize }), ...(fontSize !== undefined && { fontSize }),
...(headlineFont !== undefined && { headlineFont }), ...(headlineFont !== undefined && { headlineFont }),
...(headlineFontSize !== undefined && { headlineFontSize }), ...(headlineFontSize !== undefined && { headlineFontSize }),
@ -169,6 +171,7 @@ export async function PATCH(request: NextRequest) {
showSchedule: true, showSchedule: true,
cellDuration: true, cellDuration: true,
viewStyle: true, viewStyle: true,
viewDays: true,
fontSize: true, fontSize: true,
headlineFont: true, headlineFont: true,
headlineFontSize: true, headlineFontSize: true,

View File

@ -1013,12 +1013,20 @@ h3 {
} }
.weekly-someday-lists-grid::-webkit-scrollbar-thumb { .weekly-someday-lists-grid::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.1); background-color: rgba(0, 0, 0, 0.2);
border-radius: 4px; border-radius: 4px;
} }
.dark-mode .weekly-someday-lists-grid::-webkit-scrollbar-thumb {
background-color: rgba(255, 255, 255, 0.2);
}
.weekly-someday-lists-grid::-webkit-scrollbar-thumb:hover { .weekly-someday-lists-grid::-webkit-scrollbar-thumb:hover {
background-color: rgba(0, 0, 0, 0.2); background-color: rgba(0, 0, 0, 0.3);
}
.dark-mode .weekly-someday-lists-grid::-webkit-scrollbar-thumb:hover {
background-color: rgba(255, 255, 255, 0.3);
} }
/* Remove grid column classes as we are using flex now */ /* Remove grid column classes as we are using flex now */
@ -1060,6 +1068,36 @@ h3 {
background-position: 0 40px; /* Offset for the header */ background-position: 0 40px; /* Offset for the header */
} }
.someday-drag-handle {
cursor: grab;
color: #ccc;
padding: 2px;
margin-right: 4px;
display: flex;
align-items: center;
border-radius: 3px;
transition: background-color 0.2s, color 0.2s;
}
.someday-drag-handle:hover {
background: rgba(0, 0, 0, 0.05);
color: #888;
}
.dark-mode .someday-drag-handle:hover {
background: rgba(255, 255, 255, 0.1);
color: #fff;
}
.someday-list-delete-btn {
opacity: 0;
transition: opacity 0.2s;
}
.weekly-someday-list-title-header:hover .someday-list-delete-btn {
opacity: 1;
}
/* Placeholder styling */ /* Placeholder styling */
.weekly-someday-list.placeholder-list { .weekly-someday-list.placeholder-list {
background-image: repeating-linear-gradient( background-image: repeating-linear-gradient(
@ -1626,6 +1664,8 @@ 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;
right: -15px;
} }
.time-slot-event .event-title-row { .time-slot-event .event-title-row {
@ -1738,6 +1778,13 @@ h3 {
padding: 0 0.25rem; padding: 0 0.25rem;
min-height: 28px; min-height: 28px;
border-right: 1px solid var(--weekly-border); border-right: 1px solid var(--weekly-border);
overflow-y: auto;
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE/Edge */
}
.all-day-events-column::-webkit-scrollbar {
display: none; /* Chrome/Safari */
} }
.all-day-events-column:last-child { .all-day-events-column:last-child {

View File

@ -21,7 +21,8 @@ import {
Target, Target,
Sun, Sun,
Moon, Moon,
Repeat Repeat,
GripVertical
} from 'lucide-react'; } from 'lucide-react';
// Types // Types
@ -354,7 +355,12 @@ export default function WeeklyView() {
return { ...event, editable: isEditable }; return { ...event, editable: isEditable };
}); });
}, [rawCalendarEvents, connections]); }, [rawCalendarEvents, connections]);
const [currentWeekStart, setCurrentWeekStart] = useState(getStartOfWeek(new Date(), 1)); // Default align to Monday initially const [currentWeekStart, setCurrentWeekStart] = useState(() => {
const d = new Date();
d.setHours(0, 0, 0, 0);
d.setDate(d.getDate() - 1);
return d;
});
const [viewDays, setViewDays] = useState(7); const [viewDays, setViewDays] = useState(7);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [darkMode, setDarkMode] = useState(false); const [darkMode, setDarkMode] = useState(false);
@ -571,8 +577,9 @@ export default function WeeklyView() {
useEffect(() => { useEffect(() => {
if (!mounted) return; if (!mounted) return;
localStorage.setItem('weekly-week-start', String(weekStartDay)); localStorage.setItem('weekly-week-start', String(weekStartDay));
// Re-align current week start when start day changes // REMOVED: Re-align current week start when start day changes
setCurrentWeekStart(prev => getStartOfWeek(prev, weekStartDay)); // This was forcing the view to snap to Monday, breaking the "Yesterday as first column" setting.
// setCurrentWeekStart(prev => getStartOfWeek(prev, weekStartDay));
}, [weekStartDay, mounted]); }, [weekStartDay, mounted]);
// Translation helper // Translation helper
@ -766,44 +773,24 @@ export default function WeeklyView() {
return () => clearInterval(interval); return () => clearInterval(interval);
}, []); }, []);
const handleSettingsChanged = (newSettings: { const handleSomedayWheel = (e: React.WheelEvent) => {
showTimeGrid: boolean; if (e.currentTarget) {
cellDuration: CellDuration; e.currentTarget.scrollLeft += e.deltaY;
viewStyle: 'grid' | 'list'; }
language: string; };
dateFormat: string; const saveSetting = async (key: string, value: any) => {
timeFormat: string; try {
startHour: number; await fetch('/api/user/profile', {
endHour: number; method: 'PATCH',
fontSize: 'S' | 'M' | 'L'; headers: { 'Content-Type': 'application/json' },
showNextTask: boolean; body: JSON.stringify({ [key]: value })
showSomeday: boolean; });
showAllDayEvents: boolean; } catch (err) {
showSchedule: boolean; console.error(`Failed to save setting ${key}:`, err);
headlineFont: string; }
headlineFontSize: string; };
headlineFontWeight: string;
dateFontFamily: string; const handleSettingsChanged = (newSettings: any) => {
dateFontSize: string;
dateFontWeight: string;
timeTaskFontFamily: string;
timeTaskFontSize: string;
timeTaskFontWeight: string;
bodyFont: string;
taskFontFamily: string;
taskFontSize: string;
taskFontWeight: string;
eventFontFamily?: string;
eventFontSize?: string;
eventFontWeight?: string;
fontWeight?: string;
weekendColorSat?: string;
weekendColorSun?: string;
weekdayColor?: string;
dateColor?: string;
taskColor?: string;
todayHighlightColor?: string;
}) => {
setShowTimeGrid(newSettings.showTimeGrid); setShowTimeGrid(newSettings.showTimeGrid);
setCellDuration(newSettings.cellDuration); setCellDuration(newSettings.cellDuration);
setViewStyle(newSettings.viewStyle); setViewStyle(newSettings.viewStyle);
@ -837,15 +824,13 @@ export default function WeeklyView() {
if (newSettings.weekendColorSat) setWeekendColorSat(newSettings.weekendColorSat); if (newSettings.weekendColorSat) setWeekendColorSat(newSettings.weekendColorSat);
if (newSettings.weekendColorSun) setWeekendColorSun(newSettings.weekendColorSun); if (newSettings.weekendColorSun) setWeekendColorSun(newSettings.weekendColorSun);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
setProfile((prev: any) => ({ setProfile((prev: any) => ({
...prev, ...prev,
...newSettings,
weekdayColor: newSettings.weekdayColor || prev.weekdayColor, weekdayColor: newSettings.weekdayColor || prev.weekdayColor,
dateColor: newSettings.dateColor || prev.dateColor, dateColor: newSettings.dateColor || prev.dateColor,
taskColor: newSettings.taskColor || prev.taskColor, taskColor: newSettings.taskColor || prev.taskColor,
todayHighlightColor: newSettings.todayHighlightColor || prev.todayHighlightColor, todayHighlightColor: newSettings.todayHighlightColor || prev.todayHighlightColor,
weekendColorSat: newSettings.weekendColorSat,
weekendColorSun: newSettings.weekendColorSun,
eventFontFamily: newSettings.eventFontFamily || prev.eventFontFamily, eventFontFamily: newSettings.eventFontFamily || prev.eventFontFamily,
eventFontSize: newSettings.eventFontSize || prev.eventFontSize, eventFontSize: newSettings.eventFontSize || prev.eventFontSize,
eventFontWeight: newSettings.eventFontWeight || prev.eventFontWeight eventFontWeight: newSettings.eventFontWeight || prev.eventFontWeight
@ -867,6 +852,12 @@ export default function WeeklyView() {
setLanguage(data.user.language || 'en'); setLanguage(data.user.language || 'en');
if (data.user.startHour !== undefined) setStartHour(data.user.startHour); if (data.user.startHour !== undefined) setStartHour(data.user.startHour);
if (data.user.endHour !== undefined) setEndHour(data.user.endHour); if (data.user.endHour !== undefined) setEndHour(data.user.endHour);
if (data.user.viewStyle !== undefined) {
setViewStyle(data.user.viewStyle as 'grid' | 'list');
setShowTimeGrid(data.user.showTimeGrid ?? true);
}
if (data.user.viewDays !== undefined) setViewDays(data.user.viewDays);
if (data.user.cellDuration !== undefined) setCellDuration(data.user.cellDuration as CellDuration);
setShowNextTask(data.user.showNextTask || false); setShowNextTask(data.user.showNextTask || false);
setCalendarEditMode(data.user.calendarEditMode || false); setCalendarEditMode(data.user.calendarEditMode || false);
if (data.user.fontSize) setFontSize(data.user.fontSize as 'S' | 'M' | 'L'); if (data.user.fontSize) setFontSize(data.user.fontSize as 'S' | 'M' | 'L');
@ -886,7 +877,6 @@ export default function WeeklyView() {
if (data.user.bodyFont) setBodyFont(data.user.bodyFont); if (data.user.bodyFont) setBodyFont(data.user.bodyFont);
if (data.user.taskFontFamily) setTaskFontFamily(data.user.taskFontFamily); if (data.user.taskFontFamily) setTaskFontFamily(data.user.taskFontFamily);
if (data.user.taskFontSize) setTaskFontSize(data.user.taskFontSize); if (data.user.taskFontSize) setTaskFontSize(data.user.taskFontSize);
if (data.user.taskFontSize) setTaskFontSize(data.user.taskFontSize);
if (data.user.taskFontWeight) setTaskFontWeight(data.user.taskFontWeight); if (data.user.taskFontWeight) setTaskFontWeight(data.user.taskFontWeight);
if (data.user.eventFontFamily) setEventFontFamily(data.user.eventFontFamily); if (data.user.eventFontFamily) setEventFontFamily(data.user.eventFontFamily);
if (data.user.eventFontSize) setEventFontSize(data.user.eventFontSize); if (data.user.eventFontSize) setEventFontSize(data.user.eventFontSize);
@ -994,6 +984,11 @@ export default function WeeklyView() {
// The old code had a default list. // The old code had a default list.
// Let's ensure we use the fetched lists. // Let's ensure we use the fetched lists.
setSomedayLists(populatedLists); setSomedayLists(populatedLists);
// Roll overdue tasks
if (dayTasks.length > 0) {
rollOverdueTasks(dayTasks);
}
} }
} catch (error) { } catch (error) {
console.error('Error fetching data:', error); console.error('Error fetching data:', error);
@ -1002,6 +997,7 @@ export default function WeeklyView() {
} }
} }
// Get visible days based on current view setting // Get visible days based on current view setting
const getVisibleDays = useCallback(() => { const getVisibleDays = useCallback(() => {
const days: Date[] = []; const days: Date[] = [];
@ -1140,6 +1136,110 @@ export default function WeeklyView() {
return eventsByDay; return eventsByDay;
}, [calendarEvents, currentWeekStart, viewDays]); }, [calendarEvents, currentWeekStart, viewDays]);
const rollOverdueTasks = useCallback(async (currentTasks: Task[]) => {
const autoRolling = profile.autoRolling ?? false;
if (!autoRolling) return;
const now = new Date();
const todayStr = formatDateToISO(now);
const today = new Date(todayStr);
const overdue = currentTasks.filter(t =>
!t.completed &&
t.isRolling &&
t.scheduledDate &&
formatDateToISO(new Date(t.scheduledDate)) < todayStr
);
if (overdue.length === 0) return;
console.log(`[ROLLING] Found ${overdue.length} overdue tasks to roll to today. autoRolling=${autoRolling}`);
const updatedTasks = [...currentTasks];
let hasChanges = false;
const dailyEvents = getEventsForDate(today);
for (const task of overdue) {
let targetSlot = task.startTime || '09:00'; // Default to 9am if no time
// Collision detection
const isBlocked = (date: Date, slot: string, tasksToCheck: Task[]) => {
// Check other tasks in the updated list
const taskConflict = tasksToCheck.find(t =>
t.id !== task.id &&
t.scheduledDate &&
formatDateToISO(new Date(t.scheduledDate)) === formatDateToISO(date) &&
t.startTime === slot
);
if (taskConflict) return true;
// Check calendar events
const [h, m] = slot.split(':').map(Number);
const slotStart = new Date(date);
slotStart.setHours(h, m, 0, 0);
const slotEnd = new Date(slotStart);
slotEnd.setMinutes(slotEnd.getMinutes() + cellDuration);
return dailyEvents.some(event => {
const eventStart = new Date(event.startTime);
const eventEnd = new Date(event.endTime);
return slotStart < eventEnd && slotEnd > eventStart;
});
};
const findFreeSlot = (date: Date, preferred: string, tasksToCheck: Task[]) => {
let current = preferred;
let [h, m] = current.split(':').map(Number);
while (isBlocked(date, current, tasksToCheck)) {
m += cellDuration;
if (m >= 60) {
h += 1;
m = 0;
}
if (h >= endHour) break;
current = `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`;
}
return current;
};
const nextSlot = findFreeSlot(today, targetSlot, updatedTasks);
// Update in DB
try {
const res = await fetch('/api/tasks', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: task.id,
scheduledDate: todayStr,
startTime: nextSlot
})
});
if (res.ok) {
const data = await res.json();
const taskIndex = updatedTasks.findIndex(t => t.id === task.id);
if (taskIndex !== -1) {
updatedTasks[taskIndex] = {
...data.task,
createdAt: new Date(data.task.createdAt),
updatedAt: new Date(data.task.updatedAt)
};
hasChanges = true;
}
}
} catch (err) {
console.error(`Failed to roll task ${task.id}:`, err);
}
}
if (hasChanges) {
setTasks(updatedTasks.filter(t => t.dayOfWeek !== null && !t.somedayListId));
}
}, [profile.autoRolling, cellDuration, endHour, getEventsForDate]);
// Check if a slot is protected by calendar events (only if slot starts within event time range) // Check if a slot is protected by calendar events (only if slot starts within event time range)
const isSlotProtected = useCallback((date: Date, slot: string): boolean => { const isSlotProtected = useCallback((date: Date, slot: string): boolean => {
if (!protectEventTimes) return false; if (!protectEventTimes) return false;
@ -1187,7 +1287,12 @@ export default function WeeklyView() {
const goToNextWeek = () => navigate(new Date(currentWeekStart.getTime() + 7 * 24 * 60 * 60 * 1000), 'left', 'week'); const goToNextWeek = () => navigate(new Date(currentWeekStart.getTime() + 7 * 24 * 60 * 60 * 1000), 'left', 'week');
const goToPrevDay = () => navigate(new Date(currentWeekStart.getTime() - 24 * 60 * 60 * 1000), 'right', 'day'); const goToPrevDay = () => navigate(new Date(currentWeekStart.getTime() - 24 * 60 * 60 * 1000), 'right', 'day');
const goToNextDay = () => navigate(new Date(currentWeekStart.getTime() + 24 * 60 * 60 * 1000), 'left', 'day'); const goToNextDay = () => navigate(new Date(currentWeekStart.getTime() + 24 * 60 * 60 * 1000), 'left', 'day');
const goToToday = () => setCurrentWeekStart(getStartOfWeek(new Date(), weekStartDay)); const goToToday = () => {
const d = new Date();
d.setHours(0, 0, 0, 0);
d.setDate(d.getDate() - 1);
setCurrentWeekStart(d);
};
// Task CRUD operations // Task CRUD operations
const addTask = async (date: Date, title: string, startTime?: string) => { const addTask = async (date: Date, title: string, startTime?: string) => {
@ -1776,7 +1881,10 @@ export default function WeeklyView() {
{[15, 30, 60].map(duration => ( {[15, 30, 60].map(duration => (
<button <button
key={duration} key={duration}
onClick={() => setCellDuration(duration as CellDuration)} onClick={() => {
setCellDuration(duration as CellDuration);
saveSetting('cellDuration', duration);
}}
className={`px-2 py-0.5 text-xs rounded transition-colors ${cellDuration === duration ? 'bg-white shadow-sm font-bold text-black' : 'text-gray-500 hover:text-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700'}`} className={`px-2 py-0.5 text-xs rounded transition-colors ${cellDuration === duration ? 'bg-white shadow-sm font-bold text-black' : 'text-gray-500 hover:text-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700'}`}
> >
{duration}m {duration}m
@ -1791,7 +1899,10 @@ export default function WeeklyView() {
{[1, 3, 5, 7].map(num => ( {[1, 3, 5, 7].map(num => (
<button <button
key={num} key={num}
onClick={() => setViewDays(num)} onClick={() => {
setViewDays(num);
saveSetting('viewDays', num);
}}
className={`px-2 py-0.5 text-xs rounded transition-colors ${viewDays === num ? 'bg-white shadow-sm font-bold text-black' : 'text-gray-500 hover:text-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700'}`} className={`px-2 py-0.5 text-xs rounded transition-colors ${viewDays === num ? 'bg-white shadow-sm font-bold text-black' : 'text-gray-500 hover:text-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700'}`}
> >
{num} {num}
@ -1809,7 +1920,11 @@ export default function WeeklyView() {
min="0" min="0"
max={endHour - 1} max={endHour - 1}
value={startHour} value={startHour}
onChange={(e) => setStartHour(Math.max(0, Math.min(parseInt(e.target.value) || 0, endHour - 1)))} onChange={(e) => {
const val = Math.max(0, Math.min(parseInt(e.target.value) || 0, endHour - 1));
setStartHour(val);
saveSetting('startHour', val);
}}
className="w-10 p-0.5 border border-gray-200 dark:border-gray-700 rounded text-center bg-transparent focus:outline-none focus:border-teal-500" className="w-10 p-0.5 border border-gray-200 dark:border-gray-700 rounded text-center bg-transparent focus:outline-none focus:border-teal-500"
/> />
<span>-</span> <span>-</span>
@ -1818,7 +1933,11 @@ export default function WeeklyView() {
min={startHour + 1} min={startHour + 1}
max="24" max="24"
value={endHour} value={endHour}
onChange={(e) => setEndHour(Math.max(startHour + 1, Math.min(parseInt(e.target.value) || 24, 24)))} onChange={(e) => {
const val = Math.max(startHour + 1, Math.min(parseInt(e.target.value) || 24, 24));
setEndHour(val);
saveSetting('endHour', val);
}}
className="w-10 p-0.5 border border-gray-200 dark:border-gray-700 rounded text-center bg-transparent focus:outline-none focus:border-teal-500" className="w-10 p-0.5 border border-gray-200 dark:border-gray-700 rounded text-center bg-transparent focus:outline-none focus:border-teal-500"
/> />
</div> </div>
@ -1828,19 +1947,34 @@ export default function WeeklyView() {
{/* View Style Toggles */} {/* View Style Toggles */}
<div className="flex items-center gap-1 bg-gray-100 dark:bg-gray-800 rounded p-1" title="View Style"> <div className="flex items-center gap-1 bg-gray-100 dark:bg-gray-800 rounded p-1" title="View Style">
<button <button
onClick={() => setShowTimeGrid(!showTimeGrid)} onClick={() => {
const newVal = !showTimeGrid;
setShowTimeGrid(newVal);
if (newVal) setViewStyle('list'); // Default to list if grid is enabled
saveSetting('showTimeGrid', newVal);
}}
className={`px-2 py-0.5 text-xs rounded transition-colors ${showTimeGrid ? 'bg-white shadow-sm font-bold text-black' : 'text-gray-500 hover:text-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700'}`} className={`px-2 py-0.5 text-xs rounded transition-colors ${showTimeGrid ? 'bg-white shadow-sm font-bold text-black' : 'text-gray-500 hover:text-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700'}`}
> >
Time Grid Time Grid
</button> </button>
<button <button
onClick={() => setViewStyle('grid')} onClick={() => {
setViewStyle('grid');
setShowTimeGrid(false);
saveSetting('viewStyle', 'grid');
saveSetting('showTimeGrid', false);
}}
className={`px-2 py-0.5 text-xs rounded transition-colors ${!showTimeGrid && viewStyle === 'grid' ? 'bg-white shadow-sm font-bold text-black' : 'text-gray-500 hover:text-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700'}`} className={`px-2 py-0.5 text-xs rounded transition-colors ${!showTimeGrid && viewStyle === 'grid' ? 'bg-white shadow-sm font-bold text-black' : 'text-gray-500 hover:text-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700'}`}
> >
Grid Grid
</button> </button>
<button <button
onClick={() => setViewStyle('list')} onClick={() => {
setViewStyle('list');
if (!showTimeGrid) {
saveSetting('viewStyle', 'list');
}
}}
className={`px-2 py-0.5 text-xs rounded transition-colors ${!showTimeGrid && viewStyle === 'list' ? 'bg-white shadow-sm font-bold text-black' : 'text-gray-500 hover:text-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700'}`} className={`px-2 py-0.5 text-xs rounded transition-colors ${!showTimeGrid && viewStyle === 'list' ? 'bg-white shadow-sm font-bold text-black' : 'text-gray-500 hover:text-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700'}`}
> >
List List
@ -1899,19 +2033,33 @@ export default function WeeklyView() {
<span className="text-gray-300 mx-2">-</span> <span className="text-gray-300 mx-2">-</span>
</div> </div>
</div>
{/* RIGHT SECTION: Navigation & Tools */}
<div className="flex items-center gap-3">
<button
onClick={() => {
const newVal = !showNextTask;
setShowNextTask(newVal);
saveSetting('showNextTask', newVal);
}}
className={`weekly-btn-secondary ${showNextTask ? 'active' : ''}`}
title={showNextTask ? "Showing Next Task" : "Showing Motto"}
>
<Target size={14} className={showNextTask ? 'text-teal-500' : 'text-gray-400'} />
Now DO THIS
</button>
{/* Focus Mode Toggle */} {/* Focus Mode Toggle */}
<button <button
onClick={() => setShowFocusMode(true)} onClick={() => setShowFocusMode(true)}
className="flex items-center gap-1.5 px-3 py-1 rounded-full bg-gray-100 hover:bg-gray-200 text-gray-600 transition-colors text-xs font-medium" className="weekly-btn-secondary"
title="Enter Focus Mode" title="Enter Focus Mode"
> >
<Target size={14} /> <Target size={14} />
<span>Focus Mode</span> <span>Focus Mode</span>
</button> </button>
</div>
{/* RIGHT SECTION: Navigation & Tools */}
<div className="flex items-center gap-3">
{/* Day/Night Mode Switch */} {/* Day/Night Mode Switch */}
<button <button
onClick={() => setDarkMode(!darkMode)} onClick={() => setDarkMode(!darkMode)}
@ -2015,10 +2163,16 @@ export default function WeeklyView() {
data-nav-type={viewDays > 1 ? 'week' : 'day'} data-nav-type={viewDays > 1 ? 'week' : 'day'}
style={{ viewTransitionName: viewDays > 1 ? 'week-grid' : 'none' } as React.CSSProperties} style={{ viewTransitionName: viewDays > 1 ? 'week-grid' : 'none' } as React.CSSProperties}
> >
{getVisibleDays().map((date, colIndex) => ( {getVisibleDays().map((date, colIndex) => {
const todayMidnight = new Date();
todayMidnight.setHours(0, 0, 0, 0);
const isToday = isSameDay(date, todayMidnight);
const isPast = date < todayMidnight && !isToday;
return (
<div <div
key={date.toISOString()} key={date.toISOString()}
className={`weekly-day-column ${date.getDay() === 6 ? 'is-sat' : ''} ${date.getDay() === 0 ? 'is-sun' : ''} ${isSameDay(date, new Date()) ? 'is-today' : ''} ${date < new Date() && !isSameDay(date, new Date()) ? 'is-past' : ''}`} className={`weekly-day-column ${date.getDay() === 6 ? 'is-sat' : ''} ${date.getDay() === 0 ? 'is-sun' : ''} ${isToday ? 'is-today' : ''} ${isPast ? 'is-past' : ''}`}
style={{ viewTransitionName: `day-${date.getFullYear()}-${date.getMonth()}-${date.getDate()}` } as any} style={{ viewTransitionName: `day-${date.getFullYear()}-${date.getMonth()}-${date.getDate()}` } as any}
> >
{/* Day Header */} {/* Day Header */}
@ -2303,8 +2457,8 @@ export default function WeeklyView() {
minHeight: `15px`, minHeight: `15px`,
position: 'absolute', position: 'absolute',
top: `${topOffset}px`, top: `${topOffset}px`,
left: '4px', left: '-10px',
right: '4px', right: '-15px',
zIndex: 5, zIndex: 5,
flexDirection: 'column', flexDirection: 'column',
alignItems: 'flex-start', alignItems: 'flex-start',
@ -2445,8 +2599,9 @@ export default function WeeklyView() {
) )
} }
</div > </div>
))} );
})}
</main > </main >
</div > </div >
@ -2544,7 +2699,10 @@ export default function WeeklyView() {
</div> </div>
{somedayExpanded && ( {somedayExpanded && (
<div className={`weekly-someday-lists-grid cols-${Math.min(7, Math.max(1, viewDays))}`}> <div
className={`weekly-someday-lists-grid cols-${Math.min(7, Math.max(1, viewDays))}`}
onWheel={handleSomedayWheel}
>
{(somedayLists.length > 0 ? somedayLists : [{ id: 'default', title: 'LISTE', tasks: [] }]).slice(0, Math.max(somedayLists.length, viewDays)).map(list => ( {(somedayLists.length > 0 ? somedayLists : [{ id: 'default', title: 'LISTE', tasks: [] }]).slice(0, Math.max(somedayLists.length, viewDays)).map(list => (
<div <div
key={list.id} key={list.id}
@ -2570,9 +2728,9 @@ export default function WeeklyView() {
}} }}
draggable draggable
onDragStart={(e) => { onDragStart={(e) => {
// Only drag if clicking the header // Only drag if clicking the handle
const target = e.target as HTMLElement; const target = e.target as HTMLElement;
if (!target.closest('.weekly-someday-list-title-header')) { if (!target.closest('.someday-drag-handle')) {
e.preventDefault(); e.preventDefault();
return; return;
} }
@ -2656,7 +2814,10 @@ export default function WeeklyView() {
} }
}} }}
> >
<div className="weekly-someday-list-title-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'grab' }} title="Drag to reorder"> <div className="weekly-someday-list-title-header" style={{ display: 'flex', justifyContent: 'flex-start', alignItems: 'center' }}>
<div className="someday-drag-handle" title="Drag to reorder">
<GripVertical size={14} />
</div>
{/* Editable Title */} {/* Editable Title */}
<input <input
type="text" type="text"
@ -2683,6 +2844,7 @@ export default function WeeklyView() {
}} }}
/> />
<button <button
className="someday-list-delete-btn"
onClick={async (e) => { onClick={async (e) => {
e.stopPropagation(); e.stopPropagation();
if (confirm('Delete this list?')) { if (confirm('Delete this list?')) {
@ -2694,7 +2856,7 @@ export default function WeeklyView() {
} }
} }
}} }}
style={{ border: 'none', background: 'none', cursor: 'pointer', fontSize: '1rem', color: '#ccc', marginLeft: 'auto' }} style={{ border: 'none', background: 'none', cursor: 'pointer', fontSize: '1rem', color: '#ccc', marginLeft: 'auto', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
title="Delete List" title="Delete List"
> >
× ×
@ -2951,6 +3113,8 @@ export default function WeeklyView() {
fontWeight={fontWeight} fontWeight={fontWeight}
weekendColorSat={weekendColorSat} weekendColorSat={weekendColorSat}
weekendColorSun={weekendColorSun} weekendColorSun={weekendColorSun}
protectEventTimes={protectEventTimes}
setProtectEventTimes={setProtectEventTimes}
/> />
) )
} }
@ -3550,6 +3714,8 @@ interface SettingsSidebarProps {
fontWeight: string; fontWeight: string;
weekendColorSat: string; weekendColorSat: string;
weekendColorSun: string; weekendColorSun: string;
protectEventTimes: boolean;
setProtectEventTimes: (protect: boolean) => void;
} }
function SettingsSidebar({ function SettingsSidebar({
@ -3599,7 +3765,9 @@ function SettingsSidebar({
eventFontWeight, eventFontWeight,
fontWeight, fontWeight,
weekendColorSat, weekendColorSat,
weekendColorSun weekendColorSun,
protectEventTimes,
setProtectEventTimes
}: SettingsSidebarProps) { }: SettingsSidebarProps) {
const [activeTab, setActiveTab] = useState<'general' | 'calendar' | 'account'>('general'); const [activeTab, setActiveTab] = useState<'general' | 'calendar' | 'account'>('general');
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
@ -3902,7 +4070,12 @@ function SettingsSidebar({
dateColor: profile.dateColor, dateColor: profile.dateColor,
taskColor: profile.taskColor, taskColor: profile.taskColor,
todayHighlightColor: profile.todayHighlightColor, todayHighlightColor: profile.todayHighlightColor,
}); autoRolling: profile.autoRolling,
protectEventTimes: profile.protectEventTimes || protectEventTimes,
focusTimerDuration: profile.focusTimerDuration || focusTimerDuration,
focusBreakDuration: profile.focusBreakDuration || focusBreakDuration,
pastDayColor: profile.pastDayColor
} as any);
} }
if (profile.focusTimerDuration && setFocusTimerDuration) { if (profile.focusTimerDuration && setFocusTimerDuration) {
@ -4109,7 +4282,7 @@ function SettingsSidebar({
min="0" min="0"
max="23" max="23"
value={profile.startHour} value={profile.startHour}
onChange={(e) => setProfile(prev => ({ ...prev, startHour: parseInt(e.target.value) }))} onChange={(e) => setProfile(prev => ({ ...prev, startHour: parseInt(e.target.value) || 0 }))}
className="weekly-input" className="weekly-input"
style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px' }} style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px' }}
/> />
@ -4121,7 +4294,7 @@ function SettingsSidebar({
min="1" min="1"
max="24" max="24"
value={profile.endHour} value={profile.endHour}
onChange={(e) => setProfile(prev => ({ ...prev, endHour: parseInt(e.target.value) }))} onChange={(e) => setProfile(prev => ({ ...prev, endHour: parseInt(e.target.value) || 0 }))}
className="weekly-input" className="weekly-input"
style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px' }} style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px' }}
/> />