diff --git a/components/BoardsList.tsx b/components/BoardsList.tsx index ebb7f2b..f030885 100644 --- a/components/BoardsList.tsx +++ b/components/BoardsList.tsx @@ -1,12 +1,13 @@ import React from "react"; +import { useSelector, useDispatch } from "react-redux"; import BoardIcon from "@/icons/icon-board.svg"; import BoardListProps from "@/model/BoardListProps"; -const BoardsList: React.FC = ({ - boards, - activeBoard = 0, - onChangedaActiveBoard, - onAddNewBoard, -}) => { + +import { selectBoard, setActiveBoard, setActiveModal } from "@/store/uiSlice"; +import ModalEnum from "@/model/ModalEnum"; +const BoardsList: React.FC = ({ boards }) => { + const activeBoard = useSelector(selectBoard); + const dispatch = useDispatch(); return (

@@ -19,7 +20,7 @@ const BoardsList: React.FC = ({ className={`flex pl-8 py-4 gap-x-4 mr-6 items-center rounded-r-full cursor-pointer ${ activeBoard === i ? "bg-primary2 text-white" : "text-gray3" }`} - onClick={() => onChangedaActiveBoard(i)} + onClick={() => dispatch(setActiveBoard(i))} > @@ -30,7 +31,7 @@ const BoardsList: React.FC = ({
  • dispatch(setActiveModal(ModalEnum.CREATE_BOARD))} > diff --git a/components/UI/Modal.tsx b/components/UI/Modal.tsx index 73f09ca..304485d 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 click on backdrop"); onClickBackdrop(); } }, []); diff --git a/components/UI/TextInput.tsx b/components/UI/TextInput.tsx index a6fe888..8111119 100644 --- a/components/UI/TextInput.tsx +++ b/components/UI/TextInput.tsx @@ -27,7 +27,7 @@ const TextInput = forwardRef( setError(message); }, })); - console.log("render: ", error); + return (
    {label && ( diff --git a/data/data.json b/data/data.json index 2e03e3d..a70413c 100644 --- a/data/data.json +++ b/data/data.json @@ -1,430 +1 @@ -{ - "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": "Build UI for search", - "description": "", - "status": "Todo", - "subtasks": [ - { - "title": "Search page", - "isCompleted": false - } - ] - }, - { - "title": "Build settings UI", - "description": "", - "status": "Todo", - "subtasks": [ - { - "title": "Account page", - "isCompleted": false - }, - { - "title": "Billing 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 - } - ] - } - ] - }, - { - "name": "Doing", - "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": "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": "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": "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 - } - ] - } - ] - }, - { - "name": "Done", - "tasks": [ - { - "title": "Conduct 5 wireframe tests", - "description": "Ensure the layout continues to make sense and we have strong buy-in from potential users.", - "status": "Done", - "subtasks": [ - { - "title": "Complete 5 wireframe prototype tests", - "isCompleted": true - } - ] - }, - { - "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": "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": "Create paper prototypes and conduct 10 usability tests with potential customers", - "description": "", - "status": "Done", - "subtasks": [ - { - "title": "Create paper prototypes for version one", - "isCompleted": true - }, - { - "title": "Complete 10 usability tests", - "isCompleted": true - } - ] - }, - { - "title": "Market discovery", - "description": "We need to define and refine our core product. Interviews will help us learn common pain points and help us define the strongest MVP.", - "status": "Done", - "subtasks": [ - { - "title": "Interview 10 prospective customers", - "isCompleted": true - } - ] - }, - { - "title": "Competitor analysis", - "description": "", - "status": "Done", - "subtasks": [ - { - "title": "Find direct and indirect competitors", - "isCompleted": true - }, - { - "title": "SWOT analysis for each competitor", - "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 - } - ] - } - ] - } - ] - }, - { - "name": "Marketing Plan", - "columns": [ - { - "name": "Todo", - "tasks": [ - { - "title": "Plan Product Hunt launch", - "description": "", - "status": "Todo", - "subtasks": [ - { - "title": "Find hunter", - "isCompleted": false - }, - { - "title": "Gather assets", - "isCompleted": false - }, - { - "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": "Roadmap", - "columns": [ - { - "name": "Now", - "tasks": [ - { - "title": "Launch version one", - "description": "", - "status": "Now", - "subtasks": [ - { - "title": "Launch privately to our waitlist", - "isCompleted": false - }, - { - "title": "Launch publicly on PH, HN, etc.", - "isCompleted": false - } - ] - }, - { - "title": "Review early feedback and plan next steps for roadmap", - "description": "Beyond the initial launch, we're keeping the initial roadmap completely empty. This meeting will help us plan out our next steps based on actual customer feedback.", - "status": "Now", - "subtasks": [ - { - "title": "Interview 10 customers", - "isCompleted": false - }, - { - "title": "Review common customer pain points and suggestions", - "isCompleted": false - }, - { - "title": "Outline next steps for our roadmap", - "isCompleted": false - } - ] - } - ] - }, - { - "name": "Next", - "tasks": [] - }, - { - "name": "Later", - "tasks": [] - } - ] - } - ] -} +{"boards":[{"name":"Marketing Plan","columns":[{"name":"Todo","tasks":[{"title":"Plan Product Hunt launch","description":"","status":"Todo","subtasks":[{"title":"Find hunter","isCompleted":false},{"title":"Gather assets","isCompleted":false},{"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":"Roadmap","columns":[{"name":"Now","tasks":[{"title":"Launch version one","description":"","status":"Now","subtasks":[{"title":"Launch privately to our waitlist","isCompleted":false},{"title":"Launch publicly on PH, HN, etc.","isCompleted":false}]},{"title":"Review early feedback and plan next steps for roadmap","description":"Beyond the initial launch, we're keeping the initial roadmap completely empty. This meeting will help us plan out our next steps based on actual customer feedback.","status":"Now","subtasks":[{"title":"Interview 10 customers","isCompleted":false},{"title":"Review common customer pain points and suggestions","isCompleted":false},{"title":"Outline next steps for our roadmap","isCompleted":false}]}]},{"name":"Next","tasks":[]},{"name":"Later","tasks":[]}]},{"name":"renamed board","columns":[{"name":"col 111","tasks":[]},{"name":"col 333","tasks":[]},{"name":"9999","tasks":[]}]}]} \ No newline at end of file diff --git a/layout/Header.tsx b/layout/Header.tsx index dff3866..1888cfd 100644 --- a/layout/Header.tsx +++ b/layout/Header.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from "react"; +import { useDispatch } from "react-redux"; import Button from "../components/UI/Button"; import VerticalElipsisIcon from "@/icons/icon-vertical-ellipsis.svg"; import LogoDarkIcon from "@/icons/logo-dark.svg"; @@ -7,18 +7,10 @@ import LogoMobileIcon from "@/icons/logo-mobile.svg"; import ChevronDownIcon from "@/icons/icon-chevron-down.svg"; import AddIcon from "@/icons/icon-add-task-mobile.svg"; import Dropdown from "@/components/UI/Dropdown"; - -interface HeaderProps { - handleEditBoard: () => void; - handleDeleteBoard: () => void; - handleAddNewTask: () => void; -} - -const Header: React.FC = ({ - handleEditBoard, - handleDeleteBoard, - handleAddNewTask, -}) => { +import { setActiveModal } from "@/store/uiSlice"; +import ModalEnum from "@/model/ModalEnum"; +const Header = () => { + const dipsatch = useDispatch(); return (
    {/* Desktop Header */} @@ -33,18 +25,19 @@ const Header: React.FC = ({
    - )} + {board?.columns?.length === 0 || + (!board.columns && ( +
    +

    + This board is empty. Create a new column to get started. +

    +
    + ))}
    ); }; diff --git a/model/BoardListProps.tsx b/model/BoardListProps.tsx index cef13d5..baa0638 100644 --- a/model/BoardListProps.tsx +++ b/model/BoardListProps.tsx @@ -1,7 +1,4 @@ interface BoardListProps { boards: { name: string }[]; - onChangedaActiveBoard: (acitve: number) => void; - activeBoard: number; - onAddNewBoard: () => void; } export default BoardListProps; diff --git a/model/ModalEnum.tsx b/model/ModalEnum.tsx new file mode 100644 index 0000000..4faffe1 --- /dev/null +++ b/model/ModalEnum.tsx @@ -0,0 +1,9 @@ +enum ModalEnum { + EDIT_TASK = "EDIT_TASK", + CREATE_TASK = "CREATE_TASK", + EDIT_BOARD = "EDIT_BOARD", + CREATE_BOARD = "CREATE_BOARD", + DELETE_BOARD = "DELETE_BOARD", + DELETE_TASK = "DELETE_TASK", +} +export default ModalEnum; diff --git a/package-lock.json b/package-lock.json index f31ca32..c60c88c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "kanban-nextjs", "version": "0.1.0", "dependencies": { + "@reduxjs/toolkit": "^1.9.5", + "@tanstack/react-query": "^4.29.15", + "@tanstack/react-query-devtools": "^4.29.15", "@types/node": "20.3.1", "@types/react": "18.2.12", "@types/react-dom": "18.2.5", @@ -18,6 +21,7 @@ "postcss": "8.4.24", "react": "18.2.0", "react-dom": "18.2.0", + "react-redux": "^8.1.1", "tailwindcss": "3.3.2", "typescript": "5.1.3" }, @@ -2375,6 +2379,29 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@reduxjs/toolkit": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.5.tgz", + "integrity": "sha512-Rt97jHmfTeaxL4swLRNPD/zV4OxTes4la07Xc4hetpUW/vc75t5m1ANyxG6ymnEQ2FsLQsoMlYB2vV1sO3m8tQ==", + "dependencies": { + "immer": "^9.0.21", + "redux": "^4.2.1", + "redux-thunk": "^2.4.2", + "reselect": "^4.1.8" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18", + "react-redux": "^7.2.1 || ^8.0.2" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@rushstack/eslint-patch": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.3.1.tgz", @@ -2645,6 +2672,75 @@ "tslib": "^2.4.0" } }, + "node_modules/@tanstack/match-sorter-utils": { + "version": "8.8.4", + "resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.8.4.tgz", + "integrity": "sha512-rKH8LjZiszWEvmi01NR72QWZ8m4xmXre0OOwlRGnjU01Eqz/QnN+cqpty2PJ0efHblq09+KilvyR7lsbzmXVEw==", + "dependencies": { + "remove-accents": "0.4.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kentcdodds" + } + }, + "node_modules/@tanstack/query-core": { + "version": "4.29.15", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.29.15.tgz", + "integrity": "sha512-Recc1d5rjHesKhzlH3Aw66v+vQxtB9OHEXP/vxgEcEJ0DwEpfe3EQ4id20vuBJHY2XRjfgWGmUs6ZgK6PSsTXA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "4.29.15", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.29.15.tgz", + "integrity": "sha512-1zDkv95ljuJ623hhbYU8YIprPW2x6774kh3IQNEuZav62+S+Zr26uUOrE2zGRp9I1uO5Liw/0uYB3dWXQP5+3Q==", + "dependencies": { + "@tanstack/query-core": "4.29.15", + "use-sync-external-store": "^1.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-native": "*" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/@tanstack/react-query-devtools": { + "version": "4.29.15", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-4.29.15.tgz", + "integrity": "sha512-Mtt0c4xNVuo8T6K5+wQZ8hgL8wme99M/+XPwgLDS34PjmTPJs7kWyUuweNyWQZZZsUpAts69k4Dssh+nB5rwsg==", + "dependencies": { + "@tanstack/match-sorter-utils": "^8.7.0", + "superjson": "^1.10.0", + "use-sync-external-store": "^1.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "4.29.15", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@trysound/sax": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", @@ -2654,6 +2750,15 @@ "node": ">=10.13.0" } }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", + "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -2692,6 +2797,11 @@ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + }, "node_modules/@typescript-eslint/parser": { "version": "5.59.11", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.11.tgz", @@ -3340,6 +3450,20 @@ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "dev": true }, + "node_modules/copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/core-js-compat": { "version": "3.31.0", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.31.0.tgz", @@ -4670,6 +4794,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, "node_modules/human-signals": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", @@ -4686,6 +4818,15 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -5013,6 +5154,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-what": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.15.tgz", + "integrity": "sha512-uKua1wfy3Yt+YqsD6mTUEa2zSi3G1oPlqTflgaPJ7z63vUGN5pxFpnQfeSLMFnJDEsdvOtkp1rUWkYjB4YfhgA==", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -5935,6 +6087,49 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/react-redux": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.1.tgz", + "integrity": "sha512-5W0QaKtEhj+3bC0Nj0NkqkhIv8gLADH/2kYFMTHxCVqQILiWzLv6MaLuV5wJU3BQEdHKzTfcvPN0WMS6SC1oyA==", + "dependencies": { + "@babel/runtime": "^7.12.1", + "@types/hoist-non-react-statics": "^3.3.1", + "@types/use-sync-external-store": "^0.0.3", + "hoist-non-react-statics": "^3.3.2", + "react-is": "^18.0.0", + "use-sync-external-store": "^1.0.0" + }, + "peerDependencies": { + "@types/react": "^16.8 || ^17.0 || ^18.0", + "@types/react-dom": "^16.8 || ^17.0 || ^18.0", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0", + "react-native": ">=0.59", + "redux": "^4 || ^5.0.0-beta.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-redux/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -5954,6 +6149,22 @@ "node": ">=8.10.0" } }, + "node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, + "node_modules/redux-thunk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", + "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==", + "peerDependencies": { + "redux": "^4" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -6040,6 +6251,16 @@ "jsesc": "bin/jsesc" } }, + "node_modules/remove-accents": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz", + "integrity": "sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==" + }, + "node_modules/reselect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", + "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==" + }, "node_modules/resolve": { "version": "1.22.2", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", @@ -6482,6 +6703,17 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/superjson": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-1.12.3.tgz", + "integrity": "sha512-0j+U70KUtP8+roVPbwfqkyQI7lBt7ETnuA7KXbTDX3mCKiD/4fXs2ldKSMdt0MCfpTwiMxo20yFU3vu6ewETpQ==", + "dependencies": { + "copy-anything": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -6844,6 +7076,14 @@ "punycode": "^2.1.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", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -7422,8 +7662,7 @@ "version": "7.21.0-placeholder-for-preset-env.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", - "dev": true, - "requires": {} + "dev": true }, "@babel/plugin-proposal-unicode-property-regex": { "version": "7.18.6", @@ -8566,6 +8805,17 @@ "tslib": "^2.5.0" } }, + "@reduxjs/toolkit": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.5.tgz", + "integrity": "sha512-Rt97jHmfTeaxL4swLRNPD/zV4OxTes4la07Xc4hetpUW/vc75t5m1ANyxG6ymnEQ2FsLQsoMlYB2vV1sO3m8tQ==", + "requires": { + "immer": "^9.0.21", + "redux": "^4.2.1", + "redux-thunk": "^2.4.2", + "reselect": "^4.1.8" + } + }, "@rushstack/eslint-patch": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.3.1.tgz", @@ -8575,57 +8825,49 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", "integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==", - "dev": true, - "requires": {} + "dev": true }, "@svgr/babel-plugin-remove-jsx-attribute": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", - "dev": true, - "requires": {} + "dev": true }, "@svgr/babel-plugin-remove-jsx-empty-expression": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", - "dev": true, - "requires": {} + "dev": true }, "@svgr/babel-plugin-replace-jsx-attribute-value": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz", "integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==", - "dev": true, - "requires": {} + "dev": true }, "@svgr/babel-plugin-svg-dynamic-title": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz", "integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==", - "dev": true, - "requires": {} + "dev": true }, "@svgr/babel-plugin-svg-em-dimensions": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz", "integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==", - "dev": true, - "requires": {} + "dev": true }, "@svgr/babel-plugin-transform-react-native-svg": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.0.0.tgz", "integrity": "sha512-UKrY3860AQICgH7g+6h2zkoxeVEPLYwX/uAjmqo4PIq2FIHppwhIqZstIyTz0ZtlwreKR41O3W3BzsBBiJV2Aw==", - "dev": true, - "requires": {} + "dev": true }, "@svgr/babel-plugin-transform-svg-component": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz", "integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==", - "dev": true, - "requires": {} + "dev": true }, "@svgr/babel-preset": { "version": "8.0.0", @@ -8713,12 +8955,53 @@ "tslib": "^2.4.0" } }, + "@tanstack/match-sorter-utils": { + "version": "8.8.4", + "resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.8.4.tgz", + "integrity": "sha512-rKH8LjZiszWEvmi01NR72QWZ8m4xmXre0OOwlRGnjU01Eqz/QnN+cqpty2PJ0efHblq09+KilvyR7lsbzmXVEw==", + "requires": { + "remove-accents": "0.4.2" + } + }, + "@tanstack/query-core": { + "version": "4.29.15", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.29.15.tgz", + "integrity": "sha512-Recc1d5rjHesKhzlH3Aw66v+vQxtB9OHEXP/vxgEcEJ0DwEpfe3EQ4id20vuBJHY2XRjfgWGmUs6ZgK6PSsTXA==" + }, + "@tanstack/react-query": { + "version": "4.29.15", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.29.15.tgz", + "integrity": "sha512-1zDkv95ljuJ623hhbYU8YIprPW2x6774kh3IQNEuZav62+S+Zr26uUOrE2zGRp9I1uO5Liw/0uYB3dWXQP5+3Q==", + "requires": { + "@tanstack/query-core": "4.29.15", + "use-sync-external-store": "^1.2.0" + } + }, + "@tanstack/react-query-devtools": { + "version": "4.29.15", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-4.29.15.tgz", + "integrity": "sha512-Mtt0c4xNVuo8T6K5+wQZ8hgL8wme99M/+XPwgLDS34PjmTPJs7kWyUuweNyWQZZZsUpAts69k4Dssh+nB5rwsg==", + "requires": { + "@tanstack/match-sorter-utils": "^8.7.0", + "superjson": "^1.10.0", + "use-sync-external-store": "^1.2.0" + } + }, "@trysound/sax": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", "dev": true }, + "@types/hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", + "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "requires": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -8757,6 +9040,11 @@ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" }, + "@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + }, "@typescript-eslint/parser": { "version": "5.59.11", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.11.tgz", @@ -8813,8 +9101,7 @@ "acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "requires": {} + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==" }, "ajv": { "version": "6.12.6", @@ -9177,6 +9464,14 @@ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "dev": true }, + "copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "requires": { + "is-what": "^4.1.8" + } + }, "core-js-compat": { "version": "3.31.0", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.31.0.tgz", @@ -9782,8 +10077,7 @@ "eslint-plugin-react-hooks": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", - "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", - "requires": {} + "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==" }, "eslint-scope": { "version": "7.2.0", @@ -10136,6 +10430,14 @@ "has-symbols": "^1.0.2" } }, + "hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "requires": { + "react-is": "^16.7.0" + } + }, "human-signals": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", @@ -10146,6 +10448,11 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==" }, + "immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==" + }, "import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -10353,6 +10660,11 @@ "call-bind": "^1.0.2" } }, + "is-what": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.15.tgz", + "integrity": "sha512-uKua1wfy3Yt+YqsD6mTUEa2zSi3G1oPlqTflgaPJ7z63vUGN5pxFpnQfeSLMFnJDEsdvOtkp1rUWkYjB4YfhgA==" + }, "is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -10956,6 +11268,26 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "react-redux": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.1.tgz", + "integrity": "sha512-5W0QaKtEhj+3bC0Nj0NkqkhIv8gLADH/2kYFMTHxCVqQILiWzLv6MaLuV5wJU3BQEdHKzTfcvPN0WMS6SC1oyA==", + "requires": { + "@babel/runtime": "^7.12.1", + "@types/hoist-non-react-statics": "^3.3.1", + "@types/use-sync-external-store": "^0.0.3", + "hoist-non-react-statics": "^3.3.2", + "react-is": "^18.0.0", + "use-sync-external-store": "^1.0.0" + }, + "dependencies": { + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + } + } + }, "read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -10972,6 +11304,19 @@ "picomatch": "^2.2.1" } }, + "redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "requires": { + "@babel/runtime": "^7.9.2" + } + }, + "redux-thunk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", + "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==" + }, "regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -11042,6 +11387,16 @@ } } }, + "remove-accents": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz", + "integrity": "sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==" + }, + "reselect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", + "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==" + }, "resolve": { "version": "1.22.2", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", @@ -11329,6 +11684,14 @@ } } }, + "superjson": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-1.12.3.tgz", + "integrity": "sha512-0j+U70KUtP8+roVPbwfqkyQI7lBt7ETnuA7KXbTDX3mCKiD/4fXs2ldKSMdt0MCfpTwiMxo20yFU3vu6ewETpQ==", + "requires": { + "copy-anything": "^3.0.2" + } + }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -11579,6 +11942,11 @@ "punycode": "^2.1.0" } }, + "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", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==" + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index a845035..6e0aba1 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,9 @@ "lint": "next lint" }, "dependencies": { + "@reduxjs/toolkit": "^1.9.5", + "@tanstack/react-query": "^4.29.15", + "@tanstack/react-query-devtools": "^4.29.15", "@types/node": "20.3.1", "@types/react": "18.2.12", "@types/react-dom": "18.2.5", @@ -19,6 +22,7 @@ "postcss": "8.4.24", "react": "18.2.0", "react-dom": "18.2.0", + "react-redux": "^8.1.1", "tailwindcss": "3.3.2", "typescript": "5.1.3" }, diff --git a/pages/_app.tsx b/pages/_app.tsx index 021681f..cdb2bcb 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,6 +1,23 @@ -import '@/styles/globals.css' -import type { AppProps } from 'next/app' - +import "@/styles/globals.css"; +import type { AppProps } from "next/app"; +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) { - return + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 0, + }, + }, + }); + return ( + + + + + + + ); } diff --git a/pages/api/boards/[boardId].ts b/pages/api/boards/[boardId].ts new file mode 100644 index 0000000..551b604 --- /dev/null +++ b/pages/api/boards/[boardId].ts @@ -0,0 +1,29 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import { getData, setData } from "@/utils/boards-fs"; +import type { NextApiRequest, NextApiResponse } from "next"; + +export default function handler(req: NextApiRequest, res: NextApiResponse) { + //validate id + const id = parseInt(req.query?.boardId as string); + const data = getData(); + if (!Number.isInteger(id) || id >= data.boards.length || id < 0) { + res.status(404).json({ message: "Board not found", status: "failed" }); + return; + } + + if (req.method === "GET") { + res.status(200).json(data.boards[id]); + } + + if (req.method === "DELETE") { + data.boards.splice(id, 1); + setData(data); + res.status(200).json({}); + } + + if (req.method === "PATCH") { + data.boards[id] = req.body; + setData(data); + res.status(200).json(req.body); + } +} diff --git a/pages/api/boards/index.ts b/pages/api/boards/index.ts new file mode 100644 index 0000000..14002a7 --- /dev/null +++ b/pages/api/boards/index.ts @@ -0,0 +1,25 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type { NextApiRequest, NextApiResponse } from "next"; + +import Board from "@/model/Board"; +import { getData, setData } from "@/utils/boards-fs"; +type Data = { + boards: Board[]; +}; + +export default function handler( + req: NextApiRequest, + res: NextApiResponse +) { + const data = getData(); + + if (req.method === "GET") { + res.status(200).json(data); + } + + if (req.method === "POST") { + data.boards.push(req.body); + setData(data); + res.status(201).json(req.body); + } +} diff --git a/pages/api/hello.ts b/pages/api/hello.ts deleted file mode 100644 index f8bcc7e..0000000 --- a/pages/api/hello.ts +++ /dev/null @@ -1,13 +0,0 @@ -// Next.js API route support: https://nextjs.org/docs/api-routes/introduction -import type { NextApiRequest, NextApiResponse } from 'next' - -type Data = { - name: string -} - -export default function handler( - req: NextApiRequest, - res: NextApiResponse -) { - res.status(200).json({ name: 'John Doe' }) -} diff --git a/pages/index.tsx b/pages/index.tsx index c3332ff..ac02dee 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,11 +1,11 @@ import { useEffect } from "react"; -import Button from "@/components/UI/Button"; -import Checkbox from "@/components/UI/Checkbox"; +import { useSelector, useDispatch } from "react-redux"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; + import Header from "@/layout/Header"; import Sidebar from "@/layout/Sidebar"; import Tasks from "@/layout/Tasks"; -import data from "@/data/data.json"; -import { useState } from "react"; + import Board from "@/model/Board"; import Modal from "@/components/UI/Modal"; import CreateBoard from "@/components/CreateBoard"; @@ -14,52 +14,84 @@ import EditBoard from "@/components/EditBoard"; import CreateTask from "@/components/CreateTask"; import Task from "@/model/Task"; import ViewTask from "@/components/ViewTask"; +import { + selectBoard, + selectModal, + selectTask, + setActiveBoard, + setActiveModal, + setOpenedTask, +} from "@/store/uiSlice"; + +import ModalEnum from "@/model/ModalEnum"; +import { + createBoard, + deleteBoard, + editBoard, + getBoards, +} from "@/services/apiBoards"; + +import { getData } from "@/utils/boards-fs"; interface HomeProps { - data: { boards: Board[] }; + prefetchedData: { boards: Board[] }; } -const Home: React.FC = ({ data }) => { - const [activeBoard, setActiveBoard] = useState(0); - const [newBoardModalIsOpen, setNewBoardModalIsOpen] = useState(false); - const [editBoardModalIsOpen, setEditBoardModalIsOpen] = useState(false); - const [deleteBoardModalIsOpen, setDeleteBoardModalIsOpen] = useState(false); - const [deleteTaskModalIsOpen, setDeleteTaskModalIsOpen] = useState(false); - const [newTaskModalIsOpen, setNewTaskModalIsOpen] = useState(false); - const [openedTask, setOpenedTask] = useState<{ - taskIndex: number; - colIndex: number; - }>(); - const [boards, setBoards] = useState(data.boards); - - const handleAddNewBoard = () => { - setNewBoardModalIsOpen(true); - }; +const Home: React.FC = ({ prefetchedData }) => { + const activeBoard = useSelector(selectBoard); + const activeModal = useSelector(selectModal); + const openedTask = useSelector(selectTask); + + const dispatch = useDispatch(); + + const { + isLoading, + data: { boards }, + error, + } = useQuery({ + queryKey: ["boards"], + queryFn: getBoards, + initialData: prefetchedData, + }); const handleCreateBoard = (board: Board) => { - setBoards([...boards, board]); - setActiveBoard(boards.length); - setNewBoardModalIsOpen(false); + create(board); }; - const handleChangeActiveBoard = (active: number) => { - setActiveBoard(active); - }; + const { mutate: create } = useMutation({ + mutationFn: createBoard, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["boards"] }).then(() => { + dispatch(setActiveBoard(boards.length)); + }); + dispatch(setActiveModal(undefined)); + }, + }); const handleSaveEditBoard = (board: Board) => { - const updatedBoards = [...boards]; - const boardIndex = updatedBoards.findIndex((el) => el.name === board.name); - if (boardIndex > -1) { - updatedBoards[boardIndex] = board; - } - setBoards(updatedBoards); - setEditBoardModalIsOpen(false); + update({ id: activeBoard.toString(), board }); }; + const queryClient = useQueryClient(); + + const { isLoading: loading, mutate } = useMutation({ + mutationFn: (id: string) => deleteBoard(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["boards"] }); + dispatch(setActiveModal(undefined)); + dispatch(setActiveBoard(0)); + }, + }); + + const { mutate: update } = useMutation({ + mutationFn: ({ id, board }: { id: string; board: Board }) => + editBoard(id, board), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["boards"] }); + dispatch(setActiveModal(undefined)); + }, + }); + const handleConfirmDeleteBoard = () => { - setDeleteBoardModalIsOpen(false); - const updatedBoards = [...boards]; - updatedBoards.splice(activeBoard, 1); - setBoards(updatedBoards); - setActiveBoard(0); + mutate(activeBoard.toString()); }; const handleAddNewTask = (task: Task) => { @@ -71,13 +103,14 @@ const Home: React.FC = ({ data }) => { if (columnIndex > -1) { updatedBoards[activeBoard].columns[columnIndex].tasks.push(task); - setBoards(updatedBoards); + //setBoards(updatedBoards); } - setNewTaskModalIsOpen(false); + //setNewTaskModalIsOpen(false); }; const handleClickedTask = (colIndex: number, taskIndex: number) => { - setOpenedTask({ taskIndex, colIndex }); + //setOpenedTask({ taskIndex, colIndex }); + dispatch(setOpenedTask({ taskIndex, colIndex })); }; const handleChangeTask = (task: Task) => { @@ -86,7 +119,7 @@ const Home: React.FC = ({ data }) => { updatedBoards[activeBoard].columns[openedTask.colIndex].tasks[ openedTask.taskIndex ] = task; - setBoards(updatedBoards); + //setBoards(updatedBoards); } }; @@ -106,7 +139,7 @@ const Home: React.FC = ({ data }) => { 1 ); - //add the task to target column. firts find it's index + //add the task to target column. first find it's index const targetColIndex = updatedBoards[activeBoard].columns.findIndex( (el) => el.name === status ); @@ -125,7 +158,7 @@ const Home: React.FC = ({ data }) => { }); //finally update global state of boards - setBoards(updatedBoards); + //setBoards(updatedBoards); } }; const handleDeleteTask = () => { @@ -136,10 +169,10 @@ const Home: React.FC = ({ data }) => { 1 ); - setBoards(updatedBoards); + //setBoards(updatedBoards); } setOpenedTask(undefined); - setDeleteTaskModalIsOpen(false); + //setDeleteTaskModalIsOpen(false); }; useEffect(() => { const html = document.querySelector("html"); @@ -150,35 +183,30 @@ const Home: React.FC = ({ data }) => { return (
    -
    setDeleteBoardModalIsOpen(true)} - handleEditBoard={() => setEditBoardModalIsOpen(true)} - handleAddNewTask={() => setNewTaskModalIsOpen(true)} - /> +
    { return { name: item.name }; })} - onAddNewBoard={handleAddNewBoard} - onChangedaActiveBoard={handleChangeActiveBoard} - activeBoard={activeBoard} /> setEditBoardModalIsOpen(true)} + onCreateColumn={() => + dispatch(setActiveModal(ModalEnum.EDIT_BOARD)) + } onClickedTask={handleClickedTask} />
    - {newBoardModalIsOpen && ( - setNewBoardModalIsOpen(false)}> + {activeModal === ModalEnum.CREATE_BOARD && ( + dispatch(setActiveModal(undefined))}> )} - {editBoardModalIsOpen && ( - setEditBoardModalIsOpen(false)}> + {activeModal === ModalEnum.EDIT_BOARD && ( + dispatch(setActiveModal(undefined))}> = ({ data }) => { )} - {deleteBoardModalIsOpen && ( - setDeleteBoardModalIsOpen(false)}> + {activeModal === ModalEnum.DELETE_BOARD && ( + dispatch(setActiveModal(undefined))}> setDeleteBoardModalIsOpen(false)} + onCancel={() => dispatch(setActiveModal(undefined))} onConfirm={handleConfirmDeleteBoard} /> )} - {newTaskModalIsOpen && ( - setNewTaskModalIsOpen(false)}> + {activeModal === ModalEnum.CREATE_TASK && ( + { + dispatch(setActiveModal(undefined)); + }} + > { return { label: col.name, value: col.name }; @@ -207,8 +239,14 @@ const Home: React.FC = ({ data }) => { /> )} - {openedTask && !deleteTaskModalIsOpen && ( - setOpenedTask(undefined)}> + + {openedTask && !(activeModal === ModalEnum.DELETE_TASK) && ( + { + dispatch(setActiveModal(undefined)); + dispatch(setOpenedTask(undefined)); + }} + > = ({ data }) => { })} onChangeTask={handleChangeTask} handleChangeTaskStatus={handleChangeTaskStatus} - onDeleteTask={() => setDeleteTaskModalIsOpen(true)} + onDeleteTask={() => + dispatch(setActiveModal(ModalEnum.DELETE_TASK)) + } /> )} - {openedTask && deleteTaskModalIsOpen && ( - setOpenedTask(undefined)}> + {openedTask && activeModal === ModalEnum.DELETE_TASK && ( + dispatch(setOpenedTask(undefined))}> = ({ data }) => { ].title }’ task and its subtasks? This action cannot be reversed.`} onCancel={() => { - setDeleteTaskModalIsOpen(false); - setOpenedTask(undefined); + dispatch(setActiveModal(undefined)); + dispatch(setOpenedTask(undefined)); }} onConfirm={handleDeleteTask} /> @@ -248,5 +288,6 @@ const Home: React.FC = ({ data }) => { export default Home; export async function getStaticProps() { - return { props: { data } }; + const prefetchedData = getData(); + return { props: { prefetchedData } }; } diff --git a/services/apiBoards.ts b/services/apiBoards.ts new file mode 100644 index 0000000..5b792f6 --- /dev/null +++ b/services/apiBoards.ts @@ -0,0 +1,38 @@ +import Board from "@/model/Board"; +export const getBoards = async () => { + const response = await fetch("http://localhost:3000/api/boards"); + const data = response.json(); + return data; +}; + +export const createBoard = async (board: Board) => { + const response = await fetch("http://localhost:3000/api/boards", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(board), + }); + const data = await response.json(); + return data; +}; + +export const editBoard = async (id: string, board: Board) => { + const response = await fetch(`http://localhost:3000/api/boards/${id}`, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(board), + }); + const data = await response.json(); + return data; +}; + +export const deleteBoard = async (id: string) => { + const response = await fetch(`http://localhost:3000/api/boards/${id}`, { + method: "DELETE", + }); + const data = await response.json(); + return data; +}; diff --git a/store/store.ts b/store/store.ts new file mode 100644 index 0000000..b12b613 --- /dev/null +++ b/store/store.ts @@ -0,0 +1,11 @@ +import { configureStore } from "@reduxjs/toolkit"; +// ... +import uiReducer from "./uiSlice"; +export const store = configureStore({ + reducer: { ui: uiReducer }, +}); + +// Infer the `RootState` and `AppDispatch` types from the store itself +export type RootState = ReturnType; +// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState} +export type AppDispatch = typeof store.dispatch; diff --git a/store/uiSlice.ts b/store/uiSlice.ts new file mode 100644 index 0000000..762995e --- /dev/null +++ b/store/uiSlice.ts @@ -0,0 +1,43 @@ +import { createSlice } from "@reduxjs/toolkit"; +import type { PayloadAction } from "@reduxjs/toolkit"; +import type { RootState } from "./store"; +import ModalEnum from "@/model/ModalEnum"; +// Define a type for the slice state +interface UiState { + activeBoard: number; + openedTask: { taskIndex: number; colIndex: number } | undefined; + openedModal: ModalEnum | undefined; +} + +// Define the initial state using that type +const initialState: UiState = { + activeBoard: 0, + openedTask: undefined, + openedModal: undefined, +}; +export const uiSlice = createSlice({ + name: "ui", + initialState, + reducers: { + setActiveBoard: (state, action: PayloadAction) => { + state.activeBoard = action.payload; + }, + + setActiveModal: (state, action: PayloadAction) => { + state.openedModal = action.payload; + }, + + setOpenedTask: ( + state, + action: PayloadAction<{ taskIndex: number; colIndex: number } | undefined> + ) => { + state.openedTask = action.payload; + }, + }, +}); +export const { setActiveBoard, setActiveModal, setOpenedTask } = + 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 default uiSlice.reducer; diff --git a/utils/boards-fs.ts b/utils/boards-fs.ts new file mode 100644 index 0000000..5d11ee9 --- /dev/null +++ b/utils/boards-fs.ts @@ -0,0 +1,13 @@ +import Board from "@/model/Board"; +import fs from "fs"; +import path from "path"; +const filePath = path.join(process.cwd(), "data", "data.json"); + +export const getData = () => { + const fileData = fs.readFileSync(filePath); + const data = JSON.parse(fileData.toString()); + return data; +}; +export const setData = (data: { boards: Board[] }) => { + fs.writeFileSync(filePath, JSON.stringify(data)); +};