diff --git a/front/package-lock.json b/front/package-lock.json index e830065b7..075ea066a 100644 --- a/front/package-lock.json +++ b/front/package-lock.json @@ -23,6 +23,7 @@ "libphonenumber-js": "^1.10.26", "luxon": "^3.3.0", "react": "^18.2.0", + "react-beautiful-dnd": "^13.1.1", "react-datepicker": "^4.11.0", "react-dom": "^18.2.0", "react-hotkeys-hook": "^4.4.0", @@ -58,6 +59,7 @@ "@testing-library/user-event": "^13.5.0", "@types/jest": "^27.5.2", "@types/luxon": "^3.3.0", + "@types/react-beautiful-dnd": "^13.1.4", "@types/react-datepicker": "^4.11.2", "@types/uuid": "^9.0.1", "@typescript-eslint/eslint-plugin": "^5.45.0", @@ -10961,6 +10963,15 @@ "@types/node": "*" } }, + "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/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -11174,6 +11185,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-datepicker": { "version": "4.11.2", "resolved": "https://registry.npmjs.org/@types/react-datepicker/-/react-datepicker-4.11.2.tgz", @@ -11194,6 +11214,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/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -16071,6 +16102,14 @@ "postcss": "^8.4" } }, + "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-declaration-sorter": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.0.tgz", @@ -28433,6 +28472,11 @@ "node": ">= 4.0.0" } }, + "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/memoizerific": { "version": "1.11.3", "resolved": "https://registry.npmjs.org/memoizerific/-/memoizerific-1.11.3.tgz", @@ -32085,6 +32129,11 @@ "performance-now": "^2.1.0" } }, + "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/ramda": { "version": "0.29.0", "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.29.0.tgz", @@ -32165,6 +32214,24 @@ "asap": "~2.0.6" } }, + "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-colorful": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz", @@ -32559,6 +32626,35 @@ "react-dom": "^16.8.0 || ^17 || ^18" } }, + "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-redux/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-refresh": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", @@ -34080,6 +34176,14 @@ "node": ">=8" } }, + "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/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -36694,6 +36798,11 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "dev": true }, + "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/title-case": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/title-case/-/title-case-3.0.3.tgz", @@ -37560,6 +37669,14 @@ } } }, + "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-resize-observer": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/use-resize-observer/-/use-resize-observer-9.1.0.tgz", diff --git a/front/package.json b/front/package.json index 8b7b17530..4ff280a3f 100644 --- a/front/package.json +++ b/front/package.json @@ -18,6 +18,7 @@ "libphonenumber-js": "^1.10.26", "luxon": "^3.3.0", "react": "^18.2.0", + "react-beautiful-dnd": "^13.1.1", "react-datepicker": "^4.11.0", "react-dom": "^18.2.0", "react-hotkeys-hook": "^4.4.0", @@ -102,6 +103,7 @@ "@testing-library/user-event": "^13.5.0", "@types/jest": "^27.5.2", "@types/luxon": "^3.3.0", + "@types/react-beautiful-dnd": "^13.1.4", "@types/react-datepicker": "^4.11.2", "@types/uuid": "^9.0.1", "@typescript-eslint/eslint-plugin": "^5.45.0", diff --git a/front/src/modules/ui/components/board/Board.tsx b/front/src/modules/ui/components/board/Board.tsx new file mode 100644 index 000000000..9d6edabaf --- /dev/null +++ b/front/src/modules/ui/components/board/Board.tsx @@ -0,0 +1,127 @@ +import { useCallback, useState } from 'react'; +import { + DragDropContext, + Draggable, + Droppable, + OnDragEndResponder, +} from 'react-beautiful-dnd'; +import styled from '@emotion/styled'; + +import { BoardCard } from './BoardCard'; +import { BoardColumn } from './BoardColumn'; + +const StyledBoard = styled.div` + display: flex; + flex-direction: row; + height: 100%; +`; + +type ItemKey = `item-${number}`; +interface Item { + id: string; + content: string; +} +interface Items { + [key: string]: Item; +} +interface Column { + id: string; + title: string; + itemKeys: ItemKey[]; +} + +const items: Items = { + 'item-1': { id: 'item-1', content: 'Item 1' }, + 'item-2': { id: 'item-2', content: 'Item 2' }, + 'item-3': { id: 'item-3', content: 'Item 3' }, + 'item-4': { id: 'item-4', content: 'Item 4' }, + 'item-5': { id: 'item-5', content: 'Item 5' }, + 'item-6': { id: 'item-6', content: 'Item 6' }, +} satisfies Record; + +const initialBoard = [ + { + id: 'column-1', + title: 'Column 1', + itemKeys: ['item-1', 'item-2', 'item-3', 'item-4'], + }, + { + id: 'column-2', + title: 'Column 2', + itemKeys: ['item-5', 'item-6'], + }, +] satisfies Column[]; + +export const Board = () => { + const [board, setBoard] = useState(initialBoard); + + const onDragEnd: OnDragEndResponder = useCallback( + (result) => { + const { destination, source } = result; + if (!destination) return; + const sourceColumnIndex = board.findIndex( + (column) => column.id === source.droppableId, + ); + const sourceColumn = board[sourceColumnIndex]; + const destinationColumnIndex = board.findIndex( + (column) => column.id === destination.droppableId, + ); + const destinationColumn = board[destinationColumnIndex]; + if (!destinationColumn || !sourceColumn) return; + const sourceItems = sourceColumn.itemKeys; + const destinationItems = destinationColumn.itemKeys; + + const [removed] = sourceItems.splice(source.index, 1); + destinationItems.splice(destination.index, 0, removed); + + const newSourceColumn = { + ...sourceColumn, + itemKeys: sourceItems, + }; + + const newDestinationColumn = { + ...destinationColumn, + itemKeys: destinationItems, + }; + + const newBoard = [...board]; + newBoard.splice(sourceColumnIndex, 1, newSourceColumn); + newBoard.splice(destinationColumnIndex, 1, newDestinationColumn); + setBoard(newBoard); + }, + [board], + ); + + return ( + + + {board.map((column) => ( + + {(provided) => + provided && ( + + {column.itemKeys.map((itemKey, index) => ( + + {(provided) => + provided && ( + + ) + } + + ))} + + ) + } + + ))} + + + ); +}; diff --git a/front/src/modules/ui/components/board/BoardCard.tsx b/front/src/modules/ui/components/board/BoardCard.tsx new file mode 100644 index 000000000..20c8868b2 --- /dev/null +++ b/front/src/modules/ui/components/board/BoardCard.tsx @@ -0,0 +1,26 @@ +import { DraggableProvided } from 'react-beautiful-dnd'; +import styled from '@emotion/styled'; + +const StyledCard = styled.div` + background-color: #ffffff; + border-radius: 4px; + padding: 16px; + margin-bottom: 16px; +`; + +type BoardCardProps = { + content: string; + draggableProvided: DraggableProvided; +}; + +export const BoardCard = ({ content, draggableProvided }: BoardCardProps) => { + return ( + + {content} + + ); +}; diff --git a/front/src/modules/ui/components/board/BoardColumn.tsx b/front/src/modules/ui/components/board/BoardColumn.tsx new file mode 100644 index 000000000..fc9560c6d --- /dev/null +++ b/front/src/modules/ui/components/board/BoardColumn.tsx @@ -0,0 +1,35 @@ +import { DroppableProvided } from 'react-beautiful-dnd'; +import styled from '@emotion/styled'; + +const StyledColumn = styled.div` + display: flex; + flex-direction: column; + width: 300px; + margin-right: 16px; + background-color: #f5f5f5; + border-radius: 4px; + padding: 16px; +`; + +type BoardColumnProps = { + title: string; + children: any[]; + droppableProvided: DroppableProvided; +}; + +export const BoardColumn = ({ + title, + children, + droppableProvided, +}: BoardColumnProps) => { + return ( + +

{title}

+ {children} + {droppableProvided.placeholder} +
+ ); +}; diff --git a/front/src/modules/ui/components/board/__stories__/Board.stories.tsx b/front/src/modules/ui/components/board/__stories__/Board.stories.tsx new file mode 100644 index 000000000..06c5d2323 --- /dev/null +++ b/front/src/modules/ui/components/board/__stories__/Board.stories.tsx @@ -0,0 +1,15 @@ +import { Meta, StoryObj } from '@storybook/react'; + +import { Board } from '../Board'; + +const meta: Meta = { + title: 'Components/Board', + component: Board, +}; + +export default meta; +type Story = StoryObj; + +export const OneColumnBoard: Story = { + render: () => , +};