From 41fb21c85398ae54eb34e1bcad4a1cbcef49d431 Mon Sep 17 00:00:00 2001 From: Alexandre Pereira Date: Sun, 17 Mar 2024 21:25:46 +0000 Subject: [PATCH] feat: poc drag n drop --- app/components/Card/Card.tsx | 43 +++++++++++++++++++-- app/components/Column/Column.tsx | 62 +++++++++++++++++++++++++++--- app/routes/board/queries.server.ts | 41 ++++++++++++++++++++ app/routes/board/route.tsx | 52 +++++++++++++++++++++++-- 4 files changed, 187 insertions(+), 11 deletions(-) create mode 100644 app/routes/board/queries.server.ts diff --git a/app/components/Card/Card.tsx b/app/components/Card/Card.tsx index 68266a1..f5d03a3 100644 --- a/app/components/Card/Card.tsx +++ b/app/components/Card/Card.tsx @@ -1,11 +1,48 @@ -type CardPropsTypes = { +import clsx from "clsx"; +import { useState } from "react"; + +export type CardPropsTypes = { title: string; content: string; + id: number; + columnId: number; }; -export default function Card({ title, content }: CardPropsTypes) { +export default function Card({ title, content, id, columnId }: CardPropsTypes) { + const [isDragging, setIsDragging] = useState(false); + + const handleOnDragStart = (e: React.DragEvent) => { + e.dataTransfer.effectAllowed = "move"; + e.dataTransfer.setData("application/hunters-card", id.toString()); + e.dataTransfer.setData("application/hunters-column", columnId.toString()); + setTimeout(() => { + setIsDragging(true); // This is a hack to make the "placeholder" card a different color + }, 0); + console.log("drag start for id: ", { id, columnId }); + }; + + const handleOnDragEnd = () => { + setIsDragging(false); + }; + + const handleOnDragOver = (e: React.DragEvent) => { + // To use later for ordering within the column + e.preventDefault(); + }; + return ( -
+

{title}

{content}

diff --git a/app/components/Column/Column.tsx b/app/components/Column/Column.tsx index 331e1b8..7f62ba8 100644 --- a/app/components/Column/Column.tsx +++ b/app/components/Column/Column.tsx @@ -1,13 +1,65 @@ -import Card from "~/components/Card/Card"; +import clsx from "clsx"; +import { useState } from "react"; +import Card, { CardPropsTypes } from "~/components/Card/Card"; +import { useSubmit } from "@remix-run/react"; + +type ColumnPropsTypes = { + cards: CardPropsTypes[]; + columnId: number; +}; + +export default function Column({ cards, columnId }: ColumnPropsTypes) { + const [isDraggedOver, setIsDraggedOver] = useState(false); + const [hasDrop, setHasDrop] = useState(false); + const submit = useSubmit(); + + const handleOnDrop = (e: React.DragEvent) => { + // Needed to be a droppable zone + e.preventDefault(); // Should add filtering to check if valid drop + const cardId = e.dataTransfer.getData("application/hunters-card"); + console.log("drop", { cardId, columnId }); + submit({ cardId, columnId }, { method: "POST", navigate: false }); + setIsDraggedOver(false); + setHasDrop(true); + setTimeout(() => { + setHasDrop(false); + }, 1000); + }; + + const handleOnDragOver = (e: React.DragEvent) => { + e.dataTransfer.setData("application/hunters-column", columnId.toString()); + + // Needed to be a droppable zone + e.preventDefault(); // Should add filtering to check if valid drop + setIsDraggedOver(true); + }; + + const handleOnDragLeave = (e: React.DragEvent) => { + e.dataTransfer.clearData("application/hunters-column"); + setIsDraggedOver(false); + }; + + const handleOnDragEnter = (e: React.DragEvent) => { + e.dataTransfer.setData("application/hunters-column", columnId.toString()); + console.log("drag enter"); + }; -export default function Column() { return (
console.log("dragging over")} + className={clsx( + "border-1 min-h-48 rounded-md border border-solid border-gray-700 px-2 py-5", + isDraggedOver ? "bg-sky-50" : "bg-slate-500", + [hasDrop && "bg-red-400"], + )} + onDrop={handleOnDrop} + onDragOver={handleOnDragOver} + onDragLeave={handleOnDragLeave} + onDragEnter={handleOnDragEnter} >

This is a column

- + {cards.map((card) => ( + + ))}
); } diff --git a/app/routes/board/queries.server.ts b/app/routes/board/queries.server.ts new file mode 100644 index 0000000..620df01 --- /dev/null +++ b/app/routes/board/queries.server.ts @@ -0,0 +1,41 @@ +import db from "~/db/db.server"; + +export async function getBoards() { + const { rows } = await db.execute("SELECT * FROM queries"); + return rows; +} + +export type ColumnType = { id: number; name: string }; +export type CardType = { id: number; name: string; columnId: number }; +export async function getCardsAndColumns() { + const { rows: column } = await db.execute({ + sql: ` + SELECT column.id, column.name FROM column + LEFT JOIN board ON column.boardId = board.id + WHERE board.id = ?; + `, + args: [1], + }); + + const { rows: card } = await db.execute({ + sql: ` + SELECT card.id, card.name, card.columnId FROM card + FULL JOIN column ON card.columnId = column.id + FULL JOIN board ON column.boardId = board.id + WHERE board.id = ?; + `, + args: [1], + }); + + return { column, card } as unknown as { + column: ColumnType[]; + card: CardType[]; + }; +} + +export async function mutateCardColumn(cardId: number, columnId: number) { + await db.execute({ + sql: "UPDATE card SET columnId = ? WHERE id = ?;", + args: [columnId, cardId], + }); +} diff --git a/app/routes/board/route.tsx b/app/routes/board/route.tsx index f13ee60..1c89346 100644 --- a/app/routes/board/route.tsx +++ b/app/routes/board/route.tsx @@ -1,13 +1,59 @@ import Column from "~/components/Column/Column"; +import { + type CardType, + type ColumnType, + getCardsAndColumns, + mutateCardColumn, +} from "./queries.server"; +import { useLoaderData } from "@remix-run/react"; +import { ActionFunctionArgs } from "@remix-run/node"; +import invariant from "invariant"; + +export async function loader() { + const data = await getCardsAndColumns(); + return data; +} + +export async function action({ request }: ActionFunctionArgs) { + const data = await request.formData(); + const cardId = data.get("cardId"); + invariant(cardId, "cardId is required"); + const columnId = data.get("columnId"); + invariant(columnId, "columnId is required"); + await mutateCardColumn( + parseInt(cardId.toString()), + parseInt(columnId.toString()), + ); + + return { ok: true }; +} + +function mapCardsToColumns(columns: ColumnType[], cards: CardType[]) { + return columns.map((column) => { + return { + ...column, + cards: cards.filter((card) => card.columnId === column.id), + }; + }); +} export default function Board() { + const { column, card } = useLoaderData(); + const columns = mapCardsToColumns(column, card); return (

Board

+

{JSON.stringify(columns, null, 4)}

- - - + {columns.map((column) => { + const cards = column.cards.map((card) => ({ + ...card, + title: card.name, + content: card.name, + id: card.id, + })); + return ; + })}
);