diff --git a/bun.lockb b/bun.lockb index ccfaf062..b78bfa4b 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index fb4b8ad9..e1794cb2 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@auth/drizzle-adapter": "^0.3.2", + "@headlessui/react": "^1.7.17", "@planetscale/database": "^1.11.0", "@t3-oss/env-nextjs": "^0.7.0", "@tailwindcss/forms": "^0.5.6", diff --git a/src/app/_components/modal.tsx b/src/app/_components/modal.tsx new file mode 100644 index 00000000..fc562648 --- /dev/null +++ b/src/app/_components/modal.tsx @@ -0,0 +1,52 @@ +"use client"; + +import { Fragment } from "react"; +import { Dialog, Transition } from "@headlessui/react"; + +import { useModal } from "~/app/providers"; + +interface Props { + children: React.ReactNode; +} + +const Modal: React.FC = ({ children }) => { + const { isOpen, closeModal } = useModal(); + + return ( + + + +
+ + +
+
+ + + {children} + + +
+
+
+
+ ); +}; + +export default Modal; diff --git a/src/app/boards/boards.tsx b/src/app/boards/boards.tsx index cca8a944..57faa72e 100644 --- a/src/app/boards/boards.tsx +++ b/src/app/boards/boards.tsx @@ -1,15 +1,15 @@ import Link from "next/link"; -import { api } from "~/trpc/server"; +import { api } from "~/trpc/react"; -export async function Boards() { - const boards = await api.board.all.query(); +export function Boards() { + const { data } = api.board.all.useQuery(); - if (boards?.length === 0) return <>; + if (data?.length === 0) return <>; return (
- {boards?.map((board) => { + {data?.map((board) => { return (
diff --git a/src/app/boards/create.tsx b/src/app/boards/create.tsx new file mode 100644 index 00000000..85a656db --- /dev/null +++ b/src/app/boards/create.tsx @@ -0,0 +1,70 @@ +"use client"; + +import { api } from "~/trpc/react"; + +import { HiXMark } from "react-icons/hi2"; + +import { useModal } from "~/app/providers"; + +import { Formik, Form, Field } from "formik"; + +interface FormValues { + name: string; +} + +export function NewBoardForm() { + const { closeModal } = useModal(); + + const createBoard = api.board.create.useMutation({ + onSuccess() { + closeModal(); + }, + }); + + return ( + <> +
+

New board

+ +
+ + { + createBoard.mutate({ + name: values.name, + }); + }} + > +
+ + +
+ +
+ +
+ + ); +} diff --git a/src/app/boards/page.tsx b/src/app/boards/page.tsx index f95a92de..bd886e2f 100644 --- a/src/app/boards/page.tsx +++ b/src/app/boards/page.tsx @@ -1,7 +1,16 @@ +"use client"; + import { HiOutlinePlusSmall } from "react-icons/hi2"; import { Boards } from "./boards"; +import { useModal } from "~/app/providers"; +import Modal from "~/app/_components/modal"; + +import { NewBoardForm } from "~/app/boards/create"; + export default function BoardsPage() { + const { openModal } = useModal(); + return (
@@ -12,6 +21,7 @@ export default function BoardsPage() {
+ + + +
diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 6081a715..516086e8 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -4,6 +4,7 @@ import { Plus_Jakarta_Sans } from "next/font/google"; import { headers } from "next/headers"; import { TRPCReactProvider } from "~/trpc/react"; +import { ModalProvider } from "~/app/providers"; const jakarta = Plus_Jakarta_Sans({ subsets: ["latin"], @@ -24,7 +25,9 @@ export default function RootLayout({ return ( - {children} + + {children} + ); diff --git a/src/app/providers/index.tsx b/src/app/providers/index.tsx new file mode 100644 index 00000000..aca91c8f --- /dev/null +++ b/src/app/providers/index.tsx @@ -0,0 +1,41 @@ +"use client"; + +import { createContext, useContext, useState } from "react"; + +type ModalContextType = { + isOpen: boolean; + openModal: () => void; + closeModal: () => void; +}; + +interface Props { + children: React.ReactNode; +} + +const ModalContext = createContext(undefined); + +export const ModalProvider: React.FC = ({ children }) => { + const [isOpen, setIsOpen] = useState(false); + + const openModal = () => { + setIsOpen(true); + }; + + const closeModal = () => { + setIsOpen(false); + }; + + return ( + + {children} + + ); +}; + +export const useModal = () => { + const context = useContext(ModalContext); + if (context === undefined) { + throw new Error("useModal must be used within a ModalProvider"); + } + return context; +}; diff --git a/src/server/api/routers/board.ts b/src/server/api/routers/board.ts index f725fe41..e5c4a287 100644 --- a/src/server/api/routers/board.ts +++ b/src/server/api/routers/board.ts @@ -1,14 +1,38 @@ +import { z } from "zod"; + +import { boards } from "~/server/db/schema"; +import { generateUID } from "~/utils/generateUID"; + import { createTRPCRouter, publicProcedure, } from "~/server/api/trpc"; export const boardRouter = createTRPCRouter({ - all: publicProcedure.query(({ ctx }) => { + all: publicProcedure.query(async ({ ctx }) => { const userId = ctx.session?.user.id; if (!userId) return; - return ctx.db.query.boards.findMany(); - }) + const boards = await ctx.db.query.boards.findMany(); + + return boards.map((board) => ({ ...board, id: board.publicId })) + }), + create: publicProcedure + .input( + z.object({ + name: z.string().min(1), + }), + ) + .mutation(({ ctx, input }) => { + const userId = ctx.session?.user.id; + + if (!userId) return; + + return ctx.db.insert(boards).values({ + publicId: generateUID(), + name: input.name, + createdBy: userId, + }); + }), }); diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts index a2f2aa17..6e2f4e44 100644 --- a/src/server/db/schema.ts +++ b/src/server/db/schema.ts @@ -22,10 +22,11 @@ export const mysqlTable = mysqlTableCreator((name) => `kan.${name}`); export const boards = mysqlTable( "board", { - id: varchar("id", { length: 12 }).notNull().primaryKey(), + id: bigint("id", { mode: "number" }).primaryKey().autoincrement(), + publicId: varchar("publicId", { length: 12 }).notNull(), name: varchar("name", { length: 255 }), createdBy: varchar("createdBy", { length: 255 }).notNull(), - createdAt: timestamp("created_at") + createdAt: timestamp("createdAt") .default(sql`CURRENT_TIMESTAMP`) .notNull(), updatedAt: timestamp("updatedAt").onUpdateNow(), diff --git a/src/trpc/react.tsx b/src/trpc/react.tsx index 6429613d..3e3eb5d8 100644 --- a/src/trpc/react.tsx +++ b/src/trpc/react.tsx @@ -34,7 +34,7 @@ export function TRPCReactProvider(props: { }, }), ], - }) + }), ); return ( diff --git a/src/utils/generateUID.ts b/src/utils/generateUID.ts new file mode 100644 index 00000000..3fb25fa9 --- /dev/null +++ b/src/utils/generateUID.ts @@ -0,0 +1,10 @@ +import { customAlphabet } from "nanoid"; + +const alphabet = "0123456789abcdefghijklmnopqrstuvwxyz"; +const length = 12; + +const nanoid = customAlphabet(alphabet, length); + +export function generateUID() { + return nanoid(); +} \ No newline at end of file