feat: raw boards dnd
This commit is contained in:
@ -10,6 +10,8 @@
|
|||||||
"generate-client": "openapi --input http://127.0.0.1:8000/openapi.json --output ./src/client --client axios --useOptions --useUnionTypes --exportSchemas true && prettier --write ./src/client/**/*.ts"
|
"generate-client": "openapi --input http://127.0.0.1:8000/openapi.json --output ./src/client --client axios --useOptions --useUnionTypes --exportSchemas true && prettier --write ./src/client/**/*.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@mantine/core": "8.1.2",
|
"@mantine/core": "8.1.2",
|
||||||
"@mantine/form": "^8.1.3",
|
"@mantine/form": "^8.1.3",
|
||||||
"@mantine/hooks": "8.1.2",
|
"@mantine/hooks": "8.1.2",
|
||||||
@ -23,6 +25,7 @@
|
|||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"framer-motion": "^12.23.7",
|
"framer-motion": "^12.23.7",
|
||||||
"i18n-iso-countries": "^7.14.0",
|
"i18n-iso-countries": "^7.14.0",
|
||||||
|
"lexorank": "^1.0.5",
|
||||||
"libphonenumber-js": "^1.12.10",
|
"libphonenumber-js": "^1.12.10",
|
||||||
"next": "15.3.3",
|
"next": "15.3.3",
|
||||||
"openapi-typescript-codegen": "^0.29.0",
|
"openapi-typescript-codegen": "^0.29.0",
|
||||||
|
|||||||
13
src/app/deals/components/Board/Board.tsx
Normal file
13
src/app/deals/components/Board/Board.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import React, { FC } from "react";
|
||||||
|
import { Box } from "@mantine/core";
|
||||||
|
import { BoardSchema } from "@/types/BoardSchema";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
board: BoardSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Board: FC<Props> = ({ board }) => {
|
||||||
|
return <Box miw={100} style={{ borderWidth: 1, margin: 0 }}>{board.name}</Box>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Board;
|
||||||
33
src/app/deals/components/Boards/Boards.tsx
Normal file
33
src/app/deals/components/Boards/Boards.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
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 SortableDnd from "@/components/SortableDnd";
|
||||||
|
import { BoardSchema } from "@/types/BoardSchema";
|
||||||
|
|
||||||
|
const Boards = () => {
|
||||||
|
const { boards, setBoards } = useBoards();
|
||||||
|
|
||||||
|
const renderBoard = (board: BoardSchema) => {
|
||||||
|
return <Board board={board} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDragEnd = (itemId: number, newLexorank: string) => {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollArea
|
||||||
|
offsetScrollbars={"y"}
|
||||||
|
w={"100%"}>
|
||||||
|
<SortableDnd
|
||||||
|
initialItems={boards}
|
||||||
|
renderItem={renderBoard}
|
||||||
|
onDragEnd={onDragEnd}
|
||||||
|
rowStyle={{ flexWrap: "nowrap" }}
|
||||||
|
/>
|
||||||
|
</ScrollArea>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Boards;
|
||||||
23
src/app/deals/hooks/useBoards.ts
Normal file
23
src/app/deals/hooks/useBoards.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { BoardSchema } from "@/types/BoardSchema";
|
||||||
|
|
||||||
|
const useBoards = () => {
|
||||||
|
const [boards, setBoards] = useState<BoardSchema[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setBoards([
|
||||||
|
{ id: 1, name: "1 Item", rank: "0|aaaaaa:" },
|
||||||
|
{ id: 2, name: "2 Item", rank: "0|gggggg:" },
|
||||||
|
{ id: 3, name: "3 Item", rank: "0|mmmmmm:" },
|
||||||
|
{ id: 4, name: "4 Item", rank: "0|ssssss:" },
|
||||||
|
{ id: 5, name: "5 Item", rank: "0|zzzzzz:" },
|
||||||
|
]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
boards,
|
||||||
|
setBoards,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useBoards;
|
||||||
13
src/app/deals/page.tsx
Normal file
13
src/app/deals/page.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import Boards from "@/app/deals/components/Boards/Boards";
|
||||||
|
import PageBlock from "@/components/PageBlock/PageBlock";
|
||||||
|
import PageContainer from "@/components/PageContainer/PageContainer";
|
||||||
|
|
||||||
|
export default function DealsPage() {
|
||||||
|
return (
|
||||||
|
<PageContainer>
|
||||||
|
<PageBlock>
|
||||||
|
<Boards />
|
||||||
|
</PageBlock>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,11 +1,5 @@
|
|||||||
import { ColorSchemeToggle } from "@/components/ColorSchemeToggle/ColorSchemeToggle";
|
import { redirect } from "next/navigation";
|
||||||
import { Welcome } from "@/components/Welcome/Welcome";
|
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
return (
|
redirect("/deals");
|
||||||
<>
|
|
||||||
<Welcome />
|
|
||||||
<ColorSchemeToggle />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
30
src/components/Draggable/Draggable.tsx
Normal file
30
src/components/Draggable/Draggable.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
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;
|
||||||
25
src/components/Droppable/Droppable.tsx
Normal file
25
src/components/Droppable/Droppable.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
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;
|
||||||
41
src/components/PageBlock/PageBlock.module.css
Normal file
41
src/components/PageBlock/PageBlock.module.css
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
.container {
|
||||||
|
border-radius: rem(40);
|
||||||
|
background-color: white;
|
||||||
|
@mixin dark {
|
||||||
|
background-color: var(--mantine-color-dark-8);
|
||||||
|
box-shadow: 5px 5px 30px 1px var(--mantine-color-dark-6);
|
||||||
|
}
|
||||||
|
@mixin light {
|
||||||
|
box-shadow: 5px 5px 24px rgba(0, 0, 0, 0.16);
|
||||||
|
}
|
||||||
|
padding: rem(35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-full-height {
|
||||||
|
min-height: calc(100vh - (rem(20) * 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-full-height-fixed {
|
||||||
|
height: calc(100vh - (rem(20) * 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-no-border-radius {
|
||||||
|
border-radius: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-full-screen-mobile {
|
||||||
|
@media (max-width: 48em) {
|
||||||
|
min-height: 100vh;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
padding: rem(40) rem(20) rem(20);
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 100;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
36
src/components/PageBlock/PageBlock.tsx
Normal file
36
src/components/PageBlock/PageBlock.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { CSSProperties, FC, ReactNode } from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import styles from "./PageBlock.module.css";
|
||||||
|
5
|
||||||
|
type Props = {
|
||||||
|
children: ReactNode;
|
||||||
|
style?: CSSProperties;
|
||||||
|
fullHeight?: boolean;
|
||||||
|
fullHeightFixed?: boolean;
|
||||||
|
noBorderRadius?: boolean;
|
||||||
|
fullScreenMobile?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PageBlock: FC<Props> = ({
|
||||||
|
children,
|
||||||
|
style,
|
||||||
|
fullHeight = false,
|
||||||
|
fullHeightFixed = false,
|
||||||
|
noBorderRadius = false,
|
||||||
|
fullScreenMobile = false,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={style}
|
||||||
|
className={classNames(
|
||||||
|
styles.container,
|
||||||
|
fullHeight && styles["container-full-height"],
|
||||||
|
fullHeightFixed && styles["container-full-height-fixed"],
|
||||||
|
noBorderRadius && styles["container-no-border-radius"],
|
||||||
|
fullScreenMobile && styles["container-full-screen-mobile"]
|
||||||
|
)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default PageBlock;
|
||||||
7
src/components/PageContainer/PageContainer.module.css
Normal file
7
src/components/PageContainer/PageContainer.module.css
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: rem(10);
|
||||||
|
min-height: 86vh;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
24
src/components/PageContainer/PageContainer.tsx
Normal file
24
src/components/PageContainer/PageContainer.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { CSSProperties, FC, ReactNode } from "react";
|
||||||
|
import styles from "./PageContainer.module.css";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: ReactNode;
|
||||||
|
style?: CSSProperties;
|
||||||
|
center?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PageContainer: FC<Props> = ({ children, style, center }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.container}
|
||||||
|
style={{
|
||||||
|
...style,
|
||||||
|
alignItems: center ? "center" : "",
|
||||||
|
justifyContent: center ? "center" : "",
|
||||||
|
}}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PageContainer;
|
||||||
25
src/components/SortableDnd/DragHandle.tsx
Normal file
25
src/components/SortableDnd/DragHandle.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import React, { ReactNode, useContext } from "react";
|
||||||
|
import SortableItemContext from "@/components/SortableDnd/SortableItemContext";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DragHandle = ({ children }: Props) => {
|
||||||
|
const { attributes, listeners, ref } = useContext(SortableItemContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
cursor: "grab",
|
||||||
|
}}
|
||||||
|
ref={ref}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DragHandle;
|
||||||
134
src/components/SortableDnd/SortableDnd.tsx
Normal file
134
src/components/SortableDnd/SortableDnd.tsx
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, {
|
||||||
|
CSSProperties,
|
||||||
|
ReactNode,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import {
|
||||||
|
Active,
|
||||||
|
DndContext,
|
||||||
|
DragEndEvent,
|
||||||
|
KeyboardSensor,
|
||||||
|
PointerSensor,
|
||||||
|
useSensor,
|
||||||
|
useSensors,
|
||||||
|
} from "@dnd-kit/core";
|
||||||
|
import {
|
||||||
|
SortableContext,
|
||||||
|
sortableKeyboardCoordinates,
|
||||||
|
} from "@dnd-kit/sortable";
|
||||||
|
import { LexoRank } from "lexorank";
|
||||||
|
import { Group } from "@mantine/core";
|
||||||
|
import { SortableItem } from "@/components/SortableDnd/SortableItem";
|
||||||
|
import { SortableOverlay } from "@/components/SortableDnd/SortableOverlay";
|
||||||
|
import { getNewLexorank, sortByLexorank } from "@/utils/lexorank";
|
||||||
|
|
||||||
|
type BaseItem = {
|
||||||
|
id: number;
|
||||||
|
rank: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props<T extends BaseItem> = {
|
||||||
|
initialItems: T[];
|
||||||
|
renderItem: (item: T) => ReactNode;
|
||||||
|
onDragEnd: (itemId: number, newLexorank: string) => void;
|
||||||
|
rowStyle?: CSSProperties;
|
||||||
|
itemStyle?: CSSProperties;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SortableDnd = <T extends BaseItem>({
|
||||||
|
initialItems,
|
||||||
|
renderItem,
|
||||||
|
onDragEnd,
|
||||||
|
rowStyle,
|
||||||
|
itemStyle,
|
||||||
|
}: Props<T>) => {
|
||||||
|
const [active, setActive] = useState<Active | null>(null);
|
||||||
|
const [items, setItems] = useState<T[]>([]);
|
||||||
|
const activeItem = useMemo(
|
||||||
|
() => initialItems.find(item => item.id === active?.id),
|
||||||
|
[active, items]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(sortByLexorank(initialItems));
|
||||||
|
setItems(sortByLexorank(initialItems));
|
||||||
|
}, [initialItems]);
|
||||||
|
|
||||||
|
const sensors = useSensors(
|
||||||
|
useSensor(PointerSensor),
|
||||||
|
useSensor(KeyboardSensor, {
|
||||||
|
coordinateGetter: sortableKeyboardCoordinates,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const onDragEndLocal = ({ active, over }: DragEndEvent) => {
|
||||||
|
if (over && active.id !== over?.id && activeItem) {
|
||||||
|
const overIndex: number = items.findIndex(
|
||||||
|
({ id }) => id === over.id
|
||||||
|
);
|
||||||
|
const activeIndex: number = items.findIndex(
|
||||||
|
({ id }) => id === activeItem.id
|
||||||
|
);
|
||||||
|
|
||||||
|
let leftIndex = overIndex;
|
||||||
|
let rightIndex = overIndex + 1;
|
||||||
|
if (overIndex < activeIndex) {
|
||||||
|
leftIndex = overIndex - 1;
|
||||||
|
rightIndex = overIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
const leftLexorank: LexoRank | null =
|
||||||
|
leftIndex >= 0 ? LexoRank.parse(items[leftIndex].rank) : null;
|
||||||
|
const rightLexorank: LexoRank | null =
|
||||||
|
rightIndex < items.length
|
||||||
|
? LexoRank.parse(items[rightIndex].rank)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const newLexorank = getNewLexorank(
|
||||||
|
leftLexorank,
|
||||||
|
rightLexorank
|
||||||
|
).toString();
|
||||||
|
|
||||||
|
items[activeIndex].rank = newLexorank;
|
||||||
|
onDragEnd(items[activeIndex].id, newLexorank);
|
||||||
|
const sortedItems = sortByLexorank(items);
|
||||||
|
setItems([...sortedItems]);
|
||||||
|
}
|
||||||
|
setActive(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DndContext
|
||||||
|
sensors={sensors}
|
||||||
|
onDragStart={({ active }) => setActive(active)}
|
||||||
|
onDragEnd={onDragEndLocal}
|
||||||
|
onDragCancel={() => setActive(null)}>
|
||||||
|
<SortableContext items={items}>
|
||||||
|
<Group
|
||||||
|
gap={0}
|
||||||
|
style={rowStyle}
|
||||||
|
role="application">
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<SortableItem
|
||||||
|
key={index}
|
||||||
|
itemStyle={itemStyle}
|
||||||
|
id={item.id}>
|
||||||
|
{renderItem(item)}
|
||||||
|
</SortableItem>
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
</SortableContext>
|
||||||
|
<SortableOverlay>
|
||||||
|
<div style={{ cursor: "grabbing" }}>
|
||||||
|
{activeItem ? renderItem(activeItem) : null}
|
||||||
|
</div>
|
||||||
|
</SortableOverlay>
|
||||||
|
</DndContext>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SortableDnd;
|
||||||
47
src/components/SortableDnd/SortableItem.tsx
Normal file
47
src/components/SortableDnd/SortableItem.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import React, { CSSProperties, PropsWithChildren, useMemo } from "react";
|
||||||
|
import { useSortable } from "@dnd-kit/sortable";
|
||||||
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
|
import DragHandle from "@/components/SortableDnd/DragHandle";
|
||||||
|
import SortableItemContext from "./SortableItemContext";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: number;
|
||||||
|
itemStyle?: CSSProperties;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SortableItem = ({ children, id }: PropsWithChildren<Props>) => {
|
||||||
|
const {
|
||||||
|
attributes,
|
||||||
|
isDragging,
|
||||||
|
listeners,
|
||||||
|
setNodeRef,
|
||||||
|
setActivatorNodeRef,
|
||||||
|
transform,
|
||||||
|
transition,
|
||||||
|
} = useSortable({ id });
|
||||||
|
|
||||||
|
const context = useMemo(
|
||||||
|
() => ({
|
||||||
|
attributes,
|
||||||
|
listeners,
|
||||||
|
ref: setActivatorNodeRef,
|
||||||
|
}),
|
||||||
|
[attributes, listeners, setActivatorNodeRef]
|
||||||
|
);
|
||||||
|
|
||||||
|
const style: CSSProperties = {
|
||||||
|
opacity: isDragging ? 0.4 : undefined,
|
||||||
|
transform: CSS.Translate.toString(transform),
|
||||||
|
transition,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SortableItemContext.Provider value={context}>
|
||||||
|
<div
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={style}>
|
||||||
|
<DragHandle>{children}</DragHandle>
|
||||||
|
</div>
|
||||||
|
</SortableItemContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
16
src/components/SortableDnd/SortableItemContext.tsx
Normal file
16
src/components/SortableDnd/SortableItemContext.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
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;
|
||||||
24
src/components/SortableDnd/SortableOverlay.tsx
Normal file
24
src/components/SortableDnd/SortableOverlay.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import type { PropsWithChildren } from "react";
|
||||||
|
import {
|
||||||
|
defaultDropAnimationSideEffects,
|
||||||
|
DragOverlay,
|
||||||
|
DropAnimation,
|
||||||
|
} from "@dnd-kit/core";
|
||||||
|
|
||||||
|
const dropAnimationConfig: DropAnimation = {
|
||||||
|
sideEffects: defaultDropAnimationSideEffects({
|
||||||
|
styles: {
|
||||||
|
active: {
|
||||||
|
opacity: "0.4",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SortableOverlay({ children }: PropsWithChildren) {
|
||||||
|
return (
|
||||||
|
<DragOverlay dropAnimation={dropAnimationConfig}>
|
||||||
|
{children}
|
||||||
|
</DragOverlay>
|
||||||
|
);
|
||||||
|
}
|
||||||
3
src/components/SortableDnd/index.ts
Normal file
3
src/components/SortableDnd/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import SortableDnd from "@/components/SortableDnd/SortableDnd";
|
||||||
|
|
||||||
|
export default SortableDnd;
|
||||||
5
src/types/BoardSchema.ts
Normal file
5
src/types/BoardSchema.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export type BoardSchema = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
rank: string;
|
||||||
|
};
|
||||||
36
src/utils/lexorank.ts
Normal file
36
src/utils/lexorank.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { LexoRank } from "lexorank";
|
||||||
|
|
||||||
|
type LexorankSortable = {
|
||||||
|
rank: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function compareByLexorank<T extends LexorankSortable>(
|
||||||
|
a: T,
|
||||||
|
b: T
|
||||||
|
): -1 | 1 | 0 {
|
||||||
|
if (a.rank < b.rank) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (a.rank > b.rank) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sortByLexorank<T extends LexorankSortable>(items: T[]): T[] {
|
||||||
|
return items.sort(compareByLexorank);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNewLexorank(
|
||||||
|
left?: LexoRank | null,
|
||||||
|
right?: LexoRank | null
|
||||||
|
): LexoRank {
|
||||||
|
if (right) {
|
||||||
|
if (left) return left?.between(right);
|
||||||
|
return right.genPrev();
|
||||||
|
}
|
||||||
|
if (left) {
|
||||||
|
return left.genNext();
|
||||||
|
}
|
||||||
|
return LexoRank.middle();
|
||||||
|
}
|
||||||
59
yarn.lock
59
yarn.lock
@ -1544,6 +1544,55 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@dnd-kit/accessibility@npm:^3.1.1":
|
||||||
|
version: 3.1.1
|
||||||
|
resolution: "@dnd-kit/accessibility@npm:3.1.1"
|
||||||
|
dependencies:
|
||||||
|
tslib: "npm:^2.0.0"
|
||||||
|
peerDependencies:
|
||||||
|
react: ">=16.8.0"
|
||||||
|
checksum: 10c0/be0bf41716dc58f9386bc36906ec1ce72b7b42b6d1d0e631d347afe9bd8714a829bd6f58a346dd089b1519e93918ae2f94497411a61a4f5e4d9247c6cfd1fef8
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@dnd-kit/core@npm:^6.3.1":
|
||||||
|
version: 6.3.1
|
||||||
|
resolution: "@dnd-kit/core@npm:6.3.1"
|
||||||
|
dependencies:
|
||||||
|
"@dnd-kit/accessibility": "npm:^3.1.1"
|
||||||
|
"@dnd-kit/utilities": "npm:^3.2.2"
|
||||||
|
tslib: "npm:^2.0.0"
|
||||||
|
peerDependencies:
|
||||||
|
react: ">=16.8.0"
|
||||||
|
react-dom: ">=16.8.0"
|
||||||
|
checksum: 10c0/196db95d81096d9dc248983533eab91ba83591770fa5c894b1ac776f42af0d99522b3fd5bb3923411470e4733fcfa103e6ee17adc17b9b7eb54c7fbec5ff7c52
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@dnd-kit/sortable@npm:^10.0.0":
|
||||||
|
version: 10.0.0
|
||||||
|
resolution: "@dnd-kit/sortable@npm:10.0.0"
|
||||||
|
dependencies:
|
||||||
|
"@dnd-kit/utilities": "npm:^3.2.2"
|
||||||
|
tslib: "npm:^2.0.0"
|
||||||
|
peerDependencies:
|
||||||
|
"@dnd-kit/core": ^6.3.0
|
||||||
|
react: ">=16.8.0"
|
||||||
|
checksum: 10c0/37ee48bc6789fb512dc0e4c374a96d19abe5b2b76dc34856a5883aaa96c3297891b94cc77bbc409e074dcce70967ebcb9feb40cd9abadb8716fc280b4c7f99af
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@dnd-kit/utilities@npm:^3.2.2":
|
||||||
|
version: 3.2.2
|
||||||
|
resolution: "@dnd-kit/utilities@npm:3.2.2"
|
||||||
|
dependencies:
|
||||||
|
tslib: "npm:^2.0.0"
|
||||||
|
peerDependencies:
|
||||||
|
react: ">=16.8.0"
|
||||||
|
checksum: 10c0/9aa90526f3e3fd567b5acc1b625a63177b9e8d00e7e50b2bd0e08fa2bf4dba7e19529777e001fdb8f89a7ce69f30b190c8364d390212634e0afdfa8c395e85a0
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@dual-bundle/import-meta-resolve@npm:^4.1.0":
|
"@dual-bundle/import-meta-resolve@npm:^4.1.0":
|
||||||
version: 4.1.0
|
version: 4.1.0
|
||||||
resolution: "@dual-bundle/import-meta-resolve@npm:4.1.0"
|
resolution: "@dual-bundle/import-meta-resolve@npm:4.1.0"
|
||||||
@ -5849,6 +5898,8 @@ __metadata:
|
|||||||
resolution: "crm-frontend@workspace:."
|
resolution: "crm-frontend@workspace:."
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/core": "npm:^7.27.4"
|
"@babel/core": "npm:^7.27.4"
|
||||||
|
"@dnd-kit/core": "npm:^6.3.1"
|
||||||
|
"@dnd-kit/sortable": "npm:^10.0.0"
|
||||||
"@eslint/js": "npm:^9.29.0"
|
"@eslint/js": "npm:^9.29.0"
|
||||||
"@ianvs/prettier-plugin-sort-imports": "npm:^4.4.2"
|
"@ianvs/prettier-plugin-sort-imports": "npm:^4.4.2"
|
||||||
"@mantine/core": "npm:8.1.2"
|
"@mantine/core": "npm:8.1.2"
|
||||||
@ -5885,6 +5936,7 @@ __metadata:
|
|||||||
i18n-iso-countries: "npm:^7.14.0"
|
i18n-iso-countries: "npm:^7.14.0"
|
||||||
jest: "npm:^30.0.0"
|
jest: "npm:^30.0.0"
|
||||||
jest-environment-jsdom: "npm:^30.0.0"
|
jest-environment-jsdom: "npm:^30.0.0"
|
||||||
|
lexorank: "npm:^1.0.5"
|
||||||
libphonenumber-js: "npm:^1.12.10"
|
libphonenumber-js: "npm:^1.12.10"
|
||||||
next: "npm:15.3.3"
|
next: "npm:15.3.3"
|
||||||
openapi-typescript-codegen: "npm:^0.29.0"
|
openapi-typescript-codegen: "npm:^0.29.0"
|
||||||
@ -9404,6 +9456,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"lexorank@npm:^1.0.5":
|
||||||
|
version: 1.0.5
|
||||||
|
resolution: "lexorank@npm:1.0.5"
|
||||||
|
checksum: 10c0/76b4f4d9a837850a2f3e6d3c32dfd8700aa3bad9fb94921adf6ea0d8241c07ba89f62f106eaf54af0fac85c0f8653ef07da1489dd81556d2edd4f5e5fb4045cf
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"libphonenumber-js@npm:^1.12.10":
|
"libphonenumber-js@npm:^1.12.10":
|
||||||
version: 1.12.10
|
version: 1.12.10
|
||||||
resolution: "libphonenumber-js@npm:1.12.10"
|
resolution: "libphonenumber-js@npm:1.12.10"
|
||||||
|
|||||||
Reference in New Issue
Block a user