feat: card checklists (#141)

* feat: setup checklist schema

* feat: scaffold checklist router

* feat: add create new checklist modal

* feat: create new checklist item

* feat: toggle checklist item completed state

* feat: update checklist name

* feat: delete checklist

* feat: show checklists progress on cards

* feat: tweak light mode styling

* feat: focus item form on creation of new checklist

* feat: tweak new checklist item form styling

* feat: add checklist activity

* feat: show checklist progress on public board page

* feat: display checklist items on public card modal

* chore: add translations
This commit is contained in:
Henry
2025-08-14 19:59:36 +01:00
committed by GitHub
parent 80d1c5f805
commit 13b10471ec
45 changed files with 8171 additions and 684 deletions

View File

@@ -1,5 +1,6 @@
import Avatar from "~/components/Avatar";
import Badge from "~/components/Badge";
import CircularProgress from "~/components/CircularProgress";
import LabelIcon from "~/components/LabelIcon";
import { getAvatarUrl } from "~/utils/helpers";
@@ -7,6 +8,7 @@ const Card = ({
title,
labels,
members,
checklists,
}: {
title: string;
labels: { name: string; colourCode: string | null }[];
@@ -15,7 +17,28 @@ const Card = ({
email: string;
user: { name: string | null; email: string; image: string | null } | null;
}[];
checklists: {
publicId: string;
name: string;
items: {
publicId: string;
title: string;
completed: boolean;
index: number;
}[];
}[];
}) => {
const completedItems = checklists.reduce((acc, checklist) => {
return acc + checklist.items.filter((item) => item.completed).length;
}, 0);
const totalItems = checklists.reduce((acc, checklist) => {
return acc + checklist.items.length;
}, 0);
const progress =
totalItems > 0 ? Math.round((completedItems / totalItems) * 100) : 0;
return (
<div className="flex flex-col rounded-md border border-light-200 bg-light-50 px-3 py-2 text-sm text-neutral-900 dark:border-dark-200 dark:bg-dark-200 dark:text-dark-1000 dark:hover:bg-dark-300">
<span>{title}</span>
@@ -29,21 +52,37 @@ const Card = ({
/>
))}
</div>
<div className="isolate flex justify-end -space-x-1 overflow-hidden">
{members.map(({ user, email }) => {
const avatarUrl = user?.image
? getAvatarUrl(user.image)
: undefined;
return (
<Avatar
name={user?.name ?? ""}
email={user?.email ?? email}
imageUrl={avatarUrl}
<div className="flex items-center justify-end gap-1">
{checklists.length > 0 && (
<div className="flex items-center gap-1 rounded-full border-[1px] border-light-300 px-2 py-1 dark:border-dark-600">
<CircularProgress
progress={progress || 2}
size="sm"
className="flex-shrink-0"
/>
);
})}
<span className="text-[10px] text-light-900 dark:text-dark-950">
{completedItems}/{totalItems}
</span>
</div>
)}
{members.length > 0 && (
<div className="isolate flex justify-end -space-x-1 overflow-hidden">
{members.map(({ user, email }) => {
const avatarUrl = user?.image
? getAvatarUrl(user.image)
: undefined;
return (
<Avatar
name={user?.name ?? ""}
email={user?.email ?? email}
imageUrl={avatarUrl}
size="sm"
/>
);
})}
</div>
)}
</div>
</div>
) : null}

View File

@@ -13,7 +13,6 @@ import type { UpdateBoardInput } from "@kan/api/types";
import Button from "~/components/Button";
import { DeleteLabelConfirmation } from "~/components/DeleteLabelConfirmation";
import FeedbackModal from "~/components/FeedbackModal";
import { LabelForm } from "~/components/LabelForm";
import Modal from "~/components/modal";
import { NewWorkspaceForm } from "~/components/NewWorkspaceForm";
@@ -238,15 +237,15 @@ export default function BoardPage() {
const renderModalContent = () => {
return (
<>
<Modal
modalSize="sm"
<Modal
modalSize="sm"
isVisible={isOpen && modalContentType === "DELETE_BOARD"}
>
<DeleteBoardConfirmation boardPublicId={boardId ?? ""} />
</Modal>
<Modal
modalSize="sm"
<Modal
modalSize="sm"
isVisible={isOpen && modalContentType === "DELETE_LIST"}
>
<DeleteListConfirmation
@@ -255,8 +254,8 @@ export default function BoardPage() {
/>
</Modal>
<Modal
modalSize="md"
<Modal
modalSize="md"
isVisible={isOpen && modalContentType === "NEW_CARD"}
>
<NewCardForm
@@ -266,8 +265,8 @@ export default function BoardPage() {
/>
</Modal>
<Modal
modalSize="sm"
<Modal
modalSize="sm"
isVisible={isOpen && modalContentType === "NEW_LIST"}
>
<NewListForm
@@ -276,22 +275,22 @@ export default function BoardPage() {
/>
</Modal>
<Modal
modalSize="sm"
<Modal
modalSize="sm"
isVisible={isOpen && modalContentType === "NEW_WORKSPACE"}
>
<NewWorkspaceForm />
</Modal>
<Modal
modalSize="sm"
<Modal
modalSize="sm"
isVisible={isOpen && modalContentType === "NEW_LABEL"}
>
<LabelForm boardPublicId={boardId ?? ""} refetch={refetchBoard} />
</Modal>
<Modal
modalSize="sm"
<Modal
modalSize="sm"
isVisible={isOpen && modalContentType === "EDIT_LABEL"}
>
<LabelForm
@@ -301,8 +300,8 @@ export default function BoardPage() {
/>
</Modal>
<Modal
modalSize="sm"
<Modal
modalSize="sm"
isVisible={isOpen && modalContentType === "DELETE_LABEL"}
>
<DeleteLabelConfirmation
@@ -311,8 +310,8 @@ export default function BoardPage() {
/>
</Modal>
<Modal
modalSize="sm"
<Modal
modalSize="sm"
isVisible={isOpen && modalContentType === "UPDATE_BOARD_SLUG"}
>
<UpdateBoardSlugForm
@@ -492,6 +491,7 @@ export default function BoardPage() {
title={card.title}
labels={card.labels}
members={card.members}
checklists={card.checklists ?? []}
/>
</Link>
)}