diff --git a/package.json b/package.json index 22d7dbb..38bd595 100644 --- a/package.json +++ b/package.json @@ -34,8 +34,10 @@ "react-dom": "19.1.0", "react-imask": "^7.6.1", "react-redux": "^9.2.0", + "react-slick": "^0.31.0", "redux-persist": "^6.0.0", "sharp": "^0.34.3", + "slick-carousel": "^1.8.1", "zod": "^4.0.14" }, "devDependencies": { @@ -57,7 +59,9 @@ "@types/node": "^22.13.11", "@types/react": "19.1.8", "@types/react-redux": "^7.1.34", + "@types/react-slick": "^0", "@types/redux-persist": "^4.3.1", + "@types/slick-carousel": "^1", "autoprefixer": "^10.4.21", "babel-loader": "^10.0.0", "eslint": "^9.29.0", diff --git a/src/app/deals/components/DealContainer/DealContainer.tsx b/src/app/deals/components/DealContainer/DealContainer.tsx index f980161..99c1625 100644 --- a/src/app/deals/components/DealContainer/DealContainer.tsx +++ b/src/app/deals/components/DealContainer/DealContainer.tsx @@ -6,16 +6,14 @@ import { DealSchema } from "@/lib/client"; type Props = { deal: DealSchema; - disabled?: boolean; }; -const DealContainer: FC = ({ deal, disabled = false }) => { +const DealContainer: FC = ({ deal }) => { const dealBody = useMemo(() => , [deal]); return ( dealBody} diff --git a/src/app/deals/components/Funnel/Funnel.tsx b/src/app/deals/components/Funnel/Funnel.tsx index 6be5dc9..6a2303a 100644 --- a/src/app/deals/components/Funnel/Funnel.tsx +++ b/src/app/deals/components/Funnel/Funnel.tsx @@ -25,6 +25,59 @@ const Funnel: FC = () => { activeDeal, } = useDealsAndStatusesDnd(); + const renderFunnelDnd = () => ( + <> + `${status.id}-status`} + getItemsByContainer={( + status: StatusSchema, + items: DealSchema[] + ) => + sortByLexorank( + items.filter(deal => deal.statusId === status.id) + ) + } + renderContainer={( + status: StatusSchema, + funnelColumnComponent: ReactNode + ) => ( + + {funnelColumnComponent} + + )} + renderItem={(deal: DealSchema) => ( + + )} + activeContainer={activeStatus} + activeItem={activeDeal} + renderItemOverlay={(deal: DealSchema) => ( + + )} + renderContainerOverlay={(status: StatusSchema, children) => ( + + {children} + + )} + disabledColumns={isMobile} + /> + {!isMobile && } + + ); + + if (isMobile) return renderFunnelDnd(); + return ( { align={"start"} wrap={"nowrap"} gap={"sm"}> - - `${status.id}-status` - } - getItemsByContainer={( - status: StatusSchema, - items: DealSchema[] - ) => - sortByLexorank( - items.filter(deal => deal.statusId === status.id) - ) - } - renderContainer={( - status: StatusSchema, - funnelColumnComponent: ReactNode - ) => ( - - {funnelColumnComponent} - - )} - renderItem={(deal: DealSchema) => ( - - )} - activeContainer={activeStatus} - activeItem={activeDeal} - renderItemOverlay={(deal: DealSchema) => ( - - )} - renderContainerOverlay={( - status: StatusSchema, - children - ) => ( - - {children} - - )} - disabled={isMobile} - /> - + {renderFunnelDnd()} ); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index a3900ab..0363360 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,7 @@ import "@mantine/core/styles.css"; import "@mantine/notifications/styles.css"; +import "slick-carousel/slick/slick.css"; +import "slick-carousel/slick/slick-theme.css"; import { ReactNode } from "react"; import { ColorSchemeScript, diff --git a/src/components/dnd/Draggable/Draggable.tsx b/src/components/dnd/Draggable/Draggable.tsx deleted file mode 100644 index a12b442..0000000 --- a/src/components/dnd/Draggable/Draggable.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React, { FC, ReactNode } from "react"; -import { useDraggable } from "@dnd-kit/core"; - -type Props = { - children: ReactNode; -}; - -const Draggable: FC = props => { - const { attributes, listeners, setNodeRef, transform } = useDraggable({ - id: "draggable", - }); - - const style = transform - ? { - transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`, - } - : undefined; - - return ( -
- {props.children} -
- ); -}; - -export default Draggable; diff --git a/src/components/dnd/Droppable/Droppable.tsx b/src/components/dnd/Droppable/Droppable.tsx deleted file mode 100644 index a68c787..0000000 --- a/src/components/dnd/Droppable/Droppable.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React, { FC, ReactNode } from "react"; -import { useDroppable } from "@dnd-kit/core"; - -type Props = { - children: ReactNode; -} - -const Droppable: FC = ({ children }) => { - const { isOver, setNodeRef } = useDroppable({ - id: "droppable", - }); - const style = { - color: isOver ? "green" : undefined, - }; - - return ( -
- {children} -
- ); -} - -export default Droppable; diff --git a/src/components/dnd/FunnelDnd/FunnelColumn.tsx b/src/components/dnd/FunnelDnd/FunnelColumn.tsx index cc12372..53e6052 100644 --- a/src/components/dnd/FunnelDnd/FunnelColumn.tsx +++ b/src/components/dnd/FunnelDnd/FunnelColumn.tsx @@ -12,7 +12,6 @@ type Props = { items: TItem[]; renderItem: (item: TItem) => ReactNode; children?: ReactNode; - disabled?: boolean; }; const FunnelColumn = ({ @@ -20,7 +19,6 @@ const FunnelColumn = ({ items, renderItem, children, - disabled = false, }: Props) => { const { setNodeRef } = useDroppable({ id }); @@ -28,7 +26,6 @@ const FunnelColumn = ({ <> {children} diff --git a/src/components/dnd/FunnelDnd/FunnelDnd.tsx b/src/components/dnd/FunnelDnd/FunnelDnd.tsx index 3718d62..40f47f5 100644 --- a/src/components/dnd/FunnelDnd/FunnelDnd.tsx +++ b/src/components/dnd/FunnelDnd/FunnelDnd.tsx @@ -12,12 +12,15 @@ import { horizontalListSortingStrategy, SortableContext, } from "@dnd-kit/sortable"; +import { isMobile } from "react-device-detect"; +import Slider from "react-slick"; import { Group } from "@mantine/core"; +import CreateStatusButton from "@/app/deals/components/CreateStatusButton/CreateStatusButton"; import useDndSensors from "@/app/deals/hooks/useSensors"; -import SortableItem from "@/components/dnd/SortableItem"; +import FunnelColumn from "@/components/dnd/FunnelDnd/FunnelColumn"; +import FunnelOverlay from "@/components/dnd/FunnelDnd/FunnelOverlay"; import { BaseDraggable } from "@/components/dnd/types/types"; -import FunnelColumn from "./FunnelColumn"; -import FunnelOverlay from "./FunnelOverlay"; +import SortableItem from "../SortableItem"; type Props = { containers: TContainer[]; @@ -36,7 +39,7 @@ type Props = { getItemsByContainer: (container: TContainer, items: TItem[]) => TItem[]; activeContainer: TContainer | null; activeItem: TItem | null; - disabled?: boolean; + disabledColumns?: boolean; }; const FunnelDnd = < @@ -56,10 +59,77 @@ const FunnelDnd = < getItemsByContainer, activeContainer, activeItem, - disabled = false, + disabledColumns = false, }: Props) => { const sensors = useDndSensors(); + const settings = { + dots: true, + infinite: false, + speed: 500, + slidesToShow: 1, + slidesToScroll: 1, + draggable: !activeItem && !activeContainer, + swipe: !activeItem && !activeContainer, + arrows: false, + }; + + const renderContainers = () => + containers.map(container => { + const containerItems = getItemsByContainer(container, items); + const containerId = getContainerId(container); + return ( + + renderContainer( + container, + + ) + } + /> + ); + }); + + const renderBody = () => ( + <> + {isMobile ? ( + + {renderContainers()} + + + ) : ( + renderContainers() + )} + { + const containerItems = getItemsByContainer( + container, + items + ); + const containerId = getContainerId(container); + return renderContainerOverlay( + container, + + ); + }} + renderItem={renderItemOverlay} + /> + + ); + return ( - - {containers.map(container => { - const containerItems = getItemsByContainer( - container, - items - ); - const containerId = getContainerId(container); - return ( - - renderContainer( - container, - - ) - } - /> - ); - })} - { - const containerItems = getItemsByContainer( - container, - items - ); - const containerId = getContainerId(container); - return renderContainerOverlay( - container, - - ); - }} - renderItem={renderItemOverlay} - /> - + {isMobile ? ( + renderBody() + ) : ( + + {renderBody()} + + )} ); diff --git a/src/components/dnd/SortableItem/DragHandle.tsx b/src/components/dnd/SortableItem/DragHandle.tsx index dd103a0..f6dc78b 100644 --- a/src/components/dnd/SortableItem/DragHandle.tsx +++ b/src/components/dnd/SortableItem/DragHandle.tsx @@ -1,24 +1,32 @@ -import React, { CSSProperties, ReactNode, useContext } from "react"; -import SortableItemContext from "@/components/dnd/SortableItem/SortableItemContext"; +import React, { CSSProperties, ReactNode } from "react"; +import { useDraggable } from "@dnd-kit/core"; type Props = { + id: number | string; children: ReactNode; style?: CSSProperties; + disabled?: boolean; }; -const DragHandle = ({ children, style }: Props) => { - const { attributes, listeners, ref } = useContext(SortableItemContext); +const DragHandle = ({ id, children, style, disabled }: Props) => { + const { attributes, listeners, setNodeRef } = useDraggable({ + id, + disabled, + }); return (
!disabled && e.stopPropagation()} + onTouchMove={e => !disabled && e.stopPropagation()} style={{ - width: "100%", - cursor: "grab", + width: "100wv", + cursor: disabled ? "default" : "grab", + touchAction: "none", ...style, }} - ref={ref}> + ref={setNodeRef}> {children}
); diff --git a/src/components/dnd/SortableItem/SortableItem.tsx b/src/components/dnd/SortableItem/SortableItem.tsx index 0ac1def..0968976 100644 --- a/src/components/dnd/SortableItem/SortableItem.tsx +++ b/src/components/dnd/SortableItem/SortableItem.tsx @@ -1,8 +1,7 @@ -import React, { CSSProperties, ReactNode, useMemo } from "react"; +import React, { CSSProperties, ReactNode } from "react"; import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import DragHandle from "./DragHandle"; -import SortableItemContext from "./SortableItemContext"; type Props = { id: number | string; @@ -17,26 +16,12 @@ const SortableItem = ({ dragHandleStyle, renderDraggable, id, - disabled, + disabled = false, }: Props) => { - const { - attributes, - isDragging, - listeners, - setNodeRef, - setActivatorNodeRef, - transform, - transition, - } = useSortable({ id, disabled, animateLayoutChanges: () => false }); - - const context = useMemo( - () => ({ - attributes, - listeners, - ref: setActivatorNodeRef, - }), - [attributes, listeners, setActivatorNodeRef] - ); + const { isDragging, setNodeRef, transform, transition } = useSortable({ + id, + animateLayoutChanges: () => false, + }); const style: CSSProperties = { opacity: isDragging ? 0.4 : undefined, @@ -45,25 +30,29 @@ const SortableItem = ({ }; const renderDragHandle = () => ( - + {renderDraggable && renderDraggable()} ); return ( - -
- {renderDraggable ? ( - renderItem(renderDragHandle) - ) : ( - - {renderItem()} - - )} -
-
+
+ {renderDraggable ? ( + renderItem(renderDragHandle) + ) : ( + + {renderItem()} + + )} +
); }; diff --git a/src/components/dnd/SortableItem/SortableItemContext.tsx b/src/components/dnd/SortableItem/SortableItemContext.tsx deleted file mode 100644 index 21279d4..0000000 --- a/src/components/dnd/SortableItem/SortableItemContext.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import type { DraggableSyntheticListeners } from "@dnd-kit/core"; -import { createContext } from "react"; - -interface Context { - attributes: Record; - listeners: DraggableSyntheticListeners; - ref: (node: HTMLElement | null) => void; -} - -const SortableItemContext = createContext({ - attributes: {}, - listeners: undefined, - ref() {}, -}); - -export default SortableItemContext; diff --git a/yarn.lock b/yarn.lock index 2c755d2..989b56b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3944,6 +3944,15 @@ __metadata: languageName: node linkType: hard +"@types/jquery@npm:*": + version: 3.5.32 + resolution: "@types/jquery@npm:3.5.32" + dependencies: + "@types/sizzle": "npm:*" + checksum: 10c0/4a17ad6819b89026c21323656ab01b0b263f9d470910a87c8740920ff98319d503c7352b85b50134a39724ecbfccabc73aa4c741dfdd460cf8bbe714f9259054 + languageName: node + linkType: hard + "@types/jsdom@npm:^21.1.7": version: 21.1.7 resolution: "@types/jsdom@npm:21.1.7" @@ -4006,6 +4015,15 @@ __metadata: languageName: node linkType: hard +"@types/react-slick@npm:^0": + version: 0.23.13 + resolution: "@types/react-slick@npm:0.23.13" + dependencies: + "@types/react": "npm:*" + checksum: 10c0/35dd72dfbac234b8db019fb8af250ac3891e564d13001642353aa0b7c37f6ed1ff1f5e8fa8b034706993c4d27ac1977c031942636cfd05414ee4667d45c8aea6 + languageName: node + linkType: hard + "@types/react@npm:*, @types/react@npm:19.1.8": version: 19.1.8 resolution: "@types/react@npm:19.1.8" @@ -4038,6 +4056,22 @@ __metadata: languageName: node linkType: hard +"@types/sizzle@npm:*": + version: 2.3.9 + resolution: "@types/sizzle@npm:2.3.9" + checksum: 10c0/db0277ff62e8ebe6cdae2020fd045fd7fd19f29a3a2ce13c555b14fb00e105e79004883732118b9f2e8b943cb302645e9eddb4e7bdeef1a171da679cd4c32b72 + languageName: node + linkType: hard + +"@types/slick-carousel@npm:^1": + version: 1.6.40 + resolution: "@types/slick-carousel@npm:1.6.40" + dependencies: + "@types/jquery": "npm:*" + checksum: 10c0/3e7e9ad518ec19e5afd3b34b8546fe5f190262d8a29640c7aad19ddd1aad1d6a71dcc11a9528bafb9034e9d4031e2b3cc186c1baf5ed95d13a541e4231463d51 + languageName: node + linkType: hard + "@types/stack-utils@npm:^2.0.0, @types/stack-utils@npm:^2.0.3": version: 2.0.3 resolution: "@types/stack-utils@npm:2.0.3" @@ -5731,7 +5765,7 @@ __metadata: languageName: node linkType: hard -"classnames@npm:^2.5.1": +"classnames@npm:^2.2.5, classnames@npm:^2.5.1": version: 2.5.1 resolution: "classnames@npm:2.5.1" checksum: 10c0/afff4f77e62cea2d79c39962980bf316bacb0d7c49e13a21adaadb9221e1c6b9d3cdb829d8bb1b23c406f4e740507f37e1dcf506f7e3b7113d17c5bab787aa69 @@ -6081,7 +6115,9 @@ __metadata: "@types/node": "npm:^22.13.11" "@types/react": "npm:19.1.8" "@types/react-redux": "npm:^7.1.34" + "@types/react-slick": "npm:^0" "@types/redux-persist": "npm:^4.3.1" + "@types/slick-carousel": "npm:^1" autoprefixer: "npm:^10.4.21" axios: "npm:^1.11.0" babel-loader: "npm:^10.0.0" @@ -6107,8 +6143,10 @@ __metadata: react-dom: "npm:19.1.0" react-imask: "npm:^7.6.1" react-redux: "npm:^9.2.0" + react-slick: "npm:^0.31.0" redux-persist: "npm:^6.0.0" sharp: "npm:^0.34.3" + slick-carousel: "npm:^1.8.1" storybook: "npm:^8.6.8" storybook-dark-mode: "npm:^4.0.2" stylelint: "npm:^16.20.0" @@ -9592,6 +9630,15 @@ __metadata: languageName: node linkType: hard +"json2mq@npm:^0.2.0": + version: 0.2.0 + resolution: "json2mq@npm:0.2.0" + dependencies: + string-convert: "npm:^0.2.0" + checksum: 10c0/fc9e2f2306572522d3e61d246afdf70b56ca9ea32f4ad5924c30949867851ab59c926bd0ffc821ebb54d32f3e82e95225f3906eacdb3e54c1ad49acdadf7e0c7 + languageName: node + linkType: hard + "json5@npm:^2.1.2, json5@npm:^2.2.2, json5@npm:^2.2.3": version: 2.2.3 resolution: "json5@npm:2.2.3" @@ -11698,6 +11745,21 @@ __metadata: languageName: node linkType: hard +"react-slick@npm:^0.31.0": + version: 0.31.0 + resolution: "react-slick@npm:0.31.0" + dependencies: + classnames: "npm:^2.2.5" + json2mq: "npm:^0.2.0" + lodash.debounce: "npm:^4.0.8" + resize-observer-polyfill: "npm:^1.5.0" + peerDependencies: + react: ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 10c0/ac006104323c61af6d8e208edb49038485458756b72c9e15622beef2700085bcdb1d21f028be1b3f199ff15616eab9d61a45aa09650c9ae1d72bf2a23be48616 + languageName: node + linkType: hard + "react-style-singleton@npm:^2.2.2, react-style-singleton@npm:^2.2.3": version: 2.2.3 resolution: "react-style-singleton@npm:2.2.3" @@ -11987,6 +12049,13 @@ __metadata: languageName: node linkType: hard +"resize-observer-polyfill@npm:^1.5.0": + version: 1.5.1 + resolution: "resize-observer-polyfill@npm:1.5.1" + checksum: 10c0/5e882475067f0b97dc07e0f37c3e335ac5bc3520d463f777cec7e894bb273eddbfecb857ae668e6fb6881fd6f6bb7148246967172139302da50fa12ea3a15d95 + languageName: node + linkType: hard + "resolve-cwd@npm:^3.0.0": version: 3.0.0 resolution: "resolve-cwd@npm:3.0.0" @@ -12610,6 +12679,15 @@ __metadata: languageName: node linkType: hard +"slick-carousel@npm:^1.8.1": + version: 1.8.1 + resolution: "slick-carousel@npm:1.8.1" + peerDependencies: + jquery: ">=1.8.0" + checksum: 10c0/e8c9c9a454c107cfee88689477b453449ed66d5343cf495d3135ec25ea736a0df862f625a9cdc8abd6262629ecef3343c5de686694831f99b13dfe837a1aa587 + languageName: node + linkType: hard + "smart-buffer@npm:^4.2.0": version: 4.2.0 resolution: "smart-buffer@npm:4.2.0" @@ -12791,6 +12869,13 @@ __metadata: languageName: node linkType: hard +"string-convert@npm:^0.2.0": + version: 0.2.1 + resolution: "string-convert@npm:0.2.1" + checksum: 10c0/00673ed8a3106137395436537ace7d3672c91a3290da73466055daa0134331dc84bc58c54ba2d2ea40711adc5744426d3c8239dbfc30290438fa3e9ff65db528 + languageName: node + linkType: hard + "string-length@npm:^4.0.2": version: 4.0.2 resolution: "string-length@npm:4.0.2"