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:
parent
b97230dcb4
commit
72e62ead4a
@ -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")
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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' }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user