feat: add card labels

This commit is contained in:
Henry
2023-12-21 17:43:40 +00:00
parent 0c6140921d
commit 0116a1ae2c
13 changed files with 1685 additions and 37 deletions

View File

@@ -28,7 +28,7 @@ const Modal: React.FC<Props> = ({ children }) => {
</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">
<div className="flex min-h-full items-start justify-center p-4 text-center sm:items-start sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
@@ -38,7 +38,7 @@ const Modal: React.FC<Props> = ({ children }) => {
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">
<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">
{children}
</Dialog.Panel>
</Transition.Child>

View File

@@ -30,6 +30,17 @@ interface List {
interface Card {
publicId: string;
title: string;
labels?: Label[];
}
interface Label {
label: LabelData;
}
interface LabelData {
publicId: string;
name: string;
colourCode: string;
}
interface FormValues {
@@ -226,12 +237,32 @@ export default function BoardPage() {
<Link
key={card.publicId}
href={`/cards/${card.publicId}`}
className="mb-2 flex !cursor-pointer rounded-md border border-dark-200 bg-dark-500 px-3 py-2 text-sm text-dark-1000"
className="mb-2 flex !cursor-pointer flex-col rounded-md border border-dark-200 bg-dark-500 px-3 py-2 text-sm text-dark-1000"
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
{card.title}
<div>{card.title}</div>
{card.labels?.length ? (
<div className="mt-2 flex justify-end space-x-1">
{card.labels.map(({ label }) => (
<span
key={label.publicId}
className="inline-flex w-fit items-center gap-x-1.5 rounded-full px-2 py-1 text-[10px] font-medium text-dark-1000 ring-1 ring-inset ring-dark-800"
>
<svg
fill={label.colourCode}
className="h-2 w-2"
viewBox="0 0 6 6"
aria-hidden="true"
>
<circle cx={3} cy={3} r={3} />
</svg>
<div>{label.name}</div>
</span>
))}
</div>
) : null}
</Link>
)}
</Draggable>

View File

@@ -10,7 +10,7 @@ export default function Dropdown() {
<Menu as="div" className="relative inline-block text-left">
<div>
<Menu.Button className="flex h-8 w-8 items-center justify-center rounded-[5px] hover:bg-dark-200">
<HiEllipsisHorizontal size={25} color="white" />
<HiEllipsisHorizontal size={25} className="text-dark-900" />
</Menu.Button>
</div>

View File

@@ -0,0 +1,140 @@
import { Fragment } from "react";
import { api } from "~/trpc/react";
import { Menu, Transition } from "@headlessui/react";
import { HiMiniPlus } from "react-icons/hi2";
import { useFormik } from "formik";
interface LabelSelectorProps {
cardPublicId: string;
labels: {
publicId: string;
name: string;
selected: boolean;
colourCode: string;
}[];
}
export default function LabelSelector({
cardPublicId,
labels,
}: LabelSelectorProps) {
const utils = api.useUtils();
const refetchCard = () => utils.card.byId.refetch({ id: cardPublicId });
const addOrRemoveLabel = api.card.addOrRemoveLabel.useMutation({
onSuccess: async () => {
await refetchCard();
},
});
const formik = useFormik({
initialValues: {
...Object.fromEntries(
labels?.map((label) => [label.publicId, label.selected]) ?? [],
),
},
onSubmit: (values) => {
console.log({ values });
},
enableReinitialize: true,
});
const selectedLabels = labels.filter((label) => label.selected);
return (
<>
<Menu
as="div"
className="relative flex w-full flex-wrap items-center text-left"
>
{selectedLabels.length ? (
<>
{selectedLabels.map((label) => (
<Menu.Button
key={label.publicId}
className="my-1 mr-2 inline-flex w-fit items-center gap-x-1.5 rounded-full px-2 py-1 text-[12px] font-medium text-dark-1000 ring-1 ring-inset ring-dark-800"
>
<svg
fill={label.colourCode}
className="h-2 w-2"
viewBox="0 0 6 6"
aria-hidden="true"
>
<circle cx={3} cy={3} r={3} />
</svg>
<div>{label.name}</div>
</Menu.Button>
))}
<Menu.Button className="my-1 inline-flex w-fit items-center gap-x-1.5 rounded-full py-1 pl-2 pr-4 text-[12px] font-medium text-dark-800 ring-inset ring-dark-800 hover:bg-dark-200">
<HiMiniPlus size={16} />
Add label
</Menu.Button>
</>
) : (
<Menu.Button className="flex h-full w-full items-center rounded-[5px] border-[1px] border-dark-100 pl-2 text-left text-sm hover:border-dark-300 hover:bg-dark-200">
<HiMiniPlus size={22} className="pr-2" />
Add label
</Menu.Button>
)}
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute right-[200px] top-[30px] z-10 mt-2 w-56 origin-top-right rounded-md border-[1px] border-dark-500 bg-dark-200 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<div className="p-2">
<form onSubmit={formik.handleSubmit}>
{labels?.map((label) => (
<Menu.Item key={label.publicId}>
{() => (
<div
key={label.publicId}
className="flex items-center rounded-[5px] p-2 hover:bg-dark-300"
onClick={async (e) => {
e.preventDefault();
await formik.setFieldValue(
label.publicId,
!formik.values[label.publicId],
);
addOrRemoveLabel.mutate({
cardPublicId,
labelPublicId: label.publicId,
});
await formik.submitForm();
}}
>
<input
id={label.publicId}
name={label.publicId}
type="checkbox"
className="h-[14px] w-[14px] rounded bg-transparent"
onClick={(event) => event.stopPropagation()}
onChange={formik.handleChange}
checked={formik.values[label.publicId]}
/>
<label
htmlFor={label.publicId}
className="ml-3 text-sm text-gray-500"
>
{label.name}
</label>
</div>
)}
</Menu.Item>
))}
</form>
</div>
</Menu.Items>
</Transition>
</Menu>
</>
);
}

View File

@@ -6,11 +6,13 @@ import ContentEditable from "react-contenteditable";
import Dropdown from "./components/Dropdown";
import { DeleteCardConfirmation } from "./components/DeleteCardConfirmation";
import LabelSelector from "./components/LabelSelector";
import Modal from "~/app/_components/modal";
import { useModal } from "~/app/providers/modal";
import { api } from "~/trpc/react";
interface FormValues {
cardId: string;
title: string;
@@ -26,6 +28,21 @@ export default function CardPage() {
const { data } = api.card.byId.useQuery({ id: cardId ?? "" });
const boardId = data?.list?.board?.publicId;
const labels = data?.list?.board?.labels;
const selectedLabels = data?.labels;
const formattedLabels =
labels?.map((label) => {
const isSelected = selectedLabels?.some(
(selectedLabel) => selectedLabel.label.publicId === label.publicId,
);
return {
...label,
selected: isSelected ?? false,
colourCode: label.colourCode ?? "",
};
}) ?? [];
const updateCard = api.card.update.useMutation();
@@ -48,40 +65,49 @@ export default function CardPage() {
if (!cardId) return <></>;
return (
<div className="p-8">
<div className="mb-8 flex w-full justify-between">
<form onSubmit={formik.handleSubmit} className="w-full space-y-6">
<div>
<input
type="text"
id="title"
name="title"
value={formik.values.title}
onChange={formik.handleChange}
onBlur={formik.submitForm}
className="block w-full border-0 bg-transparent p-0 py-1.5 font-medium tracking-tight text-dark-1000 focus:ring-0 sm:text-[1.2rem] sm:leading-6"
/>
<div className="flex h-full flex-1 flex-row">
<div className="w-full p-8">
<div className="mb-8 flex w-full justify-between">
<form onSubmit={formik.handleSubmit} className="w-full space-y-6">
<div>
<input
type="text"
id="title"
name="title"
value={formik.values.title}
onChange={formik.handleChange}
onBlur={formik.submitForm}
className="block w-full border-0 bg-transparent p-0 py-0 font-medium tracking-tight text-dark-1000 focus:ring-0 sm:text-[1.2rem] sm:leading-6"
/>
</div>
</form>
<div className="flex">
<Dropdown />
</div>
</form>
<div className="flex">
<Dropdown />
</div>
<div className="mb-8 flex w-full max-w-2xl justify-between">
<form onSubmit={formik.handleSubmit} className="w-full space-y-6">
<div className="mt-2">
<ContentEditable
html={formik.values.description || "Add description..."}
disabled={false}
onChange={(e) =>
formik.setFieldValue("description", e.target.value)
}
onBlur={formik.submitForm}
className="block w-full border-0 bg-transparent py-1.5 text-dark-1000 focus-visible:outline-none sm:text-sm sm:leading-6"
/>
</div>
</form>
</div>
</div>
<div className="mb-8 flex w-full max-w-2xl justify-between">
<form onSubmit={formik.handleSubmit} className="w-full space-y-6">
<div className="mt-2">
<ContentEditable
html={formik.values.description || "Add description..."}
disabled={false}
onChange={(e) =>
formik.setFieldValue("description", e.target.value)
}
onBlur={formik.submitForm}
className="block w-full border-0 bg-transparent py-1.5 text-dark-1000 focus-visible:outline-none sm:text-sm sm:leading-6"
/>
</div>
</form>
<div className="min-w-[325px] border-l-[1px] border-dark-400 bg-dark-100 p-8 text-dark-900">
<div className="flex w-full">
<p className="my-2 pr-8 text-sm">Labels</p>
<LabelSelector cardPublicId={cardId} labels={formattedLabels} />
</div>
</div>
<Modal>
{modalContentType === "DELETE_CARD" && (
<DeleteCardConfirmation