diff --git a/README.md b/README.md index 647574f..3f023a2 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ This application can be deployed using Docker and Docker Compose. ```bash docker-compose up --build ``` -3. Access the application at http://localhost:3001 +3. Access the application at http://localhost:3000 ### Environment Variables diff --git a/SETUP.md b/SETUP.md index 79d3c5b..9cd6e13 100644 --- a/SETUP.md +++ b/SETUP.md @@ -39,7 +39,7 @@ This will create all necessary tables: npm run dev ``` -Visit: **http://localhost:3001** +Visit: **http://localhost:3000** --- @@ -47,14 +47,14 @@ Visit: **http://localhost:3001** ### Sign Up (Email/Password) -1. Go to http://localhost:3001/auth/signup +1. Go to http://localhost:3000/auth/signup 2. Enter email and password (min 8 characters) 3. Click "Sign up" 4. You'll be automatically signed in ### Sign In (Email/Password) -1. Go to http://localhost:3001/auth/login +1. Go to http://localhost:3000/auth/login 2. Enter your credentials 3. Click "Sign in" @@ -82,7 +82,7 @@ To enable "Sign in with Google": 4. Click **Create Credentials** → **OAuth client ID** 5. Choose **Web application** 6. Add these redirect URIs: - - `http://localhost:3001/api/auth/callback/google` + - `http://localhost:3000/api/auth/callback/google` - `https://todo.martin-bierschenk.de/api/auth/callback/google` (via Reverse Proxy) - `https://todo.martin-bierschenk.de/api/calendar/google/oauth` (via Reverse Proxy) 7. Copy your **Client ID** and **Client Secret** diff --git a/deployment-notes.md b/deployment-notes.md index f058f56..b590900 100644 --- a/deployment-notes.md +++ b/deployment-notes.md @@ -41,7 +41,7 @@ Since this is a Next.js application, you can deploy it with: # Health check HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD curl -f http://localhost:3001/health || exit 1 + CMD curl -f http://localhost:3000/health || exit 1 # For Next.js, run in production mode CMD ["npm", "run", "start"] diff --git a/package-lock.json b/package-lock.json index f8f6af6..f4c68fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,10 @@ "license": "MIT", "dependencies": { "@auth/prisma-adapter": "^2.11.1", + "@fortawesome/fontawesome-svg-core": "^7.2.0", + "@fortawesome/free-brands-svg-icons": "^7.2.0", + "@fortawesome/free-solid-svg-icons": "^7.2.0", + "@fortawesome/react-fontawesome": "^3.2.0", "@next-auth/prisma-adapter": "^1.0.7", "@prisma/client": "^5.22.0", "@tiptap/extension-link": "^3.20.0", @@ -873,6 +877,64 @@ "license": "MIT", "optional": true }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.2.0.tgz", + "integrity": "sha512-IpR0bER9FY25p+e7BmFH25MZKEwFHTfRAfhOyJubgiDnoJNsSvJ7nigLraHtp4VOG/cy8D7uiV0dLkHOne5Fhw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-svg-core": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.2.0.tgz", + "integrity": "sha512-6639htZMjEkwskf3J+e6/iar+4cTNM9qhoWuRfj9F3eJD6r7iCzV1SWnQr2Mdv0QT0suuqU8BoJCZUyCtP9R4Q==", + "license": "MIT", + "dependencies": { + "@fortawesome/fontawesome-common-types": "7.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-brands-svg-icons": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-7.2.0.tgz", + "integrity": "sha512-VNG8xqOip1JuJcC3zsVsKRQ60oXG9+oYNDCosjoU/H9pgYmLTEwWw8pE0jhPz/JWdHeUuK6+NQ3qsM4gIbdbYQ==", + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "7.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-solid-svg-icons": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-7.2.0.tgz", + "integrity": "sha512-YTVITFGN0/24PxzXrwqCgnyd7njDuzp5ZvaCx5nq/jg55kUYd94Nj8UTchBdBofi/L0nwRfjGOg0E41d2u9T1w==", + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "7.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/react-fontawesome": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-3.2.0.tgz", + "integrity": "sha512-E9Gu1hqd6JussVO26EC4WqRZssXMnQr2ol7ZNWkkFOH8jZUaxDJ9Z9WF9wIVkC+kJGXUdY3tlffpDwEKfgQrQw==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@fortawesome/fontawesome-svg-core": "~6 || ~7", + "react": "^18.0.0 || ^19.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", diff --git a/package.json b/package.json index 76f273d..dd2fc65 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "my-weekly-todo-list", - "version": "1.13.0", + "version": "1.13.1", "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": { - "dev": "echo \"\\n ✦ My Weekly ToDo List v$(node -p \"require('./package.json').version\") — dev mode\\n\" && next dev -H 0.0.0.0 -p 3002", + "dev": "echo \"\\n ✦ My Weekly ToDo List v$(node -p \"require('./package.json').version\") — dev mode\\n\" && next dev -H 0.0.0.0 -p 3000", "build": "next build", "start": "echo \"\\n ✦ My Weekly ToDo List v$(node -p \"require('./package.json').version\") — production\\n\" && next start", "lint": "next lint", @@ -22,6 +22,10 @@ "license": "MIT", "dependencies": { "@auth/prisma-adapter": "^2.11.1", + "@fortawesome/fontawesome-svg-core": "^7.2.0", + "@fortawesome/free-brands-svg-icons": "^7.2.0", + "@fortawesome/free-solid-svg-icons": "^7.2.0", + "@fortawesome/react-fontawesome": "^3.2.0", "@next-auth/prisma-adapter": "^1.0.7", "@prisma/client": "^5.22.0", "@tiptap/extension-link": "^3.20.0", diff --git a/src/app/api/someday-lists/external/route.ts b/src/app/api/someday-lists/external/route.ts index d751f0c..f7a9abf 100644 --- a/src/app/api/someday-lists/external/route.ts +++ b/src/app/api/someday-lists/external/route.ts @@ -7,6 +7,7 @@ import { createGoogleClient, createGoogleTaskList } from '@/lib/google-tasks'; import { createMsTodoList } from '@/lib/microsoft-todo'; import { getOutlookAccessToken } from '@/lib/outlook-token'; import { createAppleReminderList } from '@/lib/apple-reminders'; +import { createSynologyReminderList } from '@/lib/synology-tasks'; const prisma = new PrismaClient(); @@ -66,6 +67,18 @@ export async function POST(req: NextRequest) { const newList = await createAppleReminderList(email, password, title); externalId = newList.id; externalProvider = 'apple'; + } else if (provider === 'synology') { + const connection = await prisma.calendarConnection.findFirst({ + where: { userId: user.id, provider: 'synology' } + }); + if (!connection || !connection.accessToken || !connection.refreshToken) { + return NextResponse.json({ error: 'Synology account not connected or missing server URL' }, { status: 400 }); + } + const [username, password] = connection.accessToken.split(':'); + const serverUrl = connection.refreshToken; + const newList = await createSynologyReminderList(serverUrl, username, password, title); + externalId = newList.id; + externalProvider = 'synology'; } // Create local SomedayList diff --git a/src/app/globals.css b/src/app/globals.css index 2bf9df3..7ae3b19 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1191,7 +1191,7 @@ h3 { .weekly-someday.expanded { max-height: none; - overflow: hidden; + overflow: visible; } .weekly-someday-bar { @@ -1257,12 +1257,13 @@ h3 { flex-direction: row !important; flex-wrap: nowrap !important; gap: 1.5rem; - width: max-content; /* Allow grid to grow beyond parent width */ + width: max-content; min-width: 100%; padding-bottom: 1rem; align-items: flex-start; -webkit-overflow-scrolling: touch; scroll-behavior: smooth; + overflow: visible; /* Let parent container handle scrolling */ } /* Scrollbar Styling for Someday Container */ @@ -1318,7 +1319,7 @@ h3 { max-width: 100%; display: flex; flex-direction: column; - overflow-y: auto; + overflow-y: visible; scrollbar-width: none; /* Hide scrollbar Firefox */ -ms-overflow-style: none; /* Hide scrollbar IE/Edge */ } @@ -1414,6 +1415,12 @@ h3 { padding: 6px 1rem; display: flex; align-items: flex-start; + overflow: visible !important; + position: relative; +} + +.weekly-someday-list .weekly-task-item:hover { + z-index: 50; } .weekly-container.dark-mode .weekly-someday-list .weekly-task-item { @@ -1421,7 +1428,6 @@ h3 { } .weekly-someday-list .weekly-task-text { - font-size: var(--base-font-size); line-height: 1.4; word-break: break-word; overflow: visible; diff --git a/src/components/GridTaskBlock.tsx b/src/components/GridTaskBlock.tsx index c49931e..cd93679 100644 --- a/src/components/GridTaskBlock.tsx +++ b/src/components/GridTaskBlock.tsx @@ -1,5 +1,5 @@ import React, { useState, useRef, useEffect } from "react"; -import { Repeat } from "lucide-react"; +import { Repeat, Circle, X } from "lucide-react"; import { Task } from "./WeeklyView"; interface GridTaskBlockProps { @@ -30,6 +30,8 @@ interface GridTaskBlockProps { onSetEditingTaskId?: (id: string | null) => void; workingHoursStart: number; showTaskCheckboxes?: boolean; + projects?: any[]; + onProjectAssign?: (taskId: string, projectId: string | null) => void; } export function GridTaskBlock({ @@ -59,15 +61,19 @@ export function GridTaskBlock({ deleteSubTask, onSetEditingTaskId, workingHoursStart, - showTaskCheckboxes + showTaskCheckboxes, + projects, + onProjectAssign }: GridTaskBlockProps) { const [isNotesOpen, setIsNotesOpen] = useState(false); const [notesValue, setNotesValue] = useState(task.markdownContent || ""); const [isSubTasksOpen, setIsSubTasksOpen] = useState(false); const [isSubTaskInputOpen, setIsSubTaskInputOpen] = useState(false); const [newSubTaskTitle, setNewSubTaskTitle] = useState(""); + const [showProjectPicker, setShowProjectPicker] = useState(false); const notesRef = useRef(null); const subTaskInputRef = useRef(null); + const projectPickerRef = useRef(null); // Touch: tap-to-reveal actions const [touchActive, setTouchActive] = useState(false); @@ -189,8 +195,9 @@ export function GridTaskBlock({ zIndex: isResizing || isNotesOpen || isSubTasksOpen ? 10 : 5, background: (isNotesOpen || isSubTasksOpen || isResizing) ? (darkMode ? "#2a2a2a" : "#ffffff") : "transparent", border: (isNotesOpen || isSubTasksOpen || isResizing) ? `1px solid ${darkMode ? "#404040" : "#e0e0e0"}` : "none", + borderLeft: task.project?.color ? `4px solid ${task.project.color}` : (isNotesOpen || isSubTasksOpen || isResizing) ? `1px solid ${darkMode ? "#404040" : "#e0e0e0"}` : "none", borderRadius: (isNotesOpen || isSubTasksOpen || isResizing) ? "4px" : "0", - padding: "2px 4px", + padding: task.project?.color ? "2px 4px 2px 8px" : "2px 4px", boxShadow: (isNotesOpen || isSubTasksOpen || isResizing) ? "0 1px 3px rgba(0,0,0,0.05)" : "none", display: "flex", flexDirection: "column", @@ -262,7 +269,73 @@ export function GridTaskBlock({ style={{ width: "12px", height: "12px", margin: 0, cursor: "pointer", position: "relative", top: "3px", left: "-2px", accentColor: "#FFF" }} /> )} - {task.title} + + {task.title} + + {/* Subtask indicator */} + {task.subTasks && task.subTasks.length > 0 && (() => { + const completed = task.subTasks.filter(s => s.completed).length; + const total = task.subTasks.length; + const expanded = isSubTasksOpen || isSubTaskInputOpen; + return ( +
{ + e.stopPropagation(); + setIsSubTasksOpen(!isSubTasksOpen); + }} + className="subtask-indicator-badge" + title={expanded ? "Collapse subtasks" : `${completed}/${total} subtasks done`} + style={{ + display: "inline-flex", + alignItems: "center", + gap: "2px", + padding: "1px 5px", + borderRadius: "8px", + background: expanded ? "rgba(99, 102, 241, 0.15)" : (completed === total) ? "rgba(34, 197, 94, 0.15)" : "rgba(0,0,0,0.06)", + color: expanded ? "#6366f1" : (completed === total) ? "#22c55e" : (darkMode ? "#aaa" : "#666"), + border: "none", + cursor: "pointer", + fontSize: "0.65rem", + fontWeight: 600, + lineHeight: 1, + flexShrink: 0, + whiteSpace: "nowrap", + }} + > + + + + {completed}/{total} +
+ ); + })()} + + {/* Note indicator */} + {task.markdownContent && task.markdownContent.trim().length > 0 && ( +
{ + e.stopPropagation(); + setIsNotesOpen(!isNotesOpen); + }} + title={isNotesOpen ? "Collapse note" : "Expand note"} + style={{ + display: "inline-flex", + alignItems: "center", + padding: "1px 3px", + borderRadius: "3px", + background: isNotesOpen ? "rgba(245, 158, 11, 0.12)" : "rgba(0,0,0,0.04)", + color: isNotesOpen ? "#f59e0b" : (darkMode ? "#888" : "#999"), + cursor: "pointer", + marginLeft: "3px" + }} + > + + + + +
+ )} +
)} @@ -324,6 +397,57 @@ export function GridTaskBlock({ )} + + {/* Project Assignment */} + {projects && projects.length > 0 && onProjectAssign && ( +
+ + {showProjectPicker && ( +
+ {task.projectId && ( + + )} + {projects.map((p) => ( + + ))} +
+ )} +
+ )} )} -
+
{somedayExpanded && (
@@ -5489,14 +5522,14 @@ export default function WeeklyView() { display: "flex", flexDirection: "column", }} - onClick={(e) => { - // Focus the add input if clicking empty area or the list container - // Only if not clicking a task item or other interactive element + onMouseDown={(e) => { + // Only focus bottom input if clicking background area, not slots const target = e.target as HTMLElement; if ( - target.closest(".weekly-task-item") || - target.tagName === "INPUT" || - target.tagName === "BUTTON" + target.closest(".task-list-slot") || + target.closest(".someday-list-header") || + target.closest("button") || + target.tagName === "INPUT" ) { return; } @@ -5661,16 +5694,18 @@ export default function WeeklyView() { {list.externalProvider && ( {list.externalProvider === "outlook" ? ( - + ) : list.externalProvider === "google" ? ( - + ) : list.externalProvider === "apple" ? ( - + + ) : list.externalProvider === "synology" ? ( + ) : ( - + )} )} @@ -5719,121 +5754,187 @@ export default function WeeklyView() { position: "relative", }} > - {Array.from({ length: getSomedaySlotCount(list.tasks) }).map((_, slotIdx) => { - const taskInSlot = list.tasks.find(t => t.somedaySlotIndex === slotIdx); - const isTarget = dropPreview?.listId === list.id && dropPreview?.slotIdx === slotIdx; + {(() => { + const slotCount = getSomedaySlotCount(list.tasks); + const indexedTasks = list.tasks.filter(t => t.somedaySlotIndex !== null && t.somedaySlotIndex !== undefined && t.somedaySlotIndex < slotCount); + const unindexedTasks = list.tasks.filter(t => t.somedaySlotIndex === null || t.somedaySlotIndex === undefined || t.somedaySlotIndex >= slotCount); + + // Fill indexed slots and put unindexed tasks in empty slots starting from top + const slots = Array.from({ length: slotCount }, (_, i) => ({ index: i, task: indexedTasks.find(t => t.somedaySlotIndex === i) || null })); + + let unindexedIdx = 0; + const finalSlots = slots.map(slot => { + if (!slot.task && unindexedIdx < unindexedTasks.length) { + return { ...slot, task: unindexedTasks[unindexedIdx++] }; + } + return slot; + }); + + // Any remaining unindexed tasks that didn't fit in slots + const remainingTasks = unindexedTasks.slice(unindexedIdx); return ( -
handleSomedayDragOver(e, list.id, slotIdx)} - onDrop={(e) => handleSomedayDrop(e, list.id, slotIdx)} - onDragLeave={() => setDropPreview(null)} - > - {taskInSlot && ( - toggleTask(taskInSlot.id)} - onEdit={() => setEditingTaskId(taskInSlot.id)} - onUpdate={(title) => updateTask(taskInSlot.id, title)} - onDelete={() => deleteTask(taskInSlot.id)} - onNotes={() => setSelectedTaskForNotes(taskInSlot)} - onRollToggle={() => toggleTaskRolling(taskInSlot.id)} - onRecurrence={() => setSelectedTaskForRecurrence(taskInSlot)} - onDragStart={(e, t) => handleDragStart(e, t)} - onDragEnd={handleDragEnd} - variant="minimal" - isSomeday={true} - onAddSubTask={addSubTask} - onToggleSubTask={toggleSubTask} - onDeleteSubTask={deleteSubTask} - onUpdateSubTask={updateSubTask} - editingTaskId={editingTaskId} - onSetEditingTaskId={setEditingTaskId} - showTaskCheckboxes={profile.showTaskCheckboxes} - projects={projects} - onProjectAssign={assignProject} - /> - )} -
- ); - })} + <> + { + const occupiedSlots = list.tasks.map(t => t.somedaySlotIndex).filter(s => s !== null && s !== undefined) as number[]; + let nextFreeSlot = 0; + while (occupiedSlots.includes(nextFreeSlot)) { + nextFreeSlot++; + } - {/* Legacy / unindexed tasks */} - {list.tasks.filter(t => t.somedaySlotIndex === null || t.somedaySlotIndex === undefined || t.somedaySlotIndex >= getSomedaySlotCount(list.tasks)).map(task => ( -
- toggleTask(task.id)} - onEdit={() => setEditingTaskId(task.id)} - onUpdate={(title) => updateTask(task.id, title)} - onDelete={() => deleteTask(task.id)} - onNotes={() => setSelectedTaskForNotes(task)} - onRollToggle={() => toggleTaskRolling(task.id)} - onRecurrence={() => setSelectedTaskForRecurrence(task)} - onDragStart={(e, t) => handleDragStart(e, t)} - onDragEnd={handleDragEnd} - variant="minimal" - isSomeday={true} - onAddSubTask={addSubTask} - onToggleSubTask={toggleSubTask} - onDeleteSubTask={deleteSubTask} - onUpdateSubTask={updateSubTask} - editingTaskId={editingTaskId} - onSetEditingTaskId={setEditingTaskId} - showTaskCheckboxes={profile.showTaskCheckboxes} - projects={projects} - onProjectAssign={assignProject} - /> -
- ))} - - { - const occupiedSlots = list.tasks.map(t => t.somedaySlotIndex).filter(s => s !== null && s !== undefined) as number[]; - let nextFreeSlot = 0; - while (occupiedSlots.includes(nextFreeSlot)) { - nextFreeSlot++; - } - - try { - const res = await fetch("/api/tasks", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - title, - somedayListId: list.id, - somedaySlotIndex: nextFreeSlot - }), - }); - if (res.ok) { - const data = await res.json(); - const newTask = { - ...data.task, - createdAt: new Date(data.task.createdAt), - updatedAt: new Date(data.task.updatedAt), - }; - setSomedayLists((prev) => - prev.map((l) => - l.id === list.id - ? { ...l, tasks: [...l.tasks, newTask] } - : l, - ), + try { + const res = await fetch("/api/tasks", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + title, + somedayListId: list.id, + somedaySlotIndex: nextFreeSlot + }), + }); + if (res.ok) { + const data = await res.json(); + const newTask = { + ...data.task, + createdAt: new Date(data.task.createdAt), + updatedAt: new Date(data.task.updatedAt), + }; + setSomedayLists((prev) => + prev.map((l) => + l.id === list.id + ? { ...l, tasks: [...l.tasks, newTask] } + : l, + ), + ); + } + } catch (e) { + console.error(e); + } + }} + /> + {finalSlots.map((slot) => { + const isTarget = dropPreview?.listId === list.id && dropPreview?.slotIdx === slot.index; + const task = slot.task; + return ( +
handleSomedayDragOver(e, list.id, slot.index)} + onDrop={(e) => handleSomedayDrop(e, list.id, slot.index)} + onDragLeave={() => setDropPreview(null)} + onMouseDown={(e) => { + e.stopPropagation(); + if (!task) { + setActiveAddSlot({ listId: list.id, slotIdx: slot.index }); + } + }} + style={{ minHeight: "24px", cursor: task ? "default" : "text" }} + > + {task ? ( + toggleTask(task.id)} + onEdit={() => setEditingTaskId(task.id)} + onUpdate={(title) => updateTask(task.id, title)} + onDelete={() => deleteTask(task.id)} + onNotes={() => setSelectedTaskForNotes(task)} + onRollToggle={() => toggleTaskRolling(task.id)} + onRecurrence={() => setSelectedTaskForRecurrence(task)} + onDragStart={(e, t) => handleDragStart(e, t)} + onDragEnd={handleDragEnd} + variant="minimal" + isSomeday={true} + onAddSubTask={addSubTask} + onToggleSubTask={toggleSubTask} + onDeleteSubTask={deleteSubTask} + onUpdateSubTask={updateSubTask} + editingTaskId={editingTaskId} + onSetEditingTaskId={setEditingTaskId} + showTaskCheckboxes={profile.showTaskCheckboxes} + projects={projects} + onProjectAssign={assignProject} + /> + ) : ( + activeAddSlot?.listId === list.id && activeAddSlot?.slotIdx === slot.index && ( + { + try { + const res = await fetch("/api/tasks", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + title, + somedayListId: list.id, + somedaySlotIndex: slot.index + }), + }); + if (res.ok) { + const data = await res.json(); + const newTask = { + ...data.task, + createdAt: new Date(data.task.createdAt), + updatedAt: new Date(data.task.updatedAt), + }; + setSomedayLists((prev) => + prev.map((l) => + l.id === list.id + ? { ...l, tasks: [...l.tasks, newTask] } + : l, + ), + ); + } + } catch (e) { + console.error(e); + } + setActiveAddSlot(null); + }} + onCancel={() => setActiveAddSlot(null)} + /> + ) + )} +
); - } - } catch (e) { - console.error(e); - } - }} - /> + })} + {remainingTasks.map((task) => ( +
+ toggleTask(task.id)} + onEdit={() => setEditingTaskId(task.id)} + onUpdate={(title) => updateTask(task.id, title)} + onDelete={() => deleteTask(task.id)} + onNotes={() => setSelectedTaskForNotes(task)} + onRollToggle={() => toggleTaskRolling(task.id)} + onRecurrence={() => setSelectedTaskForRecurrence(task)} + onDragStart={(e, t) => handleDragStart(e, t)} + onDragEnd={handleDragEnd} + variant="minimal" + isSomeday={true} + onAddSubTask={addSubTask} + onToggleSubTask={toggleSubTask} + onDeleteSubTask={deleteSubTask} + onUpdateSubTask={updateSubTask} + editingTaskId={editingTaskId} + onSetEditingTaskId={setEditingTaskId} + showTaskCheckboxes={profile.showTaskCheckboxes} + projects={projects} + onProjectAssign={assignProject} + /> +
+ ))} + + ); + })()}
))} @@ -5893,7 +5994,10 @@ export default function WeeklyView() { -
+
c.provider === "google") ? 1 : 0) + (connections?.some(c => c.provider === "outlook") ? 1 : 0) + (connections?.some(c => c.provider === "synology") ? 1 : 0)}, minmax(0, 1fr))` }} + > - - - + {connections?.some(c => c.provider === "google") && ( + + )} + {connections?.some(c => c.provider === "outlook") && ( + + )} + {connections?.some(c => c.provider === "synology") && ( + + )}
@@ -6488,9 +6565,13 @@ function TaskInput({ onAddTask, onDragOver, onDrop }: TaskInputProps) { function SomedayAddTask({ listId, onAdd, + onCancel, + slotIdx, }: { listId: string; onAdd: (title: string) => void; + onCancel?: () => void; + slotIdx?: number; }) { const [title, setTitle] = useState(""); const inputRef = useRef(null); @@ -6499,14 +6580,24 @@ function SomedayAddTask({ if (title.trim()) { onAdd(title.trim()); setTitle(""); + } else if (onCancel) { + onCancel(); } }; + useEffect(() => { + if (slotIdx !== undefined && inputRef.current) { + inputRef.current.focus(); + } + }, [slotIdx]); + return (
  • { if (e.key === "Escape") { setTitle(""); - e.currentTarget.blur(); + if (onCancel) onCancel(); + else e.currentTarget.blur(); } }} className="weekly-task-text" @@ -6534,13 +6626,13 @@ function SomedayAddTask({ border: "none", background: "transparent", padding: "0 0", - fontSize: "0.8rem", + fontSize: "0.9375rem", outline: "none", - height: "24px", - display: "block", // Height match filler + height: slotIdx !== undefined ? "auto" : "24px", + display: "block", }} - placeholder="" - data-someday-add-input={listId} + placeholder={slotIdx !== undefined ? "" : "Add task..."} + data-someday-add-input={slotIdx !== undefined ? undefined : listId} />
  • @@ -6793,7 +6885,7 @@ function TaskItem({ )}
    {/* Visual Indicator for Rolling Tasks */} - {task.isRolling && !task.completed && ( + {task.isRolling && !task.completed && !isSomeday && (
    {([ { key: "general", icon: , label: t.general }, - { key: "calendar", icon: , label: t.calendar }, + { key: "calendar", icon: , label: t.calendar }, { key: "account", icon: , label: t.account }, { key: "styling", icon: , label: "Styling" }, { key: "motivation", icon: , label: "Motivation" }, @@ -9393,12 +9485,12 @@ function SettingsSidebar({ > {conn.provider === "google" - ? "📅" + ? : conn.provider === "apple" - ? "🍎" + ? : conn.provider === "synology" - ? "💾" - : "📧"} + ? + : } {conn.provider === "google" ? "Google Calendar" @@ -9648,25 +9740,25 @@ function SettingsSidebar({ onClick={handleGoogleConnect} className="calendar-connect-btn" > - 📅 {t.connectGoogle} + {t.connectGoogle}
    @@ -9721,7 +9813,7 @@ function SettingsSidebar({ }} > - {conn.provider === "google" ? "📅" : conn.provider === "synology" ? "💾" : "📧"} + {conn.provider === "google" ? : conn.provider === "synology" ? : } {conn.provider === "google" ? "Google Tasks" diff --git a/src/lib/synology-tasks.ts b/src/lib/synology-tasks.ts index 816dc9c..6a660e2 100644 --- a/src/lib/synology-tasks.ts +++ b/src/lib/synology-tasks.ts @@ -58,14 +58,17 @@ export const fetchSynologyReminderLists = async (serverUrl: string, username: st */ export const createSynologyReminderList = async (serverUrl: string, username: string, password: string, title: string): Promise => { try { + console.log('[SYNOLOGY] Creating reminder list:', title, 'at', serverUrl); const client = createClient(serverUrl, username, password); await client.login(); const calendars = await client.fetchCalendars(); if (calendars.length === 0) { + console.error('[SYNOLOGY] No calendars found to derive base URL'); throw new Error('No existing Synology calendars found to derive base URL'); } + // Deriving the base URL for new collections const firstCalUrl = calendars[0].url; const urlParts = firstCalUrl.split('/'); if (firstCalUrl.endsWith('/')) urlParts.pop(); @@ -74,23 +77,29 @@ export const createSynologyReminderList = async (serverUrl: string, username: st const newId = crypto.randomUUID(); const newCalendarUrl = `${baseWebDavUrl}${newId}/`; + console.log('[SYNOLOGY] Proposed new calendar URL:', newCalendarUrl); + // Create the calendar collection with explicit namespaces if possible + // Synology requires specific properties to recognize it as a Task list await (client as any).makeCalendar({ url: newCalendarUrl, props: { 'displayname': title, + 'calendar-description': 'Created by My Weekly ToDo List', + 'calendar-color': '#007AFF', 'supported-calendar-component-set': { 'comp': { _attributes: { name: 'VTODO' } } } } }); + console.log('[SYNOLOGY] List created successfully'); return { id: newCalendarUrl, title: title, }; } catch (error) { - console.error('Error creating Synology reminder list:', error); + console.error('[SYNOLOGY] Error creating reminder list:', error); throw error; } };