Feat: Markdown editor for description on cards (#43)
* feat: integrate MDXEditor for card description with markdown support * feat: replace textarea with markdown editor for card descriptions and improve styling * refactor: replace MDXEditor with Tiptap for rich text editing * fix: prevent menu item from taking focus and submitting * feat: replace editor menu with bubble menu and add placeholder text * feat: implement slash commands menu in editor with suggestion API * chore: update editor styling * chore: update editor placeholder text * fix: navigate command list by keyboard and tweak styling * fix: prevent editor onBlur from submitting NewCardForm --------- Co-authored-by: Henry <henry_ball@hotmail.co.uk>
This commit is contained in:
@@ -12,6 +12,7 @@ import { generateUID } from "@kan/shared/utils";
|
||||
import Avatar from "~/components/Avatar";
|
||||
import Button from "~/components/Button";
|
||||
import CheckboxDropdown from "~/components/CheckboxDropdown";
|
||||
import Editor from "~/components/Editor";
|
||||
import Input from "~/components/Input";
|
||||
import LabelIcon from "~/components/LabelIcon";
|
||||
import Toggle from "~/components/Toggle";
|
||||
@@ -115,9 +116,9 @@ export function NewCardForm({
|
||||
utils.board.byId.setData(queryParams, context?.previousState);
|
||||
showPopup({
|
||||
header: "Unable to create card",
|
||||
message: error.data?.zodError?.fieldErrors.title?.[0] ?
|
||||
`${error.data?.zodError?.fieldErrors.title?.[0].replace("String", "Title")}` :
|
||||
"Please try again later, or contact customer support.",
|
||||
message: error.data?.zodError?.fieldErrors.title?.[0]
|
||||
? `${error.data.zodError.fieldErrors.title[0].replace("String", "Title")}`
|
||||
: "Please try again later, or contact customer support.",
|
||||
icon: "error",
|
||||
});
|
||||
},
|
||||
@@ -253,18 +254,12 @@ export function NewCardForm({
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<Input
|
||||
placeholder="Add description..."
|
||||
onChange={(e) => setValue("description", e.target.value)}
|
||||
value={watch("description")}
|
||||
contentEditable
|
||||
onKeyDown={async (e) => {
|
||||
if (e.key === "Enter" && e.shiftKey) {
|
||||
e.preventDefault();
|
||||
await handleSubmit(onSubmit)();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="block max-h-48 min-h-24 w-full overflow-y-auto rounded-md border-0 bg-dark-300 bg-white/5 px-3 py-2 text-sm shadow-sm ring-1 ring-inset ring-light-600 focus-within:ring-2 focus-within:ring-inset focus-within:ring-light-700 dark:ring-dark-700 dark:focus-within:ring-dark-700 sm:leading-6">
|
||||
<Editor
|
||||
content=""
|
||||
onChange={(value) => setValue("description", value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 flex space-x-1">
|
||||
<div className="w-fit">
|
||||
@@ -389,7 +384,12 @@ export function NewCardForm({
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Button type="submit" disabled={title.length === 0 || createCard.isPending}>Create card</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={title.length === 0 || createCard.isPending}
|
||||
>
|
||||
Create card
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import ContentEditable from "react-contenteditable";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { IoChevronForwardSharp } from "react-icons/io5";
|
||||
|
||||
@@ -24,6 +23,7 @@ import LabelSelector from "./components/LabelSelector";
|
||||
import ListSelector from "./components/ListSelector";
|
||||
import MemberSelector from "./components/MemberSelector";
|
||||
import NewCommentForm from "./components/NewCommentForm";
|
||||
import Editor from "~/components/Editor";
|
||||
|
||||
interface FormValues {
|
||||
cardId: string;
|
||||
@@ -137,6 +137,8 @@ export default function CardPage() {
|
||||
});
|
||||
};
|
||||
|
||||
const description = watch("description");
|
||||
|
||||
if (!cardId) return <></>;
|
||||
|
||||
return (
|
||||
@@ -199,13 +201,10 @@ export default function CardPage() {
|
||||
className="w-full space-y-6"
|
||||
>
|
||||
<div className="mt-2">
|
||||
<ContentEditable
|
||||
placeholder="Add description..."
|
||||
html={watch("description")}
|
||||
disabled={false}
|
||||
onChange={(e) => setValue("description", e.target.value)}
|
||||
onBlur={handleSubmit(onSubmit)}
|
||||
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"
|
||||
<Editor
|
||||
content={card.description}
|
||||
onChange={(e) => setValue("description", e)}
|
||||
onBlur={() => handleSubmit(onSubmit)()}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { authClient } from "@kan/auth/client";
|
||||
import { useRouter } from "next/router";
|
||||
import { HiXMark } from "react-icons/hi2";
|
||||
import Editor from "~/components/Editor";
|
||||
|
||||
import { useModal } from "~/providers/modal";
|
||||
import { api } from "~/utils/api";
|
||||
@@ -16,6 +18,7 @@ export function CardModal({
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const { closeModal, isOpen } = useModal();
|
||||
const { data: session } = authClient.useSession();
|
||||
|
||||
const { data, isLoading } = api.card.byId.useQuery(
|
||||
{
|
||||
@@ -90,9 +93,7 @@ export function CardModal({
|
||||
{data?.description && (
|
||||
<div className="mb-10 flex w-full max-w-2xl justify-between">
|
||||
<div className="mt-2">
|
||||
<p 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">
|
||||
{data.description}
|
||||
</p>
|
||||
<Editor markdown={data.description} readOnly />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user