Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

poc: drag n drop feature and database seeding #42

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# Contact me on discord for the credentials
DATABASE_URL=fakevalue
# See docs for more information in `docs/database.md`
DB_TOKEN=fakevalue
DB_URL=fakevalue
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@ node_modules
/build
.env

**/.DS_Store
**/.DS_Store

# Local database
db/local.db
10 changes: 10 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

Fork and open a PR

## Get started

### Local development

```sh
npm run db:seed
npm install
npm run dev
```

## Commit message format

We have very precise rules over how our Git commit messages must be formatted. This format leads to easier to read commit history.
Expand Down
44 changes: 41 additions & 3 deletions app/components/Card/Card.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,49 @@
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<HTMLDivElement>) => {
e.dataTransfer.clearData();
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<HTMLDivElement>) => {
// To use later for ordering within the column
e.preventDefault();
};

return (
<div draggable className="border-2 bg-sky-50 text-xl">
<div
className={clsx(
"border-2",
"bg-slate-500",
isDragging && "opacity-50", // Set styles for the place holder here
"text-xl",
)}
draggable
onDragStart={handleOnDragStart}
onDragEnd={handleOnDragEnd}
onDragOver={handleOnDragOver}
>
<h1>{title}</h1>
<p>{content}</p>
</div>
Expand Down
62 changes: 57 additions & 5 deletions app/components/Column/Column.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>) => {
// 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<HTMLDivElement>) => {
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<HTMLDivElement>) => {
e.dataTransfer.setData("application/hunters-column", "");
setIsDraggedOver(false);
};

const handleOnDragEnter = (e: React.DragEvent<HTMLDivElement>) => {
e.dataTransfer.setData("application/hunters-column", columnId.toString());
console.log("drag enter");
};

export default function Column() {
return (
<div
className="border border-solid border-1 border-gray-700 rounded-md py-5 px-2"
onDragOver={() => 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}
>
<h1>This is a column</h1>
<Card title="hello" content="world" />
{cards.map((card) => (
<Card key={card.id} {...card} />
))}
</div>
);
}
29 changes: 29 additions & 0 deletions app/db/db.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { createClient, type Client } from "@libsql/client";
import invariant from "invariant";

let client: Client;

if (process.env.NODE_ENV === "production") {
console.log("🚀 Using production database");
invariant(
process.env.DB_URL,
"DB_URL is not defined in the environment variables",
);

invariant(
process.env.DB_TOKEN,
"DB_TOKEN is not defined in the environment variables",
);

client = createClient({
url: process.env.DB_URL,
authToken: process.env.DB_TOKEN,
});
} else {
console.log("🚀 Using local database");
client = createClient({
url: "file:./db/local.db",
});
}

export default client;
9 changes: 0 additions & 9 deletions app/db/pool.server.ts

This file was deleted.

6 changes: 3 additions & 3 deletions app/db/test.server.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import pool from "./pool.server";
import pool from "./db.server";

export async function getData() {
const [result] = await pool.execute("SELECT * FROM test");
return result;
const { rows } = await pool.execute("SELECT * FROM test");
return rows;
}
41 changes: 41 additions & 0 deletions app/routes/board/queries.server.ts
Original file line number Diff line number Diff line change
@@ -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],
});
}
52 changes: 49 additions & 3 deletions app/routes/board/route.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof loader>();
const columns = mapCardsToColumns(column, card);
return (
<div>
<h1>Board</h1>
<p>{JSON.stringify(columns, null, 4)}</p>
<div className="flex flex-row gap-2">
<Column />
<Column />
<Column />
{columns.map((column) => {
const cards = column.cards.map((card) => ({
...card,
title: card.name,
content: card.name,
id: card.id,
}));
return <Column cards={cards} key={column.id} columnId={column.id} />;
})}
</div>
</div>
);
Expand Down
Loading