Skip to content

Commit

Permalink
add delete buttons
Browse files Browse the repository at this point in the history
  • Loading branch information
ryanflorence committed Dec 28, 2023
1 parent f6e309c commit da5bbba
Show file tree
Hide file tree
Showing 11 changed files with 160 additions and 28 deletions.
13 changes: 13 additions & 0 deletions app/icons/icons.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 19 additions & 4 deletions app/routes/board.$id/card.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { flushSync } from "react-dom";
import invariant from "tiny-invariant";
import { useSubmit } from "@remix-run/react";
import { useFetcher, useSubmit } from "@remix-run/react";
import { useState } from "react";

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

interface CardProps {
title: string;
Expand All @@ -25,10 +25,11 @@ export function Card({
previousOrder,
}: CardProps) {
let submit = useSubmit();
let deleteFetcher = useFetcher();

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

return (
return deleteFetcher.state !== "idle" ? null : (
<li
onDragOver={(event) => {
if (event.dataTransfer.types.includes(CONTENT_TYPES.card)) {
Expand Down Expand Up @@ -83,7 +84,7 @@ export function Card({
>
<div
draggable
className="bg-white shadow shadow-slate-300 border-slate-300 text-sm rounded-lg w-full py-1 px-2"
className="bg-white shadow shadow-slate-300 border-slate-300 text-sm rounded-lg w-full py-1 px-2 relative"
onDragStart={(event) => {
event.dataTransfer.effectAllowed = "move";
event.dataTransfer.setData(
Expand All @@ -94,6 +95,20 @@ export function Card({
>
<h3>{title}</h3>
<div className="mt-2">{content || <>&nbsp;</>}</div>
<deleteFetcher.Form method="post">
<input type="hidden" name="intent" value={INTENTS.deleteCard} />
<input type="hidden" name="itemId" value={id} />
<button
aria-label="Delete card"
className="absolute top-4 right-4 hover:text-brand-red"
type="submit"
onClick={(event) => {
event.stopPropagation();
}}
>
<Icon name="trash" />
</button>
</deleteFetcher.Form>
</div>
</li>
);
Expand Down
13 changes: 7 additions & 6 deletions app/routes/board.$id/column.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState, useEffect, useRef } from "react";
import { useFetcher, useSubmit } from "@remix-run/react";
import { useState, useRef } from "react";
import { useSubmit } from "@remix-run/react";

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

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

let isEmpty = items.length === 0;

return (
<div
className={
"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 " +
(acceptDrop ? `outline outline-2 outline-brand-red` : ``)
}
onDragOver={(event) => {
if (isEmpty && event.dataTransfer.types.includes(CONTENT_TYPES.card)) {
if (
items.length === 0 &&
event.dataTransfer.types.includes(CONTENT_TYPES.card)
) {
event.preventDefault();
setAcceptDrop(true);
}
Expand Down Expand Up @@ -111,7 +112,7 @@ export function Column({ name, columnId, items }: ColumnProps) {
{edit ? (
<NewCard
columnId={columnId}
nextOrder={items.length}
nextOrder={items.length === 0 ? 1 : items[items.length - 1].order + 1}
onAddCard={() => scrollList()}
onComplete={() => setEdit(false)}
/>
Expand Down
4 changes: 4 additions & 0 deletions app/routes/board.$id/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { prisma } from "~/db/prisma";

import { ItemMutation } from "./types";

export function deleteCard(id: string) {
return prisma.item.delete({ where: { id } });
}

export async function getBoardData(boardId: number) {
return prisma.board.findUnique({
where: {
Expand Down
6 changes: 6 additions & 0 deletions app/routes/board.$id/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
getBoardData,
upsertItem,
updateBoardName,
deleteCard,
} from "./queries";
import { Board } from "./board";

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

switch (intent) {
case INTENTS.deleteCard: {
let id = String(formData.get("itemId"));
await deleteCard(id);
break;
}
case INTENTS.updateBoardName: {
let name = String(formData.get("name"));
invariant(name, "Missing name");
Expand Down
3 changes: 3 additions & 0 deletions app/routes/board.$id/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ export const INTENTS = {
moveItem: "moveItem" as const,
moveColumn: "moveColumn" as const,
updateBoardName: "updateBoardName" as const,
deleteBoard: "deleteBoard" as const,
createBoard: "createBoard" as const,
deleteCard: "deleteCard" as const,
};

export const ItemMutationFields = {
Expand Down
6 changes: 6 additions & 0 deletions app/routes/home/queries.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { prisma } from "~/db/prisma";

export async function deleteBoard(boardId: number) {
return prisma.board.delete({
where: { id: boardId },
});
}

export async function createBoard(userId: string, name: string, color: string) {
return prisma.board.create({
data: {
Expand Down
80 changes: 66 additions & 14 deletions app/routes/home/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,22 @@ import {
type LoaderFunctionArgs,
redirect,
} from "@remix-run/node";
import { Form, Link, useLoaderData, useNavigation } from "@remix-run/react";
import {
Form,
Link,
useFetcher,
useLoaderData,
useNavigation,
} from "@remix-run/react";

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

import { getHomeData, createBoard } from "./queries";
import { getHomeData, createBoard, deleteBoard } from "./queries";
import { INTENTS } from "../board.$id/types";
import { Icon } from "~/icons/icons";

export const meta = () => {
return [{ title: "Boards" }];
Expand All @@ -25,11 +33,21 @@ export async function loader({ request }: LoaderFunctionArgs) {
export async function action({ request }: ActionFunctionArgs) {
let userId = await requireAuthCookie(request);
let formData = await request.formData();
let name = String(formData.get("name"));
let color = String(formData.get("color"));
if (!name) throw badRequest("Bad request");
let board = await createBoard(userId, name, color);
throw redirect(`/board/${board.id}`);
let intent = String(formData.get("intent"));
switch (intent) {
case INTENTS.createBoard: {
let name = String(formData.get("name"));
let color = String(formData.get("color"));
if (!name) throw badRequest("Bad request");
let board = await createBoard(userId, name, color);
return redirect(`/board/${board.id}`);
}
case INTENTS.deleteBoard: {
let boardId = Number(formData.get("boardId"));
await deleteBoard(boardId);
return { ok: true };
}
}
}

export default function Projects() {
Expand All @@ -48,20 +66,54 @@ function Boards() {
<h2 className="font-bold mb-2 text-xl">Boards</h2>
<nav className="flex flex-wrap gap-8">
{boards.map((board) => (
<Link
<Board
key={board.id}
to={`/board/${board.id}`}
className="w-60 h-40 p-4 block border-b-8 shadow rounded hover:shadow-lg hover:scale-105 transition-transform bg-white"
style={{ borderColor: board.color }}
>
<div className="font-bold">{board.name}</div>
</Link>
name={board.name}
id={board.id}
color={board.color}
/>
))}
</nav>
</div>
);
}

function Board({
name,
id,
color,
}: {
name: string;
id: number;
color: string;
}) {
let fetcher = useFetcher();
let isDeleting = fetcher.state !== "idle";
return isDeleting ? null : (
<Link
to={`/board/${id}`}
className="w-60 h-40 p-4 block border-b-8 shadow rounded hover:shadow-lg bg-white relative"
style={{ borderColor: color }}
>
<div className="font-bold">{name}</div>
<fetcher.Form method="post">
<input type="hidden" name="intent" value={INTENTS.deleteBoard} />
<input type="hidden" name="boardId" value={id} />
<button
aria-label="Delete board"
className="absolute top-4 right-4 hover:text-brand-red"
type="submit"
onClick={(event) => {
event.stopPropagation();
}}
>
<Icon name="trash" />
</button>
</fetcher.Form>
</Link>
);
}

function NewBoard() {
let navigation = useNavigation();
let isCreating = navigation.formData?.get("intent") === "createBoard";
Expand Down
27 changes: 27 additions & 0 deletions prisma/migrations/20231212213852_cascade_delete/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Item" (
"id" TEXT NOT NULL PRIMARY KEY,
"title" TEXT NOT NULL,
"content" TEXT,
"order" REAL NOT NULL,
"columnId" TEXT NOT NULL,
"boardId" INTEGER NOT NULL,
CONSTRAINT "Item_columnId_fkey" FOREIGN KEY ("columnId") REFERENCES "Column" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "Item_boardId_fkey" FOREIGN KEY ("boardId") REFERENCES "Board" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO "new_Item" ("boardId", "columnId", "content", "id", "order", "title") SELECT "boardId", "columnId", "content", "id", "order", "title" FROM "Item";
DROP TABLE "Item";
ALTER TABLE "new_Item" RENAME TO "Item";
CREATE TABLE "new_Column" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"order" REAL NOT NULL DEFAULT 0,
"boardId" INTEGER NOT NULL,
CONSTRAINT "Column_boardId_fkey" FOREIGN KEY ("boardId") REFERENCES "Board" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO "new_Column" ("boardId", "id", "name", "order") SELECT "boardId", "id", "name", "order" FROM "Column";
DROP TABLE "Column";
ALTER TABLE "new_Column" RENAME TO "Column";
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;
6 changes: 3 additions & 3 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ model Column {
name String
order Float @default(0)
items Item[]
Board Board @relation(fields: [boardId], references: [id])
Board Board @relation(fields: [boardId], references: [id], onDelete: Cascade)
boardId Int
}

Expand All @@ -56,9 +56,9 @@ model Item {
// will be 1.75, etc.
order Float
Column Column @relation(fields: [columnId], references: [id])
Column Column @relation(fields: [columnId], references: [id], onDelete: Cascade)
columnId String
Board Board @relation(fields: [boardId], references: [id])
Board Board @relation(fields: [boardId], references: [id], onDelete: Cascade)
boardId Int
}
7 changes: 6 additions & 1 deletion vite.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,10 @@ import tsconfigPaths from "vite-tsconfig-paths";
import { defineConfig } from "vite";

export default defineConfig({
plugins: [tsconfigPaths(), remix()],
plugins: [
tsconfigPaths(),
remix({
ignoredRouteFiles: ["**/.*"],
}),
],
});

0 comments on commit da5bbba

Please sign in to comment.