From 1860db7a9ce75eda426aec994bbd05a472d316a0 Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 27 Nov 2023 23:27:44 +0000 Subject: [PATCH] feat: add card page --- src/app/_components/dashboard.tsx | 65 +++ src/app/boards/[...id]/page.tsx | 13 +- src/app/boards/layout.tsx | 66 +-- src/app/cards/[...id]/page.tsx | 85 +++ src/app/cards/layout.tsx | 5 + src/server/api/routers/card.ts | 96 ++-- .../migrations/0001_warm_jamie_braddock.sql | 4 + .../db/migrations/meta/0001_snapshot.json | 494 ++++++++++++++++++ src/server/db/migrations/meta/_journal.json | 7 + src/server/db/schema.ts | 3 +- 10 files changed, 728 insertions(+), 110 deletions(-) create mode 100644 src/app/_components/dashboard.tsx create mode 100644 src/app/cards/[...id]/page.tsx create mode 100644 src/app/cards/layout.tsx create mode 100644 src/server/db/migrations/0001_warm_jamie_braddock.sql create mode 100644 src/server/db/migrations/meta/0001_snapshot.json diff --git a/src/app/_components/dashboard.tsx b/src/app/_components/dashboard.tsx new file mode 100644 index 00000000..c14f0ddc --- /dev/null +++ b/src/app/_components/dashboard.tsx @@ -0,0 +1,65 @@ +import Image from "next/image"; +import { redirect } from "next/navigation"; + +import { getServerAuthSession } from "~/server/auth"; + +import boardsIcon from "~/app/assets/boards.json"; + +import ReactiveButton from "~/app/components/ReactiveButton"; + +const navigation = [ + { name: "Boards", href: "/boards", icon: boardsIcon, current: true }, +]; + +export default async function Layout(props: { children: React.ReactNode }) { + const session = await getServerAuthSession(); + + if (!session?.user) redirect("api/auth/signin"); + + return ( +
+
+
+

+ 貫 kan +

+
+
+ +
+ +
{props.children}
+
+
+ ); +} diff --git a/src/app/boards/[...id]/page.tsx b/src/app/boards/[...id]/page.tsx index 3da7b957..e0cf12e8 100644 --- a/src/app/boards/[...id]/page.tsx +++ b/src/app/boards/[...id]/page.tsx @@ -1,7 +1,7 @@ "use client"; import { useState } from "react"; - +import Link from "next/link"; import { useParams } from "next/navigation"; import { HiOutlinePlusSmall } from "react-icons/hi2"; import { @@ -180,17 +180,16 @@ export default function BoardPage() { index={index} > {(provided) => ( -
-

- {card.title} -

-
+ {card.title} + )} ))} diff --git a/src/app/boards/layout.tsx b/src/app/boards/layout.tsx index c14f0ddc..a49b1cf0 100644 --- a/src/app/boards/layout.tsx +++ b/src/app/boards/layout.tsx @@ -1,65 +1,5 @@ -import Image from "next/image"; -import { redirect } from "next/navigation"; +import Dashboard from "~/app/_components/dashboard"; -import { getServerAuthSession } from "~/server/auth"; - -import boardsIcon from "~/app/assets/boards.json"; - -import ReactiveButton from "~/app/components/ReactiveButton"; - -const navigation = [ - { name: "Boards", href: "/boards", icon: boardsIcon, current: true }, -]; - -export default async function Layout(props: { children: React.ReactNode }) { - const session = await getServerAuthSession(); - - if (!session?.user) redirect("api/auth/signin"); - - return ( -
-
-
-

- 貫 kan -

-
-
- -
- -
{props.children}
-
-
- ); +export default function Layout(props: { children: React.ReactNode }) { + return {props.children}; } diff --git a/src/app/cards/[...id]/page.tsx b/src/app/cards/[...id]/page.tsx new file mode 100644 index 00000000..6bee5f58 --- /dev/null +++ b/src/app/cards/[...id]/page.tsx @@ -0,0 +1,85 @@ +"use client"; + +import { useParams } from "next/navigation"; +import { HiOutlinePlusSmall } from "react-icons/hi2"; +import { Formik, Form, Field, type FieldProps } from "formik"; + +import { api } from "~/trpc/react"; + +interface FormValues { + cardId: string; + description: string; +} + +export default function CardPage() { + const params = useParams(); + + const cardId = params?.id?.length && params.id[0]; + + if (!cardId) return <>; + + const { data } = api.card.byId.useQuery({ id: cardId }); + + const updateCard = api.card.updateDescription.useMutation(); + + return ( +
+
+

+ {data?.title} +

+
+ +
+
+
+ { + updateCard.mutate({ + cardId: values.cardId, + description: values.description, + }); + }} + enableReinitialize + > +
+
+
+ ( +
form.submitForm()} + onInput={async (e) => { + const { innerText } = e.target as HTMLDivElement; + await form.setFieldValue("description", innerText); + }} + contentEditable + className="block w-full border-0 bg-transparent py-1.5 text-dark-1000 focus-visible:outline-none sm:text-sm sm:leading-6" + > + {field.value || "Add description..."} +
+ )} + /> +
+
+
+
+
+
+ ); +} diff --git a/src/app/cards/layout.tsx b/src/app/cards/layout.tsx new file mode 100644 index 00000000..a49b1cf0 --- /dev/null +++ b/src/app/cards/layout.tsx @@ -0,0 +1,5 @@ +import Dashboard from "~/app/_components/dashboard"; + +export default function Layout(props: { children: React.ReactNode }) { + return {props.children}; +} diff --git a/src/server/api/routers/card.ts b/src/server/api/routers/card.ts index 6f5c715f..7468af51 100644 --- a/src/server/api/routers/card.ts +++ b/src/server/api/routers/card.ts @@ -51,48 +51,68 @@ export const cardRouter = createTRPCRouter({ }); }) }), - update: publicProcedure - .input( - z.object({ - cardId: z.string().min(12), - currentListId: z.string().min(12), - newListId: z.string().min(12), - currentIndex: z.number(), - newIndex: z.number() - })) - .mutation(({ ctx, input }) => { - const userId = ctx.session?.user.id; + byId: publicProcedure + .input(z.object({ id: z.string().min(12) })) + .query(({ ctx, input }) => + ctx.db.query.cards.findFirst({ + where: eq(cards.publicId, input.id), + }) + ), + updateDescription: publicProcedure + .input( + z.object({ + cardId: z.string(), + description: z.string(), + })) + .mutation(({ ctx, input }) => { + const userId = ctx.session?.user.id; - if (!userId) return; + if (!userId) return; - return ctx.db.transaction(async (tx) => { - const [currentList] = await tx.select({ id: lists.id }).from(lists).where(eq(lists.publicId, input.currentListId)) - const [newList] = await tx.select({ id: lists.id }).from(lists).where(eq(lists.publicId, input.newListId)) + return ctx.db.update(cards).set({ description: input.description}).where(eq(cards.publicId, input.cardId)); + }), + update: publicProcedure + .input( + z.object({ + cardId: z.string().min(12), + currentListId: z.string().min(12), + newListId: z.string().min(12), + currentIndex: z.number(), + newIndex: z.number() + })) + .mutation(({ ctx, input }) => { + const userId = ctx.session?.user.id; - if (!currentList?.id || !newList?.id) return; + if (!userId) return; - if (currentList.id === newList.id) { - await tx.execute(sql` - UPDATE ${cards} - SET ${cards.index} = - CASE - WHEN ${cards.index} = ${input.currentIndex} THEN ${input.newIndex} - WHEN ${input.currentIndex} < ${input.newIndex} AND ${cards.index} > ${input.currentIndex} AND ${cards.index} <= ${input.newIndex} THEN ${cards.index} - 1 - WHEN ${input.currentIndex} > ${input.newIndex} AND ${cards.index} >= ${input.newIndex} AND ${cards.index} < ${input.currentIndex} THEN ${cards.index} + 1 - ELSE ${cards.index} - END - WHERE ${cards.listId} = ${currentList.id}; - `); - } else { - await tx.execute(sql`UPDATE ${cards} SET ${cards.index} = ${cards.index} + 1 WHERE ${cards.listId} = ${newList.id} AND ${cards.index} >= ${input.newIndex};`) + return ctx.db.transaction(async (tx) => { + const [currentList] = await tx.select({ id: lists.id }).from(lists).where(eq(lists.publicId, input.currentListId)) + const [newList] = await tx.select({ id: lists.id }).from(lists).where(eq(lists.publicId, input.newListId)) - await tx.execute(sql`UPDATE ${cards} SET ${cards.index} = ${cards.index} - 1 WHERE ${cards.listId} = ${currentList.id} AND ${cards.index} >= ${input.currentIndex};`) + if (!currentList?.id || !newList?.id) return; - await tx - .update(cards) - .set({ listId: newList.id, index: input.newIndex }) - .where(eq(cards.publicId, input.cardId)); - } - }) - }) + if (currentList.id === newList.id) { + await tx.execute(sql` + UPDATE ${cards} + SET ${cards.index} = + CASE + WHEN ${cards.index} = ${input.currentIndex} THEN ${input.newIndex} + WHEN ${input.currentIndex} < ${input.newIndex} AND ${cards.index} > ${input.currentIndex} AND ${cards.index} <= ${input.newIndex} THEN ${cards.index} - 1 + WHEN ${input.currentIndex} > ${input.newIndex} AND ${cards.index} >= ${input.newIndex} AND ${cards.index} < ${input.currentIndex} THEN ${cards.index} + 1 + ELSE ${cards.index} + END + WHERE ${cards.listId} = ${currentList.id}; + `); + } else { + await tx.execute(sql`UPDATE ${cards} SET ${cards.index} = ${cards.index} + 1 WHERE ${cards.listId} = ${newList.id} AND ${cards.index} >= ${input.newIndex};`) + + await tx.execute(sql`UPDATE ${cards} SET ${cards.index} = ${cards.index} - 1 WHERE ${cards.listId} = ${currentList.id} AND ${cards.index} >= ${input.currentIndex};`) + + await tx + .update(cards) + .set({ listId: newList.id, index: input.newIndex }) + .where(eq(cards.publicId, input.cardId)); + } + }) + }) }); \ No newline at end of file diff --git a/src/server/db/migrations/0001_warm_jamie_braddock.sql b/src/server/db/migrations/0001_warm_jamie_braddock.sql new file mode 100644 index 00000000..0ab83c3e --- /dev/null +++ b/src/server/db/migrations/0001_warm_jamie_braddock.sql @@ -0,0 +1,4 @@ +ALTER TABLE `card` DROP CONSTRAINT `card_listId_index_unique`;--> statement-breakpoint +ALTER TABLE `list` DROP CONSTRAINT `list_boardId_index_unique`;--> statement-breakpoint +ALTER TABLE `board` MODIFY COLUMN `name` varchar(255) NOT NULL;--> statement-breakpoint +ALTER TABLE `card` MODIFY COLUMN `description` text; \ No newline at end of file diff --git a/src/server/db/migrations/meta/0001_snapshot.json b/src/server/db/migrations/meta/0001_snapshot.json new file mode 100644 index 00000000..e7274387 --- /dev/null +++ b/src/server/db/migrations/meta/0001_snapshot.json @@ -0,0 +1,494 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "ab7d0b22-efdc-45b3-92cb-86bb630debe8", + "prevId": "f4bbbfaf-8b0d-40fd-9414-9e783f1a8197", + "tables": { + "account": { + "name": "account", + "columns": { + "userId": { + "name": "userId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_type": { + "name": "token_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_state": { + "name": "session_state", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "userId_idx": { + "name": "userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "account_provider_providerAccountId": { + "name": "account_provider_providerAccountId", + "columns": [ + "provider", + "providerAccountId" + ] + } + }, + "uniqueConstraints": {} + }, + "board": { + "name": "board", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdBy": { + "name": "createdBy", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "onUpdate": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "board_id": { + "name": "board_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "board_publicId_unique": { + "name": "board_publicId_unique", + "columns": [ + "publicId" + ] + } + } + }, + "card": { + "name": "card", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdBy": { + "name": "createdBy", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "onUpdate": true + }, + "listId": { + "name": "listId", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "index": { + "name": "index", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "card_id": { + "name": "card_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "card_publicId_unique": { + "name": "card_publicId_unique", + "columns": [ + "publicId" + ] + } + } + }, + "list": { + "name": "list", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdBy": { + "name": "createdBy", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "onUpdate": true + }, + "boardId": { + "name": "boardId", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "index": { + "name": "index", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "list_id": { + "name": "list_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "list_publicId_unique": { + "name": "list_publicId_unique", + "columns": [ + "publicId" + ] + } + } + }, + "session": { + "name": "session", + "columns": { + "sessionToken": { + "name": "sessionToken", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "userId_idx": { + "name": "userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "session_sessionToken": { + "name": "session_sessionToken", + "columns": [ + "sessionToken" + ] + } + }, + "uniqueConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "emailVerified": { + "name": "emailVerified", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3)" + }, + "image": { + "name": "image", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "user_id": { + "name": "user_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ] + } + } + }, + "verificationToken": { + "name": "verificationToken", + "columns": { + "identifier": { + "name": "identifier", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token": { + "name": "verificationToken_identifier_token", + "columns": [ + "identifier", + "token" + ] + } + }, + "uniqueConstraints": {} + } + }, + "schemas": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} \ No newline at end of file diff --git a/src/server/db/migrations/meta/_journal.json b/src/server/db/migrations/meta/_journal.json index 11334eea..10c04f27 100644 --- a/src/server/db/migrations/meta/_journal.json +++ b/src/server/db/migrations/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1699896771081, "tag": "0000_hesitant_nocturne", "breakpoints": true + }, + { + "idx": 1, + "version": "5", + "when": 1701122139821, + "tag": "0001_warm_jamie_braddock", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts index 09cbac6b..7396370a 100644 --- a/src/server/db/schema.ts +++ b/src/server/db/schema.ts @@ -7,7 +7,6 @@ import { primaryKey, text, timestamp, - unique, varchar, } from "drizzle-orm/mysql-core"; import { type AdapterAccount } from "next-auth/adapters"; @@ -76,7 +75,7 @@ export const cards = mySqlTable( id: bigint("id", { mode: "number" }).primaryKey().autoincrement(), publicId: varchar("publicId", { length: 12 }).notNull().unique(), title: varchar("title", { length: 256 }).notNull(), - description: varchar("description", { length: 256 }), + description: text("description"), createdBy: varchar("createdBy", { length: 256 }).notNull(), createdAt: timestamp("createdAt") .default(sql`CURRENT_TIMESTAMP`)