"use client";
import Placeholder from "@tiptap/extension-placeholder";
import { EditorContent, useEditor } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import {
Bold,
Heading1,
Heading2,
Italic,
List,
ListOrdered,
Redo2,
Save,
Trash2,
Underline as UnderlineIcon,
Undo2,
} from "lucide-react";
import { type ReactNode, useReducer, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
import { Input } from "@/components/ui/input";
import {
Toolbar,
ToolbarButton,
ToolbarGroup,
ToolbarSeparator,
} from "@/components/ui/toolbar";
import type { Note } from "@/lib/notes";
function FormatButton({
active,
disabled,
label,
onClick,
children,
}: {
active?: boolean;
disabled?: boolean;
label: string;
onClick: () => void;
children: ReactNode;
}) {
return (
e.preventDefault()}
render={
}
>
{children}
);
}
// Word-like rich-text editor for a single note: a COSS Toolbar driving Tiptap
// commands, a title field, and Save / Delete. Remount (via a `key` on the
// parent) to load a different note.
export function NotesEditor({
note,
saving,
onSave,
onDelete,
}: {
note: Note;
saving: boolean;
onSave: (data: { title: string; content: string }) => void;
onDelete?: () => void;
}) {
const { t } = useTranslation();
const [title, setTitle] = useState(note.title);
const [confirmOpen, setConfirmOpen] = useState(false);
// Force a re-render on every editor transaction so the toolbar reflects the
// current formatting/undo state.
const [, bump] = useReducer((n: number) => n + 1, 0);
const editor = useEditor({
content: note.content,
extensions: [
StarterKit,
Placeholder.configure({ placeholder: t("notes.editor.placeholder") }),
],
// Required under Next's SSR to avoid a hydration mismatch.
immediatelyRender: false,
editorProps: {
attributes: {
// `note-content` styles headings/lists/etc. (see globals.css); padding +
// min-h-full make the whole card a click target, text anchored top-left.
class: "note-content min-h-full p-4 focus:outline-none",
},
},
onTransaction: bump,
});
if (!editor) return null;
const save = () =>
onSave({
title: title.trim() || t("notes.untitled"),
content: editor.getHTML(),
});
return (
setTitle(e.target.value)}
placeholder={t("notes.editor.titlePlaceholder")}
value={title}
/>
{onDelete && (
)}
editor.chain().focus().toggleBold().run()}
>
editor.chain().focus().toggleItalic().run()}
>
editor.chain().focus().toggleUnderline().run()}
>
editor.chain().focus().toggleHeading({ level: 1 }).run()
}
>
editor.chain().focus().toggleHeading({ level: 2 }).run()
}
>
editor.chain().focus().toggleBulletList().run()}
>
editor.chain().focus().toggleOrderedList().run()}
>
editor.chain().focus().undo().run()}
>
editor.chain().focus().redo().run()}
>
{onDelete && (
)}
);
}