feat: raw slider for deals on mobile

This commit is contained in:
2025-08-10 19:29:02 +04:00
parent 54cf883a3c
commit 7815f99fa4
12 changed files with 271 additions and 227 deletions

View File

@ -6,16 +6,14 @@ import { DealSchema } from "@/lib/client";
type Props = {
deal: DealSchema;
disabled?: boolean;
};
const DealContainer: FC<Props> = ({ deal, disabled = false }) => {
const DealContainer: FC<Props> = ({ deal }) => {
const dealBody = useMemo(() => <DealCard deal={deal} />, [deal]);
return (
<Box>
<SortableItem
disabled={disabled}
dragHandleStyle={{ cursor: "pointer" }}
id={deal.id}
renderItem={() => dealBody}

View File

@ -25,6 +25,59 @@ const Funnel: FC = () => {
activeDeal,
} = useDealsAndStatusesDnd();
const renderFunnelDnd = () => (
<>
<FunnelDnd
containers={sortedStatuses}
items={deals}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
getContainerId={(status: StatusSchema) => `${status.id}-status`}
getItemsByContainer={(
status: StatusSchema,
items: DealSchema[]
) =>
sortByLexorank(
items.filter(deal => deal.statusId === status.id)
)
}
renderContainer={(
status: StatusSchema,
funnelColumnComponent: ReactNode
) => (
<StatusColumnWrapper
status={status}
isDragging={activeStatus?.id === status.id}>
{funnelColumnComponent}
</StatusColumnWrapper>
)}
renderItem={(deal: DealSchema) => (
<DealContainer
key={deal.id}
deal={deal}
/>
)}
activeContainer={activeStatus}
activeItem={activeDeal}
renderItemOverlay={(deal: DealSchema) => (
<DealCard deal={deal} />
)}
renderContainerOverlay={(status: StatusSchema, children) => (
<StatusColumnWrapper
status={status}
isDragging>
{children}
</StatusColumnWrapper>
)}
disabledColumns={isMobile}
/>
{!isMobile && <CreateStatusButton />}
</>
);
if (isMobile) return renderFunnelDnd();
return (
<ScrollArea
offsetScrollbars={"x"}
@ -33,58 +86,7 @@ const Funnel: FC = () => {
align={"start"}
wrap={"nowrap"}
gap={"sm"}>
<FunnelDnd
containers={sortedStatuses}
items={deals}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
getContainerId={(status: StatusSchema) =>
`${status.id}-status`
}
getItemsByContainer={(
status: StatusSchema,
items: DealSchema[]
) =>
sortByLexorank(
items.filter(deal => deal.statusId === status.id)
)
}
renderContainer={(
status: StatusSchema,
funnelColumnComponent: ReactNode
) => (
<StatusColumnWrapper
status={status}
isDragging={activeStatus?.id === status.id}>
{funnelColumnComponent}
</StatusColumnWrapper>
)}
renderItem={(deal: DealSchema) => (
<DealContainer
key={deal.id}
deal={deal}
disabled={isMobile}
/>
)}
activeContainer={activeStatus}
activeItem={activeDeal}
renderItemOverlay={(deal: DealSchema) => (
<DealCard deal={deal} />
)}
renderContainerOverlay={(
status: StatusSchema,
children
) => (
<StatusColumnWrapper
status={status}
isDragging>
{children}
</StatusColumnWrapper>
)}
disabled={isMobile}
/>
<CreateStatusButton />
{renderFunnelDnd()}
</Group>
</ScrollArea>
);

View File

@ -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,

View File

@ -1,30 +0,0 @@
import React, { FC, ReactNode } from "react";
import { useDraggable } from "@dnd-kit/core";
type Props = {
children: ReactNode;
};
const Draggable: FC<Props> = props => {
const { attributes, listeners, setNodeRef, transform } = useDraggable({
id: "draggable",
});
const style = transform
? {
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
}
: undefined;
return (
<div
ref={setNodeRef}
style={style}
{...listeners}
{...attributes}>
{props.children}
</div>
);
};
export default Draggable;

View File

@ -1,25 +0,0 @@
import React, { FC, ReactNode } from "react";
import { useDroppable } from "@dnd-kit/core";
type Props = {
children: ReactNode;
}
const Droppable: FC<Props> = ({ children }) => {
const { isOver, setNodeRef } = useDroppable({
id: "droppable",
});
const style = {
color: isOver ? "green" : undefined,
};
return (
<div
ref={setNodeRef}
style={style}>
{children}
</div>
);
}
export default Droppable;

View File

@ -12,7 +12,6 @@ type Props<TItem> = {
items: TItem[];
renderItem: (item: TItem) => ReactNode;
children?: ReactNode;
disabled?: boolean;
};
const FunnelColumn = <TItem extends BaseDraggable>({
@ -20,7 +19,6 @@ const FunnelColumn = <TItem extends BaseDraggable>({
items,
renderItem,
children,
disabled = false,
}: Props<TItem>) => {
const { setNodeRef } = useDroppable({ id });
@ -28,7 +26,6 @@ const FunnelColumn = <TItem extends BaseDraggable>({
<>
{children}
<SortableContext
disabled={disabled}
id={id}
items={items}
strategy={verticalListSortingStrategy}>

View File

@ -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<TContainer, TItem> = {
containers: TContainer[];
@ -36,7 +39,7 @@ type Props<TContainer, TItem> = {
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<TContainer, TItem>) => {
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 (
<SortableItem
key={containerId}
id={containerId}
disabled={disabledColumns}
renderItem={() =>
renderContainer(
container,
<FunnelColumn
id={containerId}
items={containerItems}
renderItem={renderItem}
/>
)
}
/>
);
});
const renderBody = () => (
<>
{isMobile ? (
<Slider {...settings}>
{renderContainers()}
<CreateStatusButton />
</Slider>
) : (
renderContainers()
)}
<FunnelOverlay
activeContainer={activeContainer}
activeItem={activeItem}
renderContainer={container => {
const containerItems = getItemsByContainer(
container,
items
);
const containerId = getContainerId(container);
return renderContainerOverlay(
container,
<FunnelColumn
id={containerId}
items={containerItems}
renderItem={renderItem}
/>
);
}}
renderItem={renderItemOverlay}
/>
</>
);
return (
<DndContext
sensors={sensors}
@ -70,56 +140,16 @@ const FunnelDnd = <
<SortableContext
items={containers.map(getContainerId)}
strategy={horizontalListSortingStrategy}>
<Group
gap="xs"
wrap="nowrap"
align="start">
{containers.map(container => {
const containerItems = getItemsByContainer(
container,
items
);
const containerId = getContainerId(container);
return (
<SortableItem
key={containerId}
id={containerId}
disabled={disabled}
renderItem={() =>
renderContainer(
container,
<FunnelColumn
id={containerId}
items={containerItems}
renderItem={renderItem}
/>
)
}
/>
);
})}
<FunnelOverlay
activeContainer={activeContainer}
activeItem={activeItem}
renderContainer={container => {
const containerItems = getItemsByContainer(
container,
items
);
const containerId = getContainerId(container);
return renderContainerOverlay(
container,
<FunnelColumn
id={containerId}
items={containerItems}
renderItem={renderItem}
disabled={disabled}
/>
);
}}
renderItem={renderItemOverlay}
/>
</Group>
{isMobile ? (
renderBody()
) : (
<Group
gap="xs"
wrap="nowrap"
align="start">
{renderBody()}
</Group>
)}
</SortableContext>
</DndContext>
);

View File

@ -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 (
<div
{...attributes}
{...listeners}
onTouchStart={e => !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}
</div>
);

View File

@ -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 = () => (
<DragHandle style={dragHandleStyle}>
<DragHandle
id={id}
style={dragHandleStyle}
disabled={disabled}>
{renderDraggable && renderDraggable()}
</DragHandle>
);
return (
<SortableItemContext.Provider value={context}>
<div
ref={setNodeRef}
style={style}>
{renderDraggable ? (
renderItem(renderDragHandle)
) : (
<DragHandle style={dragHandleStyle}>
{renderItem()}
</DragHandle>
)}
</div>
</SortableItemContext.Provider>
<div
ref={setNodeRef}
style={style}>
{renderDraggable ? (
renderItem(renderDragHandle)
) : (
<DragHandle
id={id}
style={dragHandleStyle}
disabled={disabled}>
{renderItem()}
</DragHandle>
)}
</div>
);
};

View File

@ -1,16 +0,0 @@
import type { DraggableSyntheticListeners } from "@dnd-kit/core";
import { createContext } from "react";
interface Context {
attributes: Record<string, any>;
listeners: DraggableSyntheticListeners;
ref: (node: HTMLElement | null) => void;
}
const SortableItemContext = createContext<Context>({
attributes: {},
listeners: undefined,
ref() {},
});
export default SortableItemContext;