diff --git a/components/BoardsList.tsx b/components/BoardsList.tsx index f030885..46e8087 100644 --- a/components/BoardsList.tsx +++ b/components/BoardsList.tsx @@ -17,10 +17,15 @@ const BoardsList: React.FC = ({ boards }) => { {boards.map((board, i) => (
  • dispatch(setActiveBoard(i))} + onClick={() => { + dispatch(setActiveBoard(i)); + dispatch(setActiveModal(undefined)); + }} > diff --git a/components/MobileBoards.tsx b/components/MobileBoards.tsx index 2b93eab..2ebc72d 100644 --- a/components/MobileBoards.tsx +++ b/components/MobileBoards.tsx @@ -1,10 +1,12 @@ +import { useBoard } from "@/hooks/useBoard"; import BoardsList from "./BoardsList"; import ToggleTheme from "./ToggleTheme"; const MobileBoards = () => { + const boards = useBoard({ boards: [] }); return ( -
    - {/* */} +
    +
    diff --git a/components/TaskColumn.tsx b/components/TaskColumn.tsx index 655c340..4d208d1 100644 --- a/components/TaskColumn.tsx +++ b/components/TaskColumn.tsx @@ -1,10 +1,21 @@ import Task from "@/model/Task"; +import { Draggable, Droppable } from "react-beautiful-dnd"; interface TaskColumnProps { col: { name: string; tasks: Task[] }; onClickedTask: (index: number) => void; + id: string; } -const TaskColumn: React.FC = ({ col, onClickedTask }) => { + +const colColors = [ + "bg-[#49C4E5]", + "bg-[#8471F2]", + "bg-[#f3e849]", + "bg-[#67E2AE]", + "bg-[#ee9b57]", + "bg-[#cbabe9]", +]; +const TaskColumn: React.FC = ({ col, onClickedTask, id }) => { const doneSubtasksNumber = (task: Task) => { return task.subtasks.filter((el) => el.isCompleted).length; }; @@ -12,28 +23,55 @@ const TaskColumn: React.FC = ({ col, onClickedTask }) => { return (
    -
    +

    {col.name} ({col.tasks.length})

    -
      - {col.tasks.map((task, index) => ( -
    • -
      onClickedTask(index)} - > -

      {task.title}

      -

      - {`${doneSubtasksNumber(task)} of ${ - task.subtasks.length - } subtasks`} -

      -
      -
    • - ))} -
    + + {(provided) => ( +
      + {col.tasks.map((task, index) => ( + + {(provided, snapshot) => { + return ( +
    • +
      onClickedTask(index)} + > +

      {task.title}

      +

      + {`${doneSubtasksNumber(task)} of ${ + task.subtasks.length + } subtasks`} +

      +
      +
    • + ); + }} +
      + ))} +
    + )} +
    ); }; diff --git a/components/UI/Dropdown.tsx b/components/UI/Dropdown.tsx index c939eac..f041b85 100644 --- a/components/UI/Dropdown.tsx +++ b/components/UI/Dropdown.tsx @@ -23,7 +23,7 @@ const Dropdown: React.FC = ({ items }) => { return (
    setMenuIsOpen(true)} > diff --git a/components/UI/Modal.tsx b/components/UI/Modal.tsx index b8aee89..c4beecb 100644 --- a/components/UI/Modal.tsx +++ b/components/UI/Modal.tsx @@ -14,7 +14,6 @@ const Modal: React.FC = ({ const backDropHandler = useCallback((e: MouseEvent) => { if (!modalRef?.current?.contains(e.target as Node)) { - console.log("on Clicked Backdrop"); onClickBackdrop(); } }, []); @@ -29,10 +28,12 @@ const Modal: React.FC = ({ }, [backDropHandler]); return ( -
    +
    diff --git a/data/data.json b/data/data.json index 3cadd8f..378833b 100644 --- a/data/data.json +++ b/data/data.json @@ -1 +1 @@ -{"boards":[{"name":"Marketing Plan","columns":[{"name":"Todo","tasks":[]},{"name":"Doing","tasks":[]},{"name":"Done","tasks":[]}]},{"name":"Roadmap","columns":[{"name":"Now","tasks":[]},{"name":"Next","tasks":[]},{"name":"Later","tasks":[]}]},{"name":"fhdf","columns":[{"name":"fghfdh","tasks":[]}]},{"name":"fdsss","columns":[{"name":"sww2222 dsfsdf","tasks":[{"title":"this is not a simple task ","description":"test desc edited","status":"sww2222 dsfsdf","subtasks":[{"title":"sub t2","isCompleted":true},{"title":"give a name","isCompleted":true}]}]},{"name":"qwesdf fsdf","tasks":[]}]},{"name":"sf","columns":[{"name":"wew","tasks":[]},{"name":"wew","tasks":[]}]},{"name":"safir","columns":[{"name":"col1","tasks":[]},{"name":"col2","tasks":[]},{"name":"sdfsf","tasks":[]},{"name":"sdfsdfsf","tasks":[]},{"name":"sdfsfsf","tasks":[]},{"name":"sfsdfsdfsfsfsf","tasks":[]}]}]} \ No newline at end of file +{"boards":[{"name":"Platform Launch","columns":[{"name":"Todo","tasks":[{"title":"Build UI for onboarding flow","description":"","status":"Todo","subtasks":[{"title":"Sign up page","isCompleted":true},{"title":"Sign in page","isCompleted":false},{"title":"Welcome page","isCompleted":false}]},{"title":"Add account management endpoints","description":"","status":"Doing","subtasks":[{"title":"Upgrade plan","isCompleted":true},{"title":"Cancel plan","isCompleted":true},{"title":"Update payment method","isCompleted":false}]},{"title":"Build UI for search","description":"","status":"Todo","subtasks":[{"title":"Search page","isCompleted":false}]},{"title":"QA and test all major user journeys","description":"Once we feel version one is ready, we need to rigorously test it both internally and externally to identify any major gaps.","status":"Todo","subtasks":[{"title":"Internal testing","isCompleted":false},{"title":"External testing","isCompleted":false}]},{"title":"Create paper prototypes and conduct 10 usability tests with potential customers","description":"","status":"Todo","subtasks":[{"title":"Create paper prototypes for version one","isCompleted":false},{"title":"Complete 10 usability tests","isCompleted":false}]}]},{"name":"Doingffff","tasks":[{"title":"Design settings and search pages","description":"","status":"Doing","subtasks":[{"title":"Settings - Account page","isCompleted":true},{"title":"Settings - Billing page","isCompleted":true},{"title":"Search page","isCompleted":false}]},{"title":"Review results of usability tests and iterate","description":"Keep iterating through the subtasks until we're clear on the core concepts for the app.","status":"Done","subtasks":[{"title":"Meet to review notes from previous tests and plan changes","isCompleted":true},{"title":"Make changes to paper prototypes","isCompleted":true},{"title":"Conduct 5 usability tests","isCompleted":true}]},{"title":"Build settings UI","description":"","status":"Todo","subtasks":[{"title":"Account page","isCompleted":false},{"title":"Billing page","isCompleted":false}]},{"title":"Design onboarding flow","description":"","status":"Doing","subtasks":[{"title":"Sign up page","isCompleted":true},{"title":"Sign in page","isCompleted":false},{"title":"Welcome page","isCompleted":false}]},{"title":"Create wireframe prototype","description":"Create a greyscale clickable wireframe prototype to test our asssumptions so far.","status":"Done","subtasks":[{"title":"Create clickable wireframe prototype in Balsamiq","isCompleted":true}]},{"title":"Research the market","description":"We need to get a solid overview of the market to ensure we have up-to-date estimates of market size and demand.","status":"Done","subtasks":[{"title":"Write up research analysis","isCompleted":true},{"title":"Calculate TAM","isCompleted":true}]},{"title":"Add search enpoints","description":"","status":"Doing","subtasks":[{"title":"Add search endpoint","isCompleted":true},{"title":"Define search filters","isCompleted":false}]},{"title":"Add authentication endpoints","description":"","status":"Doing","subtasks":[{"title":"Define user model","isCompleted":true},{"title":"Add auth endpoints","isCompleted":false}]},{"title":"Research pricing points of various competitors and trial different business models","description":"We know what we're planning to build for version one. Now we need to finalise the first pricing model we'll use. Keep iterating the subtasks until we have a coherent proposition.","status":"Doing","subtasks":[{"title":"Research competitor pricing and business models","isCompleted":true},{"title":"Outline a business model that works for our solution","isCompleted":false},{"title":"Talk to potential customers about our proposed solution and ask for fair price expectancy","isCompleted":false}]},{"title":"this is a new task","description":"dfsdfadaf","status":"Doing","subtasks":[{"title":"subtask 1","isCompleted":true},{"title":"subtask 2","isCompleted":true}]}]}]},{"name":"Marketing Planet","columns":[{"name":"Todo","tasks":[{"title":"Plan Product Hunt launch","description":"","status":"Todo","subtasks":[{"title":"Find hunter","isCompleted":true},{"title":"Gather assets","isCompleted":true},{"title":"Draft product page","isCompleted":false},{"title":"Notify customers","isCompleted":false},{"title":"Notify network","isCompleted":false},{"title":"Launch!","isCompleted":false}]},{"title":"Share on Show HN","description":"","status":"","subtasks":[{"title":"Draft out HN post","isCompleted":false},{"title":"Get feedback and refine","isCompleted":false},{"title":"Publish post","isCompleted":false}]},{"title":"Write launch article to publish on multiple channels","description":"","status":"","subtasks":[{"title":"Write article","isCompleted":false},{"title":"Publish on LinkedIn","isCompleted":false},{"title":"Publish on Inndie Hackers","isCompleted":false},{"title":"Publish on Medium","isCompleted":false}]}]},{"name":"Doing","tasks":[]},{"name":"Done","tasks":[]}]},{"name":"a test board","columns":[{"name":"col 1","tasks":[]},{"name":"col 2","tasks":[]}]},{"name":"second test board","columns":[{"name":"col 1 test","tasks":[{"title":"first task for this app","description":"this is a des","status":"col 2 test","subtasks":[{"title":"sub t1","isCompleted":false},{"title":"subt 2","isCompleted":false}]}]},{"name":"col 2 test","tasks":[]}]}]} \ No newline at end of file diff --git a/layout/Header.tsx b/layout/Header.tsx index d6378e6..7a68000 100644 --- a/layout/Header.tsx +++ b/layout/Header.tsx @@ -1,6 +1,5 @@ import { useDispatch, useSelector } from "react-redux"; import Button from "../components/UI/Button"; -import VerticalElipsisIcon from "@/icons/icon-vertical-ellipsis.svg"; import LogoDarkIcon from "@/icons/logo-dark.svg"; import LogoLightIcon from "@/icons/logo-light.svg"; import LogoMobileIcon from "@/icons/logo-mobile.svg"; @@ -12,7 +11,7 @@ import ModalEnum from "@/model/ModalEnum"; import { useQuery } from "@tanstack/react-query"; import Board from "@/model/Board"; const Header = () => { - const dipsatch = useDispatch(); + const dispatch = useDispatch(); const { data } = useQuery<{ boards: Board[] }>({ queryKey: ["boards"] }); const activeBoard = useSelector(selectBoard); @@ -31,19 +30,19 @@ const Header = () => { label="+ Add New Task" type="primary large" disabled={data?.boards?.[activeBoard]?.columns.length === 0} - onClick={() => dipsatch(setActiveModal(ModalEnum.CREATE_TASK))} + onClick={() => dispatch(setActiveModal(ModalEnum.CREATE_TASK))} /> dipsatch(setActiveModal(ModalEnum.EDIT_BOARD)), + onClick: () => dispatch(setActiveModal(ModalEnum.EDIT_BOARD)), className: "text-gray3", }, { label: "Delete Board", onClick: () => - dipsatch(setActiveModal(ModalEnum.DELETE_BOARD)), + dispatch(setActiveModal(ModalEnum.DELETE_BOARD)), className: "text-destructive2", }, ]} @@ -53,18 +52,39 @@ const Header = () => {
    {/* Mobile Header */}
    -
    +
    dispatch(setActiveModal(ModalEnum.MOBILE_MENU))} + >
    -

    Platform Launch

    +

    {data?.boards?.[activeBoard]?.name}

    - - + dispatch(setActiveModal(ModalEnum.EDIT_BOARD)), + className: "text-gray3", + }, + { + label: "Delete Board", + onClick: () => dispatch(setActiveModal(ModalEnum.DELETE_BOARD)), + className: "text-destructive2", + }, + ]} + />
    diff --git a/layout/Sidebar.tsx b/layout/Sidebar.tsx index f75a4da..5e16002 100644 --- a/layout/Sidebar.tsx +++ b/layout/Sidebar.tsx @@ -2,19 +2,35 @@ import BoardsList from "@/components/BoardsList"; import ToggleTheme from "@/components/ToggleTheme"; import { useBoard } from "@/hooks/useBoard"; import HideIcon from "@/icons/icon-hide.svg"; - +import ShowIcon from "@/icons/icon-show-sidebar.svg"; +import { useDispatch } from "react-redux"; +import { selectSidebar, toggleSidebar } from "@/store/uiSlice"; +import { useSelector } from "react-redux"; const Sidebar = () => { const boards = useBoard({ boards: [] }); + const dispatch = useDispatch(); + const sidebarIsOpen = useSelector(selectSidebar); return (
    -
    +
    dispatch(toggleSidebar())} + >

    Hide Sidebar

    + {!sidebarIsOpen && ( +
    dispatch(toggleSidebar())} + > + +
    + )}
    ); }; diff --git a/layout/Tasks.tsx b/layout/Tasks.tsx index 9945d0b..c2e13fa 100644 --- a/layout/Tasks.tsx +++ b/layout/Tasks.tsx @@ -1,14 +1,15 @@ +import { DragDropContext, DropResult } from "react-beautiful-dnd"; import TaskColumn from "@/components/TaskColumn"; import Button from "@/components/UI/Button"; import { useBoard } from "@/hooks/useBoard"; -import Board from "@/model/Board"; import ModalEnum from "@/model/ModalEnum"; import Task from "@/model/Task"; import { selectBoard, setActiveModal, setOpenedTask } from "@/store/uiSlice"; -import { useEffect } from "react"; import { useDispatch, useSelector } from "react-redux"; -const Tasks: React.FC<{ board: Board }> = () => { +const Tasks: React.FC<{ + onDragEnd: (result: DropResult) => void; +}> = ({ onDragEnd }) => { const dispatch = useDispatch(); const activeBoard = useSelector(selectBoard); const board = useBoard({ boards: [] })?.[activeBoard]; @@ -19,22 +20,26 @@ const Tasks: React.FC<{ board: Board }> = () => { return (
    - {board?.columns?.map( - (col: { name: string; tasks: Task[] }, index: number) => ( - - onClickedTask(index, taskIndex) - } - /> - ) - )} + + {board?.columns?.map( + (col: { name: string; tasks: Task[] }, index: number) => ( + + onClickedTask(index, taskIndex) + } + /> + ) + )} + + {board?.columns?.length > 0 && (

    dispatch(setActiveModal(ModalEnum.EDIT_BOARD))} - className="text-gray3 w-[280px] my-auto cursor-pointer hover:text-gray1 transition-colors" + className="text-gray3 w-[280px] my-auto cursor-pointer hover:text-gray2 transition-colors" > + New Column

    diff --git a/model/ModalEnum.tsx b/model/ModalEnum.tsx index bdd4966..68ba349 100644 --- a/model/ModalEnum.tsx +++ b/model/ModalEnum.tsx @@ -6,5 +6,6 @@ enum ModalEnum { DELETE_BOARD = "DELETE_BOARD", DELETE_TASK = "DELETE_TASK", VIEW_TASK = "VIEW_TASK", + MOBILE_MENU = "MOBILE_MENU", } export default ModalEnum; diff --git a/package-lock.json b/package-lock.json index c60c88c..5577576 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,13 +20,15 @@ "next": "13.4.5", "postcss": "8.4.24", "react": "18.2.0", + "react-beautiful-dnd": "^13.1.1", "react-dom": "18.2.0", "react-redux": "^8.1.1", "tailwindcss": "3.3.2", "typescript": "5.1.3" }, "devDependencies": { - "@svgr/webpack": "^8.0.1" + "@svgr/webpack": "^8.0.1", + "@types/react-beautiful-dnd": "^13.1.4" } }, "node_modules/@alloc/quick-lru": { @@ -2784,6 +2786,15 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/react-beautiful-dnd": { + "version": "13.1.4", + "resolved": "https://registry.npmjs.org/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.4.tgz", + "integrity": "sha512-4bIBdzOr0aavN+88q3C7Pgz+xkb7tz3whORYrmSj77wfVEMfiWiooIwVWFR7KM2e+uGTe5BVrXqSfb0aHeflJA==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-dom": { "version": "18.2.5", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.5.tgz", @@ -2792,6 +2803,17 @@ "@types/react": "*" } }, + "node_modules/@types/react-redux": { + "version": "7.1.25", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.25.tgz", + "integrity": "sha512-bAGh4e+w5D8dajd6InASVIyCo4pZLJ66oLb80F9OBLO1gKESbZcRCJpTT6uLXX+HAB57zw1WTdwJdAsewuTweg==", + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, "node_modules/@types/scheduler": { "version": "0.16.3", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", @@ -3508,6 +3530,14 @@ "node": ">= 8" } }, + "node_modules/css-box-model": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "dependencies": { + "tiny-invariant": "^1.0.6" + } + }, "node_modules/css-select": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", @@ -5370,6 +5400,11 @@ "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", "dev": true }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -6059,6 +6094,11 @@ } ] }, + "node_modules/raf-schd": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", + "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==" + }, "node_modules/react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", @@ -6070,6 +6110,53 @@ "node": ">=0.10.0" } }, + "node_modules/react-beautiful-dnd": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz", + "integrity": "sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==", + "dependencies": { + "@babel/runtime": "^7.9.2", + "css-box-model": "^1.2.0", + "memoize-one": "^5.1.1", + "raf-schd": "^4.0.2", + "react-redux": "^7.2.0", + "redux": "^4.0.4", + "use-memo-one": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8.5 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-beautiful-dnd/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + }, + "node_modules/react-beautiful-dnd/node_modules/react-redux": { + "version": "7.2.9", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", + "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", + "dependencies": { + "@babel/runtime": "^7.15.4", + "@types/react-redux": "^7.1.20", + "hoist-non-react-statics": "^3.3.2", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^17.0.2" + }, + "peerDependencies": { + "react": "^16.8.3 || ^17 || ^18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", @@ -6859,6 +6946,11 @@ "node": ">=0.8" } }, + "node_modules/tiny-invariant": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz", + "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==" + }, "node_modules/titleize": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", @@ -7076,6 +7168,14 @@ "punycode": "^2.1.0" } }, + "node_modules/use-memo-one": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz", + "integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/use-sync-external-store": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", @@ -9027,6 +9127,15 @@ "csstype": "^3.0.2" } }, + "@types/react-beautiful-dnd": { + "version": "13.1.4", + "resolved": "https://registry.npmjs.org/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.4.tgz", + "integrity": "sha512-4bIBdzOr0aavN+88q3C7Pgz+xkb7tz3whORYrmSj77wfVEMfiWiooIwVWFR7KM2e+uGTe5BVrXqSfb0aHeflJA==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/react-dom": { "version": "18.2.5", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.5.tgz", @@ -9035,6 +9144,17 @@ "@types/react": "*" } }, + "@types/react-redux": { + "version": "7.1.25", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.25.tgz", + "integrity": "sha512-bAGh4e+w5D8dajd6InASVIyCo4pZLJ66oLb80F9OBLO1gKESbZcRCJpTT6uLXX+HAB57zw1WTdwJdAsewuTweg==", + "requires": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, "@types/scheduler": { "version": "0.16.3", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", @@ -9503,6 +9623,14 @@ "which": "^2.0.1" } }, + "css-box-model": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "requires": { + "tiny-invariant": "^1.0.6" + } + }, "css-select": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", @@ -10824,6 +10952,11 @@ "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", "dev": true }, + "memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + }, "merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -11246,6 +11379,11 @@ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" }, + "raf-schd": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", + "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==" + }, "react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", @@ -11254,6 +11392,40 @@ "loose-envify": "^1.1.0" } }, + "react-beautiful-dnd": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz", + "integrity": "sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==", + "requires": { + "@babel/runtime": "^7.9.2", + "css-box-model": "^1.2.0", + "memoize-one": "^5.1.1", + "raf-schd": "^4.0.2", + "react-redux": "^7.2.0", + "redux": "^4.0.4", + "use-memo-one": "^1.1.1" + }, + "dependencies": { + "react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + }, + "react-redux": { + "version": "7.2.9", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", + "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", + "requires": { + "@babel/runtime": "^7.15.4", + "@types/react-redux": "^7.1.20", + "hoist-non-react-statics": "^3.3.2", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^17.0.2" + } + } + } + }, "react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", @@ -11798,6 +11970,11 @@ "thenify": ">= 3.1.0 < 4" } }, + "tiny-invariant": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz", + "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==" + }, "titleize": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", @@ -11942,6 +12119,11 @@ "punycode": "^2.1.0" } }, + "use-memo-one": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz", + "integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==" + }, "use-sync-external-store": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", diff --git a/package.json b/package.json index 6e0aba1..7234410 100644 --- a/package.json +++ b/package.json @@ -21,12 +21,14 @@ "next": "13.4.5", "postcss": "8.4.24", "react": "18.2.0", + "react-beautiful-dnd": "^13.1.1", "react-dom": "18.2.0", "react-redux": "^8.1.1", "tailwindcss": "3.3.2", "typescript": "5.1.3" }, "devDependencies": { - "@svgr/webpack": "^8.0.1" + "@svgr/webpack": "^8.0.1", + "@types/react-beautiful-dnd": "^13.1.4" } } diff --git a/pages/_app.tsx b/pages/_app.tsx index cdb2bcb..1f5b7a2 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -4,6 +4,7 @@ import { Provider } from "react-redux"; import { store } from "@/store/store"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; + export default function App({ Component, pageProps }: AppProps) { const queryClient = new QueryClient({ defaultOptions: { @@ -12,6 +13,7 @@ export default function App({ Component, pageProps }: AppProps) { }, }, }); + return ( diff --git a/pages/index.tsx b/pages/index.tsx index c32c512..11b9cff 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -10,6 +10,7 @@ import { useDispatch, useSelector } from "react-redux"; import { selectBoard, selectModal, + selectSidebar, selectTask, setActiveModal, setOpenedTask, @@ -27,6 +28,8 @@ import { useUpdateBoard } from "@/hooks/useUpdateBoard"; import ModalEnum from "@/model/ModalEnum"; import Task from "@/model/Task"; import EditTask from "@/components/EditTask"; +import { DropResult, resetServerContext } from "react-beautiful-dnd"; +import MobileBoards from "@/components/MobileBoards"; interface HomeProps { prefetchedData: { boards: Board[] }; @@ -37,7 +40,7 @@ const Home: React.FC = ({ prefetchedData = { boards: [] } }) => { const activeModal = useSelector(selectModal); const openedTask = useSelector(selectTask); - + const sidebarIsOpen = useSelector(selectSidebar); //remote states const { isDeleting, deleteBoard } = useDeleteBoard(); @@ -155,13 +158,58 @@ const Home: React.FC = ({ prefetchedData = { boards: [] } }) => { } }; + const reorder = (list: Task[], startIndex: number, endIndex: number) => { + const result = Array.from(list); + const [removed] = result.splice(startIndex, 1); + result.splice(endIndex, 0, removed); + + return result; + }; + + const handleDragEnd = ({ source, destination }: DropResult) => { + if (!destination) { + return; + } + + const board = { ...boards[activeBoard] }; + + const srcCol = parseInt(source.droppableId); + const desCol = parseInt(destination.droppableId); + + if (srcCol === desCol) { + const updatedTasks = reorder( + [...board.columns[srcCol].tasks], + source.index, + destination.index + ); + board.columns[srcCol].tasks = updatedTasks; + updateBoard({ id: activeBoard.toString(), board }); + + return; + } + //add to destionation col + board.columns[desCol].tasks.splice( + destination.index, + 0, + board.columns[srcCol].tasks[source.index] + ); + + //remove task from src + board.columns[srcCol].tasks.splice(source.index, 1); + updateBoard({ id: activeBoard.toString(), board }); + }; + return (
    -
    +
    - +
    {activeModal === ModalEnum.CREATE_BOARD && ( @@ -274,13 +322,22 @@ const Home: React.FC = ({ prefetchedData = { boards: [] } }) => { /> )} + {activeModal === ModalEnum.MOBILE_MENU && ( + dispatch(setActiveModal(undefined))} + > + + + )}
    ); }; export default Home; -export async function getStaticProps() { +export async function getServerSideProps() { const prefetchedData = getData(); + resetServerContext(); return { props: { prefetchedData } }; } diff --git a/store/uiSlice.ts b/store/uiSlice.ts index 762995e..aced4c0 100644 --- a/store/uiSlice.ts +++ b/store/uiSlice.ts @@ -7,6 +7,7 @@ interface UiState { activeBoard: number; openedTask: { taskIndex: number; colIndex: number } | undefined; openedModal: ModalEnum | undefined; + sidebarIsOpen: boolean; } // Define the initial state using that type @@ -14,6 +15,7 @@ const initialState: UiState = { activeBoard: 0, openedTask: undefined, openedModal: undefined, + sidebarIsOpen: true, }; export const uiSlice = createSlice({ name: "ui", @@ -33,11 +35,16 @@ export const uiSlice = createSlice({ ) => { state.openedTask = action.payload; }, + + toggleSidebar: (state) => { + state.sidebarIsOpen = !state.sidebarIsOpen; + }, }, }); -export const { setActiveBoard, setActiveModal, setOpenedTask } = +export const { setActiveBoard, setActiveModal, setOpenedTask, toggleSidebar } = uiSlice.actions; export const selectBoard = (state: RootState) => state.ui.activeBoard; export const selectModal = (state: RootState) => state.ui.openedModal; export const selectTask = (state: RootState) => state.ui.openedTask; +export const selectSidebar = (state: RootState) => state.ui.sidebarIsOpen; export default uiSlice.reducer; diff --git a/tailwind.config.js b/tailwind.config.js index 39230f4..a26941a 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -14,6 +14,25 @@ module.exports = { lg: "976px", xl: "1440px", }, + animation: { + fadeIn: "fadeIn 100ms linear", + fadeInMobile: "fadeInMobile 100ms linear", + opacityAnimate: "opacityAnimate 300ms ease", + }, + keyframes: { + fadeIn: { + "0%": { top: "49%", opacity: "0.3" }, + "100%": { top: "50%", opacity: "1" }, + }, + fadeInMobile: { + "0%": { top: "90px", opacity: "0.3" }, + "100%": { top: "96px", opacity: "1" }, + }, + opacityAnimate: { + "0%": { opacity: "0" }, + "100%": { opacity: "1" }, + }, + }, extend: { boxShadow: {