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 -
-
+
+ {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

-