From 55bf1bcd1b4db76b2ecfc42f89ef884e6c5aaa0a Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Fri, 3 Nov 2023 17:02:59 -0600 Subject: [PATCH] just some light refactoring this fine friday --- app/auth/cookie.ts | 0 app/components/button.tsx | 14 ++ app/components/input.tsx | 46 +++++++ app/icons/icons.tsx | 2 +- app/modules/create-form.ts | 21 --- app/root.tsx | 6 +- app/routes/board.$id/CONTENT_TYPES.ts | 4 - app/routes/board.$id/board.tsx | 113 ++++++++++++++++ app/routes/board.$id/card.tsx | 8 +- app/routes/board.$id/column.tsx | 13 +- app/routes/board.$id/components.tsx | 27 ++++ app/routes/board.$id/mutations.ts | 46 ------- app/routes/board.$id/new-card.tsx | 22 +-- app/routes/board.$id/new-column.tsx | 21 +-- app/routes/board.$id/queries.ts | 44 ++++++ app/routes/board.$id/route.tsx | 135 +------------------ app/routes/board.$id/server.ts | 103 ++------------ app/routes/board.$id/types.ts | 34 +++++ app/routes/board.$id/utils.ts | 18 +++ app/routes/home/new-board.tsx | 0 app/routes/home/{query.ts => queries.ts} | 2 +- app/routes/home/route.tsx | 58 ++------ app/routes/login/{login.ts => queries.ts} | 2 +- app/routes/login/route.tsx | 39 ++---- app/routes/logout.ts | 2 +- app/routes/signup/{account.ts => queries.ts} | 2 +- app/routes/signup/route.tsx | 47 +++---- app/routes/signup/validate.tsx | 2 +- package-lock.json | 48 ++++++- package.json | 3 +- tsconfig.json | 3 + vite.config.mjs | 3 +- 32 files changed, 445 insertions(+), 443 deletions(-) delete mode 100644 app/auth/cookie.ts create mode 100644 app/components/button.tsx create mode 100644 app/components/input.tsx delete mode 100644 app/modules/create-form.ts delete mode 100644 app/routes/board.$id/CONTENT_TYPES.ts create mode 100644 app/routes/board.$id/board.tsx create mode 100644 app/routes/board.$id/components.tsx delete mode 100644 app/routes/board.$id/mutations.ts create mode 100644 app/routes/board.$id/queries.ts create mode 100644 app/routes/board.$id/utils.ts delete mode 100644 app/routes/home/new-board.tsx rename app/routes/home/{query.ts => queries.ts} (90%) rename app/routes/login/{login.ts => queries.ts} (91%) rename app/routes/signup/{account.ts => queries.ts} (93%) diff --git a/app/auth/cookie.ts b/app/auth/cookie.ts deleted file mode 100644 index e69de29..0000000 diff --git a/app/components/button.tsx b/app/components/button.tsx new file mode 100644 index 0000000..1b31834 --- /dev/null +++ b/app/components/button.tsx @@ -0,0 +1,14 @@ +import { forwardRef } from "react"; + +export let Button = forwardRef< + HTMLButtonElement, + React.ButtonHTMLAttributes +>((props, ref) => { + return ( + - + Save Card + Cancel ); diff --git a/app/routes/board.$id/new-column.tsx b/app/routes/board.$id/new-column.tsx index ad61d79..12525e7 100644 --- a/app/routes/board.$id/new-column.tsx +++ b/app/routes/board.$id/new-column.tsx @@ -1,9 +1,11 @@ import { useState, useRef } from "react"; import { flushSync } from "react-dom"; import invariant from "tiny-invariant"; -import { INTENTS } from "./mutations"; +import { Icon } from "~/icons/icons"; import { Form, useSubmit } from "@remix-run/react"; -import { Icon } from "../../icons/icons"; + +import { INTENTS } from "./types"; +import { CancelButton, SaveButton } from "./components"; export function NewColumn({ boardId, @@ -48,19 +50,8 @@ export function NewColumn({ className="border border-slate-400 w-full rounded-lg py-1 px-2 font-medium text-black" />
- - + Save Column + setEdit(false)}>Cancel
) : ( diff --git a/app/routes/board.$id/queries.ts b/app/routes/board.$id/queries.ts new file mode 100644 index 0000000..05bd2c6 --- /dev/null +++ b/app/routes/board.$id/queries.ts @@ -0,0 +1,44 @@ +import { prisma } from "~/db/prisma"; + +import { ItemMutation } from "./types"; + +export async function getBoardData(boardId: number) { + return prisma.board.findUnique({ + where: { + id: boardId, + }, + include: { + items: true, + columns: { orderBy: { order: "asc" } }, + }, + }); +} + +export function upsertItem(mutation: ItemMutation & { boardId: number }) { + return prisma.item.upsert({ + where: { id: mutation.id }, + create: mutation, + update: mutation, + }); +} + +export async function updateColumnName(id: string, name: string) { + return prisma.column.update({ + where: { id }, + data: { name }, + }); +} + +export async function createColumn(boardId: number, name: string, id: string) { + let columnCount = await prisma.column.count({ + where: { boardId }, + }); + return prisma.column.create({ + data: { + id, + boardId, + name, + order: columnCount + 1, + }, + }); +} diff --git a/app/routes/board.$id/route.tsx b/app/routes/board.$id/route.tsx index 669498e..6a933a9 100644 --- a/app/routes/board.$id/route.tsx +++ b/app/routes/board.$id/route.tsx @@ -1,135 +1,10 @@ -import { Link, useFetchers, useLoaderData } from "@remix-run/react"; -import { action } from "./server"; -import { loader } from "./server"; -import { INTENTS } from "./mutations"; -import { Column } from "./column"; -import { NewColumn } from "./new-column"; -import { useRef } from "react"; -import invariant from "tiny-invariant"; -import { RenderedItem } from "./types"; -import { MetaFunction } from "@remix-run/node"; +import { type MetaFunction } from "@remix-run/node"; -export { loader, action }; +import { loader, action } from "./server"; +import { Board } from "./board"; export const meta: MetaFunction = ({ data }) => { - if (!data) return []; - return [{ title: `${data.board.name} | Trellix` }]; + return [{ title: `${data ? data.board.name : "Board"} | Trellix` }]; }; -export default function Board() { - let { board } = useLoaderData(); - - let itemsById = new Map(board.items.map((item) => [item.id, item])); - let pendingItems = usePendingItems(); - - // merge pending items and existing items - for (let pendingItem of pendingItems) { - let item = itemsById.get(pendingItem.id); - let merged = item - ? { ...item, ...pendingItem } - : { ...pendingItem, boardId: board.id }; - itemsById.set(pendingItem.id, merged); - } - - // merge pending and existing columns - let optAddingColumns = useAddingColumns(); - type Column = (typeof board.columns)[0] | (typeof optAddingColumns)[0]; - type ColumnWithItems = Column & { items: typeof board.items }; - let columns = [...board.columns, ...optAddingColumns].reduce( - (map, column) => map.set(String(column.id), { ...column, items: [] }), - new Map(), - ); - - // add items to their columns - for (let item of itemsById.values()) { - let columnId = item.columnId; - let column = columns.get(columnId); - invariant(column, "missing column"); - column.items.push(item); - } - - let scrollContainerRef = useRef(null); - function scrollRight() { - invariant(scrollContainerRef.current); - scrollContainerRef.current.scrollLeft = - scrollContainerRef.current.scrollWidth; - } - - return ( -
-

{board.name}

- -
- {[...columns.values()].map((col) => { - return ( - - ); - })} - - - -
-
-
- ); -} - -function useAddingColumns() { - type CreateColumnFetcher = ReturnType[0] & { - formData: FormData; - }; - - return useFetchers() - .filter((fetcher): fetcher is CreateColumnFetcher => { - return fetcher.formData?.get("intent") === INTENTS.createColumn; - }) - .map((fetcher) => { - let name = String(fetcher.formData.get("name")); - let id = String(fetcher.formData.get("id")); - return { name, id }; - }); -} - -function usePendingItems() { - type PendingItem = ReturnType[0] & { - formData: FormData; - }; - return useFetchers() - .filter((fetcher): fetcher is PendingItem => { - if (!fetcher.formData) return false; - let intent = fetcher.formData.get("intent"); - return intent === INTENTS.createItem || intent === INTENTS.moveItem; - }) - .map((fetcher) => { - let columnId = String(fetcher.formData.get("columnId")); - let title = String(fetcher.formData.get("title")); - let id = String(fetcher.formData.get("id")); - let order = Number(fetcher.formData.get("order")); - let item: RenderedItem = { title, id, order, columnId, content: null }; - return item; - }); -} - -export function ErrorBoundary() { - return ( -
-

Dang ... had an error.

- - Try again - -
- ); -} +export { loader, action, Board as default }; diff --git a/app/routes/board.$id/server.ts b/app/routes/board.$id/server.ts index 039557b..067a5e7 100644 --- a/app/routes/board.$id/server.ts +++ b/app/routes/board.$id/server.ts @@ -1,14 +1,17 @@ import invariant from "tiny-invariant"; -import { - redirect, - type ActionFunctionArgs, - LoaderFunctionArgs, -} from "@remix-run/node"; +import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node"; + +import { badRequest, notFound } from "~/http/bad-response"; +import { requireAuthCookie } from "~/auth/auth"; -import { prisma } from "../../db/prisma"; -import { badRequest, notFound } from "../../http/bad-response"; -import { INTENTS, ItemMutation, parseItemMutation } from "./mutations"; -import { requireAuthCookie } from "../../auth/auth"; +import { parseItemMutation } from "./utils"; +import { INTENTS } from "./types"; +import { + createColumn, + updateColumnName, + getBoardData, + upsertItem, +} from "./queries"; export async function loader({ request, params }: LoaderFunctionArgs) { await requireAuthCookie(request); @@ -22,14 +25,6 @@ export async function loader({ request, params }: LoaderFunctionArgs) { return { board }; } -function upsertItem(mutation: ItemMutation & { boardId: number }) { - return prisma.item.upsert({ - where: { id: mutation.id }, - create: mutation, - update: mutation, - }); -} - export async function action({ request, params }: ActionFunctionArgs) { let boardId = Number(params.id); invariant(boardId, "Missing boardId"); @@ -64,77 +59,5 @@ export async function action({ request, params }: ActionFunctionArgs) { } } - return request.headers.get("Sec-Fetch-Dest") === "document" - ? redirect(`/board/${boardId}`) - : { ok: true, boardId }; -} - -//////////////////////////////////////////////////////////////////////////////// -// Controller Functions -//////////////////////////////////////////////////////////////////////////////// - -async function moveItem(id: string, columnId: string, order: number) { - return prisma.item.update({ - where: { id }, - data: { columnId, order }, - }); -} - -export async function createItem( - boardId: number, - columnId: string, - title: string, -) { - let itemCountForColumn = await prisma.item.count({ - where: { columnId }, - }); - return prisma.item.create({ - data: { - title, - columnId, - boardId, - order: itemCountForColumn + 1, - }, - }); -} - -export async function updateColumnName(id: string, name: string) { - return prisma.column.update({ - where: { id }, - data: { name }, - }); -} - -async function createColumn(boardId: number, name: string, id: string) { - let columnCount = await prisma.column.count({ - where: { boardId }, - }); - return prisma.column.create({ - data: { - id, - boardId, - name, - order: columnCount + 1, - }, - }); -} - -//////////////////////////////////////////////////////////////////////////////// -// Public Functions -//////////////////////////////////////////////////////////////////////////////// - -export async function getBoardData(boardId: number) { - return prisma.board.findUnique({ - where: { - id: boardId, - }, - include: { - items: true, - columns: { - orderBy: { - order: "asc", - }, - }, - }, - }); + return { ok: true }; } diff --git a/app/routes/board.$id/types.ts b/app/routes/board.$id/types.ts index 12c15ea..b84c0d5 100644 --- a/app/routes/board.$id/types.ts +++ b/app/routes/board.$id/types.ts @@ -5,3 +5,37 @@ export interface RenderedItem { content: string | null; columnId: string; } + +export const CONTENT_TYPES = { + card: "application/remix-card", + column: "application/remix-column", +}; + +export const INTENTS = { + createColumn: "newColumn" as const, + updateColumn: "updateColumn" as const, + createItem: "createItem" as const, + moveItem: "moveItem" as const, + moveColumn: "moveColumn" as const, +}; + +export const ItemMutationFields = { + id: { type: String, name: "id" }, + columnId: { type: String, name: "columnId" }, + order: { type: Number, name: "order" }, + title: { type: String, name: "title" }, +} as const; + +export type ItemMutation = MutationFromFields; + +//////////////////////////////////////////////////////////////////////////////// +// Bonkers TypeScript +type ConstructorToType = T extends typeof String + ? string + : T extends typeof Number + ? number + : never; + +export type MutationFromFields> = { + [K in keyof T]: ConstructorToType; +}; diff --git a/app/routes/board.$id/utils.ts b/app/routes/board.$id/utils.ts new file mode 100644 index 0000000..fc1ef82 --- /dev/null +++ b/app/routes/board.$id/utils.ts @@ -0,0 +1,18 @@ +import invariant from "tiny-invariant"; +import { ItemMutation, ItemMutationFields } from "./types"; + +export function parseItemMutation(formData: FormData): ItemMutation { + let id = ItemMutationFields.id.type(formData.get("id")); + invariant(id, "Missing item id"); + + let columnId = ItemMutationFields.columnId.type(formData.get("columnId")); + invariant(columnId, "Missing column id"); + + let order = ItemMutationFields.order.type(formData.get("order")); + invariant(typeof order === "number", "Missing order"); + + let title = ItemMutationFields.title.type(formData.get("title")); + invariant(title, "Missing title"); + + return { id, columnId, order, title }; +} diff --git a/app/routes/home/new-board.tsx b/app/routes/home/new-board.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/app/routes/home/query.ts b/app/routes/home/queries.ts similarity index 90% rename from app/routes/home/query.ts rename to app/routes/home/queries.ts index 69a0040..40e82f4 100644 --- a/app/routes/home/query.ts +++ b/app/routes/home/queries.ts @@ -1,4 +1,4 @@ -import { prisma } from "../../db/prisma"; +import { prisma } from "~/db/prisma"; export async function createBoard(userId: string, name: string, color: string) { return prisma.board.create({ diff --git a/app/routes/home/route.tsx b/app/routes/home/route.tsx index 5b856a1..dfdfb25 100644 --- a/app/routes/home/route.tsx +++ b/app/routes/home/route.tsx @@ -3,15 +3,14 @@ import { type LoaderFunctionArgs, redirect, } from "@remix-run/node"; -import { requireAuthCookie } from "../../auth/auth"; -import { getHomeData, createBoard } from "./query"; -import { - Form, - Link, - useActionData, - useLoaderData, - useNavigation, -} from "@remix-run/react"; +import { Form, Link, 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"; export const meta = () => { return [{ title: "Boards" }]; @@ -28,10 +27,7 @@ export async function action({ request }: ActionFunctionArgs) { let formData = await request.formData(); let name = String(formData.get("name")); let color = String(formData.get("color")); - if (!name) { - return { ok: false, message: "Board name is required" }; - } - + if (!name) throw badRequest("Bad request"); let board = await createBoard(userId, name, color); throw redirect(`/board/${board.id}`); } @@ -67,41 +63,20 @@ function Boards() { } function NewBoard() { - let actionData = useActionData(); let navigation = useNavigation(); + let isCreating = navigation.formData?.get("intent") === "createBoard"; return (
+

New Board

- -
- -
+
- +
- +
); diff --git a/app/routes/login/login.ts b/app/routes/login/queries.ts similarity index 91% rename from app/routes/login/login.ts rename to app/routes/login/queries.ts index 5d4de0a..9d279ba 100644 --- a/app/routes/login/login.ts +++ b/app/routes/login/queries.ts @@ -1,5 +1,5 @@ import crypto from "crypto"; -import { prisma } from "../../db/prisma"; +import { prisma } from "~/db/prisma"; export async function login(email: string, password: string) { let user = await prisma.account.findUnique({ diff --git a/app/routes/login/route.tsx b/app/routes/login/route.tsx index 3b2d5af..843dfdf 100644 --- a/app/routes/login/route.tsx +++ b/app/routes/login/route.tsx @@ -1,8 +1,12 @@ import { json, redirect, type DataFunctionArgs } from "@remix-run/node"; -import { validate } from "./validate"; import { Form, Link, useActionData } from "@remix-run/react"; -import { login } from "./login"; -import { redirectIfLoggedInLoader, setAuthOnResponse } from "../../auth/auth"; + +import { redirectIfLoggedInLoader, setAuthOnResponse } from "~/auth/auth"; +import { Button } from "~/components/button"; +import { Input, Label } from "~/components/input"; + +import { validate } from "./validate"; +import { login } from "./queries"; export const loader = redirectIfLoggedInLoader; @@ -12,7 +16,6 @@ export const meta = () => { export async function action({ request }: DataFunctionArgs) { let formData = await request.formData(); - let email = String(formData.get("email")); let password = String(formData.get("password")); @@ -22,7 +25,6 @@ export async function action({ request }: DataFunctionArgs) { } let userId = await login(email, password); - if (userId === false) { return json( { ok: false, errors: { password: "Invalid credentials" } }, @@ -51,18 +53,15 @@ export default function Signup() {
-
-
- +
Don't have an account?{" "} diff --git a/app/routes/logout.ts b/app/routes/logout.ts index 953cb7b..91a17a7 100644 --- a/app/routes/logout.ts +++ b/app/routes/logout.ts @@ -1,4 +1,4 @@ -import { redirectWithClearedCookie } from "../auth/auth"; +import { redirectWithClearedCookie } from "~/auth/auth"; export function action() { return redirectWithClearedCookie(); diff --git a/app/routes/signup/account.ts b/app/routes/signup/queries.ts similarity index 93% rename from app/routes/signup/account.ts rename to app/routes/signup/queries.ts index 8d69939..5b3f283 100644 --- a/app/routes/signup/account.ts +++ b/app/routes/signup/queries.ts @@ -1,5 +1,5 @@ -import { prisma } from "../../db/prisma"; import crypto from "crypto"; +import { prisma } from "~/db/prisma"; export async function accountExists(email: string) { let account = await prisma.account.findUnique({ diff --git a/app/routes/signup/route.tsx b/app/routes/signup/route.tsx index 2b5a748..914cef4 100644 --- a/app/routes/signup/route.tsx +++ b/app/routes/signup/route.tsx @@ -1,13 +1,12 @@ -import { - json, - type ActionFunctionArgs, - redirect, - DataFunctionArgs, -} from "@remix-run/node"; -import { validate } from "./validate"; +import { json, type ActionFunctionArgs, redirect } from "@remix-run/node"; import { Form, Link, useActionData } from "@remix-run/react"; -import { createAccount } from "./account"; -import { redirectIfLoggedInLoader, setAuthOnResponse } from "../../auth/auth"; + +import { redirectIfLoggedInLoader, setAuthOnResponse } from "~/auth/auth"; +import { Label, Input } from "~/components/input"; +import { Button } from "~/components/button"; + +import { validate } from "./validate"; +import { createAccount } from "./queries"; export const loader = redirectIfLoggedInLoader; @@ -48,18 +47,15 @@ export default function Signup() {
-
-
-
- -
+ +
Already have an account?{" "} diff --git a/app/routes/signup/validate.tsx b/app/routes/signup/validate.tsx index 4f121b3..481ca68 100644 --- a/app/routes/signup/validate.tsx +++ b/app/routes/signup/validate.tsx @@ -1,4 +1,4 @@ -import { accountExists } from "./account"; +import { accountExists } from "./queries"; export async function validate(email: string, password: string) { let errors: { email?: string; password?: string } = {}; diff --git a/package-lock.json b/package-lock.json index b71a688..86b667a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,8 @@ "prettier": "^3.0.3", "prisma": "^5.3.1", "tailwindcss": "^3.3.3", - "typescript": "^5.1.6" + "typescript": "^5.1.6", + "vite-tsconfig-paths": "^4.2.1" }, "engines": { "node": ">=18.0.0" @@ -5465,6 +5466,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -10491,6 +10498,26 @@ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" }, + "node_modules/tsconfck": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-2.1.2.tgz", + "integrity": "sha512-ghqN1b0puy3MhhviwO2kGF8SeMDNhEbnKxjK7h6+fvY9JAxqvXi8y5NAHSQv687OVboS2uZIByzGd45/YxrRHg==", + "dev": true, + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^14.13.1 || ^16 || >=18" + }, + "peerDependencies": { + "typescript": "^4.3.5 || ^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/tsconfig-paths": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", @@ -11087,6 +11114,25 @@ "node": ">=0.10.0" } }, + "node_modules/vite-tsconfig-paths": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.2.1.tgz", + "integrity": "sha512-GNUI6ZgPqT3oervkvzU+qtys83+75N/OuDaQl7HmOqFTb0pjZsuARrRipsyJhJ3enqV8beI1xhGbToR4o78nSQ==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^2.1.0" + }, + "peerDependencies": { + "vite": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, "node_modules/vite/node_modules/@esbuild/android-arm": { "version": "0.18.20", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", diff --git a/package.json b/package.json index b7471c2..e854e5f 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ "prettier": "^3.0.3", "prisma": "^5.3.1", "tailwindcss": "^3.3.3", - "typescript": "^5.1.6" + "typescript": "^5.1.6", + "vite-tsconfig-paths": "^4.2.1" }, "engines": { "node": ">=18.0.0" diff --git a/tsconfig.json b/tsconfig.json index 7b3acc9..28cce91 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,9 @@ "allowJs": true, "forceConsistentCasingInFileNames": true, "baseUrl": ".", + "paths": { + "~/*": ["./app/*"] + }, // Remix takes care of building everything in `remix build`. "noEmit": true diff --git a/vite.config.mjs b/vite.config.mjs index 74b36be..40fbfc1 100644 --- a/vite.config.mjs +++ b/vite.config.mjs @@ -1,6 +1,7 @@ import { unstable_vitePlugin as remix } from "@remix-run/dev"; +import tsconfigPaths from "vite-tsconfig-paths"; import { defineConfig } from "vite"; export default defineConfig({ - plugins: [remix()], + plugins: [tsconfigPaths(), remix()], });