feat: monorepo
This commit is contained in:
221
apps/web/src/views/card/components/ActivityList.tsx
Normal file
221
apps/web/src/views/card/components/ActivityList.tsx
Normal file
@@ -0,0 +1,221 @@
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import {
|
||||
HiOutlineArrowLeft,
|
||||
HiOutlineArrowRight,
|
||||
HiOutlinePencil,
|
||||
HiOutlinePlus,
|
||||
HiOutlineTag,
|
||||
HiOutlineUserMinus,
|
||||
HiOutlineUserPlus,
|
||||
} from "react-icons/hi2";
|
||||
|
||||
import { type GetCardByIdOutput } from "@kan/api/types";
|
||||
|
||||
import Avatar from "~/components/Avatar";
|
||||
import Comment from "./Comment";
|
||||
|
||||
type ActivityType =
|
||||
NonNullable<GetCardByIdOutput>["activities"][number]["type"];
|
||||
|
||||
const ACTIVITY_TYPE_MAP = {
|
||||
"card.created": "created the card",
|
||||
"card.updated.title": "updated the title",
|
||||
"card.updated.description": "updated the description",
|
||||
"card.updated.list": "moved the card to another list",
|
||||
"card.updated.label.added": "added a label to the card",
|
||||
"card.updated.label.removed": "removed a label from the card",
|
||||
"card.updated.member.added": "added a member to the card",
|
||||
"card.updated.member.removed": "removed a member from the card",
|
||||
} as const;
|
||||
|
||||
const getActivityText = ({
|
||||
type,
|
||||
toTitle,
|
||||
fromList,
|
||||
toList,
|
||||
memberName,
|
||||
isSelf,
|
||||
label,
|
||||
}: {
|
||||
type: ActivityType;
|
||||
toTitle: string | null;
|
||||
fromList: string | null;
|
||||
toList: string | null;
|
||||
memberName: string | null;
|
||||
isSelf: boolean;
|
||||
label: string | null;
|
||||
}) => {
|
||||
if (!(type in ACTIVITY_TYPE_MAP)) return null;
|
||||
const baseText = ACTIVITY_TYPE_MAP[type as keyof typeof ACTIVITY_TYPE_MAP];
|
||||
|
||||
const TextHighlight = ({ children }: { children: React.ReactNode }) => (
|
||||
<span className="font-medium text-light-1000 dark:text-dark-1000">
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
|
||||
if (type === "card.updated.title" && toTitle) {
|
||||
return (
|
||||
<>
|
||||
updated the title to <TextHighlight>{toTitle}</TextHighlight>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === "card.updated.list" && fromList && toList) {
|
||||
return (
|
||||
<>
|
||||
moved the card from <TextHighlight>{fromList}</TextHighlight> to
|
||||
<TextHighlight>{toList}</TextHighlight>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === "card.updated.member.added" && memberName) {
|
||||
if (isSelf) return <>self-assigned the card</>;
|
||||
|
||||
return (
|
||||
<>
|
||||
assigned <TextHighlight>{memberName}</TextHighlight> to the card
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === "card.updated.member.removed" && memberName) {
|
||||
if (isSelf) return <>unassigned themselves from the card</>;
|
||||
|
||||
return (
|
||||
<>
|
||||
unassigned <TextHighlight>{memberName}</TextHighlight> from the card
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === "card.updated.label.added" && label) {
|
||||
return (
|
||||
<>
|
||||
added label <TextHighlight>{label}</TextHighlight>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === "card.updated.label.removed" && label) {
|
||||
return (
|
||||
<>
|
||||
removed label <TextHighlight>{label}</TextHighlight>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return baseText;
|
||||
};
|
||||
|
||||
const ACTIVITY_ICON_MAP: Partial<Record<ActivityType, React.ReactNode | null>> =
|
||||
{
|
||||
"card.created": <HiOutlinePlus />,
|
||||
"card.updated.title": <HiOutlinePencil />,
|
||||
"card.updated.description": <HiOutlinePencil />,
|
||||
"card.updated.label.added": <HiOutlineTag />,
|
||||
"card.updated.label.removed": <HiOutlineTag />,
|
||||
"card.updated.member.added": <HiOutlineUserPlus />,
|
||||
"card.updated.member.removed": <HiOutlineUserMinus />,
|
||||
} as const;
|
||||
|
||||
const getActivityIcon = (
|
||||
type: ActivityType,
|
||||
fromIndex?: number | null,
|
||||
toIndex?: number | null,
|
||||
): React.ReactNode | null => {
|
||||
console.log({ fromIndex, toIndex });
|
||||
if (type === "card.updated.list" && fromIndex != null && toIndex != null) {
|
||||
return fromIndex > toIndex ? (
|
||||
<HiOutlineArrowLeft />
|
||||
) : (
|
||||
<HiOutlineArrowRight />
|
||||
);
|
||||
}
|
||||
return ACTIVITY_ICON_MAP[type] ?? null;
|
||||
};
|
||||
|
||||
const ActivityList = ({
|
||||
activities,
|
||||
cardPublicId,
|
||||
isLoading,
|
||||
}: {
|
||||
activities: NonNullable<GetCardByIdOutput>["activities"];
|
||||
cardPublicId: string;
|
||||
isLoading: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex flex-col space-y-4 pt-4">
|
||||
{activities?.map((activity, index) => {
|
||||
const activityText = getActivityText({
|
||||
type: activity.type,
|
||||
toTitle: activity.toTitle,
|
||||
fromList: activity.fromList?.name ?? null,
|
||||
toList: activity.toList?.name ?? null,
|
||||
memberName: activity.member?.user?.name ?? null,
|
||||
isSelf: activity.member?.user?.id === activity.user?.id,
|
||||
label: activity.label?.name ?? null,
|
||||
});
|
||||
|
||||
if (activity.type === "card.updated.comment.added")
|
||||
return (
|
||||
<Comment
|
||||
key={activity.publicId}
|
||||
publicId={activity.comment?.publicId}
|
||||
cardPublicId={cardPublicId}
|
||||
name={activity.user?.name ?? ""}
|
||||
email={activity.user?.email ?? ""}
|
||||
isLoading={isLoading}
|
||||
createdAt={activity.createdAt}
|
||||
comment={activity.comment?.comment}
|
||||
isEdited={!!activity.comment?.updatedAt}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!activityText) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={activity.publicId}
|
||||
className="relative flex items-center space-x-2"
|
||||
>
|
||||
<div className="relative">
|
||||
<Avatar
|
||||
size="sm"
|
||||
name={activity.user?.name ?? ""}
|
||||
email={activity.user?.email ?? ""}
|
||||
icon={getActivityIcon(
|
||||
activity.type,
|
||||
activity.fromList?.index,
|
||||
activity.toList?.index,
|
||||
)}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
{index !== activities.length - 1 &&
|
||||
activities[index + 1]?.type !==
|
||||
"card.updated.comment.added" && (
|
||||
<div className="absolute bottom-[-14px] left-1/2 top-[30px] w-0.5 -translate-x-1/2 bg-light-600 dark:bg-dark-600" />
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm">
|
||||
<span className="font-medium dark:text-dark-1000">{`${activity.user?.name} `}</span>
|
||||
<span className="space-x-1 text-light-900 dark:text-dark-800">
|
||||
{activityText}
|
||||
</span>
|
||||
<span className="mx-1 text-light-900 dark:text-dark-800">·</span>
|
||||
<span className="space-x-1 text-light-900 dark:text-dark-800">
|
||||
{formatDistanceToNow(new Date(activity.createdAt), {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActivityList;
|
||||
146
apps/web/src/views/card/components/Comment.tsx
Normal file
146
apps/web/src/views/card/components/Comment.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import ContentEditable from "react-contenteditable";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
|
||||
import { api } from "~/utils/api";
|
||||
import { usePopup } from "~/providers/popup";
|
||||
|
||||
import Avatar from "~/components/Avatar";
|
||||
import Button from "~/components/Button";
|
||||
import Dropdown from "~/components/Dropdown";
|
||||
import { HiEllipsisHorizontal, HiPencil } from "react-icons/hi2";
|
||||
|
||||
interface FormValues {
|
||||
comment: string;
|
||||
}
|
||||
|
||||
const Comment = ({
|
||||
publicId,
|
||||
cardPublicId,
|
||||
name,
|
||||
email,
|
||||
isLoading,
|
||||
createdAt,
|
||||
comment,
|
||||
isEdited = false,
|
||||
}: {
|
||||
publicId: string | undefined;
|
||||
cardPublicId: string;
|
||||
name: string;
|
||||
email: string;
|
||||
isLoading: boolean;
|
||||
createdAt: string;
|
||||
comment: string | undefined;
|
||||
isEdited: boolean;
|
||||
}) => {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const utils = api.useUtils();
|
||||
const { showPopup } = usePopup();
|
||||
const { handleSubmit, setValue, watch } = useForm<FormValues>({
|
||||
defaultValues: {
|
||||
comment,
|
||||
},
|
||||
});
|
||||
|
||||
if (!publicId) return null;
|
||||
|
||||
const updateCommentMutation = api.card.updateComment.useMutation({
|
||||
onSuccess: async () => {
|
||||
await utils.card.byId.refetch();
|
||||
setIsEditing(false);
|
||||
},
|
||||
onError: () => {
|
||||
showPopup({
|
||||
header: "Unable to update comment",
|
||||
message: "Please try again later, or contact customer support.",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = (data: FormValues) => {
|
||||
updateCommentMutation.mutate({
|
||||
cardPublicId,
|
||||
comment: data.comment,
|
||||
commentPublicId: publicId,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={publicId}
|
||||
className="group relative flex w-full flex-col rounded-xl border border-light-600 bg-light-200 p-4 text-light-900 focus-visible:outline-none dark:border-dark-400 dark:bg-dark-100 dark:text-dark-1000 sm:text-sm sm:leading-6"
|
||||
>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Avatar
|
||||
size="sm"
|
||||
name={name ?? ""}
|
||||
email={email ?? ""}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
|
||||
<p className="text-sm">
|
||||
<span className="font-medium dark:text-dark-1000">{`${name} `}</span>
|
||||
<span className="mx-1 text-light-900 dark:text-dark-800">·</span>
|
||||
<span className="space-x-1 text-light-900 dark:text-dark-800">
|
||||
{formatDistanceToNow(new Date(createdAt), {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</span>
|
||||
{isEdited && (
|
||||
<span className="text-light-900 dark:text-dark-800">
|
||||
{" (edited)"}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="absolute right-4 top-4">
|
||||
<Dropdown
|
||||
items={[
|
||||
{
|
||||
label: "Edit comment",
|
||||
action: () => setIsEditing(true),
|
||||
icon: <HiPencil className="h-[18px] w-[18px] text-dark-900" />,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<HiEllipsisHorizontal className="h-5 w-5 text-light-900 dark:text-dark-800" />
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
{!isEditing ? (
|
||||
<p className="mt-2 text-sm">{comment}</p>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<ContentEditable
|
||||
placeholder="Add a comment..."
|
||||
html={watch("comment")}
|
||||
disabled={false}
|
||||
onChange={(e) => setValue("comment", e.target.value)}
|
||||
className="block w-full border-0 bg-transparent py-1.5 text-light-900 focus-visible:outline-none dark:text-dark-1000 sm:text-sm sm:leading-6"
|
||||
/>
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => setIsEditing(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
isLoading={updateCommentMutation.isPending}
|
||||
type="submit"
|
||||
size="sm"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Comment;
|
||||
@@ -0,0 +1,67 @@
|
||||
import { useRouter } from "next/navigation";
|
||||
import { api } from "~/utils/api";
|
||||
import { useModal } from "~/providers/modal";
|
||||
import { useBoard } from "~/providers/board";
|
||||
import { usePopup } from "~/providers/popup";
|
||||
|
||||
interface DeleteCardConfirmationProps {
|
||||
cardPublicId: string;
|
||||
boardPublicId: string;
|
||||
}
|
||||
|
||||
export function DeleteCardConfirmation({
|
||||
cardPublicId,
|
||||
boardPublicId,
|
||||
}: DeleteCardConfirmationProps) {
|
||||
const { closeModal } = useModal();
|
||||
const router = useRouter();
|
||||
const { removeCard, refetchBoard } = useBoard();
|
||||
const { showPopup } = usePopup();
|
||||
|
||||
const deleteCardMutation = api.card.delete.useMutation({
|
||||
onSuccess: () => refetchBoard(),
|
||||
onError: () =>
|
||||
showPopup({
|
||||
header: "Error deleting card",
|
||||
message: "Please try again later, or contact customer support.",
|
||||
}),
|
||||
});
|
||||
|
||||
const handleDeleteCard = () => {
|
||||
removeCard({
|
||||
cardPublicId,
|
||||
});
|
||||
closeModal();
|
||||
router.push(`/boards/${boardPublicId}`);
|
||||
deleteCardMutation.mutate({
|
||||
cardPublicId,
|
||||
});
|
||||
};
|
||||
|
||||
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 card?
|
||||
</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 sm:mt-6">
|
||||
<button
|
||||
className="mr-4 inline-flex justify-center rounded-md border-[1px] border-light-600 bg-light-50 px-3 py-2 text-sm font-semibold text-neutral-900 shadow-sm focus-visible:outline-none dark:border-dark-600 dark:bg-dark-300 dark:text-dark-1000"
|
||||
onClick={() => closeModal()}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDeleteCard}
|
||||
className="inline-flex justify-center rounded-md bg-light-1000 px-3 py-2 text-sm font-semibold text-light-50 shadow-sm focus-visible:outline-none dark:bg-dark-1000 dark:text-dark-50"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { api } from "~/utils/api";
|
||||
import { useModal } from "~/providers/modal";
|
||||
import { usePopup } from "~/providers/popup";
|
||||
|
||||
import Button from "~/components/Button";
|
||||
|
||||
export function DeleteLabelConfirmation({
|
||||
cardPublicId,
|
||||
labelPublicId,
|
||||
}: {
|
||||
cardPublicId: string;
|
||||
labelPublicId: string;
|
||||
}) {
|
||||
const utils = api.useUtils();
|
||||
const { closeModal } = useModal();
|
||||
const { showPopup } = usePopup();
|
||||
|
||||
const refetchCard = () => utils.card.byId.refetch({ cardPublicId });
|
||||
|
||||
const deleteLabelMutation = api.label.delete.useMutation({
|
||||
onSuccess: () => refetchCard(),
|
||||
onError: () =>
|
||||
showPopup({
|
||||
header: "Error deleting label",
|
||||
message: "Please try again later, or contact customer support.",
|
||||
}),
|
||||
});
|
||||
|
||||
const handleDeleteLabel = () => {
|
||||
closeModal();
|
||||
deleteLabelMutation.mutate({
|
||||
labelPublicId,
|
||||
});
|
||||
};
|
||||
|
||||
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 label?
|
||||
</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 variant="secondary" onClick={() => closeModal()}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleDeleteLabel}>Delete</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
44
apps/web/src/views/card/components/Dropdown.tsx
Normal file
44
apps/web/src/views/card/components/Dropdown.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 Dropdown() {
|
||||
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_CARD")}
|
||||
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 card
|
||||
</button>
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
229
apps/web/src/views/card/components/LabelForm.tsx
Normal file
229
apps/web/src/views/card/components/LabelForm.tsx
Normal file
@@ -0,0 +1,229 @@
|
||||
import { Fragment } from "react";
|
||||
import { HiChevronUpDown, HiXMark } from "react-icons/hi2";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
|
||||
import { api } from "~/utils/api";
|
||||
import { useModal } from "~/providers/modal";
|
||||
|
||||
import Button from "~/components/Button";
|
||||
import Input from "~/components/Input";
|
||||
import Toggle from "~/components/Toggle";
|
||||
|
||||
type LabelFormInput = {
|
||||
name: string;
|
||||
colour: Colour;
|
||||
isCreateAnotherEnabled?: boolean;
|
||||
};
|
||||
|
||||
type Colour = {
|
||||
name: string;
|
||||
code: 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 LabelForm({
|
||||
cardPublicId,
|
||||
isEdit,
|
||||
}: {
|
||||
cardPublicId: string;
|
||||
isEdit?: boolean;
|
||||
}) {
|
||||
const utils = api.useUtils();
|
||||
const { closeModal, entityId, openModal } = useModal();
|
||||
|
||||
const label = api.label.byPublicId.useQuery(
|
||||
{
|
||||
labelPublicId: entityId,
|
||||
},
|
||||
{
|
||||
enabled: isEdit && !!entityId,
|
||||
},
|
||||
);
|
||||
|
||||
const { control, register, reset, handleSubmit, setValue, watch } =
|
||||
useForm<LabelFormInput>({
|
||||
values: {
|
||||
name: isEdit && label?.data?.name ? label?.data?.name : "",
|
||||
colour: (isEdit && label?.data?.colourCode
|
||||
? colours.find((c) => c.code === label?.data?.colourCode)
|
||||
: colours[0]) as Colour,
|
||||
isCreateAnotherEnabled: false,
|
||||
},
|
||||
});
|
||||
|
||||
const refetchCard = () => utils.card.byId.refetch({ cardPublicId });
|
||||
|
||||
const isCreateAnotherEnabled = watch("isCreateAnotherEnabled");
|
||||
|
||||
const createLabel = api.label.create.useMutation({
|
||||
onSuccess: async () => {
|
||||
const currentColourIndex = colours.findIndex(
|
||||
(c) => c.code === watch("colour").code,
|
||||
);
|
||||
try {
|
||||
await refetchCard();
|
||||
if (!isCreateAnotherEnabled) closeModal();
|
||||
reset({
|
||||
name: "",
|
||||
colour: colours[(currentColourIndex + 1) % colours.length],
|
||||
isCreateAnotherEnabled,
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const updateLabel = api.label.update.useMutation({
|
||||
onSuccess: async () => {
|
||||
await refetchCard();
|
||||
closeModal();
|
||||
reset({
|
||||
name: "",
|
||||
colour: colours[0],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = (values: LabelFormInput) => {
|
||||
if (!values.colour?.code) return;
|
||||
|
||||
if (isEdit) {
|
||||
updateLabel.mutate({
|
||||
labelPublicId: label.data?.publicId ?? "",
|
||||
name: values.name,
|
||||
colourCode: values.colour.code,
|
||||
});
|
||||
} else {
|
||||
createLabel.mutate({
|
||||
name: values.name,
|
||||
cardPublicId,
|
||||
colourCode: values.colour.code,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="px-5 pt-5">
|
||||
<div className="flex w-full items-center justify-between pb-4 text-neutral-900 dark:text-dark-1000">
|
||||
<h2 className="text-sm font-medium">
|
||||
{isEdit ? "Edit label" : "New label"}
|
||||
</h2>
|
||||
<button
|
||||
className="rounded p-1 hover:bg-light-300 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="label-name" placeholder="Name" {...register("name")} />
|
||||
<Controller
|
||||
name="colour"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Listbox {...field}>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<div className="relative mt-4">
|
||||
<Listbox.Button className="block w-full rounded-md border-0 bg-white/5 px-4 py-1.5 shadow-sm ring-1 ring-inset ring-light-600 focus:ring-2 focus:ring-inset focus:ring-light-600 dark:bg-dark-300 dark:text-dark-1000 dark:ring-dark-700 dark:focus:ring-dark-700 sm:text-sm sm:leading-6">
|
||||
<span className="flex items-center">
|
||||
<span
|
||||
style={{ backgroundColor: field.value?.code }}
|
||||
className={`inline-block h-2 w-2 flex-shrink-0 rounded-full`}
|
||||
/>
|
||||
<span className="ml-3 block truncate">
|
||||
{field.value?.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-light-50 py-2 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:bg-dark-300 sm:text-sm">
|
||||
{colours.map((colour, index) => (
|
||||
<Listbox.Option
|
||||
key={`colours_${index}`}
|
||||
className="relative cursor-default select-none px-2 text-neutral-900 dark:text-dark-1000 "
|
||||
value={colour}
|
||||
>
|
||||
{() => (
|
||||
<>
|
||||
<div className="flex items-center rounded-[5px] p-2 hover:bg-light-200 dark: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>
|
||||
|
||||
<div className="mt-12 flex items-center justify-end border-t border-light-600 px-5 pb-5 pt-5 dark:border-dark-600">
|
||||
{!isEdit && (
|
||||
<Toggle
|
||||
label="Create another"
|
||||
isChecked={!!isCreateAnotherEnabled}
|
||||
onChange={() =>
|
||||
setValue("isCreateAnotherEnabled", !isCreateAnotherEnabled)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="space-x-2">
|
||||
{isEdit && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => openModal("DELETE_LABEL", entityId)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
<Button type="submit">
|
||||
{isEdit ? "Update label" : "Create label"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
165
apps/web/src/views/card/components/LabelSelector.tsx
Normal file
165
apps/web/src/views/card/components/LabelSelector.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
import { Fragment } from "react";
|
||||
import { api } from "~/utils/api";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
import { HiMiniPlus } from "react-icons/hi2";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { useModal } from "~/providers/modal";
|
||||
|
||||
import { HiEllipsisHorizontal } from "react-icons/hi2";
|
||||
|
||||
interface LabelSelectorProps {
|
||||
cardPublicId: string;
|
||||
labels: {
|
||||
publicId: string;
|
||||
name: string;
|
||||
selected: boolean;
|
||||
colourCode: string;
|
||||
}[];
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export default function LabelSelector({
|
||||
cardPublicId,
|
||||
labels,
|
||||
isLoading,
|
||||
}: LabelSelectorProps) {
|
||||
const { openModal } = useModal();
|
||||
const utils = api.useUtils();
|
||||
|
||||
const refetchCard = () => utils.card.byId.refetch({ cardPublicId });
|
||||
|
||||
const addOrRemoveLabel = api.card.addOrRemoveLabel.useMutation({
|
||||
onSuccess: async () => {
|
||||
await refetchCard();
|
||||
},
|
||||
});
|
||||
|
||||
const { register, handleSubmit, setValue, watch } = useForm({
|
||||
values: Object.fromEntries(
|
||||
labels?.map((label) => [label.publicId, label.selected]) ?? [],
|
||||
),
|
||||
});
|
||||
|
||||
const onSubmit = (values: Record<string, boolean>) => {
|
||||
console.log({ values });
|
||||
};
|
||||
|
||||
const selectedLabels = labels.filter((label) => label.selected);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading ? (
|
||||
<div className="flex w-full">
|
||||
<div className="h-full w-[175px] animate-pulse rounded-[5px] bg-light-300 dark:bg-dark-300" />
|
||||
</div>
|
||||
) : (
|
||||
<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-light-800 ring-1 ring-inset ring-light-600 dark:text-dark-1000 dark: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-light-400 dark: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-light-200 pl-2 text-left text-sm text-neutral-900 hover:bg-light-300 dark:border-dark-100 dark:text-dark-1000 dark:hover:border-dark-300 dark: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-light-600 bg-light-50 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-dark-500 dark:bg-dark-200">
|
||||
<div className="p-2">
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
{labels?.map((label) => (
|
||||
<Menu.Item key={label.publicId}>
|
||||
{() => (
|
||||
<div
|
||||
key={label.publicId}
|
||||
className="group flex items-center rounded-[5px] p-2 hover:bg-light-200 dark:hover:bg-dark-300"
|
||||
onClick={() => {
|
||||
const newValue = !watch(label.publicId);
|
||||
setValue(label.publicId, newValue);
|
||||
|
||||
addOrRemoveLabel.mutate({
|
||||
cardPublicId,
|
||||
labelPublicId: label.publicId,
|
||||
});
|
||||
|
||||
handleSubmit(onSubmit);
|
||||
}}
|
||||
>
|
||||
<input
|
||||
id={label.publicId}
|
||||
type="checkbox"
|
||||
className="h-[14px] w-[14px] rounded bg-transparent"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
{...register(label.publicId)}
|
||||
checked={watch(label.publicId)}
|
||||
/>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<label
|
||||
htmlFor={label.publicId}
|
||||
className="ml-3 text-sm"
|
||||
>
|
||||
{label.name}
|
||||
</label>
|
||||
<button
|
||||
className="invisible group-hover:visible"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
openModal("EDIT_LABEL", label.publicId);
|
||||
}}
|
||||
>
|
||||
<HiEllipsisHorizontal size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Menu.Item>
|
||||
))}
|
||||
<button
|
||||
onClick={() => openModal("NEW_LABEL")}
|
||||
className="flex w-full items-center rounded-[5px] p-1.5 px-2 text-sm hover:bg-light-200 dark:hover:bg-dark-300"
|
||||
>
|
||||
<HiMiniPlus size={22} className="pr-2" />
|
||||
Create new label
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
114
apps/web/src/views/card/components/ListSelector.tsx
Normal file
114
apps/web/src/views/card/components/ListSelector.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import { Fragment } from "react";
|
||||
import { api } from "~/utils/api";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
interface ListSelectorProps {
|
||||
cardPublicId: string;
|
||||
lists: {
|
||||
publicId: string;
|
||||
name: string;
|
||||
selected: boolean;
|
||||
}[];
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export default function ListSelector({
|
||||
cardPublicId,
|
||||
lists,
|
||||
isLoading,
|
||||
}: ListSelectorProps) {
|
||||
const utils = api.useUtils();
|
||||
|
||||
const refetchCard = () => utils.card.byId.refetch({ cardPublicId });
|
||||
|
||||
const updateCardList = api.card.reorder.useMutation({
|
||||
onSuccess: async () => {
|
||||
await refetchCard();
|
||||
},
|
||||
});
|
||||
|
||||
const { register, handleSubmit, setValue, watch } = useForm({
|
||||
values: Object.fromEntries(
|
||||
lists?.map((list) => [list.publicId, list.selected]) ?? [],
|
||||
),
|
||||
});
|
||||
|
||||
const onSubmit = (values: Record<string, boolean>) => {
|
||||
console.log({ values });
|
||||
};
|
||||
|
||||
const selectedList = lists.find((list) => list.selected);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading ? (
|
||||
<div className="flex w-full">
|
||||
<div className="h-full w-[150px] animate-pulse rounded-[5px] bg-light-300 dark:bg-dark-300" />
|
||||
</div>
|
||||
) : (
|
||||
<Menu
|
||||
as="div"
|
||||
className="relative flex w-full flex-wrap items-center text-left"
|
||||
>
|
||||
<Menu.Button className="flex h-full w-full items-center rounded-[5px] border-[1px] border-light-200 pl-2 text-left text-sm text-neutral-900 hover:border-light-300 hover:bg-light-300 dark:border-dark-100 dark:text-dark-1000 dark:hover:border-dark-300 dark:hover:bg-dark-200">
|
||||
{selectedList?.name}
|
||||
</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-light-600 bg-light-50 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-dark-500 dark:bg-dark-200">
|
||||
<div className="p-2">
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
{lists?.map((list) => (
|
||||
<Menu.Item key={list.publicId}>
|
||||
{() => (
|
||||
<div
|
||||
key={list.publicId}
|
||||
className="flex items-center rounded-[5px] p-2 hover:bg-light-200 dark:hover:bg-dark-300"
|
||||
onClick={() => {
|
||||
const newValue = !watch(list.publicId);
|
||||
setValue(list.publicId, newValue);
|
||||
|
||||
updateCardList.mutate({
|
||||
cardPublicId,
|
||||
newListPublicId: list.publicId,
|
||||
});
|
||||
|
||||
handleSubmit(onSubmit);
|
||||
}}
|
||||
>
|
||||
<input
|
||||
id={list.publicId}
|
||||
type="checkbox"
|
||||
className="h-[14px] w-[14px] rounded bg-transparent"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
{...register(list.publicId)}
|
||||
checked={watch(list.publicId)}
|
||||
/>
|
||||
<label
|
||||
htmlFor={list.publicId}
|
||||
className="ml-3 text-sm"
|
||||
>
|
||||
{list.name}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</form>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
141
apps/web/src/views/card/components/MemberSelector.tsx
Normal file
141
apps/web/src/views/card/components/MemberSelector.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
import { Fragment } from "react";
|
||||
import { api } from "~/utils/api";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
import { HiMiniPlus } from "react-icons/hi2";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
interface MemberSelectorProps {
|
||||
cardPublicId: string;
|
||||
members: {
|
||||
publicId: string;
|
||||
user: {
|
||||
id: string;
|
||||
name: string | null;
|
||||
};
|
||||
selected: boolean;
|
||||
}[];
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export default function MemberSelector({
|
||||
cardPublicId,
|
||||
members,
|
||||
isLoading,
|
||||
}: MemberSelectorProps) {
|
||||
const utils = api.useUtils();
|
||||
|
||||
const refetchCard = () => utils.card.byId.refetch({ cardPublicId });
|
||||
|
||||
const addOrRemoveMember = api.card.addOrRemoveMember.useMutation({
|
||||
onSuccess: async () => {
|
||||
await refetchCard();
|
||||
},
|
||||
});
|
||||
|
||||
const { register, handleSubmit, setValue, watch } = useForm({
|
||||
values: Object.fromEntries(
|
||||
members?.map((member) => [member.publicId, member.selected]) ?? [],
|
||||
),
|
||||
});
|
||||
|
||||
const onSubmit = (values: Record<string, boolean>) => {
|
||||
console.log({ values });
|
||||
};
|
||||
|
||||
const selectedMembers = members.filter((member) => member.selected);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading ? (
|
||||
<div className="flex w-full">
|
||||
<div className="h-full w-[125px] animate-pulse rounded-[5px] bg-light-300 dark:bg-dark-300" />
|
||||
</div>
|
||||
) : (
|
||||
<Menu
|
||||
as="div"
|
||||
className="relative flex w-full flex-wrap items-center text-left"
|
||||
>
|
||||
<Menu.Button className="flex h-full w-full items-center rounded-[5px] border-[1px] border-light-200 pl-2 text-left text-sm text-neutral-900 hover:bg-light-300 dark:border-dark-100 dark:text-dark-1000 dark:hover:border-dark-300 dark:hover:bg-dark-200">
|
||||
{selectedMembers.length ? (
|
||||
<div className="isolate flex -space-x-1 overflow-hidden">
|
||||
{selectedMembers.map((member) => (
|
||||
<span
|
||||
key={member.publicId}
|
||||
className="relative z-30 inline-flex h-6 w-6 items-center justify-center rounded-full bg-gray-500 ring-1 ring-light-200 dark:ring-dark-100"
|
||||
>
|
||||
<span className="text-[10px] font-medium leading-none text-white">
|
||||
{member.user?.name
|
||||
? member.user?.name
|
||||
.split(" ")
|
||||
.map((namePart) => namePart.charAt(0).toUpperCase())
|
||||
.join("")
|
||||
: null}
|
||||
</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<HiMiniPlus size={22} className="pr-2" />
|
||||
{"Add member"}
|
||||
</>
|
||||
)}
|
||||
</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-light-600 bg-light-50 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-dark-500 dark:bg-dark-200">
|
||||
<div className="p-2">
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
{members?.map((member) => (
|
||||
<Menu.Item key={member.publicId}>
|
||||
{() => (
|
||||
<div
|
||||
key={member.publicId}
|
||||
className="flex items-center rounded-[5px] p-2 hover:bg-light-200 dark:hover:bg-dark-300"
|
||||
onClick={() => {
|
||||
const newValue = !watch(member.publicId);
|
||||
setValue(member.publicId, newValue);
|
||||
|
||||
addOrRemoveMember.mutate({
|
||||
cardPublicId,
|
||||
workspaceMemberPublicId: member.publicId,
|
||||
});
|
||||
|
||||
handleSubmit(onSubmit);
|
||||
}}
|
||||
>
|
||||
<input
|
||||
id={member.publicId}
|
||||
type="checkbox"
|
||||
className="h-[14px] w-[14px] rounded bg-transparent"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
{...register(member.publicId)}
|
||||
checked={watch(member.publicId)}
|
||||
/>
|
||||
<label
|
||||
htmlFor={member.publicId}
|
||||
className="ml-3 text-sm"
|
||||
>
|
||||
{member?.user?.name}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</form>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
72
apps/web/src/views/card/components/NewCommentForm.tsx
Normal file
72
apps/web/src/views/card/components/NewCommentForm.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { useForm } from "react-hook-form";
|
||||
import ContentEditable from "react-contenteditable";
|
||||
import { HiOutlineArrowUp } from "react-icons/hi2";
|
||||
|
||||
import LoadingSpinner from "~/components/LoadingSpinner";
|
||||
|
||||
import { api } from "~/utils/api";
|
||||
import { usePopup } from "~/providers/popup";
|
||||
|
||||
interface FormValues {
|
||||
comment: string;
|
||||
}
|
||||
|
||||
const NewCommentForm = ({ cardPublicId }: { cardPublicId: string }) => {
|
||||
const utils = api.useUtils();
|
||||
const { showPopup } = usePopup();
|
||||
const { handleSubmit, setValue, watch, reset } = useForm<FormValues>({
|
||||
values: {
|
||||
comment: "",
|
||||
},
|
||||
});
|
||||
|
||||
const addCommentMutation = api.card.addComment.useMutation({
|
||||
onSuccess: async () => {
|
||||
await utils.card.byId.refetch();
|
||||
reset();
|
||||
},
|
||||
onError: () => {
|
||||
showPopup({
|
||||
header: "Unable to add comment",
|
||||
message: "Please try again later, or contact customer support.",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = (data: FormValues) => {
|
||||
addCommentMutation.mutate({
|
||||
cardPublicId,
|
||||
comment: data.comment,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="flex w-full flex-col rounded-xl border border-light-600 bg-light-200 p-4 text-light-900 focus-visible:outline-none dark:border-dark-400 dark:bg-dark-100 dark:text-dark-1000 sm:text-sm sm:leading-6"
|
||||
>
|
||||
<ContentEditable
|
||||
placeholder="Add a comment..."
|
||||
html={watch("comment")}
|
||||
disabled={false}
|
||||
onChange={(e) => setValue("comment", e.target.value)}
|
||||
className="block w-full border-0 bg-transparent py-1.5 text-light-900 focus-visible:outline-none dark:text-dark-1000 sm:text-sm sm:leading-6"
|
||||
/>
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={addCommentMutation.isPending}
|
||||
className="flex h-8 w-8 items-center justify-center rounded-full border border-light-600 bg-light-300 hover:bg-light-400 disabled:opacity-50 dark:border-dark-400 dark:bg-dark-200 dark:hover:bg-dark-400"
|
||||
>
|
||||
{addCommentMutation.isPending ? (
|
||||
<LoadingSpinner size="sm" />
|
||||
) : (
|
||||
<HiOutlineArrowUp />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewCommentForm;
|
||||
Reference in New Issue
Block a user