diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 932928d..7cc3f9c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -288,6 +288,9 @@ importers: react: specifier: ^18.2.0 version: 18.2.0 + react-beautiful-dnd: + specifier: ^13.1.1 + version: 13.1.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react-day-picker: specifier: ^8.10.1 version: 8.10.1(date-fns@3.6.0)(react@18.2.0) @@ -376,6 +379,9 @@ importers: '@types/react': specifier: ^18.2.22 version: 18.2.78 + '@types/react-beautiful-dnd': + specifier: ^13.1.8 + version: 13.1.8 '@types/react-dom': specifier: ^18.2.7 version: 18.2.7 @@ -4707,6 +4713,9 @@ packages: '@types/range-parser@1.2.4': resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==} + '@types/react-beautiful-dnd@13.1.8': + resolution: {integrity: sha512-E3TyFsro9pQuK4r8S/OL6G99eq7p8v29sX0PM7oT8Z+PJfZvSQTx4zTQbUJ+QZXioAF0e7TGBEcA1XhYhCweyQ==} + '@types/react-dom@18.2.7': resolution: {integrity: sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA==} @@ -6103,6 +6112,9 @@ packages: resolution: {integrity: sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==} engines: {node: '>=12'} + css-box-model@1.2.1: + resolution: {integrity: sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==} + css-color-converter@2.0.0: resolution: {integrity: sha512-oLIG2soZz3wcC3aAl/7Us5RS8Hvvc6I8G8LniF/qfMmrm7fIKQ8RIDDRZeKyGL2SrWfNqYspuLShbnjBMVWm8g==} @@ -8729,6 +8741,9 @@ packages: resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} engines: {node: '>= 4.0.0'} + memoize-one@5.2.1: + resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} + meow@12.1.1: resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} engines: {node: '>=16.10'} @@ -10417,6 +10432,9 @@ packages: radix3@1.1.0: resolution: {integrity: sha512-pNsHDxbGORSvuSScqNJ+3Km6QAVqk8CfsCBIEoDgpqLrkD2f3QM4I7d1ozJJ172OmIcoUcerZaNWqtLkRXTV3A==} + raf-schd@4.0.3: + resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==} + randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} @@ -10764,6 +10782,12 @@ packages: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true + react-beautiful-dnd@13.1.1: + resolution: {integrity: sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==} + peerDependencies: + react: ^16.8.5 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.5 || ^17.0.0 || ^18.0.0 + react-color@2.17.1: resolution: {integrity: sha512-S+I6TkUKJaqfALLkAIfiCZ/MANQyy7dKkf7g9ZU5GTUy2rf8c2Rx62otyvADAviWR+6HRkzdf2vL1Qvz9goCLQ==} peerDependencies: @@ -12617,6 +12641,11 @@ packages: '@types/react': optional: true + use-memo-one@1.1.3: + resolution: {integrity: sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + use-sidecar@1.1.2: resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==} engines: {node: '>=10'} @@ -18765,6 +18794,10 @@ snapshots: '@types/range-parser@1.2.4': {} + '@types/react-beautiful-dnd@13.1.8': + dependencies: + '@types/react': 18.2.78 + '@types/react-dom@18.2.7': dependencies: '@types/react': 18.2.78 @@ -20466,6 +20499,10 @@ snapshots: dependencies: type-fest: 1.4.0 + css-box-model@1.2.1: + dependencies: + tiny-invariant: 1.3.3 + css-color-converter@2.0.0: dependencies: color-convert: 0.5.3 @@ -23830,6 +23867,8 @@ snapshots: dependencies: fs-monkey: 1.0.5 + memoize-one@5.2.1: {} + meow@12.1.1: {} merge-descriptors@1.0.1: {} @@ -26000,6 +26039,8 @@ snapshots: radix3@1.1.0: {} + raf-schd@4.0.3: {} + randombytes@2.1.0: dependencies: safe-buffer: 5.2.1 @@ -26710,6 +26751,20 @@ snapshots: minimist: 1.2.8 strip-json-comments: 2.0.1 + react-beautiful-dnd@13.1.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@babel/runtime': 7.24.0 + css-box-model: 1.2.1 + memoize-one: 5.2.1 + raf-schd: 4.0.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-redux: 7.2.9(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + redux: 4.2.1 + use-memo-one: 1.1.3(react@18.2.0) + transitivePeerDependencies: + - react-native + react-color@2.17.1(react@18.2.0): dependencies: '@icons/material': 0.2.4(react@18.2.0) @@ -26896,6 +26951,18 @@ snapshots: react-magic-dropzone@1.0.1: {} + react-redux@7.2.9(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@babel/runtime': 7.24.0 + '@types/react-redux': 7.1.33 + hoist-non-react-statics: 3.3.2 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.2.0 + react-is: 17.0.2 + optionalDependencies: + react-dom: 18.2.0(react@18.2.0) + react-redux@7.2.9(react-dom@18.2.0(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.24.0 @@ -29066,6 +29133,10 @@ snapshots: optionalDependencies: '@types/react': 18.2.78 + use-memo-one@1.1.3(react@18.2.0): + dependencies: + react: 18.2.0 + use-sidecar@1.1.2(@types/react@18.2.78)(react@18.2.0): dependencies: detect-node-es: 1.1.0 diff --git a/src/client/components/SortableGroup.tsx b/src/client/components/SortableGroup.tsx new file mode 100644 index 0000000..45ee668 --- /dev/null +++ b/src/client/components/SortableGroup.tsx @@ -0,0 +1,62 @@ +import React, { useState } from 'react'; +import { DragDropContext, DropResult } from 'react-beautiful-dnd'; +import { useEvent } from '@/hooks/useEvent'; +import { reorder } from '@/utils/reorder'; + +interface BasicItem { + type: 'root' | 'group' | 'item'; + items?: BasicItem[]; +} + +interface SortableGroupProps { + list: BasicItem[]; + onChange: (list: BasicItem[]) => void; + children: React.ReactNode; +} + +export const SortableGroup: React.FC = React.memo( + (props) => { + const [list, setList] = useState(props.list); + + const handleDragEnd = useEvent((result: DropResult) => { + // dropped outside the list + if (!result.destination) { + return; + } + + if (result.type === 'root') { + setList(reorder(list, result.source.index, result.destination.index)); + return; + } + + if (result.type === 'group') { + const nestedIndex = list.findIndex((item): boolean => 'items' in item); + + if (nestedIndex === 0) { + return; + } + + const nested = list[nestedIndex].items; + if (!nested) { + return; + } + + const children = Array.from(list); + children[nestedIndex].items = reorder( + nested, + result.source.index, + result.destination.index + ); + + setList([...list]); + } + }); + + return ( + + {props.children} + + ); + } +); +SortableGroup.displayName = 'SortableGroup'; diff --git a/src/client/package.json b/src/client/package.json index fc2cd3f..e9e3af0 100644 --- a/src/client/package.json +++ b/src/client/package.json @@ -75,6 +75,7 @@ "next-themes": "^0.2.1", "pretty-ms": "^9.0.0", "react": "^18.2.0", + "react-beautiful-dnd": "^13.1.1", "react-day-picker": "^8.10.1", "react-dom": "^18.2.0", "react-easy-sort": "^1.5.3", @@ -106,6 +107,7 @@ "@types/loadable__component": "^5.13.8", "@types/lodash-es": "^4.17.12", "@types/react": "^18.2.22", + "@types/react-beautiful-dnd": "^13.1.8", "@types/react-dom": "^18.2.7", "@types/react-grid-layout": "^1.3.5", "@types/react-helmet": "^6.1.11", diff --git a/src/client/utils/reorder.ts b/src/client/utils/reorder.ts new file mode 100644 index 0000000..10a34c9 --- /dev/null +++ b/src/client/utils/reorder.ts @@ -0,0 +1,12 @@ +// a little function to help us with reordering the result +export function reorder( + list: T[], + startIndex: number, + endIndex: number +): T[] { + const result = Array.from(list); + const [removed] = result.splice(startIndex, 1); + result.splice(endIndex, 0, removed); + + return result; +}