feat: Integrate Kanban board with drag-and-drop functionality using @dnd-kit
This commit is contained in:
parent
46ded058f4
commit
608e6a8e10
5 changed files with 569 additions and 10 deletions
96
apps/infoalloggi/package-lock.json
generated
96
apps/infoalloggi/package-lock.json
generated
|
|
@ -8,11 +8,14 @@
|
|||
"name": "infoalloggi",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@fattureincloud/fattureincloud-ts-sdk": "^2.1.1",
|
||||
"@hookform/resolvers": "^5.2.1",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-popover": "^1.1.14",
|
||||
"@radix-ui/react-scroll-area": "^1.2.9",
|
||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-toggle-group": "^1.1.10",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
|
|
@ -84,6 +87,7 @@
|
|||
"tailwind-merge": "3.3",
|
||||
"tailwind-scrollbar": "^4.0.2",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"tunnel-rat": "^0.1.2",
|
||||
"tw-animate-css": "^1.3.6",
|
||||
"type-fest": "^4.41.0",
|
||||
"uuid": "^11.1.0",
|
||||
|
|
@ -1212,6 +1216,59 @@
|
|||
"integrity": "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@dnd-kit/accessibility": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
|
||||
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/core": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
||||
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dnd-kit/accessibility": "^3.1.1",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/sortable": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
|
||||
"integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dnd-kit/core": "^6.3.0",
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/utilities": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
|
||||
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@electric-sql/pglite": {
|
||||
"version": "0.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.3.7.tgz",
|
||||
|
|
@ -17908,6 +17965,15 @@
|
|||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tunnel-rat": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz",
|
||||
"integrity": "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"zustand": "^4.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/tw-animate-css": {
|
||||
"version": "1.3.7",
|
||||
"resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.3.7.tgz",
|
||||
|
|
@ -18695,6 +18761,34 @@
|
|||
"peerDependencies": {
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/zustand": {
|
||||
"version": "4.5.7",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
|
||||
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"use-sync-external-store": "^1.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=16.8",
|
||||
"immer": ">=9.0.6",
|
||||
"react": ">=16.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"immer": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,11 +19,14 @@
|
|||
},
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@fattureincloud/fattureincloud-ts-sdk": "^2.1.1",
|
||||
"@hookform/resolvers": "^5.2.1",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-popover": "^1.1.14",
|
||||
"@radix-ui/react-scroll-area": "^1.2.9",
|
||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-toggle-group": "^1.1.10",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
|
|
@ -95,6 +98,7 @@
|
|||
"tailwind-merge": "3.3",
|
||||
"tailwind-scrollbar": "^4.0.2",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"tunnel-rat": "^0.1.2",
|
||||
"tw-animate-css": "^1.3.6",
|
||||
"type-fest": "^4.41.0",
|
||||
"uuid": "^11.1.0",
|
||||
|
|
|
|||
117
apps/infoalloggi/src/components/KabanExample.tsx
Normal file
117
apps/infoalloggi/src/components/KabanExample.tsx
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar";
|
||||
import {
|
||||
KanbanBoard,
|
||||
KanbanCard,
|
||||
KanbanCards,
|
||||
KanbanHeader,
|
||||
KanbanProvider,
|
||||
} from "~/components/ui/kanban";
|
||||
|
||||
const columns = [
|
||||
{ id: "todo", name: "To Do", color: "red" },
|
||||
{ id: "inprogress", name: "In Progress", color: "blue" },
|
||||
{ id: "done", name: "Done", color: "green" },
|
||||
];
|
||||
const exampleFeatures = [
|
||||
{
|
||||
id: "1",
|
||||
name: "Design new homepage",
|
||||
column: "todo",
|
||||
startAt: new Date("2024-06-01"),
|
||||
endAt: new Date("2024-06-10"),
|
||||
owner: {
|
||||
name: "Alice",
|
||||
image: "https://i.pravatar.cc/150?img=1",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "Implement authentication",
|
||||
column: "inprogress",
|
||||
startAt: new Date("2024-06-05"),
|
||||
endAt: new Date("2024-06-15"),
|
||||
owner: {
|
||||
name: "Bob",
|
||||
image: "https://i.pravatar.cc/150?img=2",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "Set up database",
|
||||
column: "done",
|
||||
startAt: new Date("2024-05-20"),
|
||||
endAt: new Date("2024-05-30"),
|
||||
owner: {
|
||||
name: "Charlie",
|
||||
image: "https://i.pravatar.cc/150?img=3",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const dateFormatter = new Intl.DateTimeFormat("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
const shortDateFormatter = new Intl.DateTimeFormat("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
const Example = () => {
|
||||
const [features, setFeatures] = useState(exampleFeatures);
|
||||
return (
|
||||
<KanbanProvider
|
||||
columns={columns}
|
||||
data={features}
|
||||
onDataChange={setFeatures}
|
||||
>
|
||||
{(column) => (
|
||||
<KanbanBoard id={column.id} key={column.id}>
|
||||
<KanbanHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="h-2 w-2 rounded-full"
|
||||
style={{ backgroundColor: column.color }}
|
||||
/>
|
||||
<span>{column.name}</span>
|
||||
</div>
|
||||
</KanbanHeader>
|
||||
<KanbanCards id={column.id}>
|
||||
{(feature: (typeof features)[number]) => (
|
||||
<KanbanCard
|
||||
column={column.id}
|
||||
id={feature.id}
|
||||
key={feature.id}
|
||||
name={feature.name}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="m-0 flex-1 font-medium text-sm">
|
||||
{feature.name}
|
||||
</p>
|
||||
</div>
|
||||
{feature.owner && (
|
||||
<Avatar className="h-4 w-4 shrink-0">
|
||||
<AvatarImage src={feature.owner.image} />
|
||||
<AvatarFallback>
|
||||
{feature.owner.name?.slice(0, 2)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
)}
|
||||
</div>
|
||||
<p className="m-0 text-muted-foreground text-xs">
|
||||
{shortDateFormatter.format(feature.startAt)} -{" "}
|
||||
{dateFormatter.format(feature.endAt)}
|
||||
</p>
|
||||
</KanbanCard>
|
||||
)}
|
||||
</KanbanCards>
|
||||
</KanbanBoard>
|
||||
)}
|
||||
</KanbanProvider>
|
||||
);
|
||||
};
|
||||
export default Example;
|
||||
346
apps/infoalloggi/src/components/ui/kanban.tsx
Normal file
346
apps/infoalloggi/src/components/ui/kanban.tsx
Normal file
|
|
@ -0,0 +1,346 @@
|
|||
"use client";
|
||||
|
||||
import type {
|
||||
Announcements,
|
||||
DndContextProps,
|
||||
DragEndEvent,
|
||||
DragOverEvent,
|
||||
DragStartEvent,
|
||||
} from "@dnd-kit/core";
|
||||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
DragOverlay,
|
||||
KeyboardSensor,
|
||||
MouseSensor,
|
||||
TouchSensor,
|
||||
useDroppable,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from "@dnd-kit/core";
|
||||
import { arrayMove, SortableContext, useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import {
|
||||
createContext,
|
||||
type HTMLAttributes,
|
||||
type ReactNode,
|
||||
useContext,
|
||||
useState,
|
||||
} from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import tunnel from "tunnel-rat";
|
||||
import { Card } from "~/components/ui/card";
|
||||
import { ScrollArea, ScrollBar } from "~/components/ui/scroll-area";
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
//https://www.shadcn.io/components/data/kanban#installation
|
||||
const t = tunnel();
|
||||
|
||||
export type { DragEndEvent } from "@dnd-kit/core";
|
||||
|
||||
type KanbanItemProps = {
|
||||
id: string;
|
||||
name: string;
|
||||
column: string;
|
||||
} & Record<string, unknown>;
|
||||
|
||||
type KanbanColumnProps = {
|
||||
id: string;
|
||||
name: string;
|
||||
} & Record<string, unknown>;
|
||||
|
||||
type KanbanContextProps<
|
||||
T extends KanbanItemProps = KanbanItemProps,
|
||||
C extends KanbanColumnProps = KanbanColumnProps,
|
||||
> = {
|
||||
columns: C[];
|
||||
data: T[];
|
||||
activeCardId: string | null;
|
||||
};
|
||||
|
||||
const KanbanContext = createContext<KanbanContextProps>({
|
||||
columns: [],
|
||||
data: [],
|
||||
activeCardId: null,
|
||||
});
|
||||
|
||||
export type KanbanBoardProps = {
|
||||
id: string;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const KanbanBoard = ({ id, children, className }: KanbanBoardProps) => {
|
||||
const { isOver, setNodeRef } = useDroppable({
|
||||
id,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex size-full min-h-40 flex-col divide-y overflow-hidden rounded-md border bg-secondary text-xs shadow-sm ring-2 transition-all",
|
||||
isOver ? "ring-primary" : "ring-transparent",
|
||||
className,
|
||||
)}
|
||||
ref={setNodeRef}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export type KanbanCardProps<T extends KanbanItemProps = KanbanItemProps> = T & {
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const KanbanCard = <T extends KanbanItemProps = KanbanItemProps>({
|
||||
id,
|
||||
name,
|
||||
children,
|
||||
className,
|
||||
}: KanbanCardProps<T>) => {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transition,
|
||||
transform,
|
||||
isDragging,
|
||||
} = useSortable({
|
||||
id,
|
||||
});
|
||||
const { activeCardId } = useContext(KanbanContext) as KanbanContextProps;
|
||||
|
||||
const style = {
|
||||
transition,
|
||||
transform: CSS.Transform.toString(transform),
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={style} {...listeners} {...attributes} ref={setNodeRef}>
|
||||
<Card
|
||||
className={cn(
|
||||
"cursor-grab gap-4 rounded-md p-3 shadow-sm",
|
||||
isDragging && "pointer-events-none cursor-grabbing opacity-30",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children ?? <p className="m-0 font-medium text-sm">{name}</p>}
|
||||
</Card>
|
||||
</div>
|
||||
{activeCardId === id && (
|
||||
<t.In>
|
||||
<Card
|
||||
className={cn(
|
||||
"cursor-grab gap-4 rounded-md p-3 shadow-sm ring-2 ring-primary",
|
||||
isDragging && "cursor-grabbing",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children ?? <p className="m-0 font-medium text-sm">{name}</p>}
|
||||
</Card>
|
||||
</t.In>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export type KanbanCardsProps<T extends KanbanItemProps = KanbanItemProps> =
|
||||
Omit<HTMLAttributes<HTMLDivElement>, "children" | "id"> & {
|
||||
children: (item: T) => ReactNode;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export const KanbanCards = <T extends KanbanItemProps = KanbanItemProps>({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: KanbanCardsProps<T>) => {
|
||||
const { data } = useContext(KanbanContext) as KanbanContextProps<T>;
|
||||
const filteredData = data.filter((item) => item.column === props.id);
|
||||
const items = filteredData.map((item) => item.id);
|
||||
|
||||
return (
|
||||
<ScrollArea className="overflow-hidden">
|
||||
<SortableContext items={items}>
|
||||
<div
|
||||
className={cn("flex flex-grow flex-col gap-2 p-2", className)}
|
||||
{...props}
|
||||
>
|
||||
{filteredData.map(children)}
|
||||
</div>
|
||||
</SortableContext>
|
||||
<ScrollBar orientation="vertical" />
|
||||
</ScrollArea>
|
||||
);
|
||||
};
|
||||
|
||||
export type KanbanHeaderProps = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
export const KanbanHeader = ({ className, ...props }: KanbanHeaderProps) => (
|
||||
<div className={cn("m-0 p-2 font-semibold text-sm", className)} {...props} />
|
||||
);
|
||||
|
||||
export type KanbanProviderProps<
|
||||
T extends KanbanItemProps = KanbanItemProps,
|
||||
C extends KanbanColumnProps = KanbanColumnProps,
|
||||
> = Omit<DndContextProps, "children"> & {
|
||||
children: (column: C) => ReactNode;
|
||||
className?: string;
|
||||
columns: C[];
|
||||
data: T[];
|
||||
onDataChange?: (data: T[]) => void;
|
||||
onDragStart?: (event: DragStartEvent) => void;
|
||||
onDragEnd?: (event: DragEndEvent) => void;
|
||||
onDragOver?: (event: DragOverEvent) => void;
|
||||
};
|
||||
|
||||
export const KanbanProvider = <
|
||||
T extends KanbanItemProps = KanbanItemProps,
|
||||
C extends KanbanColumnProps = KanbanColumnProps,
|
||||
>({
|
||||
children,
|
||||
onDragStart,
|
||||
onDragEnd,
|
||||
onDragOver,
|
||||
className,
|
||||
columns,
|
||||
data,
|
||||
onDataChange,
|
||||
...props
|
||||
}: KanbanProviderProps<T, C>) => {
|
||||
const [activeCardId, setActiveCardId] = useState<string | null>(null);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(MouseSensor),
|
||||
useSensor(TouchSensor),
|
||||
useSensor(KeyboardSensor),
|
||||
);
|
||||
|
||||
const handleDragStart = (event: DragStartEvent) => {
|
||||
const card = data.find((item) => item.id === event.active.id);
|
||||
if (card) {
|
||||
setActiveCardId(event.active.id as string);
|
||||
}
|
||||
onDragStart?.(event);
|
||||
};
|
||||
|
||||
const handleDragOver = (event: DragOverEvent) => {
|
||||
const { active, over } = event;
|
||||
|
||||
if (!over) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeItem = data.find((item) => item.id === active.id);
|
||||
const overItem = data.find((item) => item.id === over.id);
|
||||
|
||||
if (!activeItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeColumn = activeItem.column;
|
||||
const overColumn =
|
||||
overItem?.column ||
|
||||
columns.find((col) => col.id === over.id)?.id ||
|
||||
columns[0]?.id;
|
||||
|
||||
if (!overColumn) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeColumn !== overColumn) {
|
||||
let newData = [...data];
|
||||
const activeIndex = newData.findIndex((item) => item.id === active.id);
|
||||
const overIndex = newData.findIndex((item) => item.id === over.id);
|
||||
|
||||
if (newData[activeIndex] === undefined) {
|
||||
return;
|
||||
}
|
||||
newData[activeIndex].column = overColumn; // Now TypeScript knows this is a string
|
||||
newData = arrayMove(newData, activeIndex, overIndex);
|
||||
|
||||
onDataChange?.(newData);
|
||||
}
|
||||
|
||||
onDragOver?.(event);
|
||||
};
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
setActiveCardId(null);
|
||||
|
||||
onDragEnd?.(event);
|
||||
|
||||
const { active, over } = event;
|
||||
|
||||
if (!over || active.id === over.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
let newData = [...data];
|
||||
|
||||
const oldIndex = newData.findIndex((item) => item.id === active.id);
|
||||
const newIndex = newData.findIndex((item) => item.id === over.id);
|
||||
|
||||
newData = arrayMove(newData, oldIndex, newIndex);
|
||||
|
||||
onDataChange?.(newData);
|
||||
};
|
||||
|
||||
const announcements: Announcements = {
|
||||
onDragStart({ active }) {
|
||||
const { name, column } = data.find((item) => item.id === active.id) ?? {};
|
||||
|
||||
return `Picked up the card "${name}" from the "${column}" column`;
|
||||
},
|
||||
onDragOver({ active, over }) {
|
||||
const { name } = data.find((item) => item.id === active.id) ?? {};
|
||||
const newColumn = columns.find((column) => column.id === over?.id)?.name;
|
||||
|
||||
return `Dragged the card "${name}" over the "${newColumn}" column`;
|
||||
},
|
||||
onDragEnd({ active, over }) {
|
||||
const { name } = data.find((item) => item.id === active.id) ?? {};
|
||||
const newColumn = columns.find((column) => column.id === over?.id)?.name;
|
||||
|
||||
return `Dropped the card "${name}" into the "${newColumn}" column`;
|
||||
},
|
||||
onDragCancel({ active }) {
|
||||
const { name } = data.find((item) => item.id === active.id) ?? {};
|
||||
|
||||
return `Cancelled dragging the card "${name}"`;
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<KanbanContext.Provider value={{ columns, data, activeCardId }}>
|
||||
<DndContext
|
||||
accessibility={{ announcements }}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragOver={handleDragOver}
|
||||
onDragStart={handleDragStart}
|
||||
sensors={sensors}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"grid size-full auto-cols-fr grid-flow-col gap-4",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{columns.map((column) => children(column))}
|
||||
</div>
|
||||
{typeof window !== "undefined" &&
|
||||
createPortal(
|
||||
<DragOverlay>
|
||||
<t.Out />
|
||||
</DragOverlay>,
|
||||
document.body,
|
||||
)}
|
||||
</DndContext>
|
||||
</KanbanContext.Provider>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import type { NextPage } from "next";
|
||||
import Image from "next/image";
|
||||
import Example from "~/components/KabanExample";
|
||||
import { NoSSR } from "~/lib/nossr";
|
||||
|
||||
/*
|
||||
import { useState } from "react";
|
||||
|
|
@ -149,13 +150,10 @@ export default Test;
|
|||
|
||||
const Test: NextPage = () => {
|
||||
return (
|
||||
<div>
|
||||
Test page
|
||||
<Image
|
||||
alt="asdad"
|
||||
fill
|
||||
src="/storage-api/get/0134fd282f36395a?image=true"
|
||||
/>
|
||||
<div className="p-4">
|
||||
<NoSSR>
|
||||
<Example />
|
||||
</NoSSR>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue