diff --git a/package-lock.json b/package-lock.json index 2c1c0ad..62d0a7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", "@headlessui/react": "^2.2.2", "date-fns": "^4.1.0", "lucide-react": "^0.469.0", @@ -50,6 +52,59 @@ "w3c-keyname": "^2.2.4" } }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@esbuild/android-arm": { "version": "0.17.3", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.3.tgz", @@ -2379,8 +2434,7 @@ "node_modules/tslib": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", - "dev": true + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" }, "node_modules/tsutils": { "version": "3.21.0", diff --git a/package.json b/package.json index 3da7625..6d6d9c5 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,8 @@ "typescript": "4.7.4" }, "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", "@headlessui/react": "^2.2.2", "date-fns": "^4.1.0", "lucide-react": "^0.469.0", diff --git a/src/main/components/App.tsx b/src/main/components/App.tsx index 96b4788..fe26810 100644 --- a/src/main/components/App.tsx +++ b/src/main/components/App.tsx @@ -1,3 +1,4 @@ +/* eslint-disable no-mixed-spaces-and-tabs */ import { useEffect, useState } from "react"; import { useTableContext } from "../views/TableView"; import Table from "./Table"; @@ -44,19 +45,30 @@ const App: React.FC = () => { const [dates, setDates] = useState(getWeekDates(new Date())); - const [data, setData] = useState(() => tableContext!.loadData()); + const [data, setData] = useState(() => tableContext?.loadData()); - const [taskStatuses, setTaskStatuses] = useState(getTaskStatuses()); + const [taskStatuses] = useState(getTaskStatuses()); - const [projects, setProjects] = useState( - () => tableContext!.loadData().projects as Project[] - ); - const [nextProjectId, setNextProjectId] = useState( - () => tableContext!.loadData().nextProjectId as number - ); - const [nextTaskId, setNextTaskId] = useState( - () => tableContext!.loadData().nextTaskId as number - ); + type TableData = { + projects: Project[]; + nextProjectId: number; + nextTaskId: number; + }; + + const getInitialData = (): TableData => { + const data = tableContext?.loadData() as TableData | undefined; + return ( + data || { + projects: [], + nextProjectId: 1, + nextTaskId: 1, + } + ); + }; + + const [projects, setProjects] = useState(() => getInitialData().projects); + const [nextProjectId, setNextProjectId] = useState(() => getInitialData().nextProjectId); + const [nextTaskId, setNextTaskId] = useState(() => getInitialData().nextTaskId); const incrementDates = () => { setDates((prevDates) => { @@ -201,13 +213,30 @@ const App: React.FC = () => { }); }; - const saveSpecificData = (key: string, value: any): void => { - setData((prevData: any) => { - const newData = { ...prevData, [key]: value }; + const saveSpecificData = (key: string, value: unknown): void => { + setData((prevData: unknown) => { + const baseData = (typeof prevData === "object" && prevData !== null) ? prevData : {}; + const newData = { ...baseData, [key]: value }; return newData; }); }; + const moveTask = (taskId: number, newDate: string) => { + setProjects((prevProjects) => { + const newProjects = prevProjects.map((project) => ({ + ...project, + tasks: project.tasks.map((task) => + task.id === taskId + ? { ...task, date: new Date(newDate) } + : task + ), + })); + + saveSpecificData("projects", newProjects); + return newProjects; + }); + }; + useEffect(() => { if (tableContext) { tableContext.saveData(data); @@ -234,6 +263,7 @@ const App: React.FC = () => { handleTaskNameChange={handleTaskNameChange} taskStatuses={taskStatuses} editTaskStatus={editTaskStatus} + moveTask={moveTask} /> ); diff --git a/src/main/components/Table.tsx b/src/main/components/Table.tsx index ce23c04..d920191 100644 --- a/src/main/components/Table.tsx +++ b/src/main/components/Table.tsx @@ -3,6 +3,11 @@ import { useEffect, useRef } from "react"; import * as React from "react"; import { Project, TaskStatus } from "./App"; import TaskCell from "./TaskCell"; +import { + DndContext, + useDroppable, + DragEndEvent, +} from "@dnd-kit/core"; interface TableProps { projects: Project[]; @@ -20,6 +25,30 @@ interface TableProps { handleTaskNameChange: (id: number, newName: string) => void; taskStatuses: TaskStatus[]; editTaskStatus: (id: number, newStatusId: string) => void; + moveTask: (taskId: number, newDate: string) => void; +} + +function DroppableCell({ + id, + children, + onClick, +}: { + id: string; + children: React.ReactNode; + onClick?: React.MouseEventHandler; +}) { + const { setNodeRef } = useDroppable({ id }); + + return ( + + {children} + + ); } const Table: React.FC = ({ @@ -35,6 +64,7 @@ const Table: React.FC = ({ handleTaskNameChange, taskStatuses, editTaskStatus, + moveTask, }) => { const containerRef = useRef(null); const newProjectInputRef = useRef(null); @@ -67,119 +97,149 @@ const Table: React.FC = ({ }, [containerRef]); return ( -
-
- - - - - {dates.map((date) => { - const dateObj = parseISO(date); - return ( - + + {projects.map((project) => ( + + + {dates.map((date, index) => ( + { + if ( + e.target === e.currentTarget + ) { + addTaskToProject( + project, + date + ); + setTimeout(() => { + newTaskInputRef.current?.focus(); + }, 0); + } + }} + key={`${project.id}-${date}`} // ADD KEY HERE + > + {project.tasks + .filter( + (task) => + new Date( + task.date + ).toISOString() === + new Date( + date + ).toISOString() + ) + .map((task) => ( + + ))} + + ))} + + ))} + +
Projects -
-
{format(dateObj, "EEEE")}
-
- {format(dateObj, "yyyy-MM-dd")} + { + const { active, over } = event; + if (!over || active.id === over.id) return; + + const taskId = Number(active.id); + const [, newDate] = String(over.id).split("::"); + + moveTask(taskId, newDate); + }} + > +
+
+ + + + + {dates.map((date) => { + const dateObj = parseISO(date); + return ( + - ); - })} - - - - {projects.map((project) => ( - - - {dates.map((date, index) => ( - - ))} + + ); + })} - ))} - -
Projects +
+
+ {format(dateObj, "EEEE")} +
+
+ {format( + dateObj, + "yyyy-MM-dd" + )} +
- -
- { - if (e.target.value === "") - removeProject(project.id); - }} - onChange={(e) => - handleProjectNameChange( - project.id, - e.target.value - ) - } - /> - { - if (e.target === e.currentTarget) { - addTaskToProject(project, date); - setTimeout(() => { - if ( - newTaskInputRef.current - ) { - newTaskInputRef.current.focus(); - } - }, 0); - } - }} - > - {project.tasks - .filter( - (task) => - new Date( - task.date - ).toISOString() === - new Date(date).toISOString() - ) - .map((task) => ( - - ))} -
+
+ { + if (e.target.value === "") + removeProject(project.id); + }} + onChange={(e) => + handleProjectNameChange( + project.id, + e.target.value + ) + } + /> +
+
+
+ { + createNewProject( + e.target.value, + newProjectInputRef + ); + e.target.value = ""; + }} + /> +
-
- { - createNewProject(e.target.value, newProjectInputRef); - e.target.value = ""; - }} - /> -
- + ); }; diff --git a/src/main/components/TaskCell.tsx b/src/main/components/TaskCell.tsx index a176aeb..454dd49 100644 --- a/src/main/components/TaskCell.tsx +++ b/src/main/components/TaskCell.tsx @@ -5,6 +5,7 @@ import { ListboxOption, ListboxOptions, } from "@headlessui/react"; +import { useDraggable } from "@dnd-kit/core"; const transColor = (color: string, percent: number): string => { const num = parseInt(color.replace("#", ""), 16); @@ -55,81 +56,102 @@ const TaskCell: React.FC = ({ taskStatuses, editTaskStatus, }) => { - const taskColor = status.color; + // const taskColor = status.color; const taskBackground = `${status.color}33`; - const darkTaskTextColor = transColor(status.color, -40); + // const darkTaskTextColor = transColor(status.color, -40); const lightTaskTextColor = transColor(status.color, 25); - const lightTaskBackgroundColor = `${transColor(status.color, 25)}33`; + // const lightTaskBackgroundColor = `${transColor(status.color, 25)}33`; - const placeholderColor = hexToRgba(lightTaskTextColor, 0.50); + const placeholderColor = hexToRgba(lightTaskTextColor, 0.5); + + const { attributes, listeners, setNodeRef, transform } = useDraggable({ + id: task.id, + }); return ( -
-
-
- {projectName} -
- editTaskStatus(task.id, value)} - > -
- - {status.name} - - - {taskStatuses.map((s) => ( - - `task-cell-dropdown-option ${active ? "active" : "" - }` - } - > - {s.name} - - ))} - -
-
-
-
- { - if (e.target.value === "") removeTask(task.id); - }} - ref={inputRef} - onChange={(e) => - handleTaskNameChange(task.id, e.target.value) - } - value={task.name} - className={`task-input-${task.id}`} - > - -
-
- ); +
+
+ {/* ✅ Only this part is draggable */} +
+ {projectName} +
+ + editTaskStatus(task.id, value)} + > +
+ + {status.name} + + + {taskStatuses.map((s) => ( + + `task-cell-dropdown-option ${active ? "active" : ""}` + } + > + {s.name} + + ))} + +
+
+
+ +
+ handleTaskNameChange(task.id, e.target.value)} + onBlur={(e) => { + if (e.target.value === "") removeTask(task.id); + }} + ref={inputRef} + className={`task-input-${task.id}`} + placeholder="Task name" + style={{ color: lightTaskTextColor }} + /> + +
+
+); }; export default TaskCell; diff --git a/src/main/views/TableView.tsx b/src/main/views/TableView.tsx index 4005e7a..709599d 100644 --- a/src/main/views/TableView.tsx +++ b/src/main/views/TableView.tsx @@ -5,8 +5,8 @@ import AppComponent from 'src/main/components/App'; export interface AppContext { app: App; - loadData: () => any; - saveData: (data: any) => Promise; + loadData: () => unknown; + saveData: (data: unknown) => Promise; } export const TABLE_VIEW_TYPE = 'table-view'; @@ -20,10 +20,10 @@ export const useTableContext = (): AppContext | undefined => { export class TableView extends ItemView { root: Root | null = null; - loadData: () => any; - saveData: (data: any) => Promise; + loadData: () => unknown; + saveData: (data: unknown) => Promise; - constructor(leaf: WorkspaceLeaf, loadData: () => any, saveData: (data: any) => Promise) { + constructor(leaf: WorkspaceLeaf, loadData: () => unknown, saveData: (data: unknown) => Promise) { super(leaf); this.loadData = loadData; this.saveData = saveData;