refactor: separation of shared components
This commit is contained in:
27
src/components/dnd/SortableDnd/DragHandle.tsx
Normal file
27
src/components/dnd/SortableDnd/DragHandle.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import React, { CSSProperties, ReactNode, useContext } from "react";
|
||||
import SortableItemContext from "@/components/dnd/SortableDnd/SortableItemContext";
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
style?: CSSProperties;
|
||||
};
|
||||
|
||||
const DragHandle = ({ children, style }: Props) => {
|
||||
const { attributes, listeners, ref } = useContext(SortableItemContext);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
style={{
|
||||
width: "100%",
|
||||
cursor: "grab",
|
||||
...style,
|
||||
}}
|
||||
ref={ref}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DragHandle;
|
||||
130
src/components/dnd/SortableDnd/SortableDnd.tsx
Normal file
130
src/components/dnd/SortableDnd/SortableDnd.tsx
Normal file
@ -0,0 +1,130 @@
|
||||
"use client";
|
||||
|
||||
import React, {
|
||||
CSSProperties,
|
||||
ReactNode,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Active, DndContext, DragEndEvent } from "@dnd-kit/core";
|
||||
import { SortableContext } from "@dnd-kit/sortable";
|
||||
import { LexoRank } from "lexorank";
|
||||
import { Box, Group } from "@mantine/core";
|
||||
import useDndSensors from "@/app/deals/hooks/useSensors";
|
||||
import { SortableItem } from "@/components/dnd/SortableDnd/SortableItem";
|
||||
import { SortableOverlay } from "@/components/dnd/SortableDnd/SortableOverlay";
|
||||
import { getNewLexorank, sortByLexorank } from "@/utils/lexorank";
|
||||
|
||||
type BaseItem = {
|
||||
id: number;
|
||||
lexorank: string;
|
||||
};
|
||||
|
||||
type Props<T extends BaseItem> = {
|
||||
initialItems: T[];
|
||||
renderItem: (item: T) => ReactNode;
|
||||
onDragEnd: (itemId: number, newLexorank: string) => void;
|
||||
onItemClick: (item: T) => void;
|
||||
rowStyle?: CSSProperties;
|
||||
itemStyle?: CSSProperties;
|
||||
};
|
||||
|
||||
const SortableDnd = <T extends BaseItem>({
|
||||
initialItems,
|
||||
renderItem,
|
||||
onDragEnd,
|
||||
onItemClick,
|
||||
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(() => {
|
||||
setItems(sortByLexorank(initialItems));
|
||||
}, [initialItems]);
|
||||
|
||||
const sensors = useDndSensors();
|
||||
|
||||
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].lexorank)
|
||||
: null;
|
||||
const rightLexorank: LexoRank | null =
|
||||
rightIndex < items.length
|
||||
? LexoRank.parse(items[rightIndex].lexorank)
|
||||
: null;
|
||||
|
||||
const newLexorank = getNewLexorank(
|
||||
leftLexorank,
|
||||
rightLexorank
|
||||
).toString();
|
||||
|
||||
items[activeIndex].lexorank = 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) => (
|
||||
<Box
|
||||
key={index}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onItemClick(item);
|
||||
}}>
|
||||
<SortableItem
|
||||
dragHandleStyle={{ cursor: "pointer" }}
|
||||
itemStyle={itemStyle}
|
||||
id={item.id}>
|
||||
{renderItem(item)}
|
||||
</SortableItem>
|
||||
</Box>
|
||||
))}
|
||||
</Group>
|
||||
</SortableContext>
|
||||
<SortableOverlay>
|
||||
<div style={{ cursor: "grabbing" }}>
|
||||
{activeItem ? renderItem(activeItem) : null}
|
||||
</div>
|
||||
</SortableOverlay>
|
||||
</DndContext>
|
||||
);
|
||||
};
|
||||
|
||||
export default SortableDnd;
|
||||
54
src/components/dnd/SortableDnd/SortableItem.tsx
Normal file
54
src/components/dnd/SortableDnd/SortableItem.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
import React, { CSSProperties, PropsWithChildren, useMemo } from "react";
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import DragHandle from "@/components/dnd/SortableDnd/DragHandle";
|
||||
import SortableItemContext from "./SortableItemContext";
|
||||
|
||||
type Props = {
|
||||
id: number | string;
|
||||
itemStyle?: CSSProperties;
|
||||
dragHandleStyle?: CSSProperties;
|
||||
};
|
||||
|
||||
export const SortableItem = ({
|
||||
children,
|
||||
itemStyle,
|
||||
id,
|
||||
dragHandleStyle,
|
||||
}: 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,
|
||||
...itemStyle,
|
||||
};
|
||||
|
||||
return (
|
||||
<SortableItemContext.Provider value={context}>
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}>
|
||||
<DragHandle style={dragHandleStyle}>{children}</DragHandle>
|
||||
</div>
|
||||
</SortableItemContext.Provider>
|
||||
);
|
||||
};
|
||||
16
src/components/dnd/SortableDnd/SortableItemContext.tsx
Normal file
16
src/components/dnd/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/dnd/SortableDnd/SortableOverlay.tsx
Normal file
24
src/components/dnd/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/dnd/SortableDnd/index.ts
Normal file
3
src/components/dnd/SortableDnd/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import SortableDnd from "@/components/dnd/SortableDnd/SortableDnd";
|
||||
|
||||
export default SortableDnd;
|
||||
Reference in New Issue
Block a user