feat: add create new checklist modal

This commit is contained in:
Henry
2025-08-06 22:22:28 +01:00
parent 1cf5808ce7
commit e43fa0c7b3
12 changed files with 309 additions and 14 deletions

View File

@@ -0,0 +1,58 @@
import { twMerge } from "tailwind-merge";
interface CircularProgressProps {
progress: number; // 0-100
size?: "sm" | "md" | "lg";
className?: string;
}
const CircularProgress = ({
progress,
size = "md",
className,
}: CircularProgressProps) => {
const radius = 40;
const circumference = 2 * Math.PI * radius;
const strokeDashoffset = circumference - (progress / 100) * circumference;
return (
<div className={twMerge("relative", className)}>
<svg
className={twMerge(
"-rotate-90 transform",
size === "sm" && "h-4 w-4",
size === "md" && "h-5 w-5",
size === "lg" && "h-8 w-8",
)}
viewBox="0 0 100 100"
>
<circle
cx="50"
cy="50"
r={radius}
fill="none"
stroke="currentColor"
strokeWidth="8"
className="text-light-300 dark:text-dark-300"
/>
<circle
cx="50"
cy="50"
r={radius}
fill="none"
stroke="currentColor"
strokeWidth="8"
strokeDasharray={circumference}
strokeDashoffset={strokeDashoffset}
strokeLinecap="round"
className={twMerge(
"transition-all duration-300 ease-in-out",
progress === 100 ? "text-green-500" : "text-blue-500",
)}
/>
</svg>
</div>
);
};
export default CircularProgress;

View File

@@ -1,5 +1,9 @@
import { t } from "@lingui/core/macro";
import { HiEllipsisHorizontal, HiOutlineTrash } from "react-icons/hi2";
import {
HiEllipsisHorizontal,
HiOutlineCheckCircle,
HiOutlineTrash,
} from "react-icons/hi2";
import Dropdown from "~/components/Dropdown";
import { useModal } from "~/providers/modal";
@@ -10,6 +14,13 @@ export default function BoardDropdown() {
return (
<Dropdown
items={[
{
label: t`Add checklist`,
action: () => openModal("ADD_CHECKLIST"),
icon: (
<HiOutlineCheckCircle className="h-[16px] w-[16px] text-dark-900" />
),
},
{
label: t`Delete card`,
action: () => openModal("DELETE_CARD"),

View File

@@ -0,0 +1,126 @@
import { t } from "@lingui/core/macro";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { HiXMark } from "react-icons/hi2";
import { generateUID } from "@kan/shared/utils";
import Button from "~/components/Button";
import Input from "~/components/Input";
import { useModal } from "~/providers/modal";
import { usePopup } from "~/providers/popup";
import { api } from "~/utils/api";
interface NewChecklistFormInput {
name: string;
cardPublicId: string;
}
export function NewChecklistForm({ cardPublicId }: { cardPublicId: string }) {
const { closeModal } = useModal();
const { showPopup } = usePopup();
const utils = api.useUtils();
const { register, handleSubmit, reset, setValue, watch } =
useForm<NewChecklistFormInput>({
defaultValues: {
name: "Checklist",
cardPublicId,
},
});
const createChecklist = api.checklist.create.useMutation({
onMutate: async (args) => {
// await utils.board.byId.cancel();
// const currentState = utils.board.byId.getData(queryParams);
// utils.board.byId.setData(queryParams, (oldBoard) => {
// if (!oldBoard) return oldBoard;
// const newList = {
// publicId: generateUID(),
// name: args.name,
// boardId: 1,
// boardPublicId,
// cards: [],
// index: oldBoard.lists.length,
// };
// const updatedLists = [...oldBoard.lists, newList];
// return { ...oldBoard, lists: updatedLists };
// });
// return { previousState: currentState };
},
onError: (_error, _newList, context) => {
// utils.board.byId.setData(queryParams, context?.previousState);
showPopup({
header: t`Unable to create checklist`,
message: t`Please try again later, or contact customer support.`,
icon: "error",
});
},
// onSettled: async () => {
// await utils.board.byId.invalidate(queryParams);
// },
});
useEffect(() => {
const nameElement: HTMLElement | null =
document.querySelector<HTMLElement>("#checklist-name");
if (nameElement) nameElement.focus();
}, []);
const onSubmit = (data: NewChecklistFormInput) => {
closeModal();
reset({
name: "",
});
createChecklist.mutate({
name: data.name,
cardPublicId: data.cardPublicId,
});
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div className="px-5 pt-5">
<div className="flex w-full items-center justify-between pb-4">
<h2 className="text-sm font-bold text-neutral-900 dark:text-dark-1000">
{t`New checklist`}
</h2>
<button
type="button"
className="rounded p-1 hover:bg-light-200 focus:outline-none dark:hover:bg-dark-300"
onClick={(e) => {
e.preventDefault();
closeModal();
}}
>
<HiXMark size={18} className="text-light-900 dark:text-dark-900" />
</button>
</div>
<Input
id="checklist-name"
placeholder={t`Checklist name`}
{...register("name")}
onKeyDown={async (e) => {
if (e.key === "Enter") {
e.preventDefault();
await handleSubmit(onSubmit)();
}
}}
/>
</div>
<div className="mt-12 flex items-center justify-end border-t border-light-600 px-5 pb-5 pt-5 dark:border-dark-600">
<div>
<Button
type="submit"
disabled={createChecklist.isPending || !watch("name")}
>
{t`Create checklist`}
</Button>
</div>
</div>
</form>
);
}

View File

@@ -5,6 +5,7 @@ import { useForm } from "react-hook-form";
import { IoChevronForwardSharp } from "react-icons/io5";
import Avatar from "~/components/Avatar";
import CircularProgress from "~/components/CircularProgress";
import Editor from "~/components/Editor";
import FeedbackModal from "~/components/FeedbackModal";
import { LabelForm } from "~/components/LabelForm";
@@ -25,6 +26,7 @@ import Dropdown from "./components/Dropdown";
import LabelSelector from "./components/LabelSelector";
import ListSelector from "./components/ListSelector";
import MemberSelector from "./components/MemberSelector";
import { NewChecklistForm } from "./components/NewChecklistForm";
import NewCommentForm from "./components/NewCommentForm";
interface FormValues {
@@ -251,6 +253,39 @@ export default function CardPage() {
</div>
</form>
</div>
{card.checklists.length > 0 && (
<div className="border-light-300 pb-4 dark:border-dark-300">
<div>
{card.checklists.map((checklist) => {
const completedItems = checklist.items.filter(
(item) => item.completed,
);
const progress =
checklist.items.length > 0
? (completedItems.length / checklist.items.length) *
100
: 2;
return (
<div
className="text-md flex items-center gap-3 font-medium text-light-900 dark:text-dark-1000"
key={checklist.publicId}
>
<span>{checklist.name}</span>
<CircularProgress
progress={progress}
size="md"
className="flex-shrink-0"
/>
<span className="text-sm text-light-900 dark:text-dark-900">
{completedItems.length}/{checklist.items.length}
</span>
</div>
);
})}
</div>
</div>
)}
<div className="border-t-[1px] border-light-300 pt-12 dark:border-dark-300">
<h2 className="text-md pb-4 font-medium text-light-900 dark:text-dark-1000">
{t`Activity`}
@@ -303,6 +338,9 @@ export default function CardPage() {
/>
)}
{modalContentType === "NEW_WORKSPACE" && <NewWorkspaceForm />}
{modalContentType === "ADD_CHECKLIST" && (
<NewChecklistForm cardPublicId={cardId} />
)}
</Modal>
</div>
</>