diff --git a/apps/web/app/(app)/smart-categories/board/page.tsx b/apps/web/app/(app)/smart-categories/board/page.tsx deleted file mode 100644 index b02649f43..000000000 --- a/apps/web/app/(app)/smart-categories/board/page.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { capitalCase } from "capital-case"; -import { KanbanBoard } from "@/components/kanban/KanbanBoard"; -import { auth } from "@/app/api/auth/[...nextauth]/auth"; -import prisma from "@/utils/prisma"; -import { ClientOnly } from "@/components/ClientOnly"; -import { isDefined } from "@/utils/types"; -import { getUserCategories } from "@/utils/category.server"; - -export const dynamic = "force-dynamic"; - -const CATEGORY_ORDER = [ - "Unknown", - "RequestMoreInformation", - "Newsletter", - "Marketing", - "Receipts", - "Support", -]; - -export default async function CategoriesPage() { - const session = await auth(); - const email = session?.user.email; - if (!email) throw new Error("Not authenticated"); - - const [categories, senders] = await Promise.all([ - getUserCategories(session.user.id), - prisma.newsletter.findMany({ - where: { userId: session.user.id, categoryId: { not: null } }, - select: { id: true, email: true, categoryId: true }, - }), - ]); - - if (!categories.length) return
No categories found
; - - // Order categories - const orderedCategories = [ - ...CATEGORY_ORDER.map((name) => - categories.find((c) => c.name === name), - ).filter(isDefined), - ...categories.filter((c) => !CATEGORY_ORDER.includes(c.name)), - ]; - - return ( -
- - ({ - id: c.id, - title: capitalCase(c.name), - }))} - items={senders.map((s) => ({ - id: s.id, - columnId: s.categoryId || "Uncategorized", - content: s.email, - }))} - /> - -
- ); -} diff --git a/apps/web/components/kanban/BoardColumn.tsx b/apps/web/components/kanban/BoardColumn.tsx deleted file mode 100644 index f2b02abfd..000000000 --- a/apps/web/components/kanban/BoardColumn.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { useMemo } from "react"; -import { SortableContext, useSortable } from "@dnd-kit/sortable"; -import { useDndContext, type UniqueIdentifier } from "@dnd-kit/core"; -import { CSS } from "@dnd-kit/utilities"; -import { cva } from "class-variance-authority"; -import { GripVertical } from "lucide-react"; -import { type Task, TaskCard } from "./TaskCard"; -import { Card, CardHeader, CardContent } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { ScrollBar, ScrollArea } from "@/components/ui/scroll-area"; - -export interface Column { - id: UniqueIdentifier; - title: string; -} - -export type ColumnType = "Column"; - -export interface ColumnDragData { - type: ColumnType; - column: Column; -} - -interface BoardColumnProps { - column: Column; - tasks: Task[]; - isOverlay?: boolean; -} - -export function BoardColumn({ column, tasks, isOverlay }: BoardColumnProps) { - const tasksIds = useMemo(() => { - return tasks.map((task) => task.id); - }, [tasks]); - - const { - setNodeRef, - attributes, - listeners, - transform, - transition, - isDragging, - } = useSortable({ - id: column.id, - data: { - type: "Column", - column, - } satisfies ColumnDragData, - attributes: { - roleDescription: `Column: ${column.title}`, - }, - }); - - const style = { - transition, - transform: CSS.Translate.toString(transform), - }; - - const variants = cva( - "h-[800px] max-h-[800px] w-[350px] max-w-full bg-primary-foreground flex flex-col flex-shrink-0 snap-center", - { - variants: { - dragging: { - default: "border-2 border-transparent", - over: "ring-2 opacity-30", - overlay: "ring-2 ring-primary", - }, - }, - }, - ); - - return ( - - - - {column.title} - - - - - {tasks.map((task) => ( - - ))} - - - - - ); -} - -export function BoardContainer({ children }: { children: React.ReactNode }) { - const dndContext = useDndContext(); - - const variations = cva("px-2 md:px-0 flex lg:justify-center pb-4", { - variants: { - dragging: { - default: "snap-x snap-mandatory", - active: "snap-none", - }, - }, - }); - - return ( - -
- {children} -
- -
- ); -} diff --git a/apps/web/components/kanban/KanbanBoard.tsx b/apps/web/components/kanban/KanbanBoard.tsx deleted file mode 100644 index 29d67e839..000000000 --- a/apps/web/components/kanban/KanbanBoard.tsx +++ /dev/null @@ -1,368 +0,0 @@ -"use client"; - -// based off of https://github.com/Georgegriff/react-dnd-kit-tailwind-shadcn-ui - -import { useMemo, useRef, useState } from "react"; -import { createPortal } from "react-dom"; - -import { BoardColumn, BoardContainer } from "./BoardColumn"; -import { - DndContext, - type DragEndEvent, - type DragOverEvent, - DragOverlay, - type DragStartEvent, - useSensor, - useSensors, - KeyboardSensor, - type Announcements, - type UniqueIdentifier, - TouchSensor, - MouseSensor, -} from "@dnd-kit/core"; -import { SortableContext, arrayMove } from "@dnd-kit/sortable"; -import { type Task, TaskCard } from "./TaskCard"; -import type { Column } from "./BoardColumn"; -import { hasDraggableData } from "./kanban-utils"; -import { coordinateGetter } from "./multipleContainersKeyboardPreset"; - -// const initialTasks: Task[] = [ -// { -// id: "task1", -// columnId: "done", -// content: "Project initiation and planning", -// }, -// { -// id: "task2", -// columnId: "done", -// content: "Gather requirements from stakeholders", -// }, -// { -// id: "task3", -// columnId: "done", -// content: "Create wireframes and mockups", -// }, -// { -// id: "task4", -// columnId: "in-progress", -// content: "Develop homepage layout", -// }, -// { -// id: "task5", -// columnId: "in-progress", -// content: "Design color scheme and typography", -// }, -// { -// id: "task6", -// columnId: "todo", -// content: "Implement user authentication", -// }, -// { -// id: "task7", -// columnId: "todo", -// content: "Build contact us page", -// }, -// { -// id: "task8", -// columnId: "todo", -// content: "Create product catalog", -// }, -// { -// id: "task9", -// columnId: "todo", -// content: "Develop about us page", -// }, -// { -// id: "task10", -// columnId: "todo", -// content: "Optimize website for mobile devices", -// }, -// { -// id: "task11", -// columnId: "todo", -// content: "Integrate payment gateway", -// }, -// { -// id: "task12", -// columnId: "todo", -// content: "Perform testing and bug fixing", -// }, -// { -// id: "task13", -// columnId: "todo", -// content: "Launch website and deploy to server", -// }, -// ]; - -export function KanbanBoard({ - categories, - items, -}: { - categories: Column[]; - items: Task[]; -}) { - const [columns, setColumns] = useState(categories); - const pickedUpTaskColumn = useRef(null); - const columnsId = useMemo(() => columns.map((col) => col.id), [columns]); - - const [tasks, setTasks] = useState(items); - - const [activeColumn, setActiveColumn] = useState(null); - - const [activeTask, setActiveTask] = useState(null); - - const sensors = useSensors( - useSensor(MouseSensor), - useSensor(TouchSensor), - useSensor(KeyboardSensor, { - coordinateGetter: coordinateGetter, - }), - ); - - function getDraggingTaskData(taskId: UniqueIdentifier, columnId: string) { - const tasksInColumn = tasks.filter((task) => task.columnId === columnId); - const taskPosition = tasksInColumn.findIndex((task) => task.id === taskId); - const column = columns.find((col) => col.id === columnId); - return { - tasksInColumn, - taskPosition, - column, - }; - } - - const announcements: Announcements = { - onDragStart({ active }) { - if (!hasDraggableData(active)) return; - if (active.data.current?.type === "Column") { - const startColumnIdx = columnsId.findIndex((id) => id === active.id); - const startColumn = columns[startColumnIdx]; - return `Picked up Column ${startColumn?.title} at position: ${ - startColumnIdx + 1 - } of ${columnsId.length}`; - } - if (active.data.current?.type === "Task") { - pickedUpTaskColumn.current = active.data.current.task.columnId; - const { tasksInColumn, taskPosition, column } = getDraggingTaskData( - active.id, - pickedUpTaskColumn.current!, // TODO: ! - ); - return `Picked up Task ${ - active.data.current.task.content - } at position: ${taskPosition + 1} of ${ - tasksInColumn.length - } in column ${column?.title}`; - } - }, - onDragOver({ active, over }) { - if (!hasDraggableData(active) || !hasDraggableData(over)) return; - - if ( - active.data.current?.type === "Column" && - over.data.current?.type === "Column" - ) { - const overColumnIdx = columnsId.findIndex((id) => id === over.id); - return `Column ${active.data.current.column.title} was moved over ${ - over.data.current.column.title - } at position ${overColumnIdx + 1} of ${columnsId.length}`; - } - if ( - active.data.current?.type === "Task" && - over.data.current?.type === "Task" - ) { - const { tasksInColumn, taskPosition, column } = getDraggingTaskData( - over.id, - over.data.current.task.columnId, - ); - if (over.data.current.task.columnId !== pickedUpTaskColumn.current) { - return `Task ${ - active.data.current.task.content - } was moved over column ${column?.title} in position ${ - taskPosition + 1 - } of ${tasksInColumn.length}`; - } - return `Task was moved over position ${taskPosition + 1} of ${ - tasksInColumn.length - } in column ${column?.title}`; - } - }, - onDragEnd({ active, over }) { - if (!hasDraggableData(active) || !hasDraggableData(over)) { - pickedUpTaskColumn.current = null; - return; - } - if ( - active.data.current?.type === "Column" && - over.data.current?.type === "Column" - ) { - const overColumnPosition = columnsId.findIndex((id) => id === over.id); - - return `Column ${ - active.data.current.column.title - } was dropped into position ${overColumnPosition + 1} of ${ - columnsId.length - }`; - } - if ( - active.data.current?.type === "Task" && - over.data.current?.type === "Task" - ) { - const { tasksInColumn, taskPosition, column } = getDraggingTaskData( - over.id, - over.data.current.task.columnId, - ); - if (over.data.current.task.columnId !== pickedUpTaskColumn.current) { - return `Task was dropped into column ${column?.title} in position ${ - taskPosition + 1 - } of ${tasksInColumn.length}`; - } - return `Task was dropped into position ${taskPosition + 1} of ${ - tasksInColumn.length - } in column ${column?.title}`; - } - pickedUpTaskColumn.current = null; - }, - onDragCancel({ active }) { - pickedUpTaskColumn.current = null; - if (!hasDraggableData(active)) return; - return `Dragging ${active.data.current?.type} cancelled.`; - }, - }; - - return ( - - - - {columns.map((col) => ( - task.columnId === col.id)} - /> - ))} - - - - {"document" in window && - createPortal( - - {activeColumn && ( - task.columnId === activeColumn.id, - )} - /> - )} - {activeTask && } - , - document.body, - )} - - ); - - function onDragStart(event: DragStartEvent) { - if (!hasDraggableData(event.active)) return; - const data = event.active.data.current; - if (data?.type === "Column") { - setActiveColumn(data.column); - return; - } - - if (data?.type === "Task") { - setActiveTask(data.task); - return; - } - } - - function onDragEnd(event: DragEndEvent) { - setActiveColumn(null); - setActiveTask(null); - - const { active, over } = event; - if (!over) return; - - const activeId = active.id; - const overId = over.id; - - if (!hasDraggableData(active)) return; - - const activeData = active.data.current; - - if (activeId === overId) return; - - const isActiveAColumn = activeData?.type === "Column"; - if (!isActiveAColumn) return; - - setColumns((columns) => { - const activeColumnIndex = columns.findIndex((col) => col.id === activeId); - - const overColumnIndex = columns.findIndex((col) => col.id === overId); - - return arrayMove(columns, activeColumnIndex, overColumnIndex); - }); - } - - function onDragOver(event: DragOverEvent) { - const { active, over } = event; - if (!over) return; - - const activeId = active.id; - const overId = over.id; - - if (activeId === overId) return; - - if (!hasDraggableData(active) || !hasDraggableData(over)) return; - - const activeData = active.data.current; - const overData = over.data.current; - - const isActiveATask = activeData?.type === "Task"; - const isOverATask = overData?.type === "Task"; - - if (!isActiveATask) return; - - // Im dropping a Task over another Task - if (isActiveATask && isOverATask) { - setTasks((tasks) => { - const activeIndex = tasks.findIndex((t) => t.id === activeId); - const overIndex = tasks.findIndex((t) => t.id === overId); - const activeTask = tasks[activeIndex]; - const overTask = tasks[overIndex]; - if ( - activeTask && - overTask && - activeTask.columnId !== overTask.columnId - ) { - activeTask.columnId = overTask.columnId; - return arrayMove(tasks, activeIndex, overIndex - 1); - } - - return arrayMove(tasks, activeIndex, overIndex); - }); - } - - const isOverAColumn = overData?.type === "Column"; - - // Im dropping a Task over a column - if (isActiveATask && isOverAColumn) { - setTasks((tasks) => { - const activeIndex = tasks.findIndex((t) => t.id === activeId); - const activeTask = tasks[activeIndex]; - if (activeTask) { - activeTask.columnId = overId as string; - return arrayMove(tasks, activeIndex, activeIndex); - } - return tasks; - }); - } - } -} diff --git a/apps/web/components/kanban/TaskCard.tsx b/apps/web/components/kanban/TaskCard.tsx deleted file mode 100644 index 448d22cd5..000000000 --- a/apps/web/components/kanban/TaskCard.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import type { UniqueIdentifier } from "@dnd-kit/core"; -import { useSortable } from "@dnd-kit/sortable"; -import { CSS } from "@dnd-kit/utilities"; -import { cva } from "class-variance-authority"; -import { GripVertical } from "lucide-react"; -import { Card, CardHeader } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { EmailCell } from "@/components/EmailCell"; - -export interface Task { - id: UniqueIdentifier; - columnId: string; - content: string; -} - -interface TaskCardProps { - task: Task; - isOverlay?: boolean; -} - -export type TaskType = "Task"; - -export interface TaskDragData { - type: TaskType; - task: Task; -} - -export function TaskCard({ task, isOverlay }: TaskCardProps) { - const { - setNodeRef, - attributes, - listeners, - transform, - transition, - isDragging, - } = useSortable({ - id: task.id, - data: { - type: "Task", - task, - } satisfies TaskDragData, - attributes: { - roleDescription: "Task", - }, - }); - - const style = { - transition, - transform: CSS.Translate.toString(transform), - }; - - const variants = cva("", { - variants: { - dragging: { - over: "ring-2 opacity-30", - overlay: "ring-2 ring-primary", - }, - }, - }); - - return ( - - - - - - - ); -} diff --git a/apps/web/components/kanban/kanban-utils.ts b/apps/web/components/kanban/kanban-utils.ts deleted file mode 100644 index 6bc0f1c80..000000000 --- a/apps/web/components/kanban/kanban-utils.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { Active, DataRef, Over } from "@dnd-kit/core"; -import type { ColumnDragData } from "./BoardColumn"; -import type { TaskDragData } from "./TaskCard"; - -type DraggableData = ColumnDragData | TaskDragData; - -export function hasDraggableData( - entry: T | null | undefined, -): entry is T & { - data: DataRef; -} { - if (!entry) { - return false; - } - - const data = entry.data.current; - - if (data?.type === "Column" || data?.type === "Task") { - return true; - } - - return false; -} diff --git a/apps/web/components/kanban/multipleContainersKeyboardPreset.ts b/apps/web/components/kanban/multipleContainersKeyboardPreset.ts deleted file mode 100644 index 3ef439a5a..000000000 --- a/apps/web/components/kanban/multipleContainersKeyboardPreset.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { - closestCorners, - getFirstCollision, - KeyboardCode, - type DroppableContainer, - type KeyboardCoordinateGetter, -} from "@dnd-kit/core"; - -const directions: string[] = [ - KeyboardCode.Down, - KeyboardCode.Right, - KeyboardCode.Up, - KeyboardCode.Left, -]; - -export const coordinateGetter: KeyboardCoordinateGetter = ( - event, - { context: { active, droppableRects, droppableContainers, collisionRect } }, -) => { - if (directions.includes(event.code)) { - event.preventDefault(); - - if (!active || !collisionRect) { - return; - } - - const filteredContainers: DroppableContainer[] = []; - - for (const entry of droppableContainers.getEnabled()) { - if (!entry || entry?.disabled) { - return; - } - - const rect = droppableRects.get(entry.id); - - if (!rect) { - return; - } - - const data = entry.data.current; - - if (data) { - const { type, children } = data; - - if (type === "Column" && children?.length > 0) { - if (active.data.current?.type !== "Column") { - return; - } - } - } - - switch (event.code) { - case KeyboardCode.Down: - if (active.data.current?.type === "Column") { - return; - } - if (collisionRect.top < rect.top) { - // find all droppable areas below - filteredContainers.push(entry); - } - break; - case KeyboardCode.Up: - if (active.data.current?.type === "Column") { - return; - } - if (collisionRect.top > rect.top) { - // find all droppable areas above - filteredContainers.push(entry); - } - break; - case KeyboardCode.Left: - if (collisionRect.left >= rect.left + rect.width) { - // find all droppable areas to left - filteredContainers.push(entry); - } - break; - case KeyboardCode.Right: - // find all droppable areas to right - if (collisionRect.left + collisionRect.width <= rect.left) { - filteredContainers.push(entry); - } - break; - } - } - const collisions = closestCorners({ - active, - collisionRect: collisionRect, - droppableRects, - droppableContainers: filteredContainers, - pointerCoordinates: null, - }); - const closestId = getFirstCollision(collisions, "id"); - - if (closestId != null) { - const newDroppable = droppableContainers.get(closestId); - const newNode = newDroppable?.node.current; - const newRect = newDroppable?.rect.current; - - if (newNode && newRect) { - return { - x: newRect.left, - y: newRect.top, - }; - } - } - } - - return undefined; -}; diff --git a/apps/web/package.json b/apps/web/package.json index fe2c791e6..ad1ee676f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -21,9 +21,6 @@ "@asteasolutions/zod-to-openapi": "7.3.0", "@auth/core": "0.38.0", "@auth/prisma-adapter": "2.8.0", - "@dnd-kit/core": "6.3.1", - "@dnd-kit/sortable": "10.0.0", - "@dnd-kit/utilities": "3.2.2", "@formkit/auto-animate": "0.8.2", "@googleapis/gmail": "12.0.0", "@googleapis/people": "3.0.9", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a6e4f78d6..3c88a3a78 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -128,15 +128,6 @@ importers: '@auth/prisma-adapter': specifier: 2.8.0 version: 2.8.0(@prisma/client@6.5.0(prisma@6.5.0(typescript@5.8.2))(typescript@5.8.2))(nodemailer@6.10.0) - '@dnd-kit/core': - specifier: 6.3.1 - version: 6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@dnd-kit/sortable': - specifier: 10.0.0 - version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0) - '@dnd-kit/utilities': - specifier: 3.2.2 - version: 3.2.2(react@19.1.0) '@formkit/auto-animate': specifier: 0.8.2 version: 0.8.2 @@ -1795,12 +1786,6 @@ packages: '@dnd-kit/core': ^6.0.6 react: '>=16.8.0' - '@dnd-kit/sortable@10.0.0': - resolution: {integrity: sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==} - peerDependencies: - '@dnd-kit/core': ^6.3.0 - react: '>=16.8.0' - '@dnd-kit/sortable@7.0.2': resolution: {integrity: sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==} peerDependencies: @@ -13642,13 +13627,6 @@ snapshots: react: 19.1.0 tslib: 2.8.1 - '@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)': - dependencies: - '@dnd-kit/core': 6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@dnd-kit/utilities': 3.2.2(react@19.1.0) - react: 19.1.0 - tslib: 2.8.1 - '@dnd-kit/sortable@7.0.2(@dnd-kit/core@6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)': dependencies: '@dnd-kit/core': 6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)