feat(template): add form hook for rich text editor / create template page

This commit is contained in:
Leons Aleksandrovs
2025-07-09 21:29:07 +03:00
parent 49bc7dc60a
commit 3376043428
7 changed files with 472 additions and 2 deletions
+23
View File
@@ -0,0 +1,23 @@
import { withForm } from "@/hooks/formHook";
const Template = withForm({
defaultValues: {
name: "",
template: "",
},
props: {},
render({ form }) {
return (
<div className="mt-4 flex flex-col gap-4">
<form.AppField
name="name"
children={(f) => <f.TextField label="Name" placeholder="Template name" />}
/>
<form.AppField name="template" children={(f) => <f.RichTextEdit />} />
</div>
);
},
});
export default Template;
@@ -0,0 +1,160 @@
import "../../editor.css";
import { useFieldContext } from "@/hooks/formHook";
import TextStyle from "@tiptap/extension-text-style";
import { EditorContent, useEditor } from "@tiptap/react";
import type { Editor } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import { Button } from "../ui/button";
import Link from "@tiptap/extension-link";
import {
BoldIcon,
CodeIcon,
Heading1Icon,
Heading2Icon,
Heading3Icon,
ItalicIcon,
ListIcon,
ListOrderedIcon,
PilcrowIcon,
QuoteIcon,
StrikethroughIcon,
} from "lucide-react";
const MenuBar = ({ editor }: { editor: Editor | null }) => {
if (!editor) {
return;
}
return (
<div className="control-group">
<div className="flex flex-wrap gap-2">
<Button
variant="ghost"
onClick={() => editor.chain().focus().toggleBold().run()}
disabled={!editor.can().chain().focus().toggleBold().run()}
className={editor.isActive("bold") ? "bg-accent" : ""}
>
<BoldIcon />
</Button>
<Button
variant="ghost"
onClick={() => editor.chain().focus().toggleItalic().run()}
disabled={!editor.can().chain().focus().toggleItalic().run()}
className={editor.isActive("italic") ? "bg-accent" : ""}
>
<ItalicIcon />
</Button>
<Button
variant="ghost"
onClick={() => editor.chain().focus().toggleStrike().run()}
disabled={!editor.can().chain().focus().toggleStrike().run()}
className={editor.isActive("strike") ? "bg-accent" : ""}
>
<StrikethroughIcon />
</Button>
<Button
variant="ghost"
onClick={() => editor.chain().focus().toggleCode().run()}
disabled={!editor.can().chain().focus().toggleCode().run()}
className={editor.isActive("code") ? "bg-accent" : ""}
>
<CodeIcon />
</Button>
<Button
variant="ghost"
onClick={() => editor.chain().focus().setParagraph().run()}
className={editor.isActive("paragraph") ? "bg-accent" : ""}
>
<PilcrowIcon />
</Button>
<Button
variant="ghost"
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
className={editor.isActive("heading", { level: 1 }) ? "bg-accent" : ""}
>
<Heading1Icon />
</Button>
<Button
variant="ghost"
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
className={editor.isActive("heading", { level: 2 }) ? "bg-accent" : ""}
>
<Heading2Icon />
</Button>
<Button
variant="ghost"
onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
className={editor.isActive("heading", { level: 3 }) ? "bg-accent" : ""}
>
<Heading3Icon />
</Button>
<Button
variant="ghost"
onClick={() => editor.chain().focus().toggleBulletList().run()}
className={editor.isActive("bulletList") ? "bg-accent" : ""}
>
<ListIcon />
</Button>
<Button
variant="ghost"
onClick={() => editor.chain().focus().toggleOrderedList().run()}
className={editor.isActive("orderedList") ? "bg-accent" : ""}
>
<ListOrderedIcon />
</Button>
<Button
variant="ghost"
onClick={() => editor.chain().focus().toggleBlockquote().run()}
className={editor.isActive("blockquote") ? "bg-accent" : ""}
>
<QuoteIcon />
</Button>
</div>
</div>
);
};
const extensions = [
TextStyle.configure(),
StarterKit.configure({
bulletList: {
keepMarks: true,
keepAttributes: true,
},
orderedList: {
keepMarks: true,
keepAttributes: false,
},
}),
Link.configure({
defaultProtocol: "https",
}),
];
export default () => {
// Get field with predefined text type
const field = useFieldContext<string>();
// Configure editor
const editor = useEditor({
onUpdate: ({ editor }) => field.handleChange(editor.getHTML()),
onBlur: () => field.handleBlur(),
content: field.state.value,
extensions,
});
// Render custom field
return (
<div>
<div className="tiptap-container">
<MenuBar editor={editor} />
<EditorContent editor={editor} />
</div>
{!field.state.meta.isValid && (
<span className="text-xs text-danger mt-1">
{field.state.meta.errors.map((e) => e.message).join(", ")}
</span>
)}
</div>
);
};