diff --git a/package-lock.json b/package-lock.json index 3ea36cb..7386fa6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "triggerfrontend", "version": "0.0.0", "dependencies": { + "@hello-pangea/dnd": "^18.0.1", "@iconify/react": "^5.2.0", "@mantine/core": "^7.17.2", "@mantine/hooks": "^7.17.2", @@ -294,9 +295,10 @@ } }, "node_modules/@babel/runtime": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz", - "integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", + "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", + "license": "MIT", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -981,6 +983,23 @@ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==" }, + "node_modules/@hello-pangea/dnd": { + "version": "18.0.1", + "resolved": "https://registry.npmjs.org/@hello-pangea/dnd/-/dnd-18.0.1.tgz", + "integrity": "sha512-xojVWG8s/TGrKT1fC8K2tIWeejJYTAeJuj36zM//yEm/ZrnZUSFGS15BpO+jGZT1ybWvyXmeDJwPYb4dhWlbZQ==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.26.7", + "css-box-model": "^1.2.1", + "raf-schd": "^4.0.3", + "react-redux": "^9.2.0", + "redux": "^5.0.1" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1833,6 +1852,12 @@ "@types/react": "^19.0.0" } }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.27.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.27.0.tgz", @@ -2607,6 +2632,15 @@ "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==", + "license": "MIT", + "dependencies": { + "tiny-invariant": "^1.0.6" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -4417,6 +4451,12 @@ } ] }, + "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==", + "license": "MIT" + }, "node_modules/react": { "version": "19.0.0", "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", @@ -4483,6 +4523,29 @@ } } }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", @@ -4688,6 +4751,12 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", @@ -5423,6 +5492,15 @@ } } }, + "node_modules/use-sync-external-store": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", + "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/victory-vendor": { "version": "36.9.2", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", diff --git a/package.json b/package.json index 29c20f8..259a35e 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "@hello-pangea/dnd": "^18.0.1", "@iconify/react": "^5.2.0", "@mantine/core": "^7.17.2", "@mantine/hooks": "^7.17.2", diff --git a/src/hooks/useAPI.ts b/src/hooks/useAPI.ts index 948b3ce..34345f0 100644 --- a/src/hooks/useAPI.ts +++ b/src/hooks/useAPI.ts @@ -127,14 +127,20 @@ export function useMutationAPI() { onMutate: ({ method, endpoint }) => { queryClient.refetchQueries({ queryKey: ['GET', endpoint], - force: true, }); + endpoint === 'api/linkOrder' && + queryClient.refetchQueries({ queryKey: ['GET', 'api/links'] }); + endpoint === 'api/categoryOrder' && + queryClient.refetchQueries({ queryKey: ['GET', 'api/categories'] }); }, onSuccess: (data, { method, endpoint }) => { queryClient.refetchQueries({ queryKey: ['GET', endpoint], - force: true, }); + endpoint === 'api/linkOrder' && + queryClient.refetchQueries({ queryKey: ['GET', 'api/links'] }); + endpoint === 'api/categoryOrder' && + queryClient.refetchQueries({ queryKey: ['GET', 'api/categories'] }); }, }); } diff --git a/src/pages/homepage/components/Bookmarks/Bookmarks.tsx b/src/pages/homepage/components/Bookmarks/Bookmarks.tsx index 557e807..f1a77cc 100644 --- a/src/pages/homepage/components/Bookmarks/Bookmarks.tsx +++ b/src/pages/homepage/components/Bookmarks/Bookmarks.tsx @@ -50,22 +50,24 @@ function Links({ contentType }: any) { gridTemplateRows: 'auto', }} > - {groups.map((category: any) => { - if ( - contentType.includes(category.filterName) || - contentType.includes('All') - ) { - return ( - bookmark.category === category.categoryName, - )} - /> - ); - } - })} + {groups + .sort((a: any, b: any) => (a.categoryIndex > b.categoryIndex ? 1 : -1)) + .map((category: any) => { + if ( + contentType.includes(category.filterName) || + contentType.includes('All') + ) { + return ( + bookmark.category === category.categoryName, + )} + /> + ); + } + })} )} diff --git a/src/pages/homepage/components/Bookmarks/LinkGroups.tsx b/src/pages/homepage/components/Bookmarks/LinkGroups.tsx index 86e53c0..e97b2d4 100644 --- a/src/pages/homepage/components/Bookmarks/LinkGroups.tsx +++ b/src/pages/homepage/components/Bookmarks/LinkGroups.tsx @@ -15,9 +15,11 @@ function LinkGroups({ links, category }: any) { {category} - {links?.map((link: any) => ( - - ))} + {links + ?.sort((a: any, b: any) => (a.index > b.index ? 1 : -1)) + .map((link: any) => ( + + ))} ); } diff --git a/src/pages/settings/components/Bookmarks/Bookmarks.tsx b/src/pages/settings/components/Bookmarks/Bookmarks.tsx index 8517d1d..ae11f78 100644 --- a/src/pages/settings/components/Bookmarks/Bookmarks.tsx +++ b/src/pages/settings/components/Bookmarks/Bookmarks.tsx @@ -1,12 +1,16 @@ +import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd'; import { ActionIcon, + Box, Button, + Card, Divider, Group, Highlight, Modal, Popover, Stack, + Switch, Text, TextInput, } from '@mantine/core'; @@ -30,14 +34,43 @@ const BookmarkSettings = () => { const { data: groupData, isFetched: groupsIsFetched } = useQueryAPI({ endpoint: 'api/categories', }); - const [groups, setGroups] = useState([]); + const [dragging, setDragging] = useState(false); + const [groups, setGroups] = useState([]); const [newGroup, setNewGroup] = useState({}); + const [groupList, setGroupList] = useState([]); const { theme, isMobile } = useCommonHooks(); const { mutateAsync } = useMutationAPI(); const [searchValue, setSearchValue] = useState(''); const [debouncedGroupSearch] = useDebouncedValue(searchValue, 500); const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false); + const reorder = (list: any, startIndex: any, endIndex: any) => { + const result = Array.from(list); + const [removed]: any = result.splice(startIndex, 1); + result.splice(endIndex, 0, removed); + return result; + }; + + function onDragEnd(result: any) { + if (!result.destination) { + return; + } + + if (result.destination.index === result.source.index) { + return; + } + + const items = reorder(groups, result.source.index, result.destination.index); + setGroupList( + items.map((item: any, index: number) => { + return { + ...item, + index: index, + }; + }), + ); + } + useEffect(() => { if (linkData && linksIsFetched) { setBookmarks(linkData); @@ -54,6 +87,12 @@ const BookmarkSettings = () => { } }, [groupData, groupsIsFetched, debouncedGroupSearch]); + useEffect(() => { + if (groupList.length > 0 && groupList !== groups) { + mutateAsync({ method: 'PATCH', endpoint: 'api/categoryOrder', body: groupList }); + } + }, [groupList]); + return ( { setSearchValue(event.target.value); }} /> + { + setDragging(e.target.checked); + }} + color={theme.primaryColor} + styles={{ label: { color: theme.primaryColor } }} + /> - - - {groups?.map((group: any) => ( - a.category === group.categoryName)} - /> - ))} - - + {dragging ? ( + + + + {(provided) => ( + + {groups + ?.sort((a: any, b: any) => + a.categoryIndex > b.categoryIndex ? 1 : -1, + ) + .map((group: any) => ( + + {(provided) => ( + + + + + + Name + + + {group.categoryName} + + + + + Filter Name + + + {group.filterName} + + + + + + )} + + ))} + {provided.placeholder} + + )} + + + + ) : ( + + + {groups + ?.sort((a: any, b: any) => (a.categoryIndex > b.categoryIndex ? 1 : -1)) + .map((group: any) => ( + a.category === group.categoryName) + .sort((a: any, b: any) => (a.index > b.index ? 1 : -1))} + /> + ))} + + + )} ); }; diff --git a/src/pages/settings/components/Bookmarks/DebouncedGroupSettingsItem.tsx b/src/pages/settings/components/Bookmarks/DebouncedGroupSettingsItem.tsx index 85cffcc..2158032 100644 --- a/src/pages/settings/components/Bookmarks/DebouncedGroupSettingsItem.tsx +++ b/src/pages/settings/components/Bookmarks/DebouncedGroupSettingsItem.tsx @@ -1,3 +1,4 @@ +import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd'; import { ActionIcon, Box, @@ -29,12 +30,41 @@ function DebouncedGroupSettingsItem({ bookmarkGroup, bookmarks }: any) { const [update, setUpdate] = useState({}); const [debounced] = useDebouncedValue(update, 1000); const [newBookmark, setNewBookmark] = useState({}); + const [bookmarkList, setBookmarkList] = useState(bookmarks); const handleDeleteBookmark = (application: any) => { mutateAsync({ method: 'DELETE', endpoint: 'api/links', body: application }); }; const handleDeleteGroup = (application: any) => { mutateAsync({ method: 'DELETE', endpoint: 'api/categories', body: application }); }; + + const reorder = (list: any, startIndex: any, endIndex: any) => { + const result = Array.from(list); + const [removed]: any = result.splice(startIndex, 1); + result.splice(endIndex, 0, removed); + return result; + }; + + function onDragEnd(result: any) { + if (!result.destination) { + return; + } + + if (result.destination.index === result.source.index) { + return; + } + + const items = reorder(bookmarkList, result.source.index, result.destination.index); + setBookmarkList( + items.map((item: any, index: number) => { + return { + ...item, + index: index, + }; + }), + ); + } + useEffect(() => { if (Object.keys(debounced).length) { mutateAsync({ method: 'PATCH', endpoint: 'api/categories', body: debounced }); @@ -48,6 +78,12 @@ function DebouncedGroupSettingsItem({ bookmarkGroup, bookmarks }: any) { } }, [update]); + useEffect(() => { + if (bookmarkList && bookmarkList !== bookmarks) { + mutateAsync({ method: 'PATCH', endpoint: 'api/linkOrder', body: bookmarkList }); + } + }, [bookmarkList]); + return ( - {bookmarks.map((bookmark: any) => ( - - - - - - - - - - - ))} + + + {(provided) => ( + + {bookmarks + .sort((a: any, b: any) => (a.index > b.index ? 1 : -1)) + .map((bookmark: any) => ( + + {(provided: any) => ( + + + + + + + + + + + + {provided.placeholder} + + )} + + ))} + + {provided.placeholder} + + )} + + diff --git a/src/styles/Box.module.css b/src/styles/Box.module.css index c0daae2..b175d75 100644 --- a/src/styles/Box.module.css +++ b/src/styles/Box.module.css @@ -31,4 +31,4 @@ to { stroke-dashoffset: 1000; } -} \ No newline at end of file +} diff --git a/src/styles/Card.module.css b/src/styles/Card.module.css index dd30f94..d4a7e2a 100644 --- a/src/styles/Card.module.css +++ b/src/styles/Card.module.css @@ -16,4 +16,4 @@ color: var(--color-text-acc); } } -} \ No newline at end of file +}