feat: monorepo
This commit is contained in:
44
apps/web/src/views/board/components/BoardDropdown.tsx
Normal file
44
apps/web/src/views/board/components/BoardDropdown.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Fragment } from "react";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
import { HiEllipsisHorizontal } from "react-icons/hi2";
|
||||
import { useModal } from "~/providers/modal";
|
||||
|
||||
export default function BoardDropdown() {
|
||||
const { openModal } = useModal();
|
||||
|
||||
return (
|
||||
<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-light-200 dark:hover:bg-dark-200">
|
||||
<HiEllipsisHorizontal
|
||||
size={25}
|
||||
className="text-light-900 dark:text-dark-900"
|
||||
/>
|
||||
</Menu.Button>
|
||||
</div>
|
||||
|
||||
<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-0 z-30 mt-2 w-56 origin-top-right rounded-md border border-light-200 bg-light-50 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-dark-400 dark:bg-dark-300">
|
||||
<div className="flex">
|
||||
<Menu.Item>
|
||||
<button
|
||||
onClick={() => openModal("DELETE_BOARD")}
|
||||
className="m-1 w-full rounded-[5px] px-3 py-2 text-left text-sm text-neutral-900 hover:bg-light-200 dark:text-dark-1000 dark:hover:bg-dark-400"
|
||||
>
|
||||
Delete board
|
||||
</button>
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { api } from "~/utils/api";
|
||||
import { useBoard } from "~/providers/board";
|
||||
import { useModal } from "~/providers/modal";
|
||||
|
||||
import Button from "~/components/Button";
|
||||
|
||||
export function DeleteBoardConfirmation() {
|
||||
const router = useRouter();
|
||||
const { boardData } = useBoard();
|
||||
const { closeModal } = useModal();
|
||||
|
||||
const deleteBoard = api.board.delete.useMutation({
|
||||
onSuccess: () => {
|
||||
closeModal();
|
||||
router.push(`/boards`);
|
||||
},
|
||||
});
|
||||
|
||||
const handleDeleteBoard = () => {
|
||||
if (boardData?.publicId)
|
||||
deleteBoard.mutate({
|
||||
boardPublicId: boardData.publicId,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-5">
|
||||
<div className="flex w-full flex-col justify-between pb-4">
|
||||
<h2 className="text-md pb-4 font-medium text-neutral-900 dark:text-dark-1000">
|
||||
Are you sure you want to delete this board?
|
||||
</h2>
|
||||
<p className="text-sm font-medium text-light-900 dark:text-dark-900">
|
||||
{"This action can't be undone."}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-5 flex justify-end space-x-2 sm:mt-6">
|
||||
<Button onClick={() => closeModal()} variant="secondary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleDeleteBoard}>Delete</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { api } from "~/utils/api";
|
||||
import { useBoard } from "~/providers/board";
|
||||
import { useModal } from "~/providers/modal";
|
||||
import { usePopup } from "~/providers/popup";
|
||||
|
||||
import Button from "~/components/Button";
|
||||
|
||||
interface DeleteListConfirmationProps {
|
||||
listPublicId: string;
|
||||
}
|
||||
|
||||
export function DeleteListConfirmation({
|
||||
listPublicId,
|
||||
}: DeleteListConfirmationProps) {
|
||||
const utils = api.useUtils();
|
||||
const { boardData } = useBoard();
|
||||
const { closeModal } = useModal();
|
||||
const { showPopup } = usePopup();
|
||||
|
||||
const refetchBoard = async () => {
|
||||
if (boardData?.publicId) {
|
||||
try {
|
||||
await utils.board.byId.refetch();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const deleteList = api.list.delete.useMutation({
|
||||
onSuccess: () => {
|
||||
closeModal();
|
||||
return refetchBoard();
|
||||
},
|
||||
onError: async () => {
|
||||
closeModal();
|
||||
await refetchBoard();
|
||||
showPopup({
|
||||
header: "Unable to delete list",
|
||||
message: "Please try again later, or contact customer support.",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="p-5">
|
||||
<div className="flex w-full flex-col justify-between pb-4">
|
||||
<h2 className="text-md pb-4 font-medium text-neutral-900 dark:text-dark-1000">
|
||||
Are you sure you want to delete this list?
|
||||
</h2>
|
||||
<p className="text-sm font-medium text-light-900 dark:text-dark-900">
|
||||
{"This action can't be undone."}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-5 flex justify-end space-x-2 sm:mt-6">
|
||||
<Button onClick={() => closeModal()} variant="secondary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
isLoading={deleteList.isPending}
|
||||
onClick={() => deleteList.mutate({ listPublicId })}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
139
apps/web/src/views/board/components/Filters.tsx
Normal file
139
apps/web/src/views/board/components/Filters.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import { IoFilterOutline } from "react-icons/io5";
|
||||
import {
|
||||
HiOutlineUserCircle,
|
||||
HiOutlineTag,
|
||||
HiMiniXMark,
|
||||
} from "react-icons/hi2";
|
||||
import { useRouter } from "next/router";
|
||||
import Button from "~/components/Button";
|
||||
import CheckboxDropdown from "~/components/CheckboxDropdown";
|
||||
import { formatToArray } from "~/utils/helpers";
|
||||
import { useBoard } from "~/providers/board";
|
||||
|
||||
const LabelIcon = ({ colourCode }: { colourCode: string | null }) => (
|
||||
<svg
|
||||
fill={colourCode ?? "#3730a3"}
|
||||
className="h-2 w-2"
|
||||
viewBox="0 0 6 6"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle cx={3} cy={3} r={3} />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const Avatar = ({ name }: { name: string }) => (
|
||||
<span className="inline-flex h-4 w-4 items-center justify-center rounded-full bg-gray-400 ring-1 ring-light-200 dark:ring-dark-500">
|
||||
<span className="text-[8px] font-medium leading-none text-white">
|
||||
{name
|
||||
?.split(" ")
|
||||
.map((namePart) => namePart.charAt(0).toUpperCase())
|
||||
.join("")}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
|
||||
const Filters = () => {
|
||||
const { boardData } = useBoard();
|
||||
const router = useRouter();
|
||||
|
||||
const clearFilters = async (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
try {
|
||||
await router.push({
|
||||
pathname: router.pathname,
|
||||
query: { ...router.query, members: [], labels: [] },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const formattedMembers =
|
||||
boardData?.workspace?.members?.map((member) => ({
|
||||
key: member.publicId,
|
||||
value: member.user?.name ?? "",
|
||||
selected: !!router.query.members?.includes(member.publicId),
|
||||
leftIcon: <Avatar name={member.user?.name ?? ""} />,
|
||||
})) ?? [];
|
||||
|
||||
const formattedLabels =
|
||||
boardData?.labels.map((label) => ({
|
||||
key: label.publicId,
|
||||
value: label.name,
|
||||
selected: !!router.query.labels?.includes(label.publicId),
|
||||
leftIcon: <LabelIcon colourCode={label.colourCode} />,
|
||||
})) ?? [];
|
||||
|
||||
const groups = [
|
||||
{
|
||||
key: "members",
|
||||
label: "Members",
|
||||
icon: <HiOutlineUserCircle size={16} />,
|
||||
items: formattedMembers,
|
||||
},
|
||||
{
|
||||
key: "labels",
|
||||
label: "Labels",
|
||||
icon: <HiOutlineTag size={16} />,
|
||||
items: formattedLabels,
|
||||
},
|
||||
];
|
||||
|
||||
const handleSelect = async (
|
||||
groupKey: string | null,
|
||||
item: { key: string },
|
||||
) => {
|
||||
if (groupKey === null) return;
|
||||
const currentQuery = router.query[groupKey] ?? [];
|
||||
const formattedCurrentQuery = Array.isArray(currentQuery)
|
||||
? currentQuery
|
||||
: [currentQuery];
|
||||
|
||||
const updatedQuery = formattedCurrentQuery.includes(item.key)
|
||||
? formattedCurrentQuery.filter((key) => key !== item.key)
|
||||
: [...formattedCurrentQuery, item.key];
|
||||
|
||||
try {
|
||||
await router.push({
|
||||
pathname: router.pathname,
|
||||
query: { ...router.query, [groupKey]: updatedQuery },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const numOfFilters = [
|
||||
...formatToArray(router.query.members),
|
||||
...formatToArray(router.query.labels),
|
||||
].length;
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<CheckboxDropdown
|
||||
groups={groups}
|
||||
handleSelect={handleSelect}
|
||||
menuSpacing="md"
|
||||
>
|
||||
<Button variant="secondary" iconLeft={<IoFilterOutline />}>
|
||||
Filter
|
||||
{numOfFilters > 0 && (
|
||||
<button
|
||||
onClick={clearFilters}
|
||||
className="group absolute -right-[18px] -top-[15px] flex h-5 w-5 items-center justify-center rounded-full border-2 border-light-100 bg-light-1000 text-[8px] font-[700] text-light-600 dark:border-dark-50 dark:bg-dark-1000 dark:text-dark-600 dark:text-dark-600"
|
||||
>
|
||||
<span className="group-hover:hidden">{numOfFilters}</span>
|
||||
<span className="hidden text-light-50 group-hover:inline dark:text-dark-50">
|
||||
<HiMiniXMark size={12} />
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</Button>
|
||||
</CheckboxDropdown>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Filters;
|
||||
135
apps/web/src/views/board/components/List.tsx
Normal file
135
apps/web/src/views/board/components/List.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import { type ReactNode } from "react";
|
||||
import {
|
||||
HiOutlinePlusSmall,
|
||||
HiEllipsisHorizontal,
|
||||
HiOutlineTrash,
|
||||
HiOutlineSquaresPlus,
|
||||
} from "react-icons/hi2";
|
||||
import { Draggable } from "react-beautiful-dnd";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { api } from "~/utils/api";
|
||||
import { useModal } from "~/providers/modal";
|
||||
|
||||
import Dropdown from "~/components/Dropdown";
|
||||
|
||||
interface ListProps {
|
||||
children: ReactNode;
|
||||
index: number;
|
||||
list: List;
|
||||
setSelectedPublicListId: (publicListId: PublicListId) => void;
|
||||
}
|
||||
|
||||
interface List {
|
||||
publicId: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface FormValues {
|
||||
listPublicId: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
type PublicListId = string;
|
||||
|
||||
export default function List({
|
||||
children,
|
||||
index,
|
||||
list,
|
||||
setSelectedPublicListId,
|
||||
}: ListProps) {
|
||||
const { openModal } = useModal();
|
||||
|
||||
const openNewCardForm = (publicListId: PublicListId) => {
|
||||
openModal("NEW_CARD");
|
||||
setSelectedPublicListId(publicListId);
|
||||
};
|
||||
|
||||
const updateList = api.list.update.useMutation();
|
||||
|
||||
const { register, handleSubmit } = useForm<FormValues>({
|
||||
defaultValues: {
|
||||
listPublicId: list.publicId,
|
||||
name: list.name,
|
||||
},
|
||||
values: {
|
||||
listPublicId: list.publicId,
|
||||
name: list.name,
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = (values: FormValues) => {
|
||||
updateList.mutate({
|
||||
listPublicId: values.listPublicId,
|
||||
name: values.name,
|
||||
});
|
||||
};
|
||||
|
||||
const handleOpenDeleteListConfirmation = () => {
|
||||
setSelectedPublicListId(list.publicId);
|
||||
openModal("DELETE_LIST");
|
||||
};
|
||||
|
||||
return (
|
||||
<Draggable key={list.publicId} draggableId={list.publicId} index={index}>
|
||||
{(provided) => (
|
||||
<div
|
||||
key={list.publicId}
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
className="dark-text-dark-1000 mr-5 h-fit min-w-[18rem] max-w-[18rem] rounded-md border border-light-400 bg-light-300 py-2 pl-2 pr-1 text-neutral-900 dark:border-dark-300 dark:bg-dark-100"
|
||||
>
|
||||
<div className="flex justify-between">
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="focus-visible:outline-none"
|
||||
>
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
{...register("name")}
|
||||
onBlur={handleSubmit(onSubmit)}
|
||||
className="mb-4 block border-0 bg-transparent px-4 pt-1 text-sm font-medium text-neutral-900 focus:ring-0 focus-visible:outline-none dark:text-dark-1000"
|
||||
/>
|
||||
</form>
|
||||
<div>
|
||||
<button
|
||||
className="mx-1 inline-flex h-fit items-center rounded-md p-1 px-1 text-sm font-semibold text-dark-50 hover:bg-light-400 dark:hover:bg-dark-200"
|
||||
onClick={() => openNewCardForm(list.publicId)}
|
||||
>
|
||||
<HiOutlinePlusSmall
|
||||
className="h-5 w-5 text-dark-900"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
<div className="relative mr-1 inline-block">
|
||||
<Dropdown
|
||||
items={[
|
||||
{
|
||||
label: "Add a card",
|
||||
action: () => openNewCardForm(list.publicId),
|
||||
icon: (
|
||||
<HiOutlineSquaresPlus className="h-[18px] w-[18px] text-dark-900" />
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Delete list",
|
||||
action: handleOpenDeleteListConfirmation,
|
||||
icon: (
|
||||
<HiOutlineTrash className="h-[18px] w-[18px] text-dark-900" />
|
||||
),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<HiEllipsisHorizontal className="h-5 w-5 text-dark-900" />
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
300
apps/web/src/views/board/components/NewCardForm.tsx
Normal file
300
apps/web/src/views/board/components/NewCardForm.tsx
Normal file
@@ -0,0 +1,300 @@
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import {
|
||||
HiOutlineBarsArrowDown,
|
||||
HiOutlineBarsArrowUp,
|
||||
HiXMark,
|
||||
} from "react-icons/hi2";
|
||||
|
||||
import { type NewCardInput } from "@kan/api/types";
|
||||
|
||||
import Button from "~/components/Button";
|
||||
import CheckboxDropdown from "~/components/CheckboxDropdown";
|
||||
import Input from "~/components/Input";
|
||||
import Toggle from "~/components/Toggle";
|
||||
import { useBoard } from "~/providers/board";
|
||||
import { useModal } from "~/providers/modal";
|
||||
import { usePopup } from "~/providers/popup";
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
type NewCardFormInput = NewCardInput & {
|
||||
isCreateAnotherEnabled: boolean;
|
||||
};
|
||||
|
||||
interface NewCardFormProps {
|
||||
listPublicId: string;
|
||||
}
|
||||
|
||||
export function NewCardForm({ listPublicId }: NewCardFormProps) {
|
||||
const { boardData, addCard, refetchBoard } = useBoard();
|
||||
const { showPopup } = usePopup();
|
||||
const { closeModal } = useModal();
|
||||
|
||||
const { register, handleSubmit, reset, setValue, watch } =
|
||||
useForm<NewCardFormInput>({
|
||||
defaultValues: {
|
||||
title: "",
|
||||
description: "",
|
||||
listPublicId,
|
||||
labelPublicIds: [],
|
||||
memberPublicIds: [],
|
||||
isCreateAnotherEnabled: false,
|
||||
position: "start",
|
||||
},
|
||||
});
|
||||
|
||||
const labelPublicIds = watch("labelPublicIds") || [];
|
||||
const memberPublicIds = watch("memberPublicIds") || [];
|
||||
const isCreateAnotherEnabled = watch("isCreateAnotherEnabled");
|
||||
const position = watch("position");
|
||||
|
||||
const createCard = api.card.create.useMutation({
|
||||
onSuccess: async () => {
|
||||
await refetchBoard();
|
||||
},
|
||||
onError: async () => {
|
||||
closeModal();
|
||||
await refetchBoard();
|
||||
showPopup({
|
||||
header: "Unable to create card",
|
||||
message: "Please try again later, or contact customer support.",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const titleElement: HTMLElement | null =
|
||||
document?.querySelector<HTMLElement>("#title");
|
||||
if (titleElement) titleElement.focus();
|
||||
}, []);
|
||||
|
||||
const formattedLabels =
|
||||
boardData?.labels.map((label) => ({
|
||||
key: label.publicId,
|
||||
value: label.name,
|
||||
selected: labelPublicIds.includes(label.publicId),
|
||||
})) ?? [];
|
||||
|
||||
const formattedLists =
|
||||
boardData?.lists.map((list) => ({
|
||||
key: list.publicId,
|
||||
value: list.name,
|
||||
selected: list.publicId === watch("listPublicId"),
|
||||
})) ?? [];
|
||||
|
||||
const formattedMembers =
|
||||
boardData?.workspace?.members?.map((member) => ({
|
||||
key: member.publicId,
|
||||
value: member.user?.name ?? "",
|
||||
selected: memberPublicIds.includes(member.publicId),
|
||||
})) ?? [];
|
||||
|
||||
const onSubmit = (data: NewCardInput) => {
|
||||
addCard(data);
|
||||
const isCreateAnotherEnabled = watch("isCreateAnotherEnabled");
|
||||
if (!isCreateAnotherEnabled) closeModal();
|
||||
reset({
|
||||
title: "",
|
||||
description: "",
|
||||
listPublicId: watch("listPublicId"),
|
||||
labelPublicIds: [],
|
||||
memberPublicIds: [],
|
||||
isCreateAnotherEnabled,
|
||||
position,
|
||||
});
|
||||
|
||||
createCard.mutate({
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
listPublicId: data.listPublicId,
|
||||
labelPublicIds: data.labelPublicIds,
|
||||
memberPublicIds: data.memberPublicIds,
|
||||
position: data.position,
|
||||
});
|
||||
};
|
||||
|
||||
const handleToggleCreateAnother = (): void => {
|
||||
setValue("isCreateAnotherEnabled", !isCreateAnotherEnabled);
|
||||
};
|
||||
|
||||
const handleSelectList = (listPublicId: string): void => {
|
||||
setValue("listPublicId", listPublicId);
|
||||
};
|
||||
|
||||
const handleSelectMembers = (memberPublicId: string): void => {
|
||||
const currentIndex = memberPublicIds.indexOf(memberPublicId);
|
||||
if (currentIndex === -1) {
|
||||
setValue("memberPublicIds", [...memberPublicIds, memberPublicId]);
|
||||
} else {
|
||||
const newMemberPublicIds = [...memberPublicIds];
|
||||
newMemberPublicIds.splice(currentIndex, 1);
|
||||
setValue("memberPublicIds", newMemberPublicIds);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectLabels = (labelPublicId: string): void => {
|
||||
const currentIndex = labelPublicIds.indexOf(labelPublicId);
|
||||
if (currentIndex === -1) {
|
||||
setValue("labelPublicIds", [...labelPublicIds, labelPublicId]);
|
||||
} else {
|
||||
const newLabelPublicIds = [...labelPublicIds];
|
||||
newLabelPublicIds.splice(currentIndex, 1);
|
||||
setValue("labelPublicIds", newLabelPublicIds);
|
||||
}
|
||||
};
|
||||
|
||||
const selectedList = formattedLists.find((item) => item.selected);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="px-5 pt-5">
|
||||
<div className="flex w-full items-center justify-between pb-5">
|
||||
<h2 className="text-sm font-bold text-neutral-900 dark:text-dark-1000">
|
||||
New card
|
||||
</h2>
|
||||
<button
|
||||
className="rounded p-1 hover:bg-light-200 focus:outline-none dark:hover:bg-dark-300"
|
||||
onClick={(e) => {
|
||||
closeModal();
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<HiXMark size={18} className="text-light-900 dark:text-dark-900" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Input id="title" placeholder="Card title" {...register("title")} />
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<Input
|
||||
placeholder="Add description..."
|
||||
onChange={(e) => setValue("description", e.target.value)}
|
||||
value={watch("description")}
|
||||
contentEditable
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 flex space-x-1">
|
||||
<div className="w-fit">
|
||||
<CheckboxDropdown
|
||||
items={formattedLists}
|
||||
handleSelect={(_groupKey, item) => handleSelectList(item.key)}
|
||||
>
|
||||
<div className="flex h-full w-full items-center rounded-[5px] border-[1px] border-light-600 bg-light-200 px-2 py-1 text-left text-xs text-light-800 hover:bg-light-300 dark:border-dark-600 dark:bg-dark-400 dark:text-dark-1000 dark:hover:bg-dark-500">
|
||||
{selectedList?.value}
|
||||
</div>
|
||||
</CheckboxDropdown>
|
||||
</div>
|
||||
<div className="w-fit">
|
||||
<CheckboxDropdown
|
||||
items={formattedMembers}
|
||||
handleSelect={(_groupKey, item) => handleSelectMembers(item.key)}
|
||||
>
|
||||
<div className="flex h-full w-full items-center rounded-[5px] border-[1px] border-light-600 bg-light-200 px-2 py-1 text-left text-xs text-light-800 hover:bg-light-300 dark:border-dark-600 dark:bg-dark-400 dark:text-dark-1000 dark:hover:bg-dark-500">
|
||||
{!memberPublicIds.length ? (
|
||||
"Members"
|
||||
) : (
|
||||
<div className="flex -space-x-1 overflow-hidden">
|
||||
{memberPublicIds.map((memberPublicId) => {
|
||||
const member = formattedMembers.find(
|
||||
(member) => member.key === memberPublicId,
|
||||
);
|
||||
|
||||
return (
|
||||
<span
|
||||
key={member?.key}
|
||||
className="inline-flex h-4 w-4 items-center justify-center rounded-full bg-gray-400 ring-1 ring-light-200 dark:ring-dark-500"
|
||||
>
|
||||
<span className="text-[8px] font-medium leading-none text-white">
|
||||
{member?.value
|
||||
?.split(" ")
|
||||
.map((namePart) =>
|
||||
namePart.charAt(0).toUpperCase(),
|
||||
)
|
||||
.join("")}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CheckboxDropdown>
|
||||
</div>
|
||||
<div className="w-fit">
|
||||
<CheckboxDropdown
|
||||
items={formattedLabels}
|
||||
handleSelect={(_groupKey, item) => handleSelectLabels(item.key)}
|
||||
>
|
||||
<div className="flex h-full w-full items-center rounded-[5px] border-[1px] border-light-600 bg-light-200 px-2 py-1 text-left text-xs text-light-800 hover:bg-light-300 dark:border-dark-600 dark:bg-dark-400 dark:text-dark-1000 dark:hover:bg-dark-500">
|
||||
{!labelPublicIds.length ? (
|
||||
"Labels"
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className={
|
||||
labelPublicIds.length > 1
|
||||
? "flex -space-x-[2px] overflow-hidden"
|
||||
: "flex items-center"
|
||||
}
|
||||
>
|
||||
{labelPublicIds.map((labelPublicId) => {
|
||||
const label = boardData?.labels.find(
|
||||
(label) => label.publicId === labelPublicId,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<svg
|
||||
fill={label?.colourCode ?? "#3730a3"}
|
||||
className="h-2 w-2"
|
||||
viewBox="0 0 6 6"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle cx={3} cy={3} r={3} />
|
||||
</svg>
|
||||
{labelPublicIds.length === 1 && (
|
||||
<div className="ml-1">{label?.name}</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{labelPublicIds.length > 1 && (
|
||||
<div className="ml-1">{`${labelPublicIds.length} labels`}</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</CheckboxDropdown>
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setValue("position", position === "start" ? "end" : "start");
|
||||
}}
|
||||
className="flex h-auto items-center rounded-[5px] border-[1px] border-light-600 bg-light-200 px-1.5 py-1 text-left text-xs text-light-800 hover:bg-light-300 focus-visible:outline-none dark:border-dark-600 dark:bg-dark-400 dark:text-dark-1000 dark:hover:bg-dark-500"
|
||||
>
|
||||
{position === "start" ? (
|
||||
<HiOutlineBarsArrowUp size={14} />
|
||||
) : (
|
||||
<HiOutlineBarsArrowDown size={14} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex items-center justify-end border-t border-light-600 px-5 pb-5 pt-5 dark:border-dark-600">
|
||||
<Toggle
|
||||
label="Create another"
|
||||
isChecked={isCreateAnotherEnabled}
|
||||
onChange={handleToggleCreateAnother}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Button type="submit">Create card</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
105
apps/web/src/views/board/components/NewListForm.tsx
Normal file
105
apps/web/src/views/board/components/NewListForm.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { HiXMark } from "react-icons/hi2";
|
||||
|
||||
import { type NewListInput } from "@kan/api/types";
|
||||
|
||||
import Button from "~/components/Button";
|
||||
import Input from "~/components/Input";
|
||||
import Toggle from "~/components/Toggle";
|
||||
import { useBoard } from "~/providers/board";
|
||||
import { useModal } from "~/providers/modal";
|
||||
import { usePopup } from "~/providers/popup";
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
type NewListFormInput = NewListInput & {
|
||||
isCreateAnotherEnabled: boolean;
|
||||
};
|
||||
|
||||
export function NewListForm({ boardPublicId }: { boardPublicId: string }) {
|
||||
const { refetchBoard, addList } = useBoard();
|
||||
const { closeModal } = useModal();
|
||||
const { showPopup } = usePopup();
|
||||
|
||||
const { register, handleSubmit, reset, setValue, watch } =
|
||||
useForm<NewListFormInput>({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
boardPublicId: boardPublicId,
|
||||
isCreateAnotherEnabled: false,
|
||||
},
|
||||
});
|
||||
|
||||
const isCreateAnotherEnabled = watch("isCreateAnotherEnabled");
|
||||
|
||||
const createList = api.list.create.useMutation({
|
||||
onSuccess: async () => {
|
||||
await refetchBoard();
|
||||
},
|
||||
onError: async () => {
|
||||
closeModal();
|
||||
await refetchBoard();
|
||||
showPopup({
|
||||
header: "Unable to create list",
|
||||
message: "Please try again later, or contact customer support.",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const nameElement: HTMLElement | null =
|
||||
document?.querySelector<HTMLElement>("#list-name");
|
||||
if (nameElement) nameElement.focus();
|
||||
}, []);
|
||||
|
||||
const onSubmit = (data: NewListInput) => {
|
||||
addList(data);
|
||||
const isCreateAnotherEnabled = watch("isCreateAnotherEnabled");
|
||||
if (!isCreateAnotherEnabled) closeModal();
|
||||
reset({
|
||||
name: "",
|
||||
isCreateAnotherEnabled,
|
||||
});
|
||||
|
||||
createList.mutate({
|
||||
name: data.name,
|
||||
boardPublicId,
|
||||
});
|
||||
};
|
||||
|
||||
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">
|
||||
New list
|
||||
</h2>
|
||||
<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="list-name" placeholder="List name" {...register("name")} />
|
||||
</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">
|
||||
<Toggle
|
||||
label="Create another"
|
||||
isChecked={isCreateAnotherEnabled}
|
||||
onChange={() =>
|
||||
setValue("isCreateAnotherEnabled", !isCreateAnotherEnabled)
|
||||
}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Button type="submit">Create list</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user