Skip to content

Commit da5bbba

Browse files
committed
add delete buttons
1 parent f6e309c commit da5bbba

File tree

11 files changed

+160
-28
lines changed

11 files changed

+160
-28
lines changed

app/icons/icons.svg

+13
Loading

app/routes/board.$id/card.tsx

+19-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { flushSync } from "react-dom";
21
import invariant from "tiny-invariant";
3-
import { useSubmit } from "@remix-run/react";
2+
import { useFetcher, useSubmit } from "@remix-run/react";
43
import { useState } from "react";
54

65
import { ItemMutation, INTENTS, CONTENT_TYPES } from "./types";
6+
import { Icon } from "~/icons/icons";
77

88
interface CardProps {
99
title: string;
@@ -25,10 +25,11 @@ export function Card({
2525
previousOrder,
2626
}: CardProps) {
2727
let submit = useSubmit();
28+
let deleteFetcher = useFetcher();
2829

2930
let [acceptDrop, setAcceptDrop] = useState<"none" | "top" | "bottom">("none");
3031

31-
return (
32+
return deleteFetcher.state !== "idle" ? null : (
3233
<li
3334
onDragOver={(event) => {
3435
if (event.dataTransfer.types.includes(CONTENT_TYPES.card)) {
@@ -83,7 +84,7 @@ export function Card({
8384
>
8485
<div
8586
draggable
86-
className="bg-white shadow shadow-slate-300 border-slate-300 text-sm rounded-lg w-full py-1 px-2"
87+
className="bg-white shadow shadow-slate-300 border-slate-300 text-sm rounded-lg w-full py-1 px-2 relative"
8788
onDragStart={(event) => {
8889
event.dataTransfer.effectAllowed = "move";
8990
event.dataTransfer.setData(
@@ -94,6 +95,20 @@ export function Card({
9495
>
9596
<h3>{title}</h3>
9697
<div className="mt-2">{content || <>&nbsp;</>}</div>
98+
<deleteFetcher.Form method="post">
99+
<input type="hidden" name="intent" value={INTENTS.deleteCard} />
100+
<input type="hidden" name="itemId" value={id} />
101+
<button
102+
aria-label="Delete card"
103+
className="absolute top-4 right-4 hover:text-brand-red"
104+
type="submit"
105+
onClick={(event) => {
106+
event.stopPropagation();
107+
}}
108+
>
109+
<Icon name="trash" />
110+
</button>
111+
</deleteFetcher.Form>
97112
</div>
98113
</li>
99114
);

app/routes/board.$id/column.tsx

+7-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { useState, useEffect, useRef } from "react";
2-
import { useFetcher, useSubmit } from "@remix-run/react";
1+
import { useState, useRef } from "react";
2+
import { useSubmit } from "@remix-run/react";
33

44
import { Icon } from "~/icons/icons";
55

@@ -33,16 +33,17 @@ export function Column({ name, columnId, items }: ColumnProps) {
3333
listRef.current.scrollTop = listRef.current.scrollHeight;
3434
}
3535

36-
let isEmpty = items.length === 0;
37-
3836
return (
3937
<div
4038
className={
4139
"flex-shrink-0 flex flex-col overflow-hidden max-h-full w-80 border-slate-400 rounded-xl shadow-sm shadow-slate-400 bg-slate-100 " +
4240
(acceptDrop ? `outline outline-2 outline-brand-red` : ``)
4341
}
4442
onDragOver={(event) => {
45-
if (isEmpty && event.dataTransfer.types.includes(CONTENT_TYPES.card)) {
43+
if (
44+
items.length === 0 &&
45+
event.dataTransfer.types.includes(CONTENT_TYPES.card)
46+
) {
4647
event.preventDefault();
4748
setAcceptDrop(true);
4849
}
@@ -111,7 +112,7 @@ export function Column({ name, columnId, items }: ColumnProps) {
111112
{edit ? (
112113
<NewCard
113114
columnId={columnId}
114-
nextOrder={items.length}
115+
nextOrder={items.length === 0 ? 1 : items[items.length - 1].order + 1}
115116
onAddCard={() => scrollList()}
116117
onComplete={() => setEdit(false)}
117118
/>

app/routes/board.$id/queries.ts

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import { prisma } from "~/db/prisma";
22

33
import { ItemMutation } from "./types";
44

5+
export function deleteCard(id: string) {
6+
return prisma.item.delete({ where: { id } });
7+
}
8+
59
export async function getBoardData(boardId: number) {
610
return prisma.board.findUnique({
711
where: {

app/routes/board.$id/route.tsx

+6
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
getBoardData,
1414
upsertItem,
1515
updateBoardName,
16+
deleteCard,
1617
} from "./queries";
1718
import { Board } from "./board";
1819

@@ -44,6 +45,11 @@ export async function action({ request, params }: ActionFunctionArgs) {
4445
if (!intent) throw badRequest("Missing intent");
4546

4647
switch (intent) {
48+
case INTENTS.deleteCard: {
49+
let id = String(formData.get("itemId"));
50+
await deleteCard(id);
51+
break;
52+
}
4753
case INTENTS.updateBoardName: {
4854
let name = String(formData.get("name"));
4955
invariant(name, "Missing name");

app/routes/board.$id/types.ts

+3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ export const INTENTS = {
1818
moveItem: "moveItem" as const,
1919
moveColumn: "moveColumn" as const,
2020
updateBoardName: "updateBoardName" as const,
21+
deleteBoard: "deleteBoard" as const,
22+
createBoard: "createBoard" as const,
23+
deleteCard: "deleteCard" as const,
2124
};
2225

2326
export const ItemMutationFields = {

app/routes/home/queries.ts

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { prisma } from "~/db/prisma";
22

3+
export async function deleteBoard(boardId: number) {
4+
return prisma.board.delete({
5+
where: { id: boardId },
6+
});
7+
}
8+
39
export async function createBoard(userId: string, name: string, color: string) {
410
return prisma.board.create({
511
data: {

app/routes/home/route.tsx

+66-14
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,22 @@ import {
33
type LoaderFunctionArgs,
44
redirect,
55
} from "@remix-run/node";
6-
import { Form, Link, useLoaderData, useNavigation } from "@remix-run/react";
6+
import {
7+
Form,
8+
Link,
9+
useFetcher,
10+
useLoaderData,
11+
useNavigation,
12+
} from "@remix-run/react";
713

814
import { requireAuthCookie } from "~/auth/auth";
915
import { Button } from "~/components/button";
1016
import { Label, LabeledInput } from "~/components/input";
1117
import { badRequest } from "~/http/bad-response";
1218

13-
import { getHomeData, createBoard } from "./queries";
19+
import { getHomeData, createBoard, deleteBoard } from "./queries";
20+
import { INTENTS } from "../board.$id/types";
21+
import { Icon } from "~/icons/icons";
1422

1523
export const meta = () => {
1624
return [{ title: "Boards" }];
@@ -25,11 +33,21 @@ export async function loader({ request }: LoaderFunctionArgs) {
2533
export async function action({ request }: ActionFunctionArgs) {
2634
let userId = await requireAuthCookie(request);
2735
let formData = await request.formData();
28-
let name = String(formData.get("name"));
29-
let color = String(formData.get("color"));
30-
if (!name) throw badRequest("Bad request");
31-
let board = await createBoard(userId, name, color);
32-
throw redirect(`/board/${board.id}`);
36+
let intent = String(formData.get("intent"));
37+
switch (intent) {
38+
case INTENTS.createBoard: {
39+
let name = String(formData.get("name"));
40+
let color = String(formData.get("color"));
41+
if (!name) throw badRequest("Bad request");
42+
let board = await createBoard(userId, name, color);
43+
return redirect(`/board/${board.id}`);
44+
}
45+
case INTENTS.deleteBoard: {
46+
let boardId = Number(formData.get("boardId"));
47+
await deleteBoard(boardId);
48+
return { ok: true };
49+
}
50+
}
3351
}
3452

3553
export default function Projects() {
@@ -48,20 +66,54 @@ function Boards() {
4866
<h2 className="font-bold mb-2 text-xl">Boards</h2>
4967
<nav className="flex flex-wrap gap-8">
5068
{boards.map((board) => (
51-
<Link
69+
<Board
5270
key={board.id}
53-
to={`/board/${board.id}`}
54-
className="w-60 h-40 p-4 block border-b-8 shadow rounded hover:shadow-lg hover:scale-105 transition-transform bg-white"
55-
style={{ borderColor: board.color }}
56-
>
57-
<div className="font-bold">{board.name}</div>
58-
</Link>
71+
name={board.name}
72+
id={board.id}
73+
color={board.color}
74+
/>
5975
))}
6076
</nav>
6177
</div>
6278
);
6379
}
6480

81+
function Board({
82+
name,
83+
id,
84+
color,
85+
}: {
86+
name: string;
87+
id: number;
88+
color: string;
89+
}) {
90+
let fetcher = useFetcher();
91+
let isDeleting = fetcher.state !== "idle";
92+
return isDeleting ? null : (
93+
<Link
94+
to={`/board/${id}`}
95+
className="w-60 h-40 p-4 block border-b-8 shadow rounded hover:shadow-lg bg-white relative"
96+
style={{ borderColor: color }}
97+
>
98+
<div className="font-bold">{name}</div>
99+
<fetcher.Form method="post">
100+
<input type="hidden" name="intent" value={INTENTS.deleteBoard} />
101+
<input type="hidden" name="boardId" value={id} />
102+
<button
103+
aria-label="Delete board"
104+
className="absolute top-4 right-4 hover:text-brand-red"
105+
type="submit"
106+
onClick={(event) => {
107+
event.stopPropagation();
108+
}}
109+
>
110+
<Icon name="trash" />
111+
</button>
112+
</fetcher.Form>
113+
</Link>
114+
);
115+
}
116+
65117
function NewBoard() {
66118
let navigation = useNavigation();
67119
let isCreating = navigation.formData?.get("intent") === "createBoard";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
-- RedefineTables
2+
PRAGMA foreign_keys=OFF;
3+
CREATE TABLE "new_Item" (
4+
"id" TEXT NOT NULL PRIMARY KEY,
5+
"title" TEXT NOT NULL,
6+
"content" TEXT,
7+
"order" REAL NOT NULL,
8+
"columnId" TEXT NOT NULL,
9+
"boardId" INTEGER NOT NULL,
10+
CONSTRAINT "Item_columnId_fkey" FOREIGN KEY ("columnId") REFERENCES "Column" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
11+
CONSTRAINT "Item_boardId_fkey" FOREIGN KEY ("boardId") REFERENCES "Board" ("id") ON DELETE CASCADE ON UPDATE CASCADE
12+
);
13+
INSERT INTO "new_Item" ("boardId", "columnId", "content", "id", "order", "title") SELECT "boardId", "columnId", "content", "id", "order", "title" FROM "Item";
14+
DROP TABLE "Item";
15+
ALTER TABLE "new_Item" RENAME TO "Item";
16+
CREATE TABLE "new_Column" (
17+
"id" TEXT NOT NULL PRIMARY KEY,
18+
"name" TEXT NOT NULL,
19+
"order" REAL NOT NULL DEFAULT 0,
20+
"boardId" INTEGER NOT NULL,
21+
CONSTRAINT "Column_boardId_fkey" FOREIGN KEY ("boardId") REFERENCES "Board" ("id") ON DELETE CASCADE ON UPDATE CASCADE
22+
);
23+
INSERT INTO "new_Column" ("boardId", "id", "name", "order") SELECT "boardId", "id", "name", "order" FROM "Column";
24+
DROP TABLE "Column";
25+
ALTER TABLE "new_Column" RENAME TO "Column";
26+
PRAGMA foreign_key_check;
27+
PRAGMA foreign_keys=ON;

prisma/schema.prisma

+3-3
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ model Column {
4242
name String
4343
order Float @default(0)
4444
items Item[]
45-
Board Board @relation(fields: [boardId], references: [id])
45+
Board Board @relation(fields: [boardId], references: [id], onDelete: Cascade)
4646
boardId Int
4747
}
4848

@@ -56,9 +56,9 @@ model Item {
5656
// will be 1.75, etc.
5757
order Float
5858
59-
Column Column @relation(fields: [columnId], references: [id])
59+
Column Column @relation(fields: [columnId], references: [id], onDelete: Cascade)
6060
columnId String
6161
62-
Board Board @relation(fields: [boardId], references: [id])
62+
Board Board @relation(fields: [boardId], references: [id], onDelete: Cascade)
6363
boardId Int
6464
}

vite.config.mjs

+6-1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,10 @@ import tsconfigPaths from "vite-tsconfig-paths";
33
import { defineConfig } from "vite";
44

55
export default defineConfig({
6-
plugins: [tsconfigPaths(), remix()],
6+
plugins: [
7+
tsconfigPaths(),
8+
remix({
9+
ignoredRouteFiles: ["**/.*"],
10+
}),
11+
],
712
});

0 commit comments

Comments
 (0)