feat: subtask indicators, drag & drop fixes, font layout, someday scroll & Google Tasks sync

- Move subtask indicator badge outside text span for visibility
- Prevent parent task from hijacking subtask drag events
- Restructure font settings to 2-row layout (font dropdown + size/weight)
- Fix someday horizontal scroll with callback ref wheel handler
- Sync new tasks in Google-linked someday lists to Google Tasks

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

v1.14.0
This commit is contained in:
mARTin 2026-03-07 19:06:19 +01:00
parent 3ddfc6af79
commit 3d75d7b39b
4 changed files with 77 additions and 55 deletions

View File

@ -1,6 +1,6 @@
{
"name": "my-weekly-todo-list",
"version": "1.13.1",
"version": "1.14.0",
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
"main": "index.js",
"scripts": {

View File

@ -252,6 +252,49 @@ export async function POST(request: NextRequest) {
isRolling = user.autoRolling || false;
}
// Check if someday list is linked to an external provider
let externalId: string | null = null;
let externalProvider: string | null = null;
let externalListId: string | null = null;
if (somedayListId) {
const somedayList = await prisma.somedayList.findUnique({
where: { id: somedayListId },
select: { externalId: true, externalProvider: true },
});
if (somedayList?.externalId && somedayList?.externalProvider) {
try {
if (somedayList.externalProvider === 'google') {
const { createGoogleClient, createGoogleTask } = await import('@/lib/google-tasks');
const account = await prisma.account.findFirst({
where: { userId, provider: { in: ['google-calendar', 'google'] } },
});
if (account?.access_token) {
const client = createGoogleClient(account.access_token, account.refresh_token || undefined);
const gTask = await createGoogleTask(client, somedayList.externalId, { title });
externalId = gTask.id;
externalProvider = 'google';
externalListId = somedayList.externalId;
}
} else if (somedayList.externalProvider === 'outlook') {
const { createMsTodoTask } = await import('@/lib/microsoft-todo');
const { getOutlookAccessToken } = await import('@/lib/outlook-token');
const accessToken = await getOutlookAccessToken(userId);
if (accessToken) {
const msTask = await createMsTodoTask(accessToken, somedayList.externalId, { title });
externalId = msTask.id;
externalProvider = 'outlook';
externalListId = somedayList.externalId;
}
}
} catch (syncError) {
console.error('Failed to sync new task to external provider:', syncError);
// Continue creating locally even if sync fails
}
}
}
const task = await prisma.task.create({
data: {
title,
@ -273,6 +316,7 @@ export async function POST(request: NextRequest) {
somedaySlotIndex: somedaySlotIndex !== undefined ? parseInt(somedaySlotIndex) : null,
parentTaskId: parentTaskId || null,
...(projectId !== undefined && { projectId: projectId || null }),
...(externalId && { externalId, externalProvider, externalListId }),
},
});

View File

@ -1453,7 +1453,6 @@ h3 {
.weekly-someday-list-title-input {
font-size: 1.15rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--weekly-text, #222);
background: transparent;

View File

@ -1012,6 +1012,7 @@ export default function WeeklyView() {
const isScrollSyncing = useRef(false);
const dayHeaderRef = useRef<HTMLElement>(null);
const somedayGridRef = useRef<HTMLDivElement>(null);
const somedaySectionRef = useRef<HTMLElement | null>(null);
// Scroll sync handler
const handleTimeColumnScroll = (e: React.UIEvent<HTMLDivElement>) => {
@ -1477,40 +1478,42 @@ export default function WeeklyView() {
useEffect(() => {
const el = somedayGridRef.current;
if (!el) return;
// Horizontal scroll: convert vertical wheel to horizontal in someday area
// Callback ref ensures handler is attached as soon as element mounts
const somedayWheelCleanup = useRef<(() => void) | null>(null);
const somedaySectionRefCb = useCallback((node: HTMLElement | null) => {
// Cleanup previous
if (somedayWheelCleanup.current) {
somedayWheelCleanup.current();
somedayWheelCleanup.current = null;
}
somedaySectionRef.current = node;
if (!node) return;
const handler = (e: WheelEvent) => {
// Ignore if scrolling horizontally natively (trackpad)
const grid = somedayGridRef.current;
if (!grid) return;
// Let native horizontal scroll (trackpad) pass through
if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) return;
if (e.deltaY === 0) return;
// Check if hovering over a vertically scrollable list that isn't at its boundary
let target = e.target as HTMLElement | null;
let canScrollVertically = false;
// Only convert if grid has horizontal overflow
if (grid.scrollWidth <= grid.clientWidth + 1) return;
while (target && target !== el) {
if (target.scrollHeight > target.clientHeight) {
const style = window.getComputedStyle(target);
if (style.overflowY === 'auto' || style.overflowY === 'scroll') {
const atTop = target.scrollTop <= 0;
const atBottom = target.scrollTop + target.clientHeight >= target.scrollHeight - 1;
if (!(e.deltaY < 0 && atTop) && !(e.deltaY > 0 && atBottom)) {
canScrollVertically = true;
break;
}
}
}
target = target.parentElement;
}
// Check boundaries - allow page scroll when at edges
const atLeft = grid.scrollLeft <= 0;
const atRight = grid.scrollLeft + grid.clientWidth >= grid.scrollWidth - 1;
if (e.deltaY < 0 && atLeft) return;
if (e.deltaY > 0 && atRight) return;
if (!canScrollVertically && e.deltaY !== 0) {
e.preventDefault();
el.scrollLeft += e.deltaY;
}
e.preventDefault();
grid.scrollLeft += e.deltaY;
};
el.addEventListener("wheel", handler, { passive: false });
return () => el.removeEventListener("wheel", handler);
}, [showSomeday, somedayExpanded]);
node.addEventListener("wheel", handler, { passive: false });
somedayWheelCleanup.current = () => node.removeEventListener("wheel", handler);
}, []);
const saveSetting = async (key: string, value: any) => {
// Per-device settings: save to cookie ONLY (not DB) so each device keeps its own value
if (DEVICE_SETTINGS_KEYS.includes(key)) {
@ -3511,31 +3514,6 @@ export default function WeeklyView() {
setDropPreview(null);
};
// Added: Horizontal scroll with mouse wheel for someday area
useEffect(() => {
const somedayGrid = somedayGridRef.current;
if (!somedayGrid) return;
const handleWheel = (e: WheelEvent) => {
// Only handle if there is horizontal overflow
if (somedayGrid.scrollWidth > somedayGrid.clientWidth) {
// Dominant vertical scroll -> horizontal scroll
if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) {
// Check if we can scroll further in the requested direction
const isAtLeft = somedayGrid.scrollLeft === 0;
const isAtRight = Math.abs(somedayGrid.scrollWidth - somedayGrid.clientWidth - somedayGrid.scrollLeft) < 1;
if ((e.deltaY < 0 && !isAtLeft) || (e.deltaY > 0 && !isAtRight)) {
e.preventDefault();
somedayGrid.scrollLeft += e.deltaY;
}
}
}
};
somedayGrid.addEventListener("wheel", handleWheel, { passive: false });
return () => somedayGrid.removeEventListener("wheel", handleWheel);
}, [somedayExpanded]);
const handleSomedayDragOver = (e: React.DragEvent, listId: string, slotIdx: number) => {
e.preventDefault();
@ -5345,6 +5323,7 @@ export default function WeeklyView() {
{/* Someday Section */}
{showSomeday && (
<section
ref={somedaySectionRefCb}
className={`weekly-someday ${somedayExpanded ? "expanded" : "collapsed"} transition-colors duration-200`}
>
<div style={{ display: "flex", flexDirection: "row", maxWidth: "100%", width: "100%" }}>