refactor: moved dnd part from Funnel into FunnelDnd

This commit is contained in:
2025-08-06 18:21:07 +04:00
parent 96c53380e0
commit 4b843d8e5d
23 changed files with 410 additions and 287 deletions

View File

@ -6,6 +6,7 @@ type Props = {
};
const DealCard = ({ deal }: Props) => {
console.log("deal");
return <Card>{deal.name}</Card>;
};

View File

@ -1,8 +1,8 @@
import React, { FC, useMemo } from "react";
import { Box } from "@mantine/core";
import DealCard from "@/app/deals/components/DealCard/DealCard";
import SortableItem from "@/components/dnd/SortableItem";
import { DealSchema } from "@/lib/client";
import { SortableItem } from "@/components/dnd/SortableDnd/SortableItem";
type Props = {
deal: DealSchema;

View File

@ -1,36 +0,0 @@
import React from "react";
import { defaultDropAnimation, DragOverlay } from "@dnd-kit/core";
import DealCard from "@/app/deals/components/DealCard/DealCard";
import StatusColumn from "@/app/deals/components/StatusColumn/StatusColumn";
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
import { DealSchema, StatusSchema } from "@/lib/client";
type Props = {
activeDeal: DealSchema | null;
activeStatus: StatusSchema | null;
};
const DndOverlay = ({ activeStatus, activeDeal }: Props) => {
const { deals } = useStatusesContext();
return (
<DragOverlay dropAnimation={defaultDropAnimation}>
<div style={{ cursor: "grabbing" }}>
{activeDeal ? (
<DealCard deal={activeDeal} />
) : activeStatus ? (
<StatusColumn
id={`${activeStatus.id}-status`}
status={activeStatus}
deals={deals.filter(
deal => deal.statusId === activeStatus.id
)}
isDragging
/>
) : null}
</div>
</DragOverlay>
);
};
export default DndOverlay;

View File

@ -0,0 +1,68 @@
"use client";
import React, { FC, ReactNode } from "react";
import DealCard from "@/app/deals/components/DealCard/DealCard";
import DealContainer from "@/app/deals/components/DealContainer/DealContainer";
import StatusColumnWrapper from "@/app/deals/components/StatusColumnWrapper/StatusColumnWrapper";
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
import useDealsAndStatusesDnd from "@/app/deals/hooks/useDealsAndStatusesDnd";
import FunnelDnd from "@/components/dnd/FunnelDnd/FunnelDnd";
import { DealSchema, StatusSchema } from "@/lib/client";
import { sortByLexorank } from "@/utils/lexorank";
const Funnel: FC = () => {
const { deals } = useStatusesContext();
const {
sortedStatuses,
handleDragStart,
handleDragOver,
handleDragEnd,
activeStatus,
activeDeal,
} = useDealsAndStatusesDnd();
return (
<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>
)}
/>
);
};
export default Funnel;

View File

@ -1,67 +0,0 @@
import React, { useMemo } from "react";
import { useDroppable } from "@dnd-kit/core";
import {
SortableContext,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { Box, Stack, Text } from "@mantine/core";
import DealContainer from "@/app/deals/components/DealContainer/DealContainer";
import { DealSchema, StatusSchema } from "@/lib/client";
import { sortByLexorank } from "@/utils/lexorank";
type Props = {
id: string;
status: StatusSchema;
deals: DealSchema[];
isDragging?: boolean;
};
const StatusColumn = ({ id, status, deals, isDragging }: Props) => {
const { setNodeRef } = useDroppable({ id });
const sortedDeals = useMemo(
() => sortByLexorank(deals.filter(deal => deal.statusId === status.id)),
[deals]
);
const columnBody = useMemo(() => {
return (
<SortableContext
id={id}
items={sortedDeals}
strategy={verticalListSortingStrategy}>
<Stack
gap={"xs"}
ref={setNodeRef}>
{sortedDeals.map(deal => (
<DealContainer
key={deal.id}
deal={deal}
/>
))}
</Stack>
</SortableContext>
);
}, [sortedDeals]);
return (
<Box
style={{
backgroundColor: "#eee",
padding: 2,
width: "15vw",
minWidth: 150,
}}>
<Text
style={{
cursor: "grab",
userSelect: "none",
opacity: isDragging ? 0.5 : 1,
}}>
{status.name}
</Text>
{columnBody}
</Box>
);
};
export default StatusColumn;

View File

@ -0,0 +1,33 @@
import React, { ReactNode } from "react";
import { Box, Text } from "@mantine/core";
import { StatusSchema } from "@/lib/client";
type Props = {
status: StatusSchema;
isDragging?: boolean;
children: ReactNode;
};
const StatusColumnWrapper = ({ status, children, isDragging = false }: Props) => {
return (
<Box
style={{
backgroundColor: "#eee",
padding: 2,
width: "15vw",
minWidth: 150,
}}>
<Text
style={{
cursor: "grab",
userSelect: "none",
opacity: isDragging ? 0.5 : 1,
}}>
{status.name}
</Text>
{children}
</Box>
);
};
export default StatusColumnWrapper;

View File

@ -1,76 +0,0 @@
"use client";
import { useMutation } from "@tanstack/react-query";
import StatusColumnsDnd from "@/app/deals/components/StatusColumnsDnd/StatusColumnsDnd";
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
import {
updateDealMutation,
updateStatusMutation,
} from "@/lib/client/@tanstack/react-query.gen";
import { notifications } from "@/lib/notifications";
const StatusColumns = () => {
const { refetchStatuses, refetchDeals } = useStatusesContext();
const updateStatus = useMutation({
...updateStatusMutation(),
onError: error => {
console.error(error);
notifications.error({
message: error.response?.data?.detail as string | undefined,
});
refetchStatuses();
},
});
const updateDeals = useMutation({
...updateDealMutation(),
onError: error => {
console.error(error);
notifications.error({
message: error.response?.data?.detail as string | undefined,
});
refetchDeals();
},
});
const onDealDragEnd = (
dealId: number,
statusId: number,
lexorank?: string
) => {
updateDeals.mutate({
path: {
dealId,
},
body: {
deal: {
statusId,
lexorank,
},
},
});
};
const onStatusDragEnd = (statusId: number, lexorank: string) => {
updateStatus.mutate({
path: {
statusId,
},
body: {
status: {
lexorank,
},
},
});
};
return (
<StatusColumnsDnd
onDealDragEnd={onDealDragEnd}
onStatusDragEnd={onStatusDragEnd}
/>
);
};
export default StatusColumns;

View File

@ -1,80 +0,0 @@
"use client";
import React, { FC } from "react";
import { closestCorners, DndContext } from "@dnd-kit/core";
import {
horizontalListSortingStrategy,
SortableContext,
} from "@dnd-kit/sortable";
import { Group, ScrollArea } from "@mantine/core";
import DndOverlay from "@/app/deals/components/DndOverlay/DndOverlay";
import StatusColumn from "@/app/deals/components/StatusColumn/StatusColumn";
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
import useDealsAndStatusesDnd from "@/app/deals/hooks/useDealsAndStatusesDnd";
import { SortableItem } from "@/components/dnd/SortableDnd/SortableItem";
import useDndSensors from "../../hooks/useSensors";
type Props = {
onDealDragEnd: (
dealId: number,
statusId: number,
lexorank?: string
) => void;
onStatusDragEnd: (statusId: number, lexorank: string) => void;
};
const StatusColumnsDnd: FC<Props> = props => {
const { deals } = useStatusesContext();
const {
sortedStatuses,
handleDragStart,
handleDragOver,
handleDragEnd,
activeStatus,
activeDeal,
} = useDealsAndStatusesDnd(props);
const sensors = useDndSensors();
return (
<ScrollArea
offsetScrollbars={"x"}
scrollbarSize={"0.5rem"}>
<DndContext
sensors={sensors}
collisionDetection={closestCorners}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}>
<SortableContext
items={sortedStatuses.map(status => `${status.id}-status`)}
strategy={horizontalListSortingStrategy}>
<Group
gap={"xs"}
wrap={"nowrap"}
align={"start"}>
{sortedStatuses.map(status => (
<SortableItem
key={status.id}
id={`${status.id}-status`}>
<StatusColumn
id={`${status.id}-status`}
status={status}
deals={deals}
isDragging={activeStatus?.id === status.id}
/>
</SortableItem>
))}
<DndOverlay
activeStatus={activeStatus}
activeDeal={activeDeal}
/>
</Group>
</SortableContext>
</DndContext>
</ScrollArea>
);
};
export default StatusColumnsDnd;