Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-17 01:54:20 +00:00
co-authored by renee-png
parent f3c2d1fabf
commit c7374382e1
@@ -0,0 +1,149 @@
import { useEditor, EditorContent, Editor } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import Underline from "@tiptap/extension-underline";
import TextAlign from "@tiptap/extension-text-align";
import { useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import {
Bold, Italic, Underline as UnderlineIcon, List, ListOrdered,
AlignLeft, AlignCenter, AlignRight, AlignJustify, Quote, Undo, Redo,
Heading2, Superscript as SupIcon,
} from "lucide-react";
import { cn } from "@/lib/utils";
interface RichTextEditorProps {
value: string; // HTML
onChange: (html: string) => void;
onInsertFootnote?: () => void;
className?: string;
minHeight?: number;
}
function ToolbarButton({
onClick, active, disabled, children, title,
}: {
onClick: () => void; active?: boolean; disabled?: boolean; children: React.ReactNode; title: string;
}) {
return (
<Button
type="button"
variant={active ? "secondary" : "ghost"}
size="sm"
className="h-8 w-8 p-0"
onClick={onClick}
disabled={disabled}
title={title}
>
{children}
</Button>
);
}
export function RichTextEditor({
value, onChange, onInsertFootnote, className, minHeight = 400,
}: RichTextEditorProps) {
const editor = useEditor({
extensions: [
StarterKit.configure({ heading: { levels: [2, 3] } }),
Underline,
TextAlign.configure({ types: ["heading", "paragraph"] }),
],
content: value || "<p></p>",
onUpdate: ({ editor }) => onChange(editor.getHTML()),
editorProps: {
attributes: {
class: "prose prose-sm max-w-none focus:outline-none px-10 py-8 bg-white text-black",
style: 'font-family: "Bookman Old Style", "URW Bookman", Georgia, serif; font-size: 12pt; line-height: 1.5;',
},
},
});
// Keep editor in sync when value is reset externally
useEffect(() => {
if (editor && value !== editor.getHTML()) {
editor.commands.setContent(value || "<p></p>", { emitUpdate: false });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value === ""]);
if (!editor) return null;
return (
<div className={cn("border rounded-md overflow-hidden bg-card", className)}>
<Toolbar editor={editor} onInsertFootnote={onInsertFootnote} />
<div className="bg-muted/20 overflow-auto" style={{ minHeight }}>
<div className="max-w-[8.5in] mx-auto my-4 shadow-sm">
<EditorContent editor={editor} />
</div>
</div>
</div>
);
}
function Toolbar({ editor, onInsertFootnote }: { editor: Editor; onInsertFootnote?: () => void }) {
return (
<div className="flex flex-wrap items-center gap-0.5 px-2 py-1.5 border-b bg-muted/30">
<ToolbarButton title="Undo" onClick={() => editor.chain().focus().undo().run()} disabled={!editor.can().undo()}>
<Undo className="h-4 w-4" />
</ToolbarButton>
<ToolbarButton title="Redo" onClick={() => editor.chain().focus().redo().run()} disabled={!editor.can().redo()}>
<Redo className="h-4 w-4" />
</ToolbarButton>
<Separator orientation="vertical" className="h-6 mx-1" />
<ToolbarButton title="Bold" active={editor.isActive("bold")} onClick={() => editor.chain().focus().toggleBold().run()}>
<Bold className="h-4 w-4" />
</ToolbarButton>
<ToolbarButton title="Italic" active={editor.isActive("italic")} onClick={() => editor.chain().focus().toggleItalic().run()}>
<Italic className="h-4 w-4" />
</ToolbarButton>
<ToolbarButton title="Underline" active={editor.isActive("underline")} onClick={() => editor.chain().focus().toggleUnderline().run()}>
<UnderlineIcon className="h-4 w-4" />
</ToolbarButton>
<Separator orientation="vertical" className="h-6 mx-1" />
<ToolbarButton title="Heading" active={editor.isActive("heading", { level: 2 })} onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}>
<Heading2 className="h-4 w-4" />
</ToolbarButton>
<ToolbarButton title="Bullet list" active={editor.isActive("bulletList")} onClick={() => editor.chain().focus().toggleBulletList().run()}>
<List className="h-4 w-4" />
</ToolbarButton>
<ToolbarButton title="Numbered list" active={editor.isActive("orderedList")} onClick={() => editor.chain().focus().toggleOrderedList().run()}>
<ListOrdered className="h-4 w-4" />
</ToolbarButton>
<ToolbarButton title="Block quote" active={editor.isActive("blockquote")} onClick={() => editor.chain().focus().toggleBlockquote().run()}>
<Quote className="h-4 w-4" />
</ToolbarButton>
<Separator orientation="vertical" className="h-6 mx-1" />
<ToolbarButton title="Align left" active={editor.isActive({ textAlign: "left" })} onClick={() => editor.chain().focus().setTextAlign("left").run()}>
<AlignLeft className="h-4 w-4" />
</ToolbarButton>
<ToolbarButton title="Center" active={editor.isActive({ textAlign: "center" })} onClick={() => editor.chain().focus().setTextAlign("center").run()}>
<AlignCenter className="h-4 w-4" />
</ToolbarButton>
<ToolbarButton title="Align right" active={editor.isActive({ textAlign: "right" })} onClick={() => editor.chain().focus().setTextAlign("right").run()}>
<AlignRight className="h-4 w-4" />
</ToolbarButton>
<ToolbarButton title="Justify" active={editor.isActive({ textAlign: "justify" })} onClick={() => editor.chain().focus().setTextAlign("justify").run()}>
<AlignJustify className="h-4 w-4" />
</ToolbarButton>
{onInsertFootnote && (
<>
<Separator orientation="vertical" className="h-6 mx-1" />
<Button type="button" variant="ghost" size="sm" className="h-8 px-2 text-xs" onClick={onInsertFootnote} title="Insert footnote at cursor">
<SupIcon className="h-3.5 w-3.5 mr-1" /> Footnote
</Button>
</>
)}
</div>
);
}
// Helper exposed for the parent to insert a numbered footnote marker
export function insertFootnoteMarker(editor: Editor | null, num: number) {
if (!editor) return;
editor.chain().focus().insertContent(`<sup>[${num}]</sup>`).run();
}