diff --git a/src/app/deals/components/Boards/Boards.tsx b/src/app/deals/components/Boards/Boards.tsx
index d1205cd..6630c75 100644
--- a/src/app/deals/components/Boards/Boards.tsx
+++ b/src/app/deals/components/Boards/Boards.tsx
@@ -3,12 +3,12 @@
import React from "react";
import { ScrollArea } from "@mantine/core";
import Board from "@/app/deals/components/Board/Board";
-import useBoards from "@/app/deals/hooks/useBoards";
+import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
import SortableDnd from "@/components/SortableDnd";
import { BoardSchema } from "@/types/BoardSchema";
const Boards = () => {
- const { boards, setBoards } = useBoards();
+ const { boards } = useBoardsContext();
const renderBoard = (board: BoardSchema) => {
return ;
diff --git a/src/app/deals/components/DealCard/DealCard.tsx b/src/app/deals/components/DealCard/DealCard.tsx
new file mode 100644
index 0000000..64fc7c1
--- /dev/null
+++ b/src/app/deals/components/DealCard/DealCard.tsx
@@ -0,0 +1,12 @@
+import { Card } from "@mantine/core";
+import { DealSchema } from "@/types/DealSchema";
+
+type Props = {
+ deal: DealSchema;
+};
+
+const DealCard = ({ deal }: Props) => {
+ return {deal.name};
+};
+
+export default DealCard;
diff --git a/src/app/deals/components/SortableItem/SortableItem.tsx b/src/app/deals/components/SortableItem/SortableItem.tsx
new file mode 100644
index 0000000..8a348a8
--- /dev/null
+++ b/src/app/deals/components/SortableItem/SortableItem.tsx
@@ -0,0 +1,37 @@
+import React from "react";
+import { useSortable } from "@dnd-kit/sortable";
+import { CSS } from "@dnd-kit/utilities";
+
+type SortableTaskItemProps = {
+ children: React.ReactNode;
+ id: string;
+};
+
+const SortableItem = ({ children, id }: SortableTaskItemProps) => {
+ const {
+ attributes,
+ listeners,
+ setNodeRef,
+ transform,
+ transition,
+ isDragging,
+ } = useSortable({ id });
+
+ const style = {
+ transform: CSS.Transform.toString(transform),
+ transition,
+ opacity: isDragging ? 0 : 1,
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+export default SortableItem;
diff --git a/src/app/deals/components/StatusColumn/StatusColumn.tsx b/src/app/deals/components/StatusColumn/StatusColumn.tsx
new file mode 100644
index 0000000..823ddd2
--- /dev/null
+++ b/src/app/deals/components/StatusColumn/StatusColumn.tsx
@@ -0,0 +1,48 @@
+import React, { useEffect, useState } from "react";
+import { useDroppable } from "@dnd-kit/core";
+import {
+ SortableContext,
+ verticalListSortingStrategy,
+} from "@dnd-kit/sortable";
+import { Box, Text } from "@mantine/core";
+import DealCard from "@/app/deals/components/DealCard/DealCard";
+import { SortableItem } from "@/components/SortableDnd/SortableItem";
+import { DealSchema } from "@/types/DealSchema";
+import { sortByLexorank } from "@/utils/lexorank";
+
+type BoardSectionProps = {
+ id: string;
+ title: string;
+ deals: DealSchema[];
+};
+
+const StatusColumn = ({ id, title, deals }: BoardSectionProps) => {
+ const { setNodeRef } = useDroppable({ id });
+ const [sortedDeals, setSortedDeals] = useState([]);
+
+ useEffect(() => {
+ setSortedDeals(sortByLexorank(deals));
+ }, [deals]);
+
+ return (
+
+ {title}
+
+
+ {sortedDeals.map(deal => (
+
+
+
+
+
+ ))}
+
+
+
+ );
+};
+
+export default StatusColumn;
diff --git a/src/app/deals/components/StatusColumns/StatusColumns.tsx b/src/app/deals/components/StatusColumns/StatusColumns.tsx
new file mode 100644
index 0000000..d87607e
--- /dev/null
+++ b/src/app/deals/components/StatusColumns/StatusColumns.tsx
@@ -0,0 +1,25 @@
+"use client";
+
+import StatusColumnsDnd from "@/app/deals/components/StatusColumnsDnd/StatusColumnsDnd";
+
+const StatusColumns = () => {
+ const onDealDragEnd = (
+ dealId: number,
+ statusId: number,
+ lexorank?: string
+ ) => {
+ // Send request to server
+ console.log(
+ "onDealDragEnd. dealId =",
+ dealId,
+ ", statusId =",
+ statusId,
+ ", lexorank =",
+ lexorank
+ );
+ };
+
+ return ;
+};
+
+export default StatusColumns;
diff --git a/src/app/deals/components/StatusColumnsDnd/StatusColumnsDnd.tsx b/src/app/deals/components/StatusColumnsDnd/StatusColumnsDnd.tsx
new file mode 100644
index 0000000..dd1b13c
--- /dev/null
+++ b/src/app/deals/components/StatusColumnsDnd/StatusColumnsDnd.tsx
@@ -0,0 +1,210 @@
+"use client";
+
+import React, { FC, useState } from "react";
+import {
+ closestCorners,
+ defaultDropAnimation,
+ DndContext,
+ DragOverEvent,
+ DragOverlay,
+ DragStartEvent,
+ KeyboardSensor,
+ PointerSensor,
+ useSensor,
+ useSensors,
+} from "@dnd-kit/core";
+import { sortableKeyboardCoordinates } from "@dnd-kit/sortable";
+import { LexoRank } from "lexorank";
+import { Group } from "@mantine/core";
+import DealCard from "@/app/deals/components/DealCard/DealCard";
+import StatusColumn from "@/app/deals/components/StatusColumn/StatusColumn";
+import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
+import { DealSchema } from "@/types/DealSchema";
+import { getNewLexorank, sortByLexorank } from "@/utils/lexorank";
+
+type Props = {
+ onDealDragEnd: (
+ dealId: number,
+ statusId: number,
+ lexorank?: string
+ ) => void;
+};
+
+const StatusColumnsDnd: FC = props => {
+ const { statuses, deals, setDeals } = useBoardsContext();
+ const [activeDealId, setActiveDealId] = useState(null);
+
+ const sensors = useSensors(
+ useSensor(PointerSensor),
+ useSensor(KeyboardSensor, {
+ coordinateGetter: sortableKeyboardCoordinates,
+ })
+ );
+
+ const handleDragStart = ({ active }: DragStartEvent) => {
+ setActiveDealId(active.id as number);
+ };
+
+ const getStatusByDealId = (dealId: number) => {
+ const deal = deals.find(deal => deal.id === dealId);
+ if (!deal) return;
+ return statuses.find(status => status.id === deal.statusId);
+ };
+
+ const handleDragOver = ({ active, over }: DragOverEvent) => {
+ if (!over?.id) return;
+
+ const activeDealId = Number(active.id);
+ const activeStatusId = getStatusByDealId(activeDealId)?.id;
+ if (!activeStatusId) return;
+
+ const { overStatusId, newLexorank } = getDropTarget(
+ over.id,
+ activeDealId,
+ activeStatusId
+ );
+ if (!overStatusId) return;
+
+ setDeals(deals =>
+ deals.map(deal =>
+ deal.id === activeDealId
+ ? {
+ ...deal,
+ statusId: overStatusId,
+ rank: newLexorank || deal.rank,
+ }
+ : deal
+ )
+ );
+ };
+
+ const getDropTarget = (
+ overId: string | number,
+ activeDealId: number,
+ activeStatusId: number
+ ) => {
+ if (typeof overId === "string") {
+ return {
+ overStatusId: Number(overId.replace(/-status$/, "")),
+ newLexorank: undefined,
+ };
+ }
+
+ const overDealId = Number(overId);
+ const overStatusId = getStatusByDealId(overDealId)?.id;
+
+ if (!overStatusId || activeDealId === overDealId) {
+ return { overStatusId: undefined, newLexorank: undefined };
+ }
+
+ const statusDeals = sortByLexorank(
+ deals.filter(deal => deal.statusId === overStatusId)
+ );
+ const overDealIndex = statusDeals.findIndex(
+ deal => deal.id === overDealId
+ );
+
+ let newLexorank;
+ if (activeStatusId === overStatusId) {
+ newLexorank = getNewRankForSameStatus(
+ statusDeals,
+ overDealIndex,
+ activeDealId
+ );
+ } else {
+ newLexorank = getNewRankForAnotherStatus(
+ statusDeals,
+ overDealIndex
+ );
+ }
+
+ return { overStatusId, newLexorank };
+ };
+
+ const getNewRankForSameStatus = (
+ statusDeals: DealSchema[],
+ overDealIndex: number,
+ activeDealId: number
+ ) => {
+ const activeDealIndex = statusDeals.findIndex(
+ deal => deal.id === activeDealId
+ );
+ const [leftIndex, rightIndex] =
+ overDealIndex < activeDealIndex
+ ? [overDealIndex - 1, overDealIndex]
+ : [overDealIndex, overDealIndex + 1];
+
+ const leftLexorank =
+ leftIndex >= 0 ? LexoRank.parse(deals[leftIndex].rank) : null;
+ const rightLexorank =
+ rightIndex < deals.length
+ ? LexoRank.parse(deals[rightIndex].rank)
+ : null;
+
+ return getNewLexorank(leftLexorank, rightLexorank).toString();
+ };
+
+ const getNewRankForAnotherStatus = (
+ statusDeals: DealSchema[],
+ overDealIndex: number
+ ) => {
+ const leftLexorank =
+ overDealIndex > 0
+ ? LexoRank.parse(statusDeals[overDealIndex - 1].rank)
+ : null;
+ const rightLexorank = LexoRank.parse(statusDeals[overDealIndex].rank);
+
+ return getNewLexorank(leftLexorank, rightLexorank).toString();
+ };
+
+ const handleDragEnd = ({ active, over }: DragOverEvent) => {
+ setActiveDealId(null);
+ if (!over?.id) return;
+
+ const activeDealId = Number(active.id);
+ const activeStatusId = getStatusByDealId(activeDealId)?.id;
+ if (!activeStatusId) return;
+
+ const { overStatusId, newLexorank } = getDropTarget(
+ over.id,
+ activeDealId,
+ activeStatusId
+ );
+ if (!overStatusId) return;
+
+ props.onDealDragEnd(activeDealId, overStatusId, newLexorank);
+ };
+
+ const deal = activeDealId
+ ? deals.find(deal => deal.id === activeDealId)
+ : null;
+
+ return (
+
+
+ {statuses.map(status => (
+ deal.statusId === status.id
+ )}
+ />
+ ))}
+
+ {deal ? : null}
+
+
+
+ );
+};
+
+export default StatusColumnsDnd;
diff --git a/src/app/deals/contexts/BoardsContext.tsx b/src/app/deals/contexts/BoardsContext.tsx
new file mode 100644
index 0000000..8e0f890
--- /dev/null
+++ b/src/app/deals/contexts/BoardsContext.tsx
@@ -0,0 +1,92 @@
+"use client";
+
+import React, {
+ createContext,
+ FC,
+ useContext,
+ useEffect,
+ useState,
+} from "react";
+import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
+import useBoards from "@/app/deals/hooks/useBoards";
+import useDeals from "@/app/deals/hooks/useDeals";
+import useStatuses from "@/app/deals/hooks/useStatuses";
+import { BoardSchema } from "@/types/BoardSchema";
+import { DealSchema } from "@/types/DealSchema";
+import { StatusSchema } from "@/types/StatusSchema";
+
+type BoardsContextState = {
+ boards: BoardSchema[];
+ setBoards: React.Dispatch>;
+ selectedBoard: BoardSchema | null;
+ setSelectedBoard: React.Dispatch>;
+ statuses: StatusSchema[];
+ setStatuses: React.Dispatch>;
+ deals: DealSchema[];
+ setDeals: React.Dispatch>;
+};
+
+const BoardsContext = createContext(undefined);
+
+const useBoardsContextState = () => {
+ const { boards, setBoards } = useBoards();
+ const [selectedBoard, setSelectedBoard] = useState(
+ null
+ );
+ const { selectedProject: project } = useProjectsContext();
+ const { statuses, setStatuses } = useStatuses();
+ const { deals, setDeals } = useDeals();
+
+ useEffect(() => {
+ if (boards.length > 0 && selectedBoard === null) {
+ setSelectedBoard(boards[0]);
+ return;
+ }
+
+ if (selectedBoard) {
+ let newBoard = boards.find(board => board.id === selectedBoard.id);
+
+ if (!newBoard && boards.length > 0) {
+ newBoard = boards[0];
+ }
+ setSelectedBoard(newBoard ?? null);
+ }
+ }, [boards]);
+
+ return {
+ boards,
+ setBoards,
+ selectedBoard,
+ setSelectedBoard,
+ statuses,
+ setStatuses,
+ deals,
+ setDeals,
+ };
+};
+
+type BoardsContextProviderProps = {
+ children: React.ReactNode;
+};
+
+export const BoardsContextProvider: FC = ({
+ children,
+}) => {
+ const state = useBoardsContextState();
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useBoardsContext = () => {
+ const context = useContext(BoardsContext);
+ if (!context) {
+ throw new Error(
+ "useBoardsContext must be used within a BoardsContextProvider"
+ );
+ }
+ return context;
+};
diff --git a/src/app/deals/hooks/useDeals.ts b/src/app/deals/hooks/useDeals.ts
new file mode 100644
index 0000000..c1f59a7
--- /dev/null
+++ b/src/app/deals/hooks/useDeals.ts
@@ -0,0 +1,37 @@
+import { useEffect, useState } from "react";
+import { DealSchema } from "@/types/DealSchema";
+
+const useDeals = () => {
+ const [deals, setDeals] = useState([]);
+
+ useEffect(() => {
+ const INITIAL_DEALS: DealSchema[] = [
+ {
+ id: 1,
+ name: "Deal 1",
+ rank: "0|gggggg:",
+ statusId: 1,
+ },
+ {
+ id: 2,
+ name: "Deal 2",
+ rank: "0|mmmmmm:",
+ statusId: 1,
+ },
+ {
+ id: 3,
+ name: "Deal 3",
+ rank: "0|ssssss:",
+ statusId: 2,
+ },
+ ];
+ setDeals(INITIAL_DEALS);
+ }, []);
+
+ return {
+ deals,
+ setDeals,
+ };
+};
+
+export default useDeals;
diff --git a/src/app/deals/hooks/useStatuses.ts b/src/app/deals/hooks/useStatuses.ts
new file mode 100644
index 0000000..7c3def4
--- /dev/null
+++ b/src/app/deals/hooks/useStatuses.ts
@@ -0,0 +1,22 @@
+import { useEffect, useState } from "react";
+import { StatusSchema } from "@/types/StatusSchema";
+
+const useStatuses = () => {
+ const [statuses, setStatuses] = useState([]);
+
+ useEffect(() => {
+ setStatuses([
+ { id: 1, name: "Todo", rank: "0|aaaaaa:" },
+ { id: 2, name: "In progress", rank: "0|gggggg:" },
+ { id: 3, name: "Testing", rank: "0|mmmmmm:" },
+ { id: 4, name: "Ready", rank: "0|ssssss:" },
+ ]);
+ }, []);
+
+ return {
+ statuses,
+ setStatuses,
+ };
+};
+
+export default useStatuses;
diff --git a/src/app/deals/page.tsx b/src/app/deals/page.tsx
index a95f206..da6665f 100644
--- a/src/app/deals/page.tsx
+++ b/src/app/deals/page.tsx
@@ -1,5 +1,8 @@
+import { Divider } from "@mantine/core";
import Boards from "@/app/deals/components/Boards/Boards";
import Header from "@/app/deals/components/Header/Header";
+import StatusColumns from "@/app/deals/components/StatusColumns/StatusColumns";
+import { BoardsContextProvider } from "@/app/deals/contexts/BoardsContext";
import { ProjectsContextProvider } from "@/app/deals/contexts/ProjectsContext";
import PageBlock from "@/components/PageBlock/PageBlock";
import PageContainer from "@/components/PageContainer/PageContainer";
@@ -9,8 +12,12 @@ export default function DealsPage() {
-
-
+
+
+
+
+
+
diff --git a/src/components/SortableDnd/SortableDnd.tsx b/src/components/SortableDnd/SortableDnd.tsx
index 62eec41..5de115d 100644
--- a/src/components/SortableDnd/SortableDnd.tsx
+++ b/src/components/SortableDnd/SortableDnd.tsx
@@ -54,7 +54,6 @@ const SortableDnd = ({
);
useEffect(() => {
- console.log(sortByLexorank(initialItems));
setItems(sortByLexorank(initialItems));
}, [initialItems]);
diff --git a/src/types/DealSchema.ts b/src/types/DealSchema.ts
new file mode 100644
index 0000000..a4f7656
--- /dev/null
+++ b/src/types/DealSchema.ts
@@ -0,0 +1,6 @@
+export type DealSchema = {
+ id: number;
+ name: string;
+ rank: string;
+ statusId: number;
+};
diff --git a/src/types/StatusSchema.ts b/src/types/StatusSchema.ts
new file mode 100644
index 0000000..1f44b73
--- /dev/null
+++ b/src/types/StatusSchema.ts
@@ -0,0 +1,5 @@
+export type StatusSchema = {
+ id: number;
+ name: string;
+ rank: string;
+};
diff --git a/src/utils/lexorank.ts b/src/utils/lexorank.ts
index ac6b8bd..8909ada 100644
--- a/src/utils/lexorank.ts
+++ b/src/utils/lexorank.ts
@@ -27,10 +27,10 @@ export function getNewLexorank(
): LexoRank {
if (right) {
if (left) return left?.between(right);
- return right.genPrev();
+ return right.between(LexoRank.min());
}
if (left) {
- return left.genNext();
+ return left.between(LexoRank.max());
}
return LexoRank.middle();
}