feat: add new board form

This commit is contained in:
Henry
2023-11-07 09:12:00 +00:00
parent f615198c0c
commit 1df15595f4
12 changed files with 228 additions and 12 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -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",

View File

@@ -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<Props> = ({ children }) => {
const { isOpen, closeModal } = useModal();
return (
<Transition.Root show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-10" onClose={closeModal}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-dark-50 bg-opacity-5 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 w-screen overflow-y-auto">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg border border-dark-400 bg-dark-100 px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6">
{children}
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
};
export default Modal;

View File

@@ -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 (
<div>
{boards?.map((board) => {
{data?.map((board) => {
return (
<Link key={board.id} href={`boards/${board.id}`}>
<div className="align-center mr-5 flex w-72 justify-center rounded-md border border-dashed border-dark-600 bg-dark-100 p-14 hover:bg-dark-200">

70
src/app/boards/create.tsx Normal file
View File

@@ -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 (
<>
<div className="flex w-full justify-between pb-4">
<h2 className="text-sm font-medium text-dark-1000">New board</h2>
<button
className="rounded p-1 hover:bg-dark-300"
onClick={() => closeModal()}
>
<HiXMark size={18} className="text-dark-900" />
</button>
</div>
<Formik
initialValues={{
name: "",
}}
onSubmit={(values: FormValues) => {
createBoard.mutate({
name: values.name,
});
}}
>
<Form>
<label
htmlFor="name"
className="block pb-2 text-sm font-normal leading-6 text-dark-1000"
>
Name
</label>
<Field
id="name"
name="name"
className="block w-full rounded-md border-0 bg-dark-300 bg-white/5 py-1.5 text-dark-1000 shadow-sm ring-1 ring-inset ring-dark-700 focus:ring-2 focus:ring-inset focus:ring-dark-700 sm:text-sm sm:leading-6"
/>
<div className="mt-5 sm:mt-6">
<button
type="submit"
className="inline-flex w-full justify-center rounded-md bg-dark-1000 px-3 py-2 text-sm font-semibold text-dark-50 shadow-sm focus-visible:outline-none"
>
Create board
</button>
</div>
</Form>
</Formik>
</>
);
}

View File

@@ -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 (
<div>
<div className="mb-8 flex w-full justify-between">
@@ -12,6 +21,7 @@ export default function BoardsPage() {
<button
type="button"
className="inline-flex items-center gap-x-1.5 rounded-md bg-dark-1000 px-3 py-2 text-sm font-semibold text-dark-50 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2"
onClick={() => openModal()}
>
<HiOutlinePlusSmall
className="-mr-0.5 h-5 w-5"
@@ -22,6 +32,10 @@ export default function BoardsPage() {
</div>
</div>
<Modal>
<NewBoardForm />
</Modal>
<div className="flex flex-row">
<Boards />
</div>

View File

@@ -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 (
<html lang="en">
<body className={`font-sans ${jakarta.className}}`}>
<TRPCReactProvider headers={headers()}>{children}</TRPCReactProvider>
<TRPCReactProvider headers={headers()}>
<ModalProvider>{children}</ModalProvider>
</TRPCReactProvider>
</body>
</html>
);

View File

@@ -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<ModalContextType | undefined>(undefined);
export const ModalProvider: React.FC<Props> = ({ children }) => {
const [isOpen, setIsOpen] = useState(false);
const openModal = () => {
setIsOpen(true);
};
const closeModal = () => {
setIsOpen(false);
};
return (
<ModalContext.Provider value={{ isOpen, openModal, closeModal }}>
{children}
</ModalContext.Provider>
);
};
export const useModal = () => {
const context = useContext(ModalContext);
if (context === undefined) {
throw new Error("useModal must be used within a ModalProvider");
}
return context;
};

View File

@@ -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,
});
}),
});

View File

@@ -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(),

View File

@@ -34,7 +34,7 @@ export function TRPCReactProvider(props: {
},
}),
],
})
}),
);
return (

10
src/utils/generateUID.ts Normal file
View File

@@ -0,0 +1,10 @@
import { customAlphabet } from "nanoid";
const alphabet = "0123456789abcdefghijklmnopqrstuvwxyz";
const length = 12;
const nanoid = customAlphabet(alphabet, length);
export function generateUID() {
return nanoid();
}