From 6d0ac33d45e9d1f7d76ca9fc101a8293ab5828cb Mon Sep 17 00:00:00 2001 From: Pattadon Date: Sun, 26 Nov 2023 01:40:06 +0700 Subject: [PATCH 01/27] Fix order of donut chart data --- frontend/src/components/dashboard/PieChart.jsx | 4 ++-- frontend/src/components/kanbanBoard/taskCard.jsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/dashboard/PieChart.jsx b/frontend/src/components/dashboard/PieChart.jsx index 645137b..ca825bc 100644 --- a/frontend/src/components/dashboard/PieChart.jsx +++ b/frontend/src/components/dashboard/PieChart.jsx @@ -13,8 +13,8 @@ export function DonutChartGraph() { const completedTask = response.data.total_completed_tasks || 0; const donutData = [ - { name: "Completed task", count: totalTask }, - { name: "Total task", count: completedTask }, + { name: "Completed task", count: completedTask }, + { name: "Total task", count: totalTask }, ]; setDonutData(donutData); diff --git a/frontend/src/components/kanbanBoard/taskCard.jsx b/frontend/src/components/kanbanBoard/taskCard.jsx index 1a151c8..86a2036 100644 --- a/frontend/src/components/kanbanBoard/taskCard.jsx +++ b/frontend/src/components/kanbanBoard/taskCard.jsx @@ -60,7 +60,7 @@ export function TaskCard({ task, deleteTask, updateTask }) { setMouseIsOver(false); }}>

document.getElementById(`task_detail_modal_${task.id}`).showModal()}> {task.content}

From 4a3f253e3049f97ef4479dd423642897a56e13fc Mon Sep 17 00:00:00 2001 From: sosokker Date: Sun, 26 Nov 2023 22:28:39 +0700 Subject: [PATCH 02/27] Add placeholder for ui to provide more detail on task card --- frontend/package.json | 1 + frontend/pnpm-lock.yaml | 47 ++++++ .../src/components/kanbanBoard/taskCard.jsx | 147 ++++++++++++++---- .../kanbanBoard/taskDetailModal.jsx | 142 +++++++++++++++-- 4 files changed, 290 insertions(+), 47 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index f057dce..44e4b66 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -42,6 +42,7 @@ "react": "^18.2.0", "react-beautiful-dnd": "^13.1.1", "react-bootstrap": "^2.9.1", + "react-datepicker": "^4.23.0", "react-datetime-picker": "^5.5.3", "react-dom": "^18.2.0", "react-icons": "^4.11.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 22fdadd..2bb6ef0 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -101,6 +101,9 @@ dependencies: react-bootstrap: specifier: ^2.9.1 version: 2.9.1(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + react-datepicker: + specifier: ^4.23.0 + version: 4.23.0(react-dom@18.2.0)(react@18.2.0) react-datetime-picker: specifier: ^5.5.3 version: 5.5.3(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) @@ -3473,6 +3476,22 @@ packages: - '@types/react-dom' dev: false + /react-datepicker@4.23.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-w+msqlOZ14v6H1UknTKtZw/dw9naFMgAOspf59eY130gWpvy5dvKj/bgsFICDdvxB7PtKWxDcbGlAqCloY1d2A==} + peerDependencies: + react: ^16.9.0 || ^17 || ^18 + react-dom: ^16.9.0 || ^17 || ^18 + dependencies: + '@popperjs/core': 2.11.8 + classnames: 2.3.2 + date-fns: 2.30.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-onclickoutside: 6.13.0(react-dom@18.2.0)(react@18.2.0) + react-popper: 2.3.0(@popperjs/core@2.11.8)(react-dom@18.2.0)(react@18.2.0) + dev: false + /react-datetime-picker@5.5.3(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-bWGEPwGrZjaXTB8P4pbTSDygctLaqTWp0nNibaz8po+l4eTh9gv3yiJ+n4NIcpIJDqZaQJO57Bnij2rAFVQyLw==} peerDependencies: @@ -3520,6 +3539,10 @@ packages: scheduler: 0.23.0 dev: false + /react-fast-compare@3.2.2: + resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} + dev: false + /react-fit@1.7.1(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-y/TYovCCBzfIwRJsbLj0rH4Es40wPQhU5GPPq9GlbdF09b0OdzTdMSkBza0QixSlgFzTm6dkM7oTFzaVvaBx+w==} peerDependencies: @@ -3565,6 +3588,30 @@ packages: resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} dev: false + /react-onclickoutside@6.13.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-ty8So6tcUpIb+ZE+1HAhbLROvAIJYyJe/1vRrrcmW+jLsaM+/powDRqxzo6hSh9CuRZGSL1Q8mvcF5WRD93a0A==} + peerDependencies: + react: ^15.5.x || ^16.x || ^17.x || ^18.x + react-dom: ^15.5.x || ^16.x || ^17.x || ^18.x + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /react-popper@2.3.0(@popperjs/core@2.11.8)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==} + peerDependencies: + '@popperjs/core': ^2.0.0 + react: ^16.8.0 || ^17 || ^18 + react-dom: ^16.8.0 || ^17 || ^18 + dependencies: + '@popperjs/core': 2.11.8 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-fast-compare: 3.2.2 + warning: 4.0.3 + dev: false + /react-redux@7.2.9(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==} peerDependencies: diff --git a/frontend/src/components/kanbanBoard/taskCard.jsx b/frontend/src/components/kanbanBoard/taskCard.jsx index e631a26..4858d82 100644 --- a/frontend/src/components/kanbanBoard/taskCard.jsx +++ b/frontend/src/components/kanbanBoard/taskCard.jsx @@ -1,16 +1,14 @@ import { useState } from "react"; -import { useEffect } from "react"; -import { BsFillTrashFill } from "react-icons/bs"; import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { TaskDetailModal } from "./taskDetailModal"; +import { GoChecklist, GoArchive } from "react-icons/go"; export function TaskCard({ task, deleteTask, updateTask }) { + // State to track if the mouse is over the task card const [mouseIsOver, setMouseIsOver] = useState(false); - // console.log(task.challenge); - // console.log(task.importance); - // console.log(task.difficulty); + // DnD Kit hook for sortable items const { setNodeRef, attributes, listeners, transform, transition, isDragging } = useSortable({ id: task.id, data: { @@ -18,68 +16,157 @@ export function TaskCard({ task, deleteTask, updateTask }) { task, }, }); + + // Style for the task card, adjusting for dragging animation const style = { transition, transform: CSS.Transform.toString(transform), }; + // ---- DESC AND TAG ---- */ + // Tags + const tags = + task.tags.length > 0 ? ( +
+ {task.tags.map((tag, index) => ( +
+ {tag.label} +
+ ))} +
+ ) : null; - { - /* If card is dragged */ - } + // difficulty? + const difficultyTag = task.difficulty ? ( + + difficulty + + ) : null; + + // Due Date + const dueDateTag = + task.end_event && new Date(task.end_event) > new Date() + ? (() => { + const daysUntilDue = Math.ceil((new Date(task.end_event) - new Date()) / (1000 * 60 * 60 * 24)); + + let colorClass = + daysUntilDue >= 365 + ? "gray-200" + : daysUntilDue >= 30 + ? "blue-200" + : daysUntilDue >= 7 + ? "green-200" + : daysUntilDue > 0 + ? "yellow-200" + : "red-200"; + + const formattedDueDate = + daysUntilDue >= 365 + ? new Date(task.end_event).toLocaleDateString("en-US", { + day: "numeric", + month: "short", + year: "numeric", + }) + : new Date(task.end_event).toLocaleDateString("en-US", { day: "numeric", month: "short" }); + + return ( + + Due: {formattedDueDate} + + ); + })() + : null; + + // Subtask count + const subtaskCountTag = task.subtaskCount ? ( + + {task.subtaskCount} + + ) : null; + + // ---- DRAG STATE ---- */ + + // If the card is being dragged if (isDragging) { return (
); } + // If the card is not being dragged return (
+ {/* Task Detail Modal */} + + {/* -------- Task Card -------- */}
{ setMouseIsOver(true); }} onMouseLeave={() => { setMouseIsOver(false); }}> -

document.getElementById(`task_detail_modal_${task.id}`).showModal()}> - {task.content} -

- - {mouseIsOver && ( - - )} + {/* -------- Task Content -------- */} + {/* Tags */} + {tags} +
+ {/* Title */} +

document.getElementById(`task_detail_modal_${task.id}`).showModal()}> + {task.content} +

+ {/* -------- Archive Task Button -------- */} + {mouseIsOver && ( + + )} +
+ {/* Description */} +
+ {difficultyTag} + {dueDateTag} + {subtaskCountTag} +
); -} \ No newline at end of file +} diff --git a/frontend/src/components/kanbanBoard/taskDetailModal.jsx b/frontend/src/components/kanbanBoard/taskDetailModal.jsx index d349c3d..e4317e5 100644 --- a/frontend/src/components/kanbanBoard/taskDetailModal.jsx +++ b/frontend/src/components/kanbanBoard/taskDetailModal.jsx @@ -2,14 +2,19 @@ import { useState } from "react"; import { FaTasks, FaRegListAlt } from "react-icons/fa"; import { FaPlus } from "react-icons/fa6"; import { TbChecklist } from "react-icons/tb"; +import DatePicker from "react-datepicker"; +import "react-datepicker/dist/react-datepicker.css"; -export function TaskDetailModal({ title, description, tags, difficulty, challenge, importance, taskId }) { +export function TaskDetailModal({ title, description, tags, difficulty, challenge, importance, taskId, updateTask }) { const [isChallengeChecked, setChallengeChecked] = useState(challenge); const [isImportantChecked, setImportantChecked] = useState(importance); const [currentDifficulty, setCurrentDifficulty] = useState(difficulty); - // console.log(currentDifficulty); - // console.log(isChallengeChecked); - // console.log(isImportantChecked); + const [selectedTags, setSelectedTags] = useState([]); + const [dateStart, setDateStart] = useState(new Date()); + const [dateEnd, setDateEnd] = useState(new Date()); + const [startDateEnabled, setStartDateEnabled] = useState(false); + const [endDateEnabled, setEndDateEnabled] = useState(false); + const [isTaskComplete, setTaskComplete] = useState(false); const handleChallengeChange = () => { setChallengeChecked(!isChallengeChecked); @@ -23,6 +28,51 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng setCurrentDifficulty(parseInt(event.target.value, 10)); }; + const handleTagChange = (tag) => { + const isSelected = selectedTags.includes(tag); + setSelectedTags(isSelected ? selectedTags.filter((selectedTag) => selectedTag !== tag) : [...selectedTags, tag]); + }; + + const handleStartDateChange = () => { + if (!isTaskComplete) { + setStartDateEnabled(!startDateEnabled); + } + }; + + const handleEndDateChange = () => { + if (!isTaskComplete) { + setEndDateEnabled(!endDateEnabled); + } + }; + + const handleTaskCompleteChange = () => { + if (isTaskComplete) { + setTaskComplete(false); + } else { + setTaskComplete(true); + setStartDateEnabled(false); + setEndDateEnabled(false); + } + }; + + // Existing tags + const existingTags = tags.map((tag, index) => ( +
+ {tag.label} +
+ )); + + // Selected tags + const selectedTagElements = selectedTags.map((tag, index) => ( +
+ {tag.label} +
+ )); + return (
@@ -38,7 +88,6 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng

{title}

- {/* Tags */}
@@ -46,19 +95,81 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng -
    -
  • - - - Item 2 - -
  • +
      + {tags.map((tag, index) => ( +
    • + +
    • + ))}
-
+
+ {existingTags} + {selectedTagElements} +
+ + {/* Date Picker */} +
+ {/* Start */} +
+
+

Start At

+
+ +
+ setDateStart(date)} + disabled={!startDateEnabled} + /> +
+
+
+ {/* Complete? */} +
+
+
+

Complete

+ +
+
+
+
+ {/* End */} +
+

End At

+
+ +
+ setDateEnd(date)} disabled={!endDateEnabled} /> +
+
+
- {/* Description */}

@@ -71,7 +182,6 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng {description}

- {/* Difficulty, Challenge, and Importance */}
@@ -123,7 +233,6 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng
- {/* Subtask */}

@@ -140,7 +249,6 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng

-
From 00c68538fc3bcf9edd8da425d3c6085aaa594343 Mon Sep 17 00:00:00 2001 From: Pattadon Date: Mon, 27 Nov 2023 14:24:59 +0700 Subject: [PATCH 03/27] Add react-ios-time-picker package and fix merge conflict in PieChart component --- frontend/package.json | 1 + frontend/pnpm-lock.yaml | 33 ++++++- .../src/components/dashboard/PieChart.jsx | 4 - .../src/components/kanbanBoard/taskCard.jsx | 20 +---- .../kanbanBoard/taskDetailModal.jsx | 87 +++++++++++++++---- 5 files changed, 102 insertions(+), 43 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 44e4b66..b06025e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -46,6 +46,7 @@ "react-datetime-picker": "^5.5.3", "react-dom": "^18.2.0", "react-icons": "^4.11.0", + "react-ios-time-picker": "^0.2.2", "react-router-dom": "^6.18.0", "react-tsparticles": "^2.12.2", "tsparticles": "^2.12.0" diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 2bb6ef0..aca0306 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -1,9 +1,5 @@ lockfileVersion: '6.0' -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - dependencies: '@dnd-kit/core': specifier: ^6.1.0 @@ -113,6 +109,9 @@ dependencies: react-icons: specifier: ^4.11.0 version: 4.12.0(react@18.2.0) + react-ios-time-picker: + specifier: ^0.2.2 + version: 0.2.2(react-dom@18.2.0)(react@18.2.0) react-router-dom: specifier: ^6.18.0 version: 6.19.0(react-dom@18.2.0)(react@18.2.0) @@ -3573,6 +3572,17 @@ packages: react: 18.2.0 dev: false + /react-ios-time-picker@0.2.2(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-bi+K23lK6Pf2xDXmhAlz+RJuy9/onWYi7Ye+ODVhIkis9AVFECOza2ckkZl/4vUypj2+TdTsHn+VZrTNdGIwDQ==} + peerDependencies: + react: ^18.2.0 + react-dom: ^18.2.0 + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-portal: 4.2.2(react-dom@18.2.0)(react@18.2.0) + dev: false + /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -3612,6 +3622,17 @@ packages: warning: 4.0.3 dev: false + /react-portal@4.2.2(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-vS18idTmevQxyQpnde0Td6ZcUlv+pD8GTyR42n3CHUQq9OHi1C4jDE4ZWEbEsrbrLRhSECYiao58cvocwMtP7Q==} + peerDependencies: + react: ^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0 + react-dom: ^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0 + dependencies: + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /react-redux@7.2.9(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==} peerDependencies: @@ -4663,3 +4684,7 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} dev: true + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false diff --git a/frontend/src/components/dashboard/PieChart.jsx b/frontend/src/components/dashboard/PieChart.jsx index 754ac64..c2b94c4 100644 --- a/frontend/src/components/dashboard/PieChart.jsx +++ b/frontend/src/components/dashboard/PieChart.jsx @@ -13,11 +13,7 @@ export function DonutChartGraph() { const completedTask = response.data.total_completed_tasks || 0; const donutData = [ -<<<<<<< HEAD - { name: "Completed task", count: completedTask }, -======= { name: "Completed task", count: completedTask}, ->>>>>>> 4a3f253e3049f97ef4479dd423642897a56e13fc { name: "Total task", count: totalTask }, ]; diff --git a/frontend/src/components/kanbanBoard/taskCard.jsx b/frontend/src/components/kanbanBoard/taskCard.jsx index 13817a8..179ea49 100644 --- a/frontend/src/components/kanbanBoard/taskCard.jsx +++ b/frontend/src/components/kanbanBoard/taskCard.jsx @@ -60,7 +60,7 @@ export function TaskCard({ task, deleteTask, updateTask }) { // Due Date const dueDateTag = task.end_event && new Date(task.end_event) > new Date() - ? (() => { + ? (() => { const daysUntilDue = Math.ceil((new Date(task.end_event) - new Date()) / (1000 * 60 * 60 * 24)); let colorClass = @@ -139,23 +139,6 @@ export function TaskCard({ task, deleteTask, updateTask }) { onMouseLeave={() => { setMouseIsOver(false); }}> -<<<<<<< HEAD -

document.getElementById(`task_detail_modal_${task.id}`).showModal()}> - {task.content} -

- - {mouseIsOver && ( - - )} -======= {/* -------- Task Content -------- */} {/* Tags */} {tags} @@ -183,7 +166,6 @@ export function TaskCard({ task, deleteTask, updateTask }) { {dueDateTag} {subtaskCountTag} ->>>>>>> 4a3f253e3049f97ef4479dd423642897a56e13fc ); diff --git a/frontend/src/components/kanbanBoard/taskDetailModal.jsx b/frontend/src/components/kanbanBoard/taskDetailModal.jsx index e4317e5..707572d 100644 --- a/frontend/src/components/kanbanBoard/taskDetailModal.jsx +++ b/frontend/src/components/kanbanBoard/taskDetailModal.jsx @@ -3,9 +3,21 @@ import { FaTasks, FaRegListAlt } from "react-icons/fa"; import { FaPlus } from "react-icons/fa6"; import { TbChecklist } from "react-icons/tb"; import DatePicker from "react-datepicker"; +import { TimePicker } from "react-ios-time-picker"; import "react-datepicker/dist/react-datepicker.css"; +import { borderColor } from "@mui/system"; -export function TaskDetailModal({ title, description, tags, difficulty, challenge, importance, taskId, updateTask }) { +export function TaskDetailModal({ + title, + description, + tags, + difficulty, + challenge, + importance, + taskId, + updateTask, +}) { + let date = new Date(); const [isChallengeChecked, setChallengeChecked] = useState(challenge); const [isImportantChecked, setImportantChecked] = useState(importance); const [currentDifficulty, setCurrentDifficulty] = useState(difficulty); @@ -15,7 +27,11 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng const [startDateEnabled, setStartDateEnabled] = useState(false); const [endDateEnabled, setEndDateEnabled] = useState(false); const [isTaskComplete, setTaskComplete] = useState(false); + const [value, setValue] = useState('10:00'); + const onChange = (timeValue) => { + setValue(timeValue); + }; const handleChallengeChange = () => { setChallengeChecked(!isChallengeChecked); }; @@ -30,7 +46,11 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng const handleTagChange = (tag) => { const isSelected = selectedTags.includes(tag); - setSelectedTags(isSelected ? selectedTags.filter((selectedTag) => selectedTag !== tag) : [...selectedTags, tag]); + setSelectedTags( + isSelected + ? selectedTags.filter((selectedTag) => selectedTag !== tag) + : [...selectedTags, tag] + ); }; const handleStartDateChange = () => { @@ -59,7 +79,8 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng const existingTags = tags.map((tag, index) => (
+ className={`text-xs inline-flex items-center font-bold leading-sm uppercase px-2 py-1 bg-${tag.color}-200 text-${tag.color}-700 rounded-full`} + > {tag.label}
)); @@ -68,7 +89,8 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng const selectedTagElements = selectedTags.map((tag, index) => (
+ className={`text-xs inline-flex items-center font-bold leading-sm uppercase px-2 py-1 bg-${tag.color}-200 text-${tag.color}-700 rounded-full`} + > {tag.label}
)); @@ -92,10 +114,16 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng
-
+ +
+ +
{/* Complete? */}
@@ -147,7 +188,7 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng
@@ -161,11 +202,19 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng -
- setDateEnd(date)} disabled={!endDateEnabled} /> +
+ setDateEnd(date)} + disabled={!endDateEnabled} + />
@@ -211,7 +260,7 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng @@ -226,7 +275,7 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng @@ -242,7 +291,11 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng
- +
- +
From 0c60f48d1845dad342d9717f87543dccdf00ca27 Mon Sep 17 00:00:00 2001 From: Pattadon Date: Mon, 27 Nov 2023 15:37:39 +0700 Subject: [PATCH 04/27] Refactor navigation links and update profile component --- .../src/components/navigations/Navbar.jsx | 4 +- .../profile/ProfileUpdateComponent.jsx | 50 +++++++--- .../src/components/profile/profilePage.jsx | 92 ++++++++++++++----- 3 files changed, 108 insertions(+), 38 deletions(-) diff --git a/frontend/src/components/navigations/Navbar.jsx b/frontend/src/components/navigations/Navbar.jsx index 22120b3..a31def6 100644 --- a/frontend/src/components/navigations/Navbar.jsx +++ b/frontend/src/components/navigations/Navbar.jsx @@ -39,12 +39,12 @@ export function NavBar() { tabIndex={0} className="mt-3 z-[10] p-2 shadow menu menu-sm dropdown-content bg-base-100 rounded-box w-52">
  • - + Navigate(settings.Profile)} className="justify-between"> Profile
  • - Settings + Navigate(settings.Account)}>Settings
  • Logout diff --git a/frontend/src/components/profile/ProfileUpdateComponent.jsx b/frontend/src/components/profile/ProfileUpdateComponent.jsx index 12f5e98..a38558c 100644 --- a/frontend/src/components/profile/ProfileUpdateComponent.jsx +++ b/frontend/src/components/profile/ProfileUpdateComponent.jsx @@ -1,13 +1,30 @@ import { useState, useRef } from "react"; import { ApiUpdateUserProfile } from "src/api/UserProfileApi"; +import { axiosInstance } from "src/api/AxiosConfig"; +import { useEffect } from "react"; export function ProfileUpdateComponent() { const [file, setFile] = useState(null); - const [username, setUsername] = useState(""); - const [fullName, setFullName] = useState(""); - const [about, setAbout] = useState(""); - const defaultImage = "https://i1.sndcdn.com/artworks-cTz48e4f1lxn5Ozp-L3hopw-t500x500.jpg"; + const [firstName, setFirstName] = useState(""); + const [about, setAbout] = useState(); const fileInputRef = useRef(null); + const [profile_pic, setProfilePic] = useState(undefined); + useEffect(() => { + const fetchUser = async () => { + try { + const response = await axiosInstance.get("/user/data/"); + const fetchedProfilePic = response.data.profile_pic; + const fetchedName = response.data.first_name; + const fetchedAbout = response.data.about; + setProfilePic(fetchedProfilePic); + setAbout(fetchedAbout); + setFirstName(fetchedName); + } catch (error) { + console.error("Error fetching user:", error); + } + }; + fetchUser(); + }, []); const handleImageUpload = () => { if (fileInputRef.current) { @@ -25,7 +42,7 @@ export function ProfileUpdateComponent() { const handleSave = () => { const formData = new FormData(); formData.append("profile_pic", file); - formData.append("first_name", username); + formData.append("first_name", firstName); formData.append("about", about); ApiUpdateUserProfile(formData); @@ -45,12 +62,19 @@ export function ProfileUpdateComponent() { ref={fileInputRef} /> -
    +
    {file ? ( - Profile + Profile ) : ( <> - Default + Default @@ -58,7 +82,7 @@ export function ProfileUpdateComponent() {
    - {/* Username Field */} + {/* Username Field
    setUsername(e.target.value)} /> -
    + */} {/* Full Name Field */}
    - + setFullName(e.target.value)} />
    diff --git a/frontend/src/components/profile/profilePage.jsx b/frontend/src/components/profile/profilePage.jsx index d61368a..31284e3 100644 --- a/frontend/src/components/profile/profilePage.jsx +++ b/frontend/src/components/profile/profilePage.jsx @@ -1,36 +1,63 @@ import { ProfileUpdateComponent } from "./ProfileUpdateComponent"; +import { axiosInstance } from "src/api/AxiosConfig"; +import { useEffect, useState } from "react"; export function ProfileUpdatePage() { + const [profile_pic, setProfilePic] = useState(undefined); + const [about, setAbout] = useState(); + useEffect(() => { + const fetchUser = async () => { + try { + const response = await axiosInstance.get("/user/data/"); + const fetchedProfilePic = response.data.profile_pic; + const fetchedAbout = response.data.about; + setProfilePic(fetchedProfilePic); + setAbout(fetchedAbout); + } catch (error) { + console.error("Error fetching user:", error); + } + }; + fetchUser(); + }, []); return (
    -
    Username
    +
    Firstname
    Sirin
    -
    User ID
    + {/*
    User ID
    */}
    - + Profile Picture
    -
    + {/*
    Health
    234/3213
    - +
    32% Remain
    - -
    - + +
    */} +{/*
    Level
    @@ -40,13 +67,18 @@ export function ProfileUpdatePage() { xmlns="http://www.w3.org/2000/svg" fill="#3abff8" viewBox="0 0 24 24" - className="inline-block w-8 h-8"> + className="inline-block w-8 h-8" + >
    3213/321312321 points
    - +
    @@ -58,34 +90,43 @@ export function ProfileUpdatePage() { xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" - className="inline-block w-8 h-8 stroke-current"> + className="inline-block w-8 h-8 stroke-current" + > + d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" + >
    Top 12% of Global Ranking
    - - + + */}

    About me

    - +
    -
    + {/*
    @@ -110,18 +151,21 @@ export function ProfileUpdatePage() {
    -
    +
    */}
    From cbe1e04da0ddef6598f85e920bd47e7aedf22065 Mon Sep 17 00:00:00 2001 From: Chaiyawut Thengket Date: Mon, 27 Nov 2023 15:51:32 +0700 Subject: [PATCH 06/27] add field --- backend/users/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/users/views.py b/backend/users/views.py index 7446d7b..19db6d2 100644 --- a/backend/users/views.py +++ b/backend/users/views.py @@ -72,4 +72,4 @@ class UserDataRetriveViewset(viewsets.GenericViewSet, mixins.RetrieveModelMixin) def retrieve(self, request, *args, **kwargs): serializer = self.get_serializer(request.user) return Response(serializer.data) - \ No newline at end of file + From 3c74d43f4921144e0644d59b16222ec080c7c6ae Mon Sep 17 00:00:00 2001 From: Chaiyawut Thengket Date: Mon, 27 Nov 2023 15:52:13 +0700 Subject: [PATCH 07/27] add field --- backend/users/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/users/views.py b/backend/users/views.py index 19db6d2..9470dc9 100644 --- a/backend/users/views.py +++ b/backend/users/views.py @@ -63,7 +63,7 @@ class CustomUserProfileUpdate(APIView): return Response(serializer.data) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - + class UserDataRetriveViewset(viewsets.GenericViewSet, mixins.RetrieveModelMixin): queryset = CustomUser.objects.all() permission_classes = (IsAuthenticated,) From c98ff37b1b09bd1f8c90332becdd8424cd0ae743 Mon Sep 17 00:00:00 2001 From: Pattadon Date: Mon, 27 Nov 2023 15:56:44 +0700 Subject: [PATCH 08/27] Fix progress calculation and add totalTaskToday state --- .../src/components/dashboard/dashboard.jsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/dashboard/dashboard.jsx b/frontend/src/components/dashboard/dashboard.jsx index 005f98a..74b4ac8 100644 --- a/frontend/src/components/dashboard/dashboard.jsx +++ b/frontend/src/components/dashboard/dashboard.jsx @@ -28,6 +28,7 @@ export function Dashboard() { const [totalTask, setTotalTask] = useState(0); const [totalCompletedTasks, settotalCompletedTasks] = useState(0); const [totalCompletedTasksToday, setTotalCompletedTasksToday] = useState(0); + const [totalTaskToday, setTotalTaskToday] = useState(0); const [progressData, setProgressData] = useState(0); const [overdueTask, setOverdueTask] = useState(0); @@ -36,19 +37,16 @@ export function Dashboard() { const response = await axiosInstance.get("/dashboard/todostats/"); const totalTaskValue = response.data.total_tasks || 0; const totalCompletedTasksValue = response.data.total_completed_tasks || 0; + const totalTaskTodayValue = response.data.total_task_today || 0; const totalCompletedTasksTodayValue = - response.data.total_completed_tasks_today || 0; - const totalTaskToday = response.data.total_task_today || 0; - const totalCompletedTasksToday = response.data.tasks_completed_today || 0; + response.data.tasks_completed_today || 0; const overdueTasks = response.data.overdue_tasks || 0; - - const progress = - (totalCompletedTasksToday / totalCompletedTasksToday) * 100; + const progress = (totalCompletedTasksToday / totalTaskToday) * 100; setTotalTask(totalTaskValue); settotalCompletedTasks(totalCompletedTasksValue); setTotalCompletedTasksToday(totalCompletedTasksTodayValue); - setTotalTaskToday(totalTaskToday); + setTotalTaskToday(totalTaskTodayValue); setProgressData(progress); setOverdueTask(overdueTasks); }; @@ -147,7 +145,11 @@ export function Dashboard() { Date: Mon, 27 Nov 2023 22:14:57 +0700 Subject: [PATCH 09/27] Fix task can't move to other column --- .../kanbanBoard/columnContainer.jsx | 11 +++ .../components/kanbanBoard/kanbanBoard.jsx | 74 +++---------------- 2 files changed, 20 insertions(+), 65 deletions(-) diff --git a/frontend/src/components/kanbanBoard/columnContainer.jsx b/frontend/src/components/kanbanBoard/columnContainer.jsx index d6f4565..6435d31 100644 --- a/frontend/src/components/kanbanBoard/columnContainer.jsx +++ b/frontend/src/components/kanbanBoard/columnContainer.jsx @@ -9,8 +9,17 @@ export function ColumnContainer({ column, createTask, tasks, deleteTask, updateT return tasks.map((task) => task.id); }, [tasks]); + const { setNodeRef, attributes, listeners, transform, transition, isDragging } = useSortable({ + id: column.id, + data: { + type: "Column", + column, + }, + }); + return (
    {/* Column title */}
    { - const updatedTasks = tasks.map((task) => - task.id === updatedTask.id ? updatedTask : task - ); + const updatedTasks = tasks.map((task) => (task.id === updatedTask.id ? updatedTask : task)); setTasks(updatedTasks); }; @@ -176,14 +168,8 @@ export function KanbanBoard() { justify-center overflow-x-auto overflow-y-hidden - " - > - + "> +
    {!isLoading ? ( @@ -195,9 +181,7 @@ export function KanbanBoard() { createTask={createTask} deleteTask={deleteTask} updateTask={updateTask} - tasks={(tasks || []).filter( - (task) => task.columnId === col.id - )} + tasks={(tasks || []).filter((task) => task.columnId === col.id)} /> ))}{" "} @@ -210,11 +194,7 @@ export function KanbanBoard() { {createPortal( {/* Render the active task as a draggable overlay */} - + , document.body )} @@ -240,8 +220,6 @@ export function KanbanBoard() { if (!over) return; // If not dropped over anything, exit const activeId = active.id; - const overId = over.id; - const isActiveATask = active.data.current?.type === "Task"; const isOverAColumn = over.data.current?.type === "Column"; @@ -250,8 +228,7 @@ export function KanbanBoard() { setTasks((tasks) => { const activeIndex = tasks.findIndex((t) => t.id === activeId); - // Extract the column ID from overId - const columnId = extractColumnId(overId); + const columnId = over.data.current.column.id; tasks[activeIndex].columnId = columnId; @@ -259,7 +236,7 @@ export function KanbanBoard() { axiosInstance .put(`todo/change_task_list_board/`, { todo_id: activeId, - new_list_board_id: over.data.current.task.columnId, + new_list_board_id: columnId, new_index: 0, }) .then((response) => {}) @@ -271,15 +248,6 @@ export function KanbanBoard() { }); } } - - // Helper function to extract the column ID from the element ID - function extractColumnId(elementId) { - // Implement logic to extract the column ID from elementId - // For example, if elementId is in the format "column-123", you can do: - const parts = elementId.split("-"); - return parts.length === 2 ? parseInt(parts[1], 10) : null; - } - // Handle the drag-over event function onDragOver(event) { const { active, over } = event; @@ -306,39 +274,15 @@ export function KanbanBoard() { tasks[activeIndex].columnId = tasks[overIndex].columnId; return arrayMove(tasks, activeIndex, overIndex - 1); } - axiosInstance - .put(`todo/change_task_list_board/`, { - todo_id: activeId, - new_list_board_id: over.data.current.task.columnId, - new_index: 0, - }) - .then((response) => {}) - .catch((error) => { - console.error("Error updating task columnId:", error); - }); return arrayMove(tasks, activeIndex, overIndex); }); } const isOverAColumn = over.data.current?.type === "Column"; // Move the Task to a different column and update columnId - if ( - isActiveATask && - isOverAColumn && - tasks.some((task) => task.columnId !== overId) - ) { + if (isActiveATask && isOverAColumn && tasks.some((task) => task.columnId !== overId)) { setTasks((tasks) => { const activeIndex = tasks.findIndex((t) => t.id === activeId); - axiosInstance - .put(`todo/change_task_list_board/`, { - todo_id: activeId, - new_list_board_id: over.data.current.task.columnId, - new_index: 0, - }) - .then((response) => {}) - .catch((error) => { - console.error("Error updating task columnId:", error); - }); tasks[activeIndex].columnId = overId; return arrayMove(tasks, activeIndex, activeIndex); }); From 2b013cda45bfcca80e14cc40b64ae5767160614e Mon Sep 17 00:00:00 2001 From: sosokker Date: Mon, 27 Nov 2023 22:37:51 +0700 Subject: [PATCH 10/27] Fix move task over task error --- .../kanbanBoard/columnContainer.jsx | 2 +- .../components/kanbanBoard/kanbanBoard.jsx | 24 +++++++++++++++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/kanbanBoard/columnContainer.jsx b/frontend/src/components/kanbanBoard/columnContainer.jsx index 6435d31..f81b694 100644 --- a/frontend/src/components/kanbanBoard/columnContainer.jsx +++ b/frontend/src/components/kanbanBoard/columnContainer.jsx @@ -9,7 +9,7 @@ export function ColumnContainer({ column, createTask, tasks, deleteTask, updateT return tasks.map((task) => task.id); }, [tasks]); - const { setNodeRef, attributes, listeners, transform, transition, isDragging } = useSortable({ + const { setNodeRef, attributes, listeners } = useSortable({ id: column.id, data: { type: "Column", diff --git a/frontend/src/components/kanbanBoard/kanbanBoard.jsx b/frontend/src/components/kanbanBoard/kanbanBoard.jsx index ccfe1b1..0ec4686 100644 --- a/frontend/src/components/kanbanBoard/kanbanBoard.jsx +++ b/frontend/src/components/kanbanBoard/kanbanBoard.jsx @@ -221,17 +221,37 @@ export function KanbanBoard() { const activeId = active.id; const isActiveATask = active.data.current?.type === "Task"; + const isOverATask = over.data.current?.type === "Task"; const isOverAColumn = over.data.current?.type === "Column"; + if (isActiveATask && isOverATask) { + setTasks((tasks) => { + const activeIndex = tasks.findIndex((t) => t.id === activeId); + const columnId = over.data.current.task.columnId; + tasks[activeIndex].columnId = columnId; + // API call to update task's columnId + axiosInstance + .put(`todo/change_task_list_board/`, { + todo_id: activeId, + new_list_board_id: columnId, + new_index: 0, + }) + .then((response) => {}) + .catch((error) => { + console.error("Error updating task columnId:", error); + }); + + return arrayMove(tasks, activeIndex, activeIndex); + }); + } + // Move tasks between columns and update columnId if (isActiveATask && isOverAColumn) { setTasks((tasks) => { const activeIndex = tasks.findIndex((t) => t.id === activeId); - const columnId = over.data.current.column.id; tasks[activeIndex].columnId = columnId; - // API call to update task's columnId axiosInstance .put(`todo/change_task_list_board/`, { From d3a8c90c30a764b89b799153791666678b8a6d6d Mon Sep 17 00:00:00 2001 From: sosokker Date: Tue, 28 Nov 2023 00:33:39 +0700 Subject: [PATCH 11/27] Add subtask api --- backend/tasks/tasks/serializers.py | 14 ++++++- backend/tasks/tasks/views.py | 60 +++++++++++++++++++++++++++++- backend/tasks/urls.py | 3 +- 3 files changed, 72 insertions(+), 5 deletions(-) diff --git a/backend/tasks/tasks/serializers.py b/backend/tasks/tasks/serializers.py index 12bea72..65ea0c2 100644 --- a/backend/tasks/tasks/serializers.py +++ b/backend/tasks/tasks/serializers.py @@ -1,7 +1,7 @@ from rest_framework import serializers from users.models import CustomUser from boards.models import ListBoard -from tasks.models import Todo, RecurrenceTask, Habit +from tasks.models import Todo, RecurrenceTask, Habit, Subtask class TaskSerializer(serializers.ModelSerializer): class Meta: @@ -97,4 +97,14 @@ class HabitTaskSerializer(serializers.ModelSerializer): class HabitTaskCreateSerializer(serializers.ModelSerializer): class Meta: model = Habit - exclude = ('tags',) \ No newline at end of file + exclude = ('tags',) + + +class SubTaskSerializer(serializers.ModelSerializer): + class Meta: + model = Subtask + fields = '__all__' + + def create(self, validated_data): + # Create a new task with validated data + return Todo.objects.create(**validated_data) \ No newline at end of file diff --git a/backend/tasks/tasks/views.py b/backend/tasks/tasks/views.py index fe1c82d..18a3410 100644 --- a/backend/tasks/tasks/views.py +++ b/backend/tasks/tasks/views.py @@ -4,10 +4,13 @@ from rest_framework import viewsets, status, serializers from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response +from rest_framework import mixins -from .serializers import ChangeTaskListBoardSerializer, ChangeTaskOrderSerializer +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter + +from tasks.tasks.serializers import ChangeTaskListBoardSerializer, ChangeTaskOrderSerializer, SubTaskSerializer from boards.models import ListBoard, KanbanTaskOrder -from tasks.models import Todo, RecurrenceTask, Habit +from tasks.models import Todo, RecurrenceTask, Habit, Subtask from tasks.tasks.serializers import (TaskCreateSerializer, TaskSerializer, RecurrenceTaskSerializer, @@ -117,6 +120,59 @@ class TodoViewSet(viewsets.ModelViewSet): return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) +@extend_schema_view( +list=extend_schema( + parameters=[ + OpenApiParameter(name='parent_task', description='Parent Task ID', type=int), + ] + ) +) +class SubTaskViewset(viewsets.GenericViewSet, + mixins.CreateModelMixin, + mixins.DestroyModelMixin, + mixins.ListModelMixin): + queryset = Subtask.objects.all() + permission_classes = (IsAuthenticated,) + + def get_serializer_class(self): + return SubTaskSerializer + + def list(self, request, *args, **kwargs): + """List only subtask of parent task.""" + try: + parent_task = request.query_params.get('parent_task') + if not parent_task: + raise serializers.ValidationError('parent_task is required.') + queryset = self.get_queryset().filter(parent_task_id=parent_task) + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data) + + except Exception as e: + return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + def create(self, request, *args, **kwargs): + """Create a new subtask, point to some parent tasks.""" + try: + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + self.perform_create(serializer) + return Response(serializer.data) + + except Exception as e: + return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + def destroy(self, request, *args, **kwargs): + """Delete a subtask.""" + try: + instance = self.get_object() + self.perform_destroy(instance) + return Response(status=status.HTTP_204_NO_CONTENT) + + except Exception as e: + return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + class RecurrenceTaskViewSet(viewsets.ModelViewSet): queryset = RecurrenceTask.objects.all() serializer_class = RecurrenceTaskSerializer diff --git a/backend/tasks/urls.py b/backend/tasks/urls.py index d830a65..cf37e23 100644 --- a/backend/tasks/urls.py +++ b/backend/tasks/urls.py @@ -3,7 +3,7 @@ from django.urls import path, include from rest_framework.routers import DefaultRouter from tasks.api import GoogleCalendarEventViewset -from tasks.tasks.views import TodoViewSet, RecurrenceTaskViewSet, HabitTaskViewSet +from tasks.tasks.views import TodoViewSet, RecurrenceTaskViewSet, HabitTaskViewSet, SubTaskViewset from tasks.misc.views import TagViewSet @@ -13,6 +13,7 @@ router.register(r'daily', RecurrenceTaskViewSet) router.register(r'habit', HabitTaskViewSet) router.register(r'tags', TagViewSet) router.register(r'calendar-events', GoogleCalendarEventViewset, basename='calendar-events') +router.register(r'subtasks', SubTaskViewset, basename='subtasks') urlpatterns = [ path('', include(router.urls)), From f9e1250c566cb8b93c993853e86be0313c3c18d0 Mon Sep 17 00:00:00 2001 From: sosokker Date: Tue, 28 Nov 2023 00:43:37 +0700 Subject: [PATCH 12/27] Add subtasks api in react --- frontend/src/api/SubTaskApi.jsx | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 frontend/src/api/SubTaskApi.jsx diff --git a/frontend/src/api/SubTaskApi.jsx b/frontend/src/api/SubTaskApi.jsx new file mode 100644 index 0000000..11a556f --- /dev/null +++ b/frontend/src/api/SubTaskApi.jsx @@ -0,0 +1,33 @@ +import { axiosInstance } from "./AxiosConfig"; + +export const getSubtasks = async (parentTaskId) => { + try { + const response = await axiosInstance.get(`subtasks?parent_task=${parentTaskId}`); + return response.data; + } catch (error) { + console.error("Error fetching subtasks:", error); + throw error; + } +}; + +export const addSubtask = async (parentTaskId, text) => { + try { + const response = await axiosInstance.post("subtasks/", { + text, + parent_task: parentTaskId, + }); + return response.data; + } catch (error) { + console.error("Error adding subtask:", error); + throw error; + } +}; + +export const deleteSubtask = async (subtaskId) => { + try { + await axiosInstance.delete(`subtasks/${subtaskId}/`); + } catch (error) { + console.error("Error deleting subtask:", error); + throw error; + } +}; From cfccebf18903e126590008abe991dd8eeea0d11f Mon Sep 17 00:00:00 2001 From: sosokker Date: Tue, 28 Nov 2023 01:20:01 +0700 Subject: [PATCH 13/27] Incorrect model in subtask serializer and add PUT PATCH for subtask --- backend/tasks/tasks/serializers.py | 2 +- backend/tasks/tasks/views.py | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/backend/tasks/tasks/serializers.py b/backend/tasks/tasks/serializers.py index 65ea0c2..d7249c6 100644 --- a/backend/tasks/tasks/serializers.py +++ b/backend/tasks/tasks/serializers.py @@ -107,4 +107,4 @@ class SubTaskSerializer(serializers.ModelSerializer): def create(self, validated_data): # Create a new task with validated data - return Todo.objects.create(**validated_data) \ No newline at end of file + return Subtask.objects.create(**validated_data) \ No newline at end of file diff --git a/backend/tasks/tasks/views.py b/backend/tasks/tasks/views.py index 18a3410..545320a 100644 --- a/backend/tasks/tasks/views.py +++ b/backend/tasks/tasks/views.py @@ -130,7 +130,8 @@ list=extend_schema( class SubTaskViewset(viewsets.GenericViewSet, mixins.CreateModelMixin, mixins.DestroyModelMixin, - mixins.ListModelMixin): + mixins.ListModelMixin, + mixins.UpdateModelMixin): queryset = Subtask.objects.all() permission_classes = (IsAuthenticated,) @@ -173,6 +174,19 @@ class SubTaskViewset(viewsets.GenericViewSet, return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + def partial_update(self, request, *args, **kwargs): + """Update a subtask.""" + try: + instance = self.get_o + serializer = self.get_serializer(instance, data=request.data, partial=True) + serializer.is_valid(raise_exception=True) + self.perform_update(serializer) + return Response(serializer.data) + + except Exception as e: + return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + class RecurrenceTaskViewSet(viewsets.ModelViewSet): queryset = RecurrenceTask.objects.all() serializer_class = RecurrenceTaskSerializer From 97e4cc5a0dae8e34039a60f766968f37892e5620 Mon Sep 17 00:00:00 2001 From: sosokker Date: Tue, 28 Nov 2023 01:26:40 +0700 Subject: [PATCH 14/27] Fix subtask viewset typo error --- backend/tasks/tasks/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/tasks/tasks/views.py b/backend/tasks/tasks/views.py index 545320a..20c4835 100644 --- a/backend/tasks/tasks/views.py +++ b/backend/tasks/tasks/views.py @@ -177,7 +177,7 @@ class SubTaskViewset(viewsets.GenericViewSet, def partial_update(self, request, *args, **kwargs): """Update a subtask.""" try: - instance = self.get_o + instance = self.get_object() serializer = self.get_serializer(instance, data=request.data, partial=True) serializer.is_valid(raise_exception=True) self.perform_update(serializer) From 3a7437aa099218b9950ef9bd8a3e322ba783aad9 Mon Sep 17 00:00:00 2001 From: sosokker Date: Tue, 28 Nov 2023 01:28:06 +0700 Subject: [PATCH 15/27] Add subtask CRUD and axios for it --- frontend/src/api/SubTaskApi.jsx | 19 ++- .../kanbanBoard/taskDetailModal.jsx | 145 +++++++++++------- 2 files changed, 103 insertions(+), 61 deletions(-) diff --git a/frontend/src/api/SubTaskApi.jsx b/frontend/src/api/SubTaskApi.jsx index 11a556f..1458153 100644 --- a/frontend/src/api/SubTaskApi.jsx +++ b/frontend/src/api/SubTaskApi.jsx @@ -1,6 +1,6 @@ import { axiosInstance } from "./AxiosConfig"; -export const getSubtasks = async (parentTaskId) => { +export const getSubtask = async (parentTaskId) => { try { const response = await axiosInstance.get(`subtasks?parent_task=${parentTaskId}`); return response.data; @@ -10,10 +10,11 @@ export const getSubtasks = async (parentTaskId) => { } }; -export const addSubtask = async (parentTaskId, text) => { +export const addSubtasks = async (parentTaskId, text) => { try { const response = await axiosInstance.post("subtasks/", { - text, + description: text, + completed: false, parent_task: parentTaskId, }); return response.data; @@ -23,7 +24,7 @@ export const addSubtask = async (parentTaskId, text) => { } }; -export const deleteSubtask = async (subtaskId) => { +export const deleteSubtasks = async (subtaskId) => { try { await axiosInstance.delete(`subtasks/${subtaskId}/`); } catch (error) { @@ -31,3 +32,13 @@ export const deleteSubtask = async (subtaskId) => { throw error; } }; + +export const updateSubtask = async (subtaskId, data) => { + try { + const response = await axiosInstance.patch(`subtasks/${subtaskId}/`, data); + return response.data; + } catch (error) { + console.error("Error updating subtask:", error); + throw error; + } +}; diff --git a/frontend/src/components/kanbanBoard/taskDetailModal.jsx b/frontend/src/components/kanbanBoard/taskDetailModal.jsx index 707572d..ddae1cf 100644 --- a/frontend/src/components/kanbanBoard/taskDetailModal.jsx +++ b/frontend/src/components/kanbanBoard/taskDetailModal.jsx @@ -1,23 +1,13 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { FaTasks, FaRegListAlt } from "react-icons/fa"; -import { FaPlus } from "react-icons/fa6"; +import { FaPlus, FaRegTrashCan } from "react-icons/fa6"; import { TbChecklist } from "react-icons/tb"; import DatePicker from "react-datepicker"; import { TimePicker } from "react-ios-time-picker"; import "react-datepicker/dist/react-datepicker.css"; -import { borderColor } from "@mui/system"; +import { addSubtasks, deleteSubtasks, getSubtask, updateSubtask } from "src/api/SubTaskApi"; -export function TaskDetailModal({ - title, - description, - tags, - difficulty, - challenge, - importance, - taskId, - updateTask, -}) { - let date = new Date(); +export function TaskDetailModal({ title, description, tags, difficulty, challenge, importance, taskId, updateTask }) { const [isChallengeChecked, setChallengeChecked] = useState(challenge); const [isImportantChecked, setImportantChecked] = useState(importance); const [currentDifficulty, setCurrentDifficulty] = useState(difficulty); @@ -27,7 +17,9 @@ export function TaskDetailModal({ const [startDateEnabled, setStartDateEnabled] = useState(false); const [endDateEnabled, setEndDateEnabled] = useState(false); const [isTaskComplete, setTaskComplete] = useState(false); - const [value, setValue] = useState('10:00'); + const [value, setValue] = useState("10:00"); + const [subtaskText, setSubtaskText] = useState(""); + const [subtasks, setSubtasks] = useState([]); const onChange = (timeValue) => { setValue(timeValue); @@ -46,11 +38,7 @@ export function TaskDetailModal({ const handleTagChange = (tag) => { const isSelected = selectedTags.includes(tag); - setSelectedTags( - isSelected - ? selectedTags.filter((selectedTag) => selectedTag !== tag) - : [...selectedTags, tag] - ); + setSelectedTags(isSelected ? selectedTags.filter((selectedTag) => selectedTag !== tag) : [...selectedTags, tag]); }; const handleStartDateChange = () => { @@ -75,12 +63,73 @@ export function TaskDetailModal({ } }; + const addSubtask = async () => { + try { + if (subtaskText.trim() !== "") { + const newSubtask = await addSubtasks(taskId, subtaskText.trim()); + setSubtasks([...subtasks, newSubtask]); + setSubtaskText(""); + } + } catch (error) { + console.error("Error adding subtask:", error); + } + }; + + const toggleSubtaskCompletion = async (index) => { + try { + const updatedSubtasks = [...subtasks]; + updatedSubtasks[index].completed = !updatedSubtasks[index].completed; + await updateSubtask(updatedSubtasks[index].id, { completed: updatedSubtasks[index].completed }); + setSubtasks(updatedSubtasks); + } catch (error) { + console.error("Error updating subtask:", error); + } + }; + + const deleteSubtask = async (index) => { + try { + await deleteSubtasks(subtasks[index].id); + const updatedSubtasks = [...subtasks]; + updatedSubtasks.splice(index, 1); + setSubtasks(updatedSubtasks); + } catch (error) { + console.error("Error deleting subtask:", error); + } + }; + + const subtaskElements = subtasks.map((subtask, index) => ( +
    + toggleSubtaskCompletion(index)} + /> +
    + {subtask.description} + deleteSubtask(index)} /> +
    +
    + )); + + useEffect(() => { + const fetchSubtasks = async () => { + try { + const fetchedSubtasks = await getSubtask(taskId); + setSubtasks(fetchedSubtasks); + } catch (error) { + console.error("Error fetching subtasks:", error); + } + }; + + fetchSubtasks(); + }, [taskId]); + // Existing tags const existingTags = tags.map((tag, index) => (
    + className={`text-xs inline-flex items-center font-bold leading-sm uppercase px-2 py-1 bg-${tag.color}-200 text-${tag.color}-700 rounded-full`}> {tag.label}
    )); @@ -89,8 +138,7 @@ export function TaskDetailModal({ const selectedTagElements = selectedTags.map((tag, index) => (
    + className={`text-xs inline-flex items-center font-bold leading-sm uppercase px-2 py-1 bg-${tag.color}-200 text-${tag.color}-700 rounded-full`}> {tag.label}
    )); @@ -114,16 +162,10 @@ export function TaskDetailModal({
    -
    +
    - -
    @@ -202,19 +239,11 @@ export function TaskDetailModal({ -
    - setDateEnd(date)} - disabled={!endDateEnabled} - /> +
    + setDateEnd(date)} disabled={!endDateEnabled} />
    @@ -295,17 +324,19 @@ export function TaskDetailModal({ type="text" placeholder="subtask topic" className="input input-bordered flex-1 w-full" + value={subtaskText} + onChange={(e) => setSubtaskText(e.target.value)} /> -
    + {/* Display Subtasks */} +
    {subtaskElements}
    - +
    From 22e2521413936bb4543b6bca398959a937a10ff3 Mon Sep 17 00:00:00 2001 From: Pattadon Date: Tue, 28 Nov 2023 10:27:14 +0700 Subject: [PATCH 16/27] Add profile picture to navbar --- .../src/components/navigations/Navbar.jsx | 42 ++++++++++++++++--- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/navigations/Navbar.jsx b/frontend/src/components/navigations/Navbar.jsx index a31def6..dadb503 100644 --- a/frontend/src/components/navigations/Navbar.jsx +++ b/frontend/src/components/navigations/Navbar.jsx @@ -1,6 +1,8 @@ import { useNavigate } from "react-router-dom"; import { apiUserLogout } from "src/api/AuthenticationApi"; import { useAuth } from "src/hooks/AuthHooks"; +import { axiosInstance } from "src/api/AxiosConfig"; +import { useEffect, useState } from "react"; const settings = { Profile: "/profile", @@ -10,6 +12,7 @@ const settings = { export function NavBar() { const Navigate = useNavigate(); const { isAuthenticated, setIsAuthenticated } = useAuth(); + const [profile_pic, setProfilePic] = useState(undefined); const logout = () => { apiUserLogout(); @@ -17,6 +20,25 @@ export function NavBar() { Navigate("/"); }; + useEffect(() => { + const fetchUser = async () => { + if (isAuthenticated) { + try { + const response = await axiosInstance.get("/user/data/"); + const fetchedProfilePic = response.data.profile_pic; + setProfilePic(fetchedProfilePic); + } catch (error) { + console.error("Error fetching user:", error); + } + } else { + setProfilePic( + "https://upload.wikimedia.org/wikipedia/commons/8/89/Portrait_Placeholder.png" + ); + } + }; + fetchUser(); + }, []); + return (
    @@ -32,14 +54,18 @@ export function NavBar() { ) : (
    - -
    From 34cc54f8a7b64eb5d2252fa219b018ef994689ab Mon Sep 17 00:00:00 2001 From: Pattadon Date: Tue, 28 Nov 2023 10:45:57 +0700 Subject: [PATCH 17/27] Update profile page with first name and about me fields --- frontend/src/components/profile/profilePage.jsx | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/profile/profilePage.jsx b/frontend/src/components/profile/profilePage.jsx index 31284e3..b48912a 100644 --- a/frontend/src/components/profile/profilePage.jsx +++ b/frontend/src/components/profile/profilePage.jsx @@ -5,14 +5,18 @@ import { useEffect, useState } from "react"; export function ProfileUpdatePage() { const [profile_pic, setProfilePic] = useState(undefined); const [about, setAbout] = useState(); + const [firstName, setFirstname] = useState(); useEffect(() => { const fetchUser = async () => { try { const response = await axiosInstance.get("/user/data/"); const fetchedProfilePic = response.data.profile_pic; const fetchedAbout = response.data.about; + console.log(fetchedAbout); + const fetchedFirstname = response.data.first_name; setProfilePic(fetchedProfilePic); setAbout(fetchedAbout); + setFirstname(fetchedFirstname); } catch (error) { console.error("Error fetching user:", error); } @@ -24,7 +28,7 @@ export function ProfileUpdatePage() {
    Firstname
    -
    Sirin
    +
    {firstName}
    {/*
    User ID
    */}
    @@ -57,7 +61,7 @@ export function ProfileUpdatePage() { max="100" >
    */} -{/* + {/*
    Level
    @@ -118,14 +122,11 @@ export function ProfileUpdatePage() { - + placeholder="Enter your about me" + value={about} + >
    - {/*
    From 06b188752f45e51d9047c91e6e2b90e407aa3f14 Mon Sep 17 00:00:00 2001 From: Pattadon Date: Tue, 28 Nov 2023 10:59:05 +0700 Subject: [PATCH 18/27] Update profile page component to display username instead of first name --- frontend/src/components/profile/profilePage.jsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/profile/profilePage.jsx b/frontend/src/components/profile/profilePage.jsx index b48912a..b544323 100644 --- a/frontend/src/components/profile/profilePage.jsx +++ b/frontend/src/components/profile/profilePage.jsx @@ -5,18 +5,17 @@ import { useEffect, useState } from "react"; export function ProfileUpdatePage() { const [profile_pic, setProfilePic] = useState(undefined); const [about, setAbout] = useState(); - const [firstName, setFirstname] = useState(); + const [username, setUsernames] = useState(); useEffect(() => { const fetchUser = async () => { try { const response = await axiosInstance.get("/user/data/"); const fetchedProfilePic = response.data.profile_pic; const fetchedAbout = response.data.about; - console.log(fetchedAbout); - const fetchedFirstname = response.data.first_name; + const fetchedUsernames = response.data.username; setProfilePic(fetchedProfilePic); setAbout(fetchedAbout); - setFirstname(fetchedFirstname); + setUsernames(fetchedUsernames); } catch (error) { console.error("Error fetching user:", error); } @@ -27,8 +26,8 @@ export function ProfileUpdatePage() {
    -
    Firstname
    -
    {firstName}
    +
    Username
    +
    {username}
    {/*
    User ID
    */}
    From 6a583dee903d2ea32d05a986592e1bb8803435d0 Mon Sep 17 00:00:00 2001 From: sosokker Date: Tue, 28 Nov 2023 11:01:10 +0700 Subject: [PATCH 19/27] Give sub_task_count to frontend --- backend/tasks/misc/views.py | 4 +++- backend/tasks/tasks/serializers.py | 9 +++++++++ backend/tasks/tasks/views.py | 12 ++++++++++++ frontend/src/components/kanbanBoard/kanbanBoard.jsx | 1 + 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/backend/tasks/misc/views.py b/backend/tasks/misc/views.py index 3fbc837..8f00572 100644 --- a/backend/tasks/misc/views.py +++ b/backend/tasks/misc/views.py @@ -1,8 +1,10 @@ from rest_framework import viewsets +from rest_framework.permissions import IsAuthenticated from ..models import Tag from .serializers import TagSerializer class TagViewSet(viewsets.ModelViewSet): queryset = Tag.objects.all() - serializer_class = TagSerializer \ No newline at end of file + serializer_class = TagSerializer + permission_classes = (IsAuthenticated,) \ No newline at end of file diff --git a/backend/tasks/tasks/serializers.py b/backend/tasks/tasks/serializers.py index d7249c6..9a56632 100644 --- a/backend/tasks/tasks/serializers.py +++ b/backend/tasks/tasks/serializers.py @@ -4,6 +4,9 @@ from boards.models import ListBoard from tasks.models import Todo, RecurrenceTask, Habit, Subtask class TaskSerializer(serializers.ModelSerializer): + tags = serializers.SerializerMethodField() + sub_task_count = serializers.SerializerMethodField() + class Meta: model = Todo fields = '__all__' @@ -19,6 +22,12 @@ class TaskSerializer(serializers.ModelSerializer): validated_data['user'] = user return Todo.objects.create(**validated_data) + def get_tags(self, instance): + return [tag.name for tag in instance.tags.all()] + + def get_sub_task_count(self, instance): + return instance.subtask_set.count() + class TaskCreateSerializer(serializers.ModelSerializer): class Meta: model = Todo diff --git a/backend/tasks/tasks/views.py b/backend/tasks/tasks/views.py index 20c4835..bd5d568 100644 --- a/backend/tasks/tasks/views.py +++ b/backend/tasks/tasks/views.py @@ -35,6 +35,18 @@ class TodoViewSet(viewsets.ModelViewSet): return TaskCreateSerializer return TaskSerializer + def list(self, request, *args, **kwargs): + """ + list all tasks of the authenticated + user and send tags if those Todo too. + """ + try: + queryset = self.get_queryset() + serializer = TaskSerializer(queryset, many=True) + return Response(serializer.data) + except Exception as e: + return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + def create(self, request, *args, **kwargs): try: new_task_data = request.data diff --git a/frontend/src/components/kanbanBoard/kanbanBoard.jsx b/frontend/src/components/kanbanBoard/kanbanBoard.jsx index 0ec4686..0a9049d 100644 --- a/frontend/src/components/kanbanBoard/kanbanBoard.jsx +++ b/frontend/src/components/kanbanBoard/kanbanBoard.jsx @@ -121,6 +121,7 @@ export function KanbanBoard() { user: task.user, list_board: task.list_board, tags: task.tags, + subtaskCount: task.sub_task_count, })); setTasks(transformedTasks); From 1876569ec10ef89dd1dd5b6ab3dea1ca45b24a36 Mon Sep 17 00:00:00 2001 From: sosokker Date: Tue, 28 Nov 2023 11:26:10 +0700 Subject: [PATCH 20/27] Fix error when not upload image / Use username instead of first name in profile --- backend/users/serializers.py | 24 +++++++++++++++-- backend/users/views.py | 6 ++++- .../profile/ProfileUpdateComponent.jsx | 27 +++++++------------ 3 files changed, 37 insertions(+), 20 deletions(-) diff --git a/backend/users/serializers.py b/backend/users/serializers.py index 962d789..f7ed6fa 100644 --- a/backend/users/serializers.py +++ b/backend/users/serializers.py @@ -32,12 +32,32 @@ class UpdateProfileSerializer(serializers.ModelSerializer): Serializer for updating user profile. """ profile_pic = serializers.ImageField(required=False) - first_name = serializers.CharField(max_length=255, required=False) + username = serializers.CharField(max_length=255, required=False) about = serializers.CharField(required=False) class Meta: model = CustomUser - fields = ('profile_pic', 'first_name', 'about') + fields = ('profile_pic', 'username', 'about') + + def update(self, instance, validated_data): + """ + Update an existing user's profile. + """ + for attr, value in validated_data.items(): + setattr(instance, attr, value) + instance.save() + return instance + +class UpdateProfileNopicSerializer(serializers.ModelSerializer): + """ + Serializer for updating user profile. + """ + username = serializers.CharField(max_length=255, required=False) + about = serializers.CharField(required=False) + + class Meta: + model = CustomUser + fields = ('username', 'about') def update(self, instance, validated_data): """ diff --git a/backend/users/views.py b/backend/users/views.py index 9470dc9..0157c0c 100644 --- a/backend/users/views.py +++ b/backend/users/views.py @@ -9,7 +9,7 @@ from rest_framework.parsers import MultiPartParser from rest_framework_simplejwt.tokens import RefreshToken -from users.serializers import CustomUserSerializer, UpdateProfileSerializer +from users.serializers import CustomUserSerializer, UpdateProfileSerializer, UpdateProfileNopicSerializer from users.models import CustomUser class CustomUserCreate(APIView): @@ -57,7 +57,11 @@ class CustomUserProfileUpdate(APIView): return Response ({ 'error': 'User does not exist' }, status=status.HTTP_404_NOT_FOUND) + serializer = UpdateProfileSerializer(request.user, data=request.data) + if request.data.get('profile_pic') == "null": + serializer = UpdateProfileNopicSerializer(request.user, data=request.data) + if serializer.is_valid(): serializer.save() return Response(serializer.data) diff --git a/frontend/src/components/profile/ProfileUpdateComponent.jsx b/frontend/src/components/profile/ProfileUpdateComponent.jsx index ce3758c..6a918af 100644 --- a/frontend/src/components/profile/ProfileUpdateComponent.jsx +++ b/frontend/src/components/profile/ProfileUpdateComponent.jsx @@ -5,7 +5,7 @@ import { useEffect } from "react"; export function ProfileUpdateComponent() { const [file, setFile] = useState(null); - const [firstName, setFirstName] = useState(""); + const [username, setUserName] = useState(""); const [about, setAbout] = useState(); const fileInputRef = useRef(null); const [profile_pic, setProfilePic] = useState(undefined); @@ -14,11 +14,11 @@ export function ProfileUpdateComponent() { try { const response = await axiosInstance.get("/user/data/"); const fetchedProfilePic = response.data.profile_pic; - const fetchedName = response.data.first_name; + const fetchedName = response.data.username; const fetchedAbout = response.data.about; setProfilePic(fetchedProfilePic); setAbout(fetchedAbout); - setFirstName(fetchedName); + setUserName(fetchedName); } catch (error) { console.error("Error fetching user:", error); } @@ -42,7 +42,7 @@ export function ProfileUpdateComponent() { const handleSave = () => { const formData = new FormData(); formData.append("profile_pic", file); - formData.append("first_name", firstName); + formData.append("username", username); formData.append("about", about); ApiUpdateUserProfile(formData); @@ -62,16 +62,9 @@ export function ProfileUpdateComponent() { ref={fileInputRef} /> -
    +
    {file ? ( - Profile + Profile ) : ( <> Default @@ -96,13 +89,13 @@ export function ProfileUpdateComponent() { {/* Full Name Field */}
    - + setFirstName(e.target.value)} + value={username} + onChange={(e) => setUserName(e.target.value)} />
    From 275738d902583803fd8614a8fbd0a5a55f2ce667 Mon Sep 17 00:00:00 2001 From: sosokker Date: Tue, 28 Nov 2023 11:27:06 +0700 Subject: [PATCH 21/27] Remove console.log from userAPI --- frontend/src/api/UserProfileApi.jsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/src/api/UserProfileApi.jsx b/frontend/src/api/UserProfileApi.jsx index 6dfc050..1386e33 100644 --- a/frontend/src/api/UserProfileApi.jsx +++ b/frontend/src/api/UserProfileApi.jsx @@ -11,8 +11,6 @@ const ApiUpdateUserProfile = async (formData) => { }, }); - console.log(response.data); - return response.data; } catch (error) { console.error("Error updating user profile:", error); From 5704d7c9171f3fe2ec034804601033688008a7ef Mon Sep 17 00:00:00 2001 From: Pattadon Date: Tue, 28 Nov 2023 11:45:23 +0700 Subject: [PATCH 22/27] Refactor EisenhowerMatrix component --- .../EisenhowerMatrix/Eisenhower.jsx | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/EisenhowerMatrix/Eisenhower.jsx b/frontend/src/components/EisenhowerMatrix/Eisenhower.jsx index cc19aaa..9954950 100644 --- a/frontend/src/components/EisenhowerMatrix/Eisenhower.jsx +++ b/frontend/src/components/EisenhowerMatrix/Eisenhower.jsx @@ -1,5 +1,10 @@ import { useState, useEffect } from "react"; -import { FiAlertCircle, FiClock, FiXCircle, FiCheckCircle } from "react-icons/fi"; +import { + FiAlertCircle, + FiClock, + FiXCircle, + FiCheckCircle, +} from "react-icons/fi"; import { readTodoTasks } from "../../api/TaskApi"; import { axiosInstance } from "src/api/AxiosConfig"; @@ -26,7 +31,9 @@ function EachBlog({ name, colorCode, contentList, icon }) { }; return ( -
    +
    {icon} {name} @@ -39,10 +46,14 @@ function EachBlog({ name, colorCode, contentList, icon }) { handleCheckboxChange(index)} /> -
    From c2824ec383897c97701b2cc1171267ac8f53d112 Mon Sep 17 00:00:00 2001 From: sosokker Date: Tue, 28 Nov 2023 11:52:03 +0700 Subject: [PATCH 23/27] Calendar can now only view and delete task --- frontend/src/components/calendar/calendar.jsx | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/frontend/src/components/calendar/calendar.jsx b/frontend/src/components/calendar/calendar.jsx index 79dae97..c03637a 100644 --- a/frontend/src/components/calendar/calendar.jsx +++ b/frontend/src/components/calendar/calendar.jsx @@ -5,6 +5,7 @@ import dayGridPlugin from "@fullcalendar/daygrid"; import timeGridPlugin from "@fullcalendar/timegrid"; import interactionPlugin from "@fullcalendar/interaction"; import { getEvents, createEventId } from "./TaskDataHandler"; +import { axiosInstance } from "src/api/AxiosConfig"; export class Calendar extends React.Component { state = { @@ -25,13 +26,13 @@ export class Calendar extends React.Component { right: "dayGridMonth,timeGridWeek,timeGridDay", }} initialView="dayGridMonth" - editable={true} - selectable={true} + editable={false} + selectable={false} selectMirror={true} dayMaxEvents={true} weekends={this.state.weekendsVisible} initialEvents={getEvents} - select={this.handleDateSelect} + // select={this.handleDateSelect} eventContent={renderEventContent} eventClick={this.handleEventClick} eventsSet={this.handleEvents} @@ -85,22 +86,22 @@ export class Calendar extends React.Component { }); }; - handleDateSelect = (selectInfo) => { - let title = prompt("Please enter a new title for your event"); - let calendarApi = selectInfo.view.calendar; + // handleDateSelect = (selectInfo) => { + // let title = prompt("Please enter a new title for your event"); + // let calendarApi = selectInfo.view.calendar; - calendarApi.unselect(); // clear date selection + // calendarApi.unselect(); // clear date selection - if (title) { - calendarApi.addEvent({ - id: createEventId(), - title, - start: selectInfo.startStr, - end: selectInfo.endStr, - allDay: selectInfo.allDay, - }); - } - }; + // if (title) { + // calendarApi.addEvent({ + // id: createEventId(), + // title, + // start: selectInfo.startStr, + // end: selectInfo.endStr, + // allDay: selectInfo.allDay, + // }); + // } + // }; handleEventClick = (clickInfo) => { if (confirm(`Are you sure you want to delete the event '${clickInfo.event.title}'`)) { From b7bb1017593e3e70d2639f1be52dd8198e0ac442 Mon Sep 17 00:00:00 2001 From: sosokker Date: Tue, 28 Nov 2023 11:55:39 +0700 Subject: [PATCH 24/27] When move task to Todo, make it completed --- backend/tasks/models.py | 5 ++++ backend/tasks/tests/test_todo_signal.py | 33 +++++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 backend/tasks/tests/test_todo_signal.py diff --git a/backend/tasks/models.py b/backend/tasks/models.py index c848e00..97a2c4b 100644 --- a/backend/tasks/models.py +++ b/backend/tasks/models.py @@ -81,6 +81,11 @@ class Todo(Task): priority = models.PositiveSmallIntegerField(choices=EisenhowerMatrix.choices, default=EisenhowerMatrix.NOT_IMPORTANT_NOT_URGENT) def save(self, *args, **kwargs): + done_list_name = "Done" + if self.list_board.name == done_list_name: + self.completed = True + Todo.objects.filter(list_board=self.list_board).update(completed=True) + if self.completed and not self.completion_date: self.completion_date = timezone.now() elif not self.completed: diff --git a/backend/tasks/tests/test_todo_signal.py b/backend/tasks/tests/test_todo_signal.py new file mode 100644 index 0000000..fa7fba5 --- /dev/null +++ b/backend/tasks/tests/test_todo_signal.py @@ -0,0 +1,33 @@ +from datetime import datetime, timedelta +from django.urls import reverse +from rest_framework import status +from rest_framework.test import APITestCase +from tasks.tests.utils import create_test_user +from tasks.models import Todo +from boards.models import ListBoard, Board + + +class TodoSignalHandlersTests(APITestCase): + def setUp(self): + self.user = create_test_user() + self.client.force_authenticate(user=self.user) + self.list_board = Board.objects.get(user=self.user).listboard_set.first() + + def test_update_priority_signal_handler(self): + """ + Test the behavior of the update_priority signal handler. + """ + due_date = datetime.now() + timedelta(days=5) + data = { + 'title': 'Test Task', + 'type': 'habit', + 'difficulty': 1, + 'end_event': due_date.strftime('%Y-%m-%dT%H:%M:%S'), + 'list_board': self.list_board.id, + } + response = self.client.post(reverse("todo-list"), data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + # Retrieve the created task and check if priority is updated + task = Todo.objects.get(title='Test Task') + self.assertIsNotNone(task.priority) # Check if priority is not None \ No newline at end of file From 9affc1ecba59f4553c6542b1089dd1cd5cf60712 Mon Sep 17 00:00:00 2001 From: sosokker Date: Tue, 28 Nov 2023 11:56:25 +0700 Subject: [PATCH 25/27] Add update partial of task (patch api) --- frontend/src/api/TaskApi.jsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/frontend/src/api/TaskApi.jsx b/frontend/src/api/TaskApi.jsx index 098d934..0a56767 100644 --- a/frontend/src/api/TaskApi.jsx +++ b/frontend/src/api/TaskApi.jsx @@ -38,6 +38,15 @@ export const updateTask = (endpoint, id, data) => { }); }; +export const updateTaskPartial = (endpoint, id, data) => { + return axiosInstance + .patch(`${baseURL}${endpoint}/${id}/`, data) + .then((response) => response.data) + .catch((error) => { + throw error; + }); +}; + export const deleteTask = (endpoint, id) => { return axiosInstance .delete(`${baseURL}${endpoint}/${id}/`) @@ -64,6 +73,7 @@ export const readHabitTaskByID = (id) => readTaskByID("habit", id); // Update export const updateTodoTask = (id, data) => updateTask("todo", id, data); +export const updateTodoTaskPartial = (id, data) => updateTaskPartial("todo", id, data); export const updateRecurrenceTask = (id, data) => updateTask("daily", id, data); export const updateHabitTask = (id, data) => updateTask("habit", id, data); From 36958d5256053bc4c47a6e58960ffbbef1d4cf9c Mon Sep 17 00:00:00 2001 From: sosokker Date: Tue, 28 Nov 2023 11:57:53 +0700 Subject: [PATCH 26/27] taskmodal will now work with some apis (need to work with time and desc) - Editing Title - Editing complete, challenge, difficulty --- frontend/package.json | 1 + frontend/pnpm-lock.yaml | 11 +- .../kanbanBoard/taskDetailModal.jsx | 198 ++++++++++++++---- 3 files changed, 167 insertions(+), 43 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index b06025e..618e30c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -34,6 +34,7 @@ "@wojtekmaj/react-daterange-picker": "^5.4.4", "axios": "^1.6.1", "bootstrap": "^5.3.2", + "date-fns": "^2.30.0", "dotenv": "^16.3.1", "framer-motion": "^10.16.4", "gapi-script": "^1.2.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index aca0306..b514b56 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -1,5 +1,9 @@ lockfileVersion: '6.0' +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + dependencies: '@dnd-kit/core': specifier: ^6.1.0 @@ -73,6 +77,9 @@ dependencies: bootstrap: specifier: ^5.3.2 version: 5.3.2(@popperjs/core@2.11.8) + date-fns: + specifier: ^2.30.0 + version: 2.30.0 dotenv: specifier: ^16.3.1 version: 16.3.1 @@ -4684,7 +4691,3 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} dev: true - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false diff --git a/frontend/src/components/kanbanBoard/taskDetailModal.jsx b/frontend/src/components/kanbanBoard/taskDetailModal.jsx index ddae1cf..3941ba1 100644 --- a/frontend/src/components/kanbanBoard/taskDetailModal.jsx +++ b/frontend/src/components/kanbanBoard/taskDetailModal.jsx @@ -1,44 +1,141 @@ import { useState, useEffect } from "react"; import { FaTasks, FaRegListAlt } from "react-icons/fa"; -import { FaPlus, FaRegTrashCan } from "react-icons/fa6"; +import { FaPlus, FaRegTrashCan, FaPencil } from "react-icons/fa6"; import { TbChecklist } from "react-icons/tb"; import DatePicker from "react-datepicker"; -import { TimePicker } from "react-ios-time-picker"; import "react-datepicker/dist/react-datepicker.css"; import { addSubtasks, deleteSubtasks, getSubtask, updateSubtask } from "src/api/SubTaskApi"; +import { updateTodoTaskPartial } from "src/api/TaskApi"; +import format from "date-fns/format"; -export function TaskDetailModal({ title, description, tags, difficulty, challenge, importance, taskId, updateTask }) { +export function TaskDetailModal({ + title, + description, + tags, + difficulty, + challenge, + importance, + taskId, + updateTask, + completed, +}) { const [isChallengeChecked, setChallengeChecked] = useState(challenge); const [isImportantChecked, setImportantChecked] = useState(importance); - const [currentDifficulty, setCurrentDifficulty] = useState(difficulty); + const [currentDifficulty, setCurrentDifficulty] = useState((difficulty - 1) * 25); const [selectedTags, setSelectedTags] = useState([]); const [dateStart, setDateStart] = useState(new Date()); const [dateEnd, setDateEnd] = useState(new Date()); const [startDateEnabled, setStartDateEnabled] = useState(false); const [endDateEnabled, setEndDateEnabled] = useState(false); - const [isTaskComplete, setTaskComplete] = useState(false); - const [value, setValue] = useState("10:00"); + const [isTaskComplete, setTaskComplete] = useState(completed); + const [starteventValue, setStartEventValue] = useState("10:00 PM"); + const [endeventValue, setEndEventValue] = useState("11:00 AM"); const [subtaskText, setSubtaskText] = useState(""); const [subtasks, setSubtasks] = useState([]); + const [currentTitle, setTitle] = useState(title); + const [isTitleEditing, setTitleEditing] = useState(false); - const onChange = (timeValue) => { - setValue(timeValue); + const handleTitleChange = async () => { + const data = { + title: currentTitle, + }; + await updateTodoTaskPartial(taskId, data); + setTitleEditing(false); }; - const handleChallengeChange = () => { + + const handleStartEventTimeChange = async (timeValue) => { + const formattedTime = convertToFormattedTime(timeValue); + setStartEventValue(formattedTime); + console.log(formattedTime); + const data = { + startTime: formattedTime, + }; + await updateTodoTaskPartial(taskId, data); + }; + + const handleEndEventTimeChange = async (timeValue) => { + const inputTime = event.target.value; + // Validate the input time format + if (!validateTimeFormat(inputTime)) { + // Display an error message or handle invalid format + console.error("Invalid time format. Please use HH:mm AM/PM"); + return; + } + + const formattedTime = convertToFormattedTime(timeValue); + setEndEventValue(formattedTime); + const data = { + endTime: formattedTime, + }; + await updateTodoTaskPartial(taskId, data); + }; + + const convertToFormattedTime = (timeValue) => { + const formattedTime = format(timeValue, "HH:mm:ss.SSSX", { timeZone: "UTC" }); + return formattedTime; + }; + + const validateTimeFormat = (time) => { + const timeFormatRegex = /^(0[1-9]|1[0-2]):[0-5][0-9] (AM|PM)$/i; + return timeFormatRegex.test(time); + }; + + const handleChallengeChange = async () => { setChallengeChecked(!isChallengeChecked); + const data = { + challenge: !isChallengeChecked, + }; + await updateTodoTaskPartial(taskId, data); }; - const handleImportantChange = () => { + const handleImportantChange = async () => { setImportantChecked(!isImportantChecked); + const data = { + important: !isImportantChecked, + }; + await updateTodoTaskPartial(taskId, data); }; - const handleDifficultyChange = (event) => { + const handleDifficultyChange = async (event) => { setCurrentDifficulty(parseInt(event.target.value, 10)); + let diff = event.target.value / 25 + 1; + const data = { + difficulty: diff, + }; + await updateTodoTaskPartial(taskId, data); }; const handleTagChange = (tag) => { const isSelected = selectedTags.includes(tag); setSelectedTags(isSelected ? selectedTags.filter((selectedTag) => selectedTag !== tag) : [...selectedTags, tag]); + ``; + }; + + const handleStartDateValueChange = (date) => { + if (!isTaskComplete) { + setDateStart(date); + const formattedStartDate = convertToFormattedDate(date); + const data = { + startTime: formattedStartDate, + }; + updateTodoTaskPartial(taskId, data); + } + }; + + const handleEndDateValueChange = (date) => { + if (!isTaskComplete) { + setDateEnd(date); + const formattedEndDate = convertToFormattedDate(date); + const data = { + endTime: formattedEndDate, + }; + updateTodoTaskPartial(taskId, data); + } + }; + + const convertToFormattedDate = (dateValue) => { + const formattedDate = format(dateValue, "yyyy-MM-dd'T'", { timeZone: "UTC" }); + return formattedDate; }; const handleStartDateChange = () => { @@ -53,14 +150,21 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng } }; - const handleTaskCompleteChange = () => { + const handleTaskCompleteChange = async () => { + let completed = false; if (isTaskComplete) { setTaskComplete(false); + completed = false; } else { setTaskComplete(true); + completed = true; setStartDateEnabled(false); setEndDateEnabled(false); } + const data = { + completed: completed, + }; + await updateTodoTaskPartial(taskId, data); }; const addSubtask = async () => { @@ -130,7 +234,7 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng
    - {tag.label} + {tag.name}
    )); @@ -139,7 +243,7 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng
    - {tag.label} + {tag.name}
    )); @@ -149,13 +253,29 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng {/* Title */}
    -

    - - {} - {title} - -

    -

    {title}

    + {isTitleEditing ? ( +
    + + setTitle(e.target.value)} + /> + +
    + ) : ( +

    + + {} + {currentTitle} + setTitleEditing(true)} /> + +

    + )} +

    {currentTitle}

    {/* Tags */} @@ -175,7 +295,7 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng className="checkbox checkbox-sm" onChange={() => handleTagChange(tag)} /> - {tag.label} + {tag}
  • ))} @@ -201,33 +321,31 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng onChange={handleStartDateChange} />
    - setDateStart(date)} - disabled={!startDateEnabled} - /> +
    -
    - + {/* handleStartEventTimeChange */} +
    + {/* Complete? */}

    Complete

    - + +
    @@ -243,8 +361,10 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng onChange={handleEndDateChange} />
    - setDateEnd(date)} disabled={!endDateEnabled} /> +
    + {/* End event time picker */} +
    this is time picker
    From 145f19f49b583afb900e6eb3d59244b311f837a7 Mon Sep 17 00:00:00 2001 From: sosokker Date: Tue, 28 Nov 2023 11:58:08 +0700 Subject: [PATCH 27/27] Change card Style --- .../src/components/kanbanBoard/taskCard.jsx | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/kanbanBoard/taskCard.jsx b/frontend/src/components/kanbanBoard/taskCard.jsx index 179ea49..4c40a9e 100644 --- a/frontend/src/components/kanbanBoard/taskCard.jsx +++ b/frontend/src/components/kanbanBoard/taskCard.jsx @@ -25,6 +25,10 @@ export function TaskCard({ task, deleteTask, updateTask }) { // ---- DESC AND TAG ---- */ + if (task.tags === undefined) { + task.tags = []; + } + // Tags const tags = task.tags.length > 0 ? ( @@ -32,8 +36,8 @@ export function TaskCard({ task, deleteTask, updateTask }) { {task.tags.map((tag, index) => (
    - {tag.label} + className={`inline-flex items-center font-bold leading-sm uppercase w-1/3 h-3 p-2 mr-1 bg-${tag.color}-200 text-${tag.color}-700 rounded`}> +

    {tag}

    ))} @@ -42,7 +46,7 @@ export function TaskCard({ task, deleteTask, updateTask }) { // difficulty? const difficultyTag = task.difficulty ? ( difficulty @@ -60,7 +64,7 @@ export function TaskCard({ task, deleteTask, updateTask }) { // Due Date const dueDateTag = task.end_event && new Date(task.end_event) > new Date() - ? (() => { + ? (() => { const daysUntilDue = Math.ceil((new Date(task.end_event) - new Date()) / (1000 * 60 * 60 * 24)); let colorClass = @@ -93,7 +97,7 @@ export function TaskCard({ task, deleteTask, updateTask }) { // Subtask count const subtaskCountTag = task.subtaskCount ? ( - + {task.subtaskCount} ) : null; @@ -124,6 +128,7 @@ export function TaskCard({ task, deleteTask, updateTask }) { challenge={task.challenge} importance={task.importance} updateTask={updateTask} + completed={task.completed} /> {/* -------- Task Card -------- */} @@ -138,7 +143,8 @@ export function TaskCard({ task, deleteTask, updateTask }) { }} onMouseLeave={() => { setMouseIsOver(false); - }}> + }} + onClick={() => document.getElementById(`task_detail_modal_${task.id}`).showModal()}> {/* -------- Task Content -------- */} {/* Tags */} {tags}