chore: change development port to 3000
v1.13.1
This commit is contained in:
parent
d41bc25fb0
commit
2777601031
@ -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
|
||||
|
||||
|
||||
8
SETUP.md
8
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**
|
||||
|
||||
@ -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"]
|
||||
|
||||
62
package-lock.json
generated
62
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
13
src/app/api/someday-lists/external/route.ts
vendored
13
src/app/api/someday-lists/external/route.ts
vendored
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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<HTMLTextAreaElement>(null);
|
||||
const subTaskInputRef = useRef<HTMLInputElement>(null);
|
||||
const projectPickerRef = useRef<HTMLDivElement>(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" }}
|
||||
/>
|
||||
)}
|
||||
<span style={task.completed && showTaskCheckboxes ? { opacity: 0.5, flex: 1 } : { flex: 1 }}>{task.title}</span>
|
||||
<span style={task.completed && showTaskCheckboxes ? { opacity: 0.5, flex: 1, display: "flex", alignItems: "center", flexWrap: "wrap", rowGap: "2px" } : { flex: 1, display: "flex", alignItems: "center", flexWrap: "wrap", rowGap: "2px" }}>
|
||||
<span style={{ marginRight: "4px" }}>{task.title}</span>
|
||||
|
||||
{/* 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 (
|
||||
<div
|
||||
onClick={(e) => {
|
||||
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",
|
||||
}}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" width="8" height="8" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" style={{ transform: expanded ? "rotate(90deg)" : "rotate(0deg)", transition: "transform 0.2s" }}>
|
||||
<polyline points="9 18 15 12 9 6" />
|
||||
</svg>
|
||||
{completed}/{total}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Note indicator */}
|
||||
{task.markdownContent && task.markdownContent.trim().length > 0 && (
|
||||
<div
|
||||
onClick={(e) => {
|
||||
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"
|
||||
}}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" width="10" height="10" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@ -324,6 +397,57 @@ export function GridTaskBlock({
|
||||
<Repeat size={12} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Project Assignment */}
|
||||
{projects && projects.length > 0 && onProjectAssign && (
|
||||
<div className="relative" ref={projectPickerRef}>
|
||||
<button
|
||||
className={`task-action-btn ${task.project ? "active" : ""}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowProjectPicker(!showProjectPicker);
|
||||
}}
|
||||
title={task.project ? task.project.name : "Assign project"}
|
||||
>
|
||||
<Circle
|
||||
size={12}
|
||||
fill={task.project?.color || "none"}
|
||||
stroke={task.project?.color || "currentColor"}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</button>
|
||||
{showProjectPicker && (
|
||||
<div className="absolute z-[100] top-full left-1/2 -translate-x-1/2 mt-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-lg shadow-lg py-1 min-w-[140px]" style={{ whiteSpace: "nowrap" }}>
|
||||
{task.projectId && (
|
||||
<button
|
||||
className="flex items-center gap-2 w-full px-3 py-1.5 text-xs hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-500"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onProjectAssign(task.id, null);
|
||||
setShowProjectPicker(false);
|
||||
}}
|
||||
>
|
||||
<X size={10} /> Remove
|
||||
</button>
|
||||
)}
|
||||
{projects.map((p) => (
|
||||
<button
|
||||
key={p.id}
|
||||
className={`flex items-center gap-2 w-full px-3 py-1.5 text-xs hover:bg-gray-100 dark:hover:bg-gray-700 ${task.projectId === p.id ? "font-bold" : ""}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onProjectAssign(task.id, task.projectId === p.id ? null : p.id);
|
||||
setShowProjectPicker(false);
|
||||
}}
|
||||
>
|
||||
<Circle size={10} fill={p.color || "#999"} stroke={p.color || "#999"} strokeWidth={0} />
|
||||
{p.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<button className="task-action-btn delete" onClick={(e) => { e.stopPropagation(); deleteTask(task.id); }} title="Delete">
|
||||
<svg viewBox="0 0 24 24" width="12" height="12" stroke="currentColor" strokeWidth="2.5" fill="none" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="3 6 5 6 21 6"></polyline>
|
||||
|
||||
@ -12,6 +12,9 @@ import { useSession, signOut } from "next-auth/react";
|
||||
import CalendarEventModal from "./CalendarEventModal";
|
||||
import TaskRecurrenceModal from "./RecurrenceModal";
|
||||
import { GridTaskBlock } from "./GridTaskBlock";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faApple, faGoogle, faMicrosoft } from "@fortawesome/free-brands-svg-icons";
|
||||
import { faServer } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import FocusModeOverlay from "./FocusModeOverlay";
|
||||
import {
|
||||
@ -53,6 +56,8 @@ import {
|
||||
FolderOpen,
|
||||
Circle,
|
||||
X,
|
||||
Cable,
|
||||
Link,
|
||||
} from "lucide-react";
|
||||
|
||||
// Types
|
||||
@ -638,6 +643,7 @@ export default function WeeklyView() {
|
||||
const [editingTaskId, setEditingTaskId] = useState<string | null>(null);
|
||||
const [draggingListId, setDraggingListId] = useState<string | null>(null);
|
||||
const [dropTargetListIndex, setDropTargetListIndex] = useState<number | null>(null);
|
||||
const [activeAddSlot, setActiveAddSlot] = useState<{ listId: string; slotIdx: number } | null>(null);
|
||||
const isDragFromHandle = useRef(false);
|
||||
|
||||
// Undo/Redo state
|
||||
@ -3505,6 +3511,32 @@ 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();
|
||||
setDropPreview({ listId, slotIdx });
|
||||
@ -5011,6 +5043,8 @@ export default function WeeklyView() {
|
||||
onSetEditingTaskId={setEditingTaskId}
|
||||
workingHoursStart={workingHoursStart}
|
||||
showTaskCheckboxes={profile.showTaskCheckboxes}
|
||||
projects={projects}
|
||||
onProjectAssign={assignProject}
|
||||
/>
|
||||
))}
|
||||
{visibleSlots.map((slot) => {
|
||||
@ -5455,10 +5489,9 @@ export default function WeeklyView() {
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ flex: 1, minWidth: 0, overflowX: "auto" }}>
|
||||
<div ref={somedayGridRef} style={{ flex: 1, minWidth: 0, overflowX: "auto" }}>
|
||||
{somedayExpanded && (
|
||||
<div
|
||||
ref={somedayGridRef}
|
||||
className={`weekly-someday-lists-grid cols-${Math.min(7, Math.max(1, viewDays))}`}
|
||||
style={{ display: "flex", flexDirection: "row", flexWrap: "nowrap" }}
|
||||
>
|
||||
@ -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 && (
|
||||
<span
|
||||
title={`Synced with ${list.externalProvider === "outlook" ? "Microsoft" : list.externalProvider === "google" ? "Google" : list.externalProvider === "apple" ? "Apple" : list.externalProvider}`}
|
||||
style={{ display: "inline-flex", alignItems: "center", marginLeft: "4px", opacity: 0.5, flexShrink: 0 }}
|
||||
style={{ display: "inline-flex", alignItems: "center", marginLeft: "8px", opacity: 0.8, flexShrink: 0 }}
|
||||
>
|
||||
{list.externalProvider === "outlook" ? (
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="3" width="20" height="18" rx="2" /><path d="M7 3v18" /><path d="M7 8h10" /><path d="M7 13h10" /></svg>
|
||||
<FontAwesomeIcon icon={faMicrosoft} className="w-4 h-4 text-zinc-600 dark:text-zinc-400 hover:text-[#0078D4] dark:hover:text-[#00A4EF] transition-colors" />
|
||||
) : list.externalProvider === "google" ? (
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10" /><path d="M12 8v8" /><path d="M8 12h8" /></svg>
|
||||
<FontAwesomeIcon icon={faGoogle} className="w-4 h-4 text-zinc-600 dark:text-zinc-400 hover:text-[#4285F4] dark:hover:text-[#8AB4F8] transition-colors" />
|
||||
) : list.externalProvider === "apple" ? (
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.8-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83" /><path d="M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11" /></svg>
|
||||
<FontAwesomeIcon icon={faApple} className="w-4 h-4 text-zinc-600 dark:text-zinc-400 hover:text-[#555] dark:hover:text-[#ccc] transition-colors" />
|
||||
) : list.externalProvider === "synology" ? (
|
||||
<FontAwesomeIcon icon={faServer} className="w-4 h-4 text-zinc-600 dark:text-zinc-400 hover:text-[#007AFF] dark:hover:text-[#3A9CFF] transition-colors" />
|
||||
) : (
|
||||
<RefreshCcw size={12} />
|
||||
<RefreshCcw size={14} className="text-zinc-400" />
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
@ -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 (
|
||||
<div
|
||||
key={slotIdx}
|
||||
className={`task-list-slot ${isTarget ? 'drop-target' : ''}`}
|
||||
onDragOver={(e) => handleSomedayDragOver(e, list.id, slotIdx)}
|
||||
onDrop={(e) => handleSomedayDrop(e, list.id, slotIdx)}
|
||||
onDragLeave={() => setDropPreview(null)}
|
||||
>
|
||||
{taskInSlot && (
|
||||
<TaskItem
|
||||
key={taskInSlot.id}
|
||||
task={taskInSlot}
|
||||
isEditing={editingTaskId === taskInSlot.id}
|
||||
onToggle={() => 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}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<>
|
||||
<SomedayAddTask
|
||||
listId={list.id}
|
||||
onAdd={async (title) => {
|
||||
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 => (
|
||||
<div key={task.id} className="task-list-slot">
|
||||
<TaskItem
|
||||
key={task.id}
|
||||
task={task}
|
||||
isEditing={editingTaskId === task.id}
|
||||
onToggle={() => 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}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<SomedayAddTask
|
||||
listId={list.id}
|
||||
onAdd={async (title) => {
|
||||
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 (
|
||||
<div
|
||||
key={slot.index}
|
||||
className={`task-list-slot ${isTarget ? 'drop-target' : ''}`}
|
||||
onDragOver={(e) => 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 ? (
|
||||
<TaskItem
|
||||
key={task.id}
|
||||
task={task}
|
||||
isEditing={editingTaskId === task.id}
|
||||
onToggle={() => 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 && (
|
||||
<SomedayAddTask
|
||||
listId={list.id}
|
||||
slotIdx={slot.index}
|
||||
onAdd={async (title) => {
|
||||
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)}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
})}
|
||||
{remainingTasks.map((task) => (
|
||||
<div key={task.id} className="task-list-slot">
|
||||
<TaskItem
|
||||
key={task.id}
|
||||
task={task}
|
||||
isEditing={editingTaskId === task.id}
|
||||
onToggle={() => 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}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@ -5893,7 +5994,10 @@ export default function WeeklyView() {
|
||||
<label className="block text-xs font-black text-zinc-400 dark:text-zinc-500 mb-3 uppercase tracking-tighter">
|
||||
Sync with External Provider (Optional)
|
||||
</label>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
<div
|
||||
className="grid gap-2"
|
||||
style={{ gridTemplateColumns: `repeat(${1 + (connections?.some(c => c.provider === "google") ? 1 : 0) + (connections?.some(c => c.provider === "outlook") ? 1 : 0) + (connections?.some(c => c.provider === "synology") ? 1 : 0)}, minmax(0, 1fr))` }}
|
||||
>
|
||||
<button
|
||||
onClick={() => setSelectedSomedayProvider(null)}
|
||||
className={`flex flex-col items-center justify-center p-3 rounded-xl border-2 transition-all ${!selectedSomedayProvider ? "border-zinc-900 bg-zinc-900 text-white" : "border-zinc-100 dark:border-zinc-800 hover:border-zinc-300 dark:hover:border-zinc-600"}`}
|
||||
@ -5905,78 +6009,51 @@ export default function WeeklyView() {
|
||||
Local
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
setSelectedSomedayProvider("google")
|
||||
}
|
||||
className={`flex flex-col items-center justify-center p-3 rounded-xl border-2 transition-all ${selectedSomedayProvider === "google" ? "border-blue-500 bg-blue-50 dark:bg-blue-900/20" : "border-zinc-100 dark:border-zinc-800 hover:border-blue-200 dark:hover:border-blue-800/40"}`}
|
||||
>
|
||||
<div className="w-6 h-6 flex items-center justify-center mb-1">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
className="w-4 h-4"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M12.48 10.92v3.28h7.84c-.24 1.84-.9 3.32-2.18 4.36-1.52 1.2-3.8 2.36-7.66 2.36-6.4 0-11.64-5.16-11.64-11.56S3.96 5.8 10.36 5.8c3.48 0 6.08 1.36 8 3.16l2.32-2.32C18.4 4.56 14.88 3 10.36 3 4.2 3 0 7.12 0 12.32S4.2 21.64 10.36 21.64c3.28 0 5.84-1.08 7.84-3.16 2.08-2.08 2.76-4.96 2.76-7.36 0-.72-.04-1.4-.16-2.2h-8.32z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-[10px] font-black uppercase">
|
||||
Google
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
setSelectedSomedayProvider("outlook")
|
||||
}
|
||||
className={`flex flex-col items-center justify-center p-3 rounded-xl border-2 transition-all ${selectedSomedayProvider === "outlook" ? "border-blue-600 bg-blue-100 dark:bg-blue-900/30" : "border-zinc-100 dark:border-zinc-800 hover:border-blue-300 dark:hover:border-blue-800/50"}`}
|
||||
>
|
||||
<div className="w-6 h-6 flex items-center justify-center mb-1">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
className="w-4 h-4"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M1.5 6L11.5 1L21.5 6V18L11.5 23L1.5 18V6Z"
|
||||
fill="#0078D4"
|
||||
/>
|
||||
<path
|
||||
d="M11.5 12.5V23L21.5 18V6L11.5 12.5Z"
|
||||
fill="#005A9E"
|
||||
/>
|
||||
<path
|
||||
d="M11.5 12.5L1.5 6V18L11.5 23V12.5Z"
|
||||
fill="#0078D4"
|
||||
/>
|
||||
<path
|
||||
d="M16.5 7.5L6.5 2.5V14.5L16.5 19.5V7.5Z"
|
||||
fill="#28A8EA"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-[10px] font-black uppercase">
|
||||
Outlook
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
setSelectedSomedayProvider("apple")
|
||||
}
|
||||
className={`flex flex-col items-center justify-center p-3 rounded-xl border-2 transition-all ${selectedSomedayProvider === "apple" ? "border-zinc-900 bg-zinc-50 dark:bg-zinc-800" : "border-zinc-100 dark:border-zinc-800 hover:border-zinc-300 dark:hover:border-zinc-600"}`}
|
||||
>
|
||||
<div className="w-6 h-6 flex items-center justify-center mb-1">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
className="w-4 h-4"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.69 3.33 1.66-3.04 1.77-2.54 5.92.51 7.14-.58 1.51-1.32 3.03-2.45 4.21zM11.95 7.21c-.08-2.67 2.2-4.96 4.79-5.11.31 2.94-2.81 5.34-4.79 5.11z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-[10px] font-black uppercase">
|
||||
iCloud
|
||||
</span>
|
||||
</button>
|
||||
{connections?.some(c => c.provider === "google") && (
|
||||
<button
|
||||
onClick={() =>
|
||||
setSelectedSomedayProvider("google")
|
||||
}
|
||||
className={`flex flex-col items-center justify-center p-3 rounded-xl border-2 transition-all ${selectedSomedayProvider === "google" ? "border-blue-500 bg-blue-50 dark:bg-blue-900/20" : "border-zinc-100 dark:border-zinc-800 hover:border-blue-200 dark:hover:border-blue-800/40"}`}
|
||||
>
|
||||
<div className="w-6 h-6 flex items-center justify-center mb-1">
|
||||
<FontAwesomeIcon icon={faGoogle} className="w-4 h-4 text-[#4285F4]" />
|
||||
</div>
|
||||
<span className="text-[10px] font-black uppercase">
|
||||
Google
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
{connections?.some(c => c.provider === "outlook") && (
|
||||
<button
|
||||
onClick={() =>
|
||||
setSelectedSomedayProvider("outlook")
|
||||
}
|
||||
className={`flex flex-col items-center justify-center p-3 rounded-xl border-2 transition-all ${selectedSomedayProvider === "outlook" ? "border-blue-600 bg-blue-100 dark:bg-blue-900/30" : "border-zinc-100 dark:border-zinc-800 hover:border-blue-300 dark:hover:border-blue-800/50"}`}
|
||||
>
|
||||
<div className="w-6 h-6 flex items-center justify-center mb-1">
|
||||
<FontAwesomeIcon icon={faMicrosoft} className="w-4 h-4 text-[#00A4EF]" />
|
||||
</div>
|
||||
<span className="text-[10px] font-black uppercase">
|
||||
Outlook
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
{connections?.some(c => c.provider === "synology") && (
|
||||
<button
|
||||
onClick={() =>
|
||||
setSelectedSomedayProvider("synology")
|
||||
}
|
||||
className={`flex flex-col items-center justify-center p-3 rounded-xl border-2 transition-all ${selectedSomedayProvider === "synology" ? "border-zinc-900 bg-zinc-50 dark:bg-zinc-800" : "border-zinc-100 dark:border-zinc-800 hover:border-zinc-300 dark:hover:border-zinc-600"}`}
|
||||
>
|
||||
<div className="w-6 h-6 flex items-center justify-center mb-1">
|
||||
<FontAwesomeIcon icon={faServer} className="w-4 h-4 text-zinc-500" />
|
||||
</div>
|
||||
<span className="text-[10px] font-black uppercase">
|
||||
Synology
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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<HTMLInputElement>(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 (
|
||||
<li
|
||||
className="weekly-task-item minimal"
|
||||
style={{
|
||||
margin: "0 0.5rem",
|
||||
margin: slotIdx !== undefined ? "0" : "0 0.5rem",
|
||||
listStyle: "none",
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<form
|
||||
@ -6525,7 +6616,8 @@ function SomedayAddTask({
|
||||
onKeyDown={(e) => {
|
||||
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}
|
||||
/>
|
||||
</form>
|
||||
</li>
|
||||
@ -6793,7 +6885,7 @@ function TaskItem({
|
||||
)}
|
||||
<div style={{ width: "100%", position: "relative", transform: swipeX !== 0 ? `translateX(${swipeX}px)` : undefined, transition: swipeX === 0 ? "transform 0.2s ease" : "none", background: "inherit" }}>
|
||||
{/* Visual Indicator for Rolling Tasks */}
|
||||
{task.isRolling && !task.completed && (
|
||||
{task.isRolling && !task.completed && !isSomeday && (
|
||||
<div className="rolling-icon-indicator" title="Auto-rolling task">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
@ -8558,7 +8650,7 @@ function SettingsSidebar({
|
||||
>
|
||||
{([
|
||||
{ key: "general", icon: <Settings size={18} />, label: t.general },
|
||||
{ key: "calendar", icon: <Calendar size={18} />, label: t.calendar },
|
||||
{ key: "calendar", icon: <Link size={18} />, label: t.calendar },
|
||||
{ key: "account", icon: <User size={18} />, label: t.account },
|
||||
{ key: "styling", icon: <Palette size={18} />, label: "Styling" },
|
||||
{ key: "motivation", icon: <Sparkles size={18} />, label: "Motivation" },
|
||||
@ -9393,12 +9485,12 @@ function SettingsSidebar({
|
||||
>
|
||||
<span>
|
||||
{conn.provider === "google"
|
||||
? "📅"
|
||||
? <FontAwesomeIcon icon={faGoogle} />
|
||||
: conn.provider === "apple"
|
||||
? "🍎"
|
||||
? <FontAwesomeIcon icon={faApple} />
|
||||
: conn.provider === "synology"
|
||||
? "💾"
|
||||
: "📧"}
|
||||
? <FontAwesomeIcon icon={faServer} />
|
||||
: <FontAwesomeIcon icon={faMicrosoft} />}
|
||||
</span>
|
||||
{conn.provider === "google"
|
||||
? "Google Calendar"
|
||||
@ -9648,25 +9740,25 @@ function SettingsSidebar({
|
||||
onClick={handleGoogleConnect}
|
||||
className="calendar-connect-btn"
|
||||
>
|
||||
<span>📅</span> {t.connectGoogle}
|
||||
<FontAwesomeIcon icon={faGoogle} className="mr-2" /> {t.connectGoogle}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleAppleCalendarConnect}
|
||||
className="calendar-connect-btn"
|
||||
>
|
||||
<span>🍎</span> {t.connectApple}
|
||||
<FontAwesomeIcon icon={faApple} className="mr-2" /> {t.connectApple}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleOutlookConnect}
|
||||
className="calendar-connect-btn"
|
||||
>
|
||||
<span>📧</span> Connect Outlook
|
||||
<FontAwesomeIcon icon={faMicrosoft} className="mr-2" /> Connect Outlook
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSynologyCalendarConnect}
|
||||
className="calendar-connect-btn"
|
||||
>
|
||||
<span>💾</span> {t.connectSynology || "Connect Synology"}
|
||||
<FontAwesomeIcon icon={faServer} className="mr-2" /> {t.connectSynology || "Connect Synology"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -9721,7 +9813,7 @@ function SettingsSidebar({
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
{conn.provider === "google" ? "📅" : conn.provider === "synology" ? "💾" : "📧"}
|
||||
{conn.provider === "google" ? <FontAwesomeIcon icon={faGoogle} /> : conn.provider === "synology" ? <FontAwesomeIcon icon={faServer} /> : <FontAwesomeIcon icon={faMicrosoft} />}
|
||||
</span>
|
||||
{conn.provider === "google"
|
||||
? "Google Tasks"
|
||||
|
||||
@ -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<SynologyReminderList> => {
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user