feat: integrate DnD Kit for enhanced kanban functionality and add Potenziali management components
This commit is contained in:
parent
a71d9f8d2e
commit
3143870cfd
8 changed files with 737 additions and 0 deletions
57
apps/infoalloggi/package-lock.json
generated
57
apps/infoalloggi/package-lock.json
generated
|
|
@ -8,6 +8,9 @@
|
||||||
"name": "infoalloggi",
|
"name": "infoalloggi",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"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",
|
"@fattureincloud/fattureincloud-ts-sdk": "^2.1.1",
|
||||||
"@hookform/resolvers": "^5.2.1",
|
"@hookform/resolvers": "^5.2.1",
|
||||||
"@next/bundle-analyzer": "^16.0.2",
|
"@next/bundle-analyzer": "^16.0.2",
|
||||||
|
|
@ -1227,6 +1230,60 @@
|
||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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",
|
||||||
|
"peer": true,
|
||||||
|
"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": {
|
"node_modules/@electric-sql/pglite": {
|
||||||
"version": "0.3.7",
|
"version": "0.3.7",
|
||||||
"resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.3.7.tgz",
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,9 @@
|
||||||
},
|
},
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"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",
|
"@fattureincloud/fattureincloud-ts-sdk": "^2.1.1",
|
||||||
"@hookform/resolvers": "^5.2.1",
|
"@hookform/resolvers": "^5.2.1",
|
||||||
"@next/bundle-analyzer": "^16.0.2",
|
"@next/bundle-analyzer": "^16.0.2",
|
||||||
|
|
|
||||||
187
apps/infoalloggi/src/components/area-riservata/potenziali.tsx
Normal file
187
apps/infoalloggi/src/components/area-riservata/potenziali.tsx
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import {
|
||||||
|
KanbanBoard,
|
||||||
|
KanbanCard,
|
||||||
|
KanbanCards,
|
||||||
|
KanbanHeader,
|
||||||
|
KanbanProvider,
|
||||||
|
} from "~/components/ui/kanban";
|
||||||
|
import {
|
||||||
|
type ElementoPotenziali,
|
||||||
|
PotenzialiProvider,
|
||||||
|
usePotenziali,
|
||||||
|
} from "~/providers/PotenzialiProvider";
|
||||||
|
import type { UsersId } from "~/schemas/public/Users";
|
||||||
|
import {
|
||||||
|
Credenza,
|
||||||
|
CredenzaBody,
|
||||||
|
CredenzaContent,
|
||||||
|
CredenzaDescription,
|
||||||
|
CredenzaHeader,
|
||||||
|
CredenzaTitle,
|
||||||
|
} from "../custom_ui/credenza";
|
||||||
|
import {
|
||||||
|
ContextMenu,
|
||||||
|
ContextMenuContent,
|
||||||
|
ContextMenuItem,
|
||||||
|
ContextMenuTrigger,
|
||||||
|
} from "../ui/context-menu";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "../ui/dialog";
|
||||||
|
import { UserAvatar } from "../user_avatar";
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ id: "todo", name: "To Do", color: "red" },
|
||||||
|
{ id: "inprogress", name: "In Progress", color: "blue" },
|
||||||
|
{ id: "done", name: "Done", color: "green" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const SampleData: ElementoPotenziali[] = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
name: "Mario Rossi",
|
||||||
|
column: "todo",
|
||||||
|
descrizione:
|
||||||
|
"Single, cerca appartamento in centro da subito, budget 800 euro",
|
||||||
|
createdAt: new Date(),
|
||||||
|
owner: {
|
||||||
|
userId: "019b9a25-eff1-420a-9017-b0add205d7b6" as UsersId,
|
||||||
|
username: "Marco Pedone",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
name: "Luigi Bianchi",
|
||||||
|
column: "inprogress",
|
||||||
|
descrizione: "Coppia, cerca bilocale in periferia, budget 600 euro",
|
||||||
|
createdAt: new Date(),
|
||||||
|
owner: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
name: "Giovanni Verdi",
|
||||||
|
column: "done",
|
||||||
|
descrizione:
|
||||||
|
"Famiglia con 2 figli, cerca trilocale vicino a scuole, budget 1000 euro",
|
||||||
|
createdAt: new Date(),
|
||||||
|
owner: {
|
||||||
|
userId: "019b9a25-eff1-420a-9017-b0add205d7b6" as UsersId,
|
||||||
|
username: "Marco Pedone",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const Potenziali = () => {
|
||||||
|
return (
|
||||||
|
<PotenzialiProvider data={SampleData}>
|
||||||
|
<Kaban />
|
||||||
|
</PotenzialiProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Kaban = () => {
|
||||||
|
const { data, updateData, setEditingId, editingId } = usePotenziali();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<KanbanProvider
|
||||||
|
className="w-max"
|
||||||
|
columns={columns}
|
||||||
|
data={data}
|
||||||
|
onDataChange={updateData}
|
||||||
|
>
|
||||||
|
{(column) => (
|
||||||
|
<KanbanBoard
|
||||||
|
className="w-fit overflow-auto"
|
||||||
|
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 className="w-68" id={column.id}>
|
||||||
|
{(data: ElementoPotenziali) => (
|
||||||
|
<KanbanCard
|
||||||
|
cardOnClick={() => {
|
||||||
|
setEditingId(data.id);
|
||||||
|
}}
|
||||||
|
column={column.id}
|
||||||
|
id={data.id}
|
||||||
|
key={data.id}
|
||||||
|
name={data.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">
|
||||||
|
{data.name}
|
||||||
|
</p>
|
||||||
|
<p className="m-0 text-muted-foreground text-xs">
|
||||||
|
{data.descrizione}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{data.owner && (
|
||||||
|
<UserAvatar
|
||||||
|
className="size-6 shrink-0 text-sm"
|
||||||
|
userId={data.owner.userId}
|
||||||
|
username={data.owner.username}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="mt-2 text-muted-foreground text-xs">
|
||||||
|
{format(data.createdAt, "dd/MM/yyyy HH:mm")}
|
||||||
|
</p>
|
||||||
|
</KanbanCard>
|
||||||
|
)}
|
||||||
|
</KanbanCards>
|
||||||
|
</KanbanBoard>
|
||||||
|
)}
|
||||||
|
</KanbanProvider>
|
||||||
|
<Credenza
|
||||||
|
onOpenChange={(v) => {
|
||||||
|
if (!v) {
|
||||||
|
setEditingId(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
open={!!editingId}
|
||||||
|
>
|
||||||
|
<EditingContent />
|
||||||
|
</Credenza>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const EditingContent = () => {
|
||||||
|
const { getEditingItem } = usePotenziali();
|
||||||
|
const item = getEditingItem();
|
||||||
|
if (!item) return null;
|
||||||
|
return (
|
||||||
|
<CredenzaContent className="max-h-[90vh]">
|
||||||
|
<CredenzaHeader>
|
||||||
|
<CredenzaTitle className="text-center font-semibold text-2xl">
|
||||||
|
Modifica {item.name}
|
||||||
|
</CredenzaTitle>
|
||||||
|
<CredenzaDescription className="sr-only">desc</CredenzaDescription>
|
||||||
|
</CredenzaHeader>
|
||||||
|
<CredenzaBody className="flex max-h-[80vh] flex-col gap-4 overflow-auto pb-5">
|
||||||
|
<p>Descrizione: {item.descrizione}</p>
|
||||||
|
<p>Creato il: {format(item.createdAt, "dd/MM/yyyy HH:mm")}</p>
|
||||||
|
<p>Utente Assegnato: {item.owner ? item.owner.username : "Nessuno"}</p>
|
||||||
|
</CredenzaBody>
|
||||||
|
</CredenzaContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Potenziali;
|
||||||
369
apps/infoalloggi/src/components/ui/kanban.tsx
Normal file
369
apps/infoalloggi/src/components/ui/kanban.tsx
Normal file
|
|
@ -0,0 +1,369 @@
|
||||||
|
"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 { GripHorizontal } from "lucide-react";
|
||||||
|
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;
|
||||||
|
cardOnClick?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const KanbanCard = <T extends KanbanItemProps = KanbanItemProps>({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
cardOnClick,
|
||||||
|
}: KanbanCardProps<T>) => {
|
||||||
|
const {
|
||||||
|
attributes,
|
||||||
|
listeners,
|
||||||
|
setNodeRef,
|
||||||
|
setActivatorNodeRef,
|
||||||
|
transition,
|
||||||
|
transform,
|
||||||
|
isDragging,
|
||||||
|
} = useSortable({
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
const { activeCardId } = useContext(KanbanContext) as KanbanContextProps;
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
transition,
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div style={style} {...attributes} ref={setNodeRef}>
|
||||||
|
<Card
|
||||||
|
className={cn(
|
||||||
|
"relative flex w-full cursor-pointer flex-col gap-0 rounded-md py-0 shadow-sm", // Removed default padding to handle it manually
|
||||||
|
isDragging && "opacity-30",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
onClick={cardOnClick}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
{...listeners}
|
||||||
|
className={cn(
|
||||||
|
"flex h-6 w-full cursor-grab items-center justify-center rounded-t-md bg-muted/50 transition-colors hover:bg-muted",
|
||||||
|
isDragging && "cursor-grabbing",
|
||||||
|
)}
|
||||||
|
ref={setActivatorNodeRef}
|
||||||
|
>
|
||||||
|
<GripHorizontal className="text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-3">
|
||||||
|
{children ?? <p className="m-0 font-medium text-sm">{name}</p>}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{activeCardId === id && (
|
||||||
|
<t.In>
|
||||||
|
<Card
|
||||||
|
className={cn(
|
||||||
|
"relative flex w-full flex-col gap-0 rounded-md py-0 shadow-sm ring-2 ring-primary",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex h-6 w-full cursor-grabbing items-center justify-center rounded-t-md bg-muted/50">
|
||||||
|
<GripHorizontal className="text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<div className="p-3">
|
||||||
|
{children ?? <p className="m-0 font-medium text-sm">{name}</p>}
|
||||||
|
</div>
|
||||||
|
</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-3",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{columns.map((column) => children(column))}
|
||||||
|
</div>
|
||||||
|
{typeof window !== "undefined" &&
|
||||||
|
createPortal(
|
||||||
|
<DragOverlay>
|
||||||
|
<t.Out />
|
||||||
|
</DragOverlay>,
|
||||||
|
document.body,
|
||||||
|
)}
|
||||||
|
</DndContext>
|
||||||
|
</KanbanContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -605,6 +605,7 @@ export const en: LangDict = {
|
||||||
{
|
{
|
||||||
icon: "cog",
|
icon: "cog",
|
||||||
items: [
|
items: [
|
||||||
|
{ href: "/area-riservata/admin/potenziali", title: "Potenziali" },
|
||||||
{ href: "/area-riservata/admin/utenti", title: "Users" },
|
{ href: "/area-riservata/admin/utenti", title: "Users" },
|
||||||
{ href: "/area-riservata/admin/chats", title: "Chat List" },
|
{ href: "/area-riservata/admin/chats", title: "Chat List" },
|
||||||
{ href: "/area-riservata/admin/ordini", title: "Orders" },
|
{ href: "/area-riservata/admin/ordini", title: "Orders" },
|
||||||
|
|
|
||||||
|
|
@ -610,6 +610,7 @@ export const it: LangDict = {
|
||||||
{
|
{
|
||||||
icon: "cog",
|
icon: "cog",
|
||||||
items: [
|
items: [
|
||||||
|
{ href: "/area-riservata/admin/potenziali", title: "Potenziali" },
|
||||||
{ href: "/area-riservata/admin/utenti", title: "Utenti" },
|
{ href: "/area-riservata/admin/utenti", title: "Utenti" },
|
||||||
{ href: "/area-riservata/admin/chats", title: "Lista Chat" },
|
{ href: "/area-riservata/admin/chats", title: "Lista Chat" },
|
||||||
{ href: "/area-riservata/admin/ordini", title: "Ordini" },
|
{ href: "/area-riservata/admin/ordini", title: "Ordini" },
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
import { AreaRiservataLayout } from "~/components/Layout";
|
||||||
|
import { LoadingPage } from "~/components/loading";
|
||||||
|
import type { NextPageWithLayout } from "~/pages/_app";
|
||||||
|
|
||||||
|
const Potenziali = dynamic(
|
||||||
|
() => import("~/components/area-riservata/potenziali"),
|
||||||
|
{
|
||||||
|
loading: () => <LoadingPage />,
|
||||||
|
ssr: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const AdminPotenziali: NextPageWithLayout = () => {
|
||||||
|
return (
|
||||||
|
<div className="mx-1 flex-1 overflow-auto">
|
||||||
|
<div className="mx-auto pt-4">
|
||||||
|
<div className="mx-3 items-start justify-between md:flex">
|
||||||
|
<div className="flex max-w-lg items-center gap-3">
|
||||||
|
<h3 className="font-bold text-accent-foreground text-xl sm:text-2xl">
|
||||||
|
Potenziali
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-4">
|
||||||
|
<Potenziali />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default AdminPotenziali;
|
||||||
|
|
||||||
|
// export const getServerSideProps = (async (context) => {
|
||||||
|
// const access_token = context.req.cookies.access_token;
|
||||||
|
|
||||||
|
// const helpers = await TrpcAuthedFetchingIstance({ access_token });
|
||||||
|
// if (helpers) {
|
||||||
|
// await helpers.trpc.etichette.getAllEtichette.prefetch();
|
||||||
|
// return {
|
||||||
|
// props: {
|
||||||
|
// trpcState: helpers.trpc.dehydrate(),
|
||||||
|
// },
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
// return {
|
||||||
|
// props: {},
|
||||||
|
// };
|
||||||
|
// }) satisfies GetServerSideProps;
|
||||||
|
|
||||||
|
AdminPotenziali.getLayout = function getLayout(page) {
|
||||||
|
return <AreaRiservataLayout>{page}</AreaRiservataLayout>;
|
||||||
|
};
|
||||||
66
apps/infoalloggi/src/providers/PotenzialiProvider.tsx
Normal file
66
apps/infoalloggi/src/providers/PotenzialiProvider.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { createContext, useContext, useState } from "react";
|
||||||
|
import type { UsersId } from "~/schemas/public/Users";
|
||||||
|
|
||||||
|
export type ElementoPotenziali = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
column: string;
|
||||||
|
descrizione: string;
|
||||||
|
createdAt: Date;
|
||||||
|
owner: { userId: UsersId; username: string } | null;
|
||||||
|
};
|
||||||
|
type PotenzialiContextType = {
|
||||||
|
data: ElementoPotenziali[];
|
||||||
|
editingId: string | null;
|
||||||
|
updateData: (data: ElementoPotenziali[]) => void;
|
||||||
|
setEditingId: (id: string | null) => void;
|
||||||
|
getEditingItem: () => ElementoPotenziali | null;
|
||||||
|
addItem: (item: ElementoPotenziali) => void;
|
||||||
|
removeItem: (id: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PotenzialiContext = createContext<PotenzialiContextType | null>(null);
|
||||||
|
|
||||||
|
type PotenzialiProviderProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
data: ElementoPotenziali[];
|
||||||
|
};
|
||||||
|
export const PotenzialiProvider = ({
|
||||||
|
children,
|
||||||
|
data: initial,
|
||||||
|
}: PotenzialiProviderProps) => {
|
||||||
|
const [data, setData] = useState<ElementoPotenziali[]>(initial);
|
||||||
|
const [editingId, setEditingId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const getEditingItem = () => {
|
||||||
|
return data.find((item) => item.id === editingId) || null;
|
||||||
|
};
|
||||||
|
const addItem = (item: ElementoPotenziali) => {
|
||||||
|
setData((prev) => [...prev, item]);
|
||||||
|
};
|
||||||
|
const removeItem = (id: string) => {
|
||||||
|
setData((prev) => prev.filter((item) => item.id !== id));
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<PotenzialiContext.Provider
|
||||||
|
value={{
|
||||||
|
data,
|
||||||
|
editingId,
|
||||||
|
updateData: setData,
|
||||||
|
setEditingId,
|
||||||
|
getEditingItem,
|
||||||
|
addItem,
|
||||||
|
removeItem,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</PotenzialiContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export const usePotenziali = () => {
|
||||||
|
const context = useContext(PotenzialiContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("usePotenziali must be used within a PotenzialiProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
Loading…
Add table
Reference in a new issue