feat: create new label
This commit is contained in:
@@ -38,7 +38,7 @@ const Modal: React.FC<Props> = ({ children }) => {
|
|||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="relative mt-[25vh] 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:mb-8 sm:w-full sm:max-w-sm sm:p-6">
|
<Dialog.Panel className="relative mt-[25vh] transform rounded-lg border border-dark-400 bg-dark-100 px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:mb-8 sm:w-full sm:max-w-sm sm:p-6">
|
||||||
{children}
|
{children}
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { Menu, Transition } from "@headlessui/react";
|
|||||||
import { HiMiniPlus } from "react-icons/hi2";
|
import { HiMiniPlus } from "react-icons/hi2";
|
||||||
import { useFormik } from "formik";
|
import { useFormik } from "formik";
|
||||||
|
|
||||||
|
import { useModal } from "~/app/providers/modal";
|
||||||
|
|
||||||
interface LabelSelectorProps {
|
interface LabelSelectorProps {
|
||||||
cardPublicId: string;
|
cardPublicId: string;
|
||||||
labels: {
|
labels: {
|
||||||
@@ -18,6 +20,7 @@ export default function LabelSelector({
|
|||||||
cardPublicId,
|
cardPublicId,
|
||||||
labels,
|
labels,
|
||||||
}: LabelSelectorProps) {
|
}: LabelSelectorProps) {
|
||||||
|
const { openModal } = useModal();
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
const refetchCard = () => utils.card.byId.refetch({ id: cardPublicId });
|
const refetchCard = () => utils.card.byId.refetch({ id: cardPublicId });
|
||||||
@@ -122,7 +125,7 @@ export default function LabelSelector({
|
|||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
htmlFor={label.publicId}
|
htmlFor={label.publicId}
|
||||||
className="ml-3 text-sm text-gray-500"
|
className="ml-3 text-sm"
|
||||||
>
|
>
|
||||||
{label.name}
|
{label.name}
|
||||||
</label>
|
</label>
|
||||||
@@ -130,6 +133,13 @@ export default function LabelSelector({
|
|||||||
)}
|
)}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
))}
|
))}
|
||||||
|
<button
|
||||||
|
onClick={() => openModal("NEW_LABEL")}
|
||||||
|
className="flex w-full items-center rounded-[5px] p-1.5 px-2 text-sm hover:bg-dark-300"
|
||||||
|
>
|
||||||
|
<HiMiniPlus size={22} className="pr-2" />
|
||||||
|
Create new label
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</Menu.Items>
|
</Menu.Items>
|
||||||
|
|||||||
164
src/app/cards/[...id]/components/NewLabelForm.tsx
Normal file
164
src/app/cards/[...id]/components/NewLabelForm.tsx
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
"use client";
|
||||||
|
import { Fragment, useState } from "react";
|
||||||
|
import { Listbox, Transition } from "@headlessui/react";
|
||||||
|
import { api } from "~/trpc/react";
|
||||||
|
|
||||||
|
import { HiChevronUpDown, HiXMark } from "react-icons/hi2";
|
||||||
|
|
||||||
|
import { useModal } from "~/app/providers/modal";
|
||||||
|
|
||||||
|
import { Formik, Form, Field } from "formik";
|
||||||
|
|
||||||
|
interface FormValues {
|
||||||
|
name: string;
|
||||||
|
colour: Colour | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Colour {
|
||||||
|
name: string;
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface cardPublicId {
|
||||||
|
cardPublicId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const colours = [
|
||||||
|
{ name: "Teal", code: "#0d9488" },
|
||||||
|
{ name: "Green", code: "#65a30d" },
|
||||||
|
{ name: "Blue", code: "#0284c7" },
|
||||||
|
{ name: "Purple", code: "#4f46e5" },
|
||||||
|
{ name: "Yellow", code: "#ca8a04" },
|
||||||
|
{ name: "Orange", code: "#ea580c " },
|
||||||
|
{ name: "Red", code: "#dc2626" },
|
||||||
|
{ name: "Pink", code: "#db2777" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function NewLabelForm({ cardPublicId }: cardPublicId) {
|
||||||
|
const [selected, setSelected] = useState(colours[0]);
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const { closeModal } = useModal();
|
||||||
|
|
||||||
|
const refetchCard = () => utils.card.byId.refetch({ id: cardPublicId });
|
||||||
|
|
||||||
|
const createLabel = api.label.create.useMutation({
|
||||||
|
onSuccess: async () => {
|
||||||
|
try {
|
||||||
|
await refetchCard();
|
||||||
|
closeModal();
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex w-full justify-between pb-4">
|
||||||
|
<h2 className="text-sm font-medium text-dark-1000">New label</h2>
|
||||||
|
<button
|
||||||
|
className="rounded p-1 hover:bg-dark-300"
|
||||||
|
onClick={() => closeModal()}
|
||||||
|
>
|
||||||
|
<HiXMark size={18} className="text-dark-900" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
name: "",
|
||||||
|
colour: colours[0],
|
||||||
|
}}
|
||||||
|
onSubmit={(values: FormValues) => {
|
||||||
|
if (!values.colour?.code) return;
|
||||||
|
|
||||||
|
createLabel.mutate({
|
||||||
|
name: values.name,
|
||||||
|
cardPublicId,
|
||||||
|
colourCode: values.colour.code,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
<Listbox value={selected} onChange={setSelected}>
|
||||||
|
{({ open }) => (
|
||||||
|
<>
|
||||||
|
<div className="relative mt-4">
|
||||||
|
<Listbox.Button className="block w-full rounded-md border-0 bg-dark-300 bg-white/5 px-4 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">
|
||||||
|
<span className="flex items-center">
|
||||||
|
<span
|
||||||
|
style={{ backgroundColor: selected?.code }}
|
||||||
|
className={`inline-block h-2 w-2 flex-shrink-0 rounded-full`}
|
||||||
|
/>
|
||||||
|
<span className="ml-3 block truncate">
|
||||||
|
{selected?.name}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||||
|
<HiChevronUpDown
|
||||||
|
className="h-5 w-5 text-gray-400"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</Listbox.Button>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
show={open}
|
||||||
|
as={Fragment}
|
||||||
|
leave="transition ease-in duration-100"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<Listbox.Options className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-dark-300 py-2 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
|
||||||
|
{colours.map((colour, index) => (
|
||||||
|
<Listbox.Option
|
||||||
|
key={`colours_${index}`}
|
||||||
|
className="relative cursor-default select-none px-2 text-dark-1000 "
|
||||||
|
value={colour}
|
||||||
|
>
|
||||||
|
{() => (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center rounded-[5px] p-2 hover:bg-dark-400">
|
||||||
|
<span
|
||||||
|
style={{ backgroundColor: colour?.code }}
|
||||||
|
className="ml-2 inline-block h-2 w-2 flex-shrink-0 rounded-full"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<span className="ml-3 block truncate font-normal">
|
||||||
|
{colour.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Listbox.Option>
|
||||||
|
))}
|
||||||
|
</Listbox.Options>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Listbox>
|
||||||
|
<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 label
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</Formik>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import ContentEditable from "react-contenteditable";
|
|||||||
import Dropdown from "./components/Dropdown";
|
import Dropdown from "./components/Dropdown";
|
||||||
import { DeleteCardConfirmation } from "./components/DeleteCardConfirmation";
|
import { DeleteCardConfirmation } from "./components/DeleteCardConfirmation";
|
||||||
import LabelSelector from "./components/LabelSelector";
|
import LabelSelector from "./components/LabelSelector";
|
||||||
|
import { NewLabelForm } from "./components/NewLabelForm";
|
||||||
|
|
||||||
import Modal from "~/app/_components/modal";
|
import Modal from "~/app/_components/modal";
|
||||||
import { useModal } from "~/app/providers/modal";
|
import { useModal } from "~/app/providers/modal";
|
||||||
@@ -109,6 +110,9 @@ export default function CardPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal>
|
<Modal>
|
||||||
|
{modalContentType === "NEW_LABEL" && (
|
||||||
|
<NewLabelForm cardPublicId={cardId} />
|
||||||
|
)}
|
||||||
{modalContentType === "DELETE_CARD" && (
|
{modalContentType === "DELETE_CARD" && (
|
||||||
<DeleteCardConfirmation
|
<DeleteCardConfirmation
|
||||||
boardPublicId={boardId ?? ""}
|
boardPublicId={boardId ?? ""}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { boardRouter } from "~/server/api/routers/board";
|
import { boardRouter } from "~/server/api/routers/board";
|
||||||
import { cardRouter } from "~/server/api/routers/card";
|
import { cardRouter } from "~/server/api/routers/card";
|
||||||
|
import { labelRouter } from "~/server/api/routers/label";
|
||||||
import { listRouter } from "~/server/api/routers/list";
|
import { listRouter } from "~/server/api/routers/list";
|
||||||
import { createTRPCRouter } from "~/server/api/trpc";
|
import { createTRPCRouter } from "~/server/api/trpc";
|
||||||
|
|
||||||
@@ -11,6 +12,7 @@ import { createTRPCRouter } from "~/server/api/trpc";
|
|||||||
export const appRouter = createTRPCRouter({
|
export const appRouter = createTRPCRouter({
|
||||||
board: boardRouter,
|
board: boardRouter,
|
||||||
card: cardRouter,
|
card: cardRouter,
|
||||||
|
label: labelRouter,
|
||||||
list: listRouter,
|
list: listRouter,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
54
src/server/api/routers/label.ts
Normal file
54
src/server/api/routers/label.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import { and, eq, isNull } from "drizzle-orm";
|
||||||
|
|
||||||
|
import { cards, cardsToLabels, labels } from "~/server/db/schema";
|
||||||
|
import { generateUID } from "~/utils/generateUID";
|
||||||
|
|
||||||
|
import {
|
||||||
|
createTRPCRouter,
|
||||||
|
publicProcedure,
|
||||||
|
} from "~/server/api/trpc";
|
||||||
|
|
||||||
|
export const labelRouter = createTRPCRouter({
|
||||||
|
create: publicProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
name: z.string().min(1).max(36),
|
||||||
|
cardPublicId: z.string().min(12),
|
||||||
|
colourCode: z.string().length(7),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(({ ctx, input }) => {
|
||||||
|
const userId = ctx.session?.user.id;
|
||||||
|
|
||||||
|
if (!userId) return;
|
||||||
|
|
||||||
|
return ctx.db.transaction(async (tx) => {
|
||||||
|
const card = await tx.query.cards.findFirst({
|
||||||
|
where: and(eq(cards.publicId, input.cardPublicId), isNull(cards.deletedAt)),
|
||||||
|
with: {
|
||||||
|
list: {
|
||||||
|
with: {
|
||||||
|
board: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!card) return;
|
||||||
|
|
||||||
|
const newLabel = await tx.insert(labels).values({
|
||||||
|
publicId: generateUID(),
|
||||||
|
name: input.name,
|
||||||
|
colourCode: input.colourCode,
|
||||||
|
createdBy: userId,
|
||||||
|
boardId: card.list.boardId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return tx.insert(cardsToLabels).values({
|
||||||
|
cardId: card.id,
|
||||||
|
labelId: Number(newLabel.insertId),
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
});
|
||||||
@@ -79,7 +79,5 @@ export const listRouter = createTRPCRouter({
|
|||||||
WHERE ${lists.boardId} = ${list.boardId};
|
WHERE ${lists.boardId} = ${list.boardId};
|
||||||
`);
|
`);
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
Reference in New Issue
Block a user