feat: implement Tiptap rich text editor and integrate it into admin question management

This commit is contained in:
2026-06-05 19:05:21 +07:00 Unverified
parent 2014c1470f
commit 005e95a556
6 changed files with 374 additions and 12 deletions
+4 -3
View File
@@ -21,6 +21,7 @@ import {
} from "@/app/actions/questions"; } from "@/app/actions/questions";
import { Plus, Trash, ArrowUp, ArrowDown, Loader2, Save, Image as ImageIcon, X } from "lucide-react"; import { Plus, Trash, ArrowUp, ArrowDown, Loader2, Save, Image as ImageIcon, X } from "lucide-react";
import Image from 'next/image'; import Image from 'next/image';
import { RichTextEditor } from "@/components/ui/rich-text-editor";
interface Question { interface Question {
id: string; id: string;
@@ -286,11 +287,11 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC
<CardContent className="pt-6 space-y-4"> <CardContent className="pt-6 space-y-4">
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4"> <div className="flex flex-col sm:flex-row items-start sm:items-center gap-4">
<div className="flex-1 w-full"> <div className="flex-1 w-full">
<Textarea <RichTextEditor
value={q.label} value={q.label}
onChange={(e) => handleUpdateQuestion(q.id, { label: e.target.value })} onChange={(html) => handleUpdateQuestion(q.id, { label: html })}
placeholder="Question Text" placeholder="Question Text"
className="font-medium text-base w-full min-h-10 py-2" className="w-full"
/> />
</div> </div>
<div className="flex items-center gap-2 self-end sm:self-auto"> <div className="flex items-center gap-2 self-end sm:self-auto">
+18 -8
View File
@@ -119,16 +119,21 @@ interface FormFillerClientProps {
const LS_KEY = (formId: string) => `form-draft-${formId}`; const LS_KEY = (formId: string) => `form-draft-${formId}`;
const QUESTIONS_PER_STEP = 3; const QUESTIONS_PER_STEP = 3;
const stripHtml = (html: string) => {
return (html || "").replace(/<[^>]*>?/gm, '').trim();
};
const buildSchema = (questions: Question[]) => { const buildSchema = (questions: Question[]) => {
const shape: Record<string, z.ZodTypeAny> = {}; const shape: Record<string, z.ZodTypeAny> = {};
questions.forEach(q => { questions.forEach(q => {
const plainLabel = stripHtml(q.label);
if (q.type === "checkbox") { if (q.type === "checkbox") {
shape[q.id] = q.required shape[q.id] = q.required
? z.array(z.string()).min(1, `จำเป็นต้องตอบ "${q.label}"`) ? z.array(z.string()).min(1, `จำเป็นต้องตอบ "${plainLabel}"`)
: z.array(z.string()).optional(); : z.array(z.string()).optional();
} else { } else {
shape[q.id] = q.required shape[q.id] = q.required
? z.string().min(1, `จำเป็นต้องตอบ "${q.label}"`) ? z.string().min(1, `จำเป็นต้องตอบ "${plainLabel}"`)
: z.string().optional(); : z.string().optional();
} }
}); });
@@ -244,7 +249,7 @@ export function FormFillerClient({ form, questions, initialAnswers = {} }: FormF
} }
const finalString = Array.isArray(val) ? val.join(", ") : (val || ""); const finalString = Array.isArray(val) ? val.join(", ") : (val || "");
if (q.required && !finalString.trim()) { if (q.required && !finalString.trim()) {
throw new Error(`จำเป็นต้องตอบ "${q.label}" (สำหรับตัวเลือก 'อื่นๆ')`); throw new Error(`จำเป็นต้องตอบ "${stripHtml(q.label)}" (สำหรับตัวเลือก 'อื่นๆ')`);
} }
return { return {
questionId: q.id, questionId: q.id,
@@ -309,11 +314,14 @@ export function FormFillerClient({ form, questions, initialAnswers = {} }: FormF
<Card key={q.id} className="shadow-sm"> <Card key={q.id} className="shadow-sm">
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<div className="flex items-start justify-between gap-4"> <div className="flex items-start justify-between gap-4">
<Label className="text-base font-semibold leading-normal mt-1"> <div className="text-base leading-normal mt-1 flex-1">
<FormattedText text={q.label} /> <div
{q.required && <span className="text-destructive ml-1">*</span>} className="prose prose-sm dark:prose-invert max-w-none [&>p]:m-0"
</Label> dangerouslySetInnerHTML={{ __html: q.label || "" }}
{q.imageUrl && ( />
</div>
<div className="flex items-start gap-4 shrink-0">
{q.imageUrl && (
<Dialog> <Dialog>
<DialogTrigger asChild> <DialogTrigger asChild>
<div className="relative max-w-full h-auto overflow-hidden rounded-md border inline-block shrink-0 cursor-pointer hover:opacity-80 transition-opacity"> <div className="relative max-w-full h-auto overflow-hidden rounded-md border inline-block shrink-0 cursor-pointer hover:opacity-80 transition-opacity">
@@ -341,6 +349,8 @@ export function FormFillerClient({ form, questions, initialAnswers = {} }: FormF
</DialogContent> </DialogContent>
</Dialog> </Dialog>
)} )}
{q.required && <span className="text-destructive mt-1">*</span>}
</div>
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
+1
View File
@@ -1,4 +1,5 @@
@import "tailwindcss"; @import "tailwindcss";
@plugin "@tailwindcss/typography";
@import "tw-animate-css"; @import "tw-animate-css";
@import "shadcn/tailwind.css"; @import "shadcn/tailwind.css";
+106 -1
View File
@@ -9,6 +9,13 @@
"@hookform/resolvers": "^5.4.0", "@hookform/resolvers": "^5.4.0",
"@radix-ui/react-label": "^2.1.8", "@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-slot": "^1.2.4",
"@tailwindcss/typography": "^0.5.19",
"@tiptap/extension-bubble-menu": "^3.26.0",
"@tiptap/extension-link": "^3.26.0",
"@tiptap/extension-underline": "^3.26.0",
"@tiptap/pm": "^3.26.0",
"@tiptap/react": "^3.26.0",
"@tiptap/starter-kit": "^3.26.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"drizzle-orm": "^0.45.2", "drizzle-orm": "^0.45.2",
@@ -495,6 +502,64 @@
"@tailwindcss/postcss": ["@tailwindcss/[email protected]", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.3.0", "@tailwindcss/oxide": "4.3.0", "postcss": "^8.5.10", "tailwindcss": "4.3.0" } }, "sha512-Jm05Tjx+9yCLGv5qw1c+84Psds8MnyrEQYCB+FFk2lgGiUjlRqdxke4mVTuYrj2xnVZqKim2Apr5ySuQRYAw/w=="], "@tailwindcss/postcss": ["@tailwindcss/[email protected]", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.3.0", "@tailwindcss/oxide": "4.3.0", "postcss": "^8.5.10", "tailwindcss": "4.3.0" } }, "sha512-Jm05Tjx+9yCLGv5qw1c+84Psds8MnyrEQYCB+FFk2lgGiUjlRqdxke4mVTuYrj2xnVZqKim2Apr5ySuQRYAw/w=="],
"@tailwindcss/typography": ["@tailwindcss/[email protected]", "", { "dependencies": { "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg=="],
"@tiptap/core": ["@tiptap/[email protected]", "", { "peerDependencies": { "@tiptap/pm": "3.26.0" } }, "sha512-7jTed/RirIVsp+lLdLvGzGqF3EBGpnGHGYKOwz6t28V2BIJLAFdUhfEVdWie7xPxQNWK0TP+fPlsqZS0vxfHBg=="],
"@tiptap/extension-blockquote": ["@tiptap/[email protected]", "", { "peerDependencies": { "@tiptap/core": "3.26.0" } }, "sha512-57accpka9affjiJRjP2LMNCDJDTMjTvO23RJCxtP43sp9cTIZ7YZnyDfRxCINTRBNK0X4o4w2+emOLyRwsk3CA=="],
"@tiptap/extension-bold": ["@tiptap/[email protected]", "", { "peerDependencies": { "@tiptap/core": "3.26.0" } }, "sha512-j6CzTMofcGJ5iMoUgDRQpM0FkG00jBID3aKqs+UBbgtzLgtG/CI/91tMFv0XPC30LeFA895qYgvGZtHdejZhiQ=="],
"@tiptap/extension-bubble-menu": ["@tiptap/[email protected]", "", { "dependencies": { "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "@tiptap/core": "3.26.0", "@tiptap/pm": "3.26.0" } }, "sha512-H2E3Hp0lV79jQV8YGtdDJkXkUalXZeYzKCx+vCZlDpb2ChS7/rNT9YY7poRA1NlJLUO0DH1wbAnFhx9KZMUx5g=="],
"@tiptap/extension-bullet-list": ["@tiptap/[email protected]", "", { "peerDependencies": { "@tiptap/extension-list": "3.26.0" } }, "sha512-Jv7BX+kBB2wUIvO/NhuUjv+T3kAed2Tjr664fgQ2zKT6X69jKIkYuCCedrIHuOyaOQ+SBDuH9h51wYv/E97QgQ=="],
"@tiptap/extension-code": ["@tiptap/[email protected]", "", { "peerDependencies": { "@tiptap/core": "3.26.0" } }, "sha512-VJYcV6rvjnENRTroOi9tDcHWW6G0pmCoRETwatlbgfDzuCmkTOwVwQjeJCXOVMMLNPzNiXZzibsRCUt+Azq/jw=="],
"@tiptap/extension-code-block": ["@tiptap/[email protected]", "", { "peerDependencies": { "@tiptap/core": "3.26.0", "@tiptap/pm": "3.26.0" } }, "sha512-WPN9iZ3UjeDD2ckDzSs9tleibXv0cLj7j575NxuvjhwZTehYGNeYDSUTi+6DQUG6bKbhGg9Wcei5H0131vvJHg=="],
"@tiptap/extension-document": ["@tiptap/[email protected]", "", { "peerDependencies": { "@tiptap/core": "3.26.0" } }, "sha512-Xhd6DCjaxCN4otQNvV6qra+XuoIjk6Vyjm87E5xn5Y/BMw7UGAG7LTkk3C2IEvxKrVZwJjalfxEqdHOgXQzVfw=="],
"@tiptap/extension-dropcursor": ["@tiptap/[email protected]", "", { "peerDependencies": { "@tiptap/extensions": "3.26.0" } }, "sha512-rhAtp5J/YVDUCUIc5T7b0XY9dLeuI72JgOr53w0QQc0VA0uwbfTn7sx0LI9PDCE9uwmDH8H3snVRZRnAvlM8oA=="],
"@tiptap/extension-floating-menu": ["@tiptap/[email protected]", "", { "peerDependencies": { "@floating-ui/dom": "^1.0.0", "@tiptap/core": "3.26.0", "@tiptap/pm": "3.26.0" } }, "sha512-reQ77NRYAOP7iPudsNbzLBuBTdL2aGxZzjccUFmE2lNdmwP23n9A/JhkuUhshVBs/6IozvahI+smG3Bnea0TCQ=="],
"@tiptap/extension-gapcursor": ["@tiptap/[email protected]", "", { "peerDependencies": { "@tiptap/extensions": "3.26.0" } }, "sha512-SIe68SDwx2fozt/XKG0FhCwzz/yRN6Bvo4D5TqvfDg6NK3PQb1DS4GN9PilmJqbY+kXryuiWEEJOWi7HpO8SuQ=="],
"@tiptap/extension-hard-break": ["@tiptap/[email protected]", "", { "peerDependencies": { "@tiptap/core": "3.26.0" } }, "sha512-baXvv/rtOTVd2Axjb7Zbb41Y9Qmy3U2fP7EHqLuhViqGxVX8LwQtP0PHUXEZkPokbBpRez10+dmOlvvsYFKAZQ=="],
"@tiptap/extension-heading": ["@tiptap/[email protected]", "", { "peerDependencies": { "@tiptap/core": "3.26.0" } }, "sha512-qenEQEgzE5FjQay/H6iKOnwIt6DPO27cS+v0mGhXmrL1MjrNER4X0ZkATJbVd0WA6ffsAGaP44NKYDworGeidw=="],
"@tiptap/extension-horizontal-rule": ["@tiptap/[email protected]", "", { "peerDependencies": { "@tiptap/core": "3.26.0", "@tiptap/pm": "3.26.0" } }, "sha512-a+N/C4wkQV+/8x4ShdoiC2JdTW3Tw84C5cAloYLFMeaWmRa2me9ACSI+zo0SO9bbH9RJwsoRp7eaxBbk27eF1Q=="],
"@tiptap/extension-italic": ["@tiptap/[email protected]", "", { "peerDependencies": { "@tiptap/core": "3.26.0" } }, "sha512-s8oFpH+0xmhvY19f452/2dExO3p1tjxh761g6cg4irwEUNUEAJKF2VLcjiaeOhNJ+pmnQYxb+VSkwkXvO+7vHQ=="],
"@tiptap/extension-link": ["@tiptap/[email protected]", "", { "dependencies": { "linkifyjs": "^4.3.3" }, "peerDependencies": { "@tiptap/core": "3.26.0", "@tiptap/pm": "3.26.0" } }, "sha512-FA/d157aBxyvZFvsdc5eSu46tmHWXebAsqOQSvivOMyw+deBb00VlMsf+iD2J8+sekjbMYwx/hvbsu+xUoX43Q=="],
"@tiptap/extension-list": ["@tiptap/[email protected]", "", { "peerDependencies": { "@tiptap/core": "3.26.0", "@tiptap/pm": "3.26.0" } }, "sha512-EM8woyHDNKLEQ+lWUEoDtA4KrwP6fei/mYX1NxseMzKHHo7LFecx7wk6sovAXZrUvdML/yFBihgiMiO5VIsfkg=="],
"@tiptap/extension-list-item": ["@tiptap/[email protected]", "", { "peerDependencies": { "@tiptap/extension-list": "3.26.0" } }, "sha512-MccGyj9HY4fkl04eIiFoTCkr8067Jku/VVdJNtRWW104Spx43C/7V2zpbxPvpcDhq3dW384fDxYXfpnb186xLg=="],
"@tiptap/extension-list-keymap": ["@tiptap/[email protected]", "", { "peerDependencies": { "@tiptap/extension-list": "3.26.0" } }, "sha512-oBcj6qaNrRHQ+N0+pDuOVAQa4Nx9r8Cm5ANvyM2lTpoy60sOLOizuVvcvw1andVxbSrsZ1N/Sk+RZWyv1uoWyQ=="],
"@tiptap/extension-ordered-list": ["@tiptap/[email protected]", "", { "peerDependencies": { "@tiptap/extension-list": "3.26.0" } }, "sha512-ItLdFlcMsJz2vhbs1PcUfcN7nzVqGBOwPeCrrWxjrgscp+K3JoOGD+HhVVpBACOMwivUrlh8Ry5Ohvues2nOeA=="],
"@tiptap/extension-paragraph": ["@tiptap/[email protected]", "", { "peerDependencies": { "@tiptap/core": "3.26.0" } }, "sha512-h8fYLikg4qN39IghQ1y9g+zzUsgxBpDi5YS3IZbWoxWYYx1YqLL8nAvOiPr7Us14aQ0TjA2/xY7zqmyf29rX1A=="],
"@tiptap/extension-strike": ["@tiptap/[email protected]", "", { "peerDependencies": { "@tiptap/core": "3.26.0" } }, "sha512-jUll3Pqhq7u1JKvO0B6USW/bmVmUsO6sRcxo/d5tXqLhS0tWAobOGoGU2IgwXnQDSjf+vF73RYD5tRGDLkRC9Q=="],
"@tiptap/extension-text": ["@tiptap/[email protected]", "", { "peerDependencies": { "@tiptap/core": "3.26.0" } }, "sha512-yZXdevp3/8omGbb40Z52VfvID+tsRNhPQ1GNUToD56XSr2BjdJyAzAb9rWGgDKgVMUPLgJ26yT0O278RFqOKhA=="],
"@tiptap/extension-underline": ["@tiptap/[email protected]", "", { "peerDependencies": { "@tiptap/core": "3.26.0" } }, "sha512-LlVkivH5cBwov/EMD8BL7ZRcU6YcadiSVIffLW1hyalw9YfhaFzoLxjtWhL7jiU/n2Kg+9dXSZxmV2hTeTwyrQ=="],
"@tiptap/extensions": ["@tiptap/[email protected]", "", { "peerDependencies": { "@tiptap/core": "3.26.0", "@tiptap/pm": "3.26.0" } }, "sha512-4wajuqnO2X0+LVvsBjW/xk3/tmdb16bNL939QhicAay4YYqXITeV2v3XJsryzmG4L5GkK1yLxvRGk4aLoxWrnA=="],
"@tiptap/pm": ["@tiptap/[email protected]", "", { "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-commands": "^1.6.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", "prosemirror-inputrules": "^1.4.0", "prosemirror-keymap": "^1.2.3", "prosemirror-model": "^1.25.7", "prosemirror-schema-list": "^1.5.0", "prosemirror-state": "^1.4.4", "prosemirror-tables": "^1.8.0", "prosemirror-transform": "^1.12.0", "prosemirror-view": "^1.41.8" } }, "sha512-q4RDeWwVrhOL0jJCGRgGxLSdjOYwzQ4h2InURZVhC66433ipcHd6f3bqSOhcXZ4r0sFmMNsuF7aZmUntjWLc7w=="],
"@tiptap/react": ["@tiptap/[email protected]", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "fast-equals": "^5.3.3", "use-sync-external-store": "^1.4.0" }, "optionalDependencies": { "@tiptap/extension-bubble-menu": "^3.26.0", "@tiptap/extension-floating-menu": "^3.26.0" }, "peerDependencies": { "@tiptap/core": "3.26.0", "@tiptap/pm": "3.26.0", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-NLPAG6tk4/AsfOsUNsbGqdgIHuGsD4A/hlYriozuo+LCAAduuluhzsL/MEHZXtFT4GXUOlCdaEqNCOrMuz/zaw=="],
"@tiptap/starter-kit": ["@tiptap/[email protected]", "", { "dependencies": { "@tiptap/core": "^3.26.0", "@tiptap/extension-blockquote": "^3.26.0", "@tiptap/extension-bold": "^3.26.0", "@tiptap/extension-bullet-list": "^3.26.0", "@tiptap/extension-code": "^3.26.0", "@tiptap/extension-code-block": "^3.26.0", "@tiptap/extension-document": "^3.26.0", "@tiptap/extension-dropcursor": "^3.26.0", "@tiptap/extension-gapcursor": "^3.26.0", "@tiptap/extension-hard-break": "^3.26.0", "@tiptap/extension-heading": "^3.26.0", "@tiptap/extension-horizontal-rule": "^3.26.0", "@tiptap/extension-italic": "^3.26.0", "@tiptap/extension-link": "^3.26.0", "@tiptap/extension-list": "^3.26.0", "@tiptap/extension-list-item": "^3.26.0", "@tiptap/extension-list-keymap": "^3.26.0", "@tiptap/extension-ordered-list": "^3.26.0", "@tiptap/extension-paragraph": "^3.26.0", "@tiptap/extension-strike": "^3.26.0", "@tiptap/extension-text": "^3.26.0", "@tiptap/extension-underline": "^3.26.0", "@tiptap/extensions": "^3.26.0", "@tiptap/pm": "^3.26.0" } }, "sha512-o34EtMfqtBaljdmeElZsRG/067oGx9Zcq+j2GWo71KlZe22ga/ALexeTf1c+ETsjCxSTKR6eyQ4RZvz/2JpYfg=="],
"@ts-morph/common": ["@ts-morph/[email protected]", "", { "dependencies": { "fast-glob": "^3.3.3", "minimatch": "^10.0.1", "path-browserify": "^1.0.1" } }, "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ=="], "@ts-morph/common": ["@ts-morph/[email protected]", "", { "dependencies": { "fast-glob": "^3.3.3", "minimatch": "^10.0.1", "path-browserify": "^1.0.1" } }, "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ=="],
"@tybys/wasm-util": ["@tybys/[email protected]", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], "@tybys/wasm-util": ["@tybys/[email protected]", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="],
@@ -515,6 +580,8 @@
"@types/statuses": ["@types/[email protected]", "", {}, "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA=="], "@types/statuses": ["@types/[email protected]", "", {}, "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA=="],
"@types/use-sync-external-store": ["@types/[email protected]", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="],
"@types/validate-npm-package-name": ["@types/[email protected]", "", {}, "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw=="], "@types/validate-npm-package-name": ["@types/[email protected]", "", {}, "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/[email protected]", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.59.4", "@typescript-eslint/type-utils": "8.59.4", "@typescript-eslint/utils": "8.59.4", "@typescript-eslint/visitor-keys": "8.59.4", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.59.4", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-PegsU+XfyJJNjd4+u/k6f9yTyp0lEXXiPopUNobZcIAUJFGICFLN+sP0Rb3JehVmiij1Ph0dFGYqODoRo/2+6A=="], "@typescript-eslint/eslint-plugin": ["@typescript-eslint/[email protected]", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.59.4", "@typescript-eslint/type-utils": "8.59.4", "@typescript-eslint/utils": "8.59.4", "@typescript-eslint/visitor-keys": "8.59.4", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.59.4", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-PegsU+XfyJJNjd4+u/k6f9yTyp0lEXXiPopUNobZcIAUJFGICFLN+sP0Rb3JehVmiij1Ph0dFGYqODoRo/2+6A=="],
@@ -843,6 +910,8 @@
"fast-deep-equal": ["[email protected]", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-deep-equal": ["[email protected]", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-equals": ["[email protected]", "", {}, "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw=="],
"fast-glob": ["[email protected]", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], "fast-glob": ["[email protected]", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
"fast-json-stable-stringify": ["[email protected]", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], "fast-json-stable-stringify": ["[email protected]", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
@@ -1123,6 +1192,8 @@
"lines-and-columns": ["[email protected]", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], "lines-and-columns": ["[email protected]", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
"linkifyjs": ["[email protected]", "", {}, "sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg=="],
"locate-path": ["[email protected]", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], "locate-path": ["[email protected]", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
"lodash.merge": ["[email protected]", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], "lodash.merge": ["[email protected]", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
@@ -1231,6 +1302,8 @@
"ora": ["[email protected]", "", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw=="], "ora": ["[email protected]", "", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw=="],
"orderedmap": ["[email protected]", "", {}, "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g=="],
"outvariant": ["[email protected]", "", {}, "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA=="], "outvariant": ["[email protected]", "", {}, "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA=="],
"own-keys": ["[email protected]", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], "own-keys": ["[email protected]", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="],
@@ -1267,7 +1340,7 @@
"postcss": ["[email protected]", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="], "postcss": ["[email protected]", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="],
"postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], "postcss-selector-parser": ["postcss-selector-parser@6.0.10", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="],
"postgres": ["[email protected]", "", {}, "sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw=="], "postgres": ["[email protected]", "", {}, "sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw=="],
@@ -1287,6 +1360,32 @@
"prop-types": ["[email protected]", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], "prop-types": ["[email protected]", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
"prosemirror-changeset": ["[email protected]", "", { "dependencies": { "prosemirror-transform": "^1.0.0" } }, "sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw=="],
"prosemirror-commands": ["[email protected]", "", { "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.10.2" } }, "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w=="],
"prosemirror-dropcursor": ["[email protected]", "", { "dependencies": { "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0", "prosemirror-view": "^1.1.0" } }, "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw=="],
"prosemirror-gapcursor": ["[email protected]", "", { "dependencies": { "prosemirror-keymap": "^1.0.0", "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-view": "^1.0.0" } }, "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw=="],
"prosemirror-history": ["[email protected]", "", { "dependencies": { "prosemirror-state": "^1.2.2", "prosemirror-transform": "^1.0.0", "prosemirror-view": "^1.31.0", "rope-sequence": "^1.3.0" } }, "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg=="],
"prosemirror-inputrules": ["[email protected]", "", { "dependencies": { "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.0.0" } }, "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw=="],
"prosemirror-keymap": ["[email protected]", "", { "dependencies": { "prosemirror-state": "^1.0.0", "w3c-keyname": "^2.2.0" } }, "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw=="],
"prosemirror-model": ["[email protected]", "", { "dependencies": { "orderedmap": "^2.0.0" } }, "sha512-A79aN8QEFUwI6cax8Yq4Rpcx1TJZ3Kagn+ii7qLo4/V8H3mMiHrhFyhTyHHvpSnOgMPpWiDGSwM3etwrxE50ug=="],
"prosemirror-schema-list": ["[email protected]", "", { "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.7.3" } }, "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q=="],
"prosemirror-state": ["[email protected]", "", { "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-transform": "^1.0.0", "prosemirror-view": "^1.27.0" } }, "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw=="],
"prosemirror-tables": ["[email protected]", "", { "dependencies": { "prosemirror-keymap": "^1.2.3", "prosemirror-model": "^1.25.4", "prosemirror-state": "^1.4.4", "prosemirror-transform": "^1.10.5", "prosemirror-view": "^1.41.4" } }, "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw=="],
"prosemirror-transform": ["[email protected]", "", { "dependencies": { "prosemirror-model": "^1.21.0" } }, "sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w=="],
"prosemirror-view": ["[email protected]", "", { "dependencies": { "prosemirror-model": "^1.20.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0" } }, "sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA=="],
"proxy-addr": ["[email protected]", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], "proxy-addr": ["[email protected]", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
"punycode": ["[email protected]", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "punycode": ["[email protected]", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
@@ -1337,6 +1436,8 @@
"reusify": ["[email protected]", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], "reusify": ["[email protected]", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
"rope-sequence": ["[email protected]", "", {}, "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ=="],
"router": ["[email protected]", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], "router": ["[email protected]", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
"run-applescript": ["[email protected]", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], "run-applescript": ["[email protected]", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="],
@@ -1521,6 +1622,8 @@
"vary": ["[email protected]", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], "vary": ["[email protected]", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"w3c-keyname": ["[email protected]", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="],
"web-streams-polyfill": ["[email protected]", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], "web-streams-polyfill": ["[email protected]", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
"which": ["[email protected]", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "which": ["[email protected]", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
@@ -1749,6 +1852,8 @@
"router/path-to-regexp": ["[email protected]", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], "router/path-to-regexp": ["[email protected]", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="],
"shadcn/postcss-selector-parser": ["[email protected]", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="],
"shadcn/zod": ["[email protected]", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "shadcn/zod": ["[email protected]", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"sharp/semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], "sharp/semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="],
+238
View File
@@ -0,0 +1,238 @@
"use client";
import { useEditor, EditorContent } from "@tiptap/react";
import { BubbleMenu } from "@tiptap/react/menus";
import StarterKit from "@tiptap/starter-kit";
import Underline from "@tiptap/extension-underline";
import Link from "@tiptap/extension-link";
import { useState } from "react";
import { Bold, Italic, Underline as UnderlineIcon, Strikethrough, Link as LinkIcon, Unlink } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
interface RichTextEditorProps {
value: string;
onChange: (value: string) => void;
placeholder?: string;
className?: string;
}
export function RichTextEditor({ value, onChange, placeholder, className }: RichTextEditorProps) {
const [isLinkDialogOpen, setIsLinkDialogOpen] = useState(false);
const [linkUrl, setLinkUrl] = useState("");
const [linkText, setLinkText] = useState("");
const editor = useEditor({
extensions: [
StarterKit,
Underline,
Link.configure({
openOnClick: false,
HTMLAttributes: {
class: "text-primary hover:underline underline-offset-4",
target: "_blank",
rel: "noopener noreferrer"
},
}),
],
content: value,
onUpdate: ({ editor }) => {
// Get HTML output. If empty, maybe output empty string to avoid empty p tags.
let html = editor.getHTML();
if (html === "<p></p>") html = "";
onChange(html);
},
editorProps: {
attributes: {
class: "prose prose-sm dark:prose-invert max-w-none focus:outline-none min-h-[40px] px-3 py-2",
},
},
immediatelyRender: false,
});
if (!editor) {
return null;
}
const toggleLink = () => {
const previousUrl = editor.getAttributes("link").href;
if (previousUrl) {
editor.chain().focus().unsetLink().run();
return;
}
const { from, to } = editor.state.selection;
const selectedText = editor.state.doc.textBetween(from, to, ' ');
setLinkText(selectedText || "");
setLinkUrl("");
setIsLinkDialogOpen(true);
};
const saveLink = () => {
if (linkUrl === "") {
editor.chain().focus().extendMarkRange("link").unsetLink().run();
} else {
if (linkText) {
editor.chain().focus().insertContent(`<a href="${linkUrl}">${linkText}</a>`).run();
} else {
editor.chain().focus().extendMarkRange("link").setLink({ href: linkUrl }).run();
}
}
setIsLinkDialogOpen(false);
setLinkUrl("");
setLinkText("");
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
e.preventDefault();
saveLink();
}
};
return (
<div className={`border rounded-md focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 ${className || ""}`}>
<div className="flex flex-wrap items-center gap-1 border-b p-1 bg-muted/50 rounded-t-md">
<Button
type="button"
variant="ghost"
size="icon"
className={`h-8 w-8 ${editor.isActive("bold") ? "bg-muted" : ""}`}
onClick={() => editor.chain().focus().toggleBold().run()}
title="Bold"
>
<Bold className="h-4 w-4" />
</Button>
<Button
type="button"
variant="ghost"
size="icon"
className={`h-8 w-8 ${editor.isActive("italic") ? "bg-muted" : ""}`}
onClick={() => editor.chain().focus().toggleItalic().run()}
title="Italic"
>
<Italic className="h-4 w-4" />
</Button>
<Button
type="button"
variant="ghost"
size="icon"
className={`h-8 w-8 ${editor.isActive("underline") ? "bg-muted" : ""}`}
onClick={() => editor.chain().focus().toggleUnderline().run()}
title="Underline"
>
<UnderlineIcon className="h-4 w-4" />
</Button>
<Button
type="button"
variant="ghost"
size="icon"
className={`h-8 w-8 ${editor.isActive("strike") ? "bg-muted" : ""}`}
onClick={() => editor.chain().focus().toggleStrike().run()}
title="Strikethrough"
>
<Strikethrough className="h-4 w-4" />
</Button>
<div className="w-px h-5 bg-border mx-1" />
<Button
type="button"
variant="ghost"
size="icon"
className={`h-8 w-8 ${editor.isActive("link") ? "bg-muted" : ""}`}
onClick={toggleLink}
title={editor.isActive("link") ? "Remove Link" : "Add Link"}
>
{editor.isActive("link") ? <Unlink className="h-4 w-4" /> : <LinkIcon className="h-4 w-4" />}
</Button>
</div>
{editor && (
<BubbleMenu
editor={editor}
className="flex items-center gap-1 p-1 bg-background border rounded-md shadow-md"
>
<Button
type="button"
variant="ghost"
size="icon"
className={`h-8 w-8 ${editor.isActive("bold") ? "bg-muted" : ""}`}
onClick={() => editor.chain().focus().toggleBold().run()}
title="Bold"
>
<Bold className="h-4 w-4" />
</Button>
<Button
type="button"
variant="ghost"
size="icon"
className={`h-8 w-8 ${editor.isActive("italic") ? "bg-muted" : ""}`}
onClick={() => editor.chain().focus().toggleItalic().run()}
title="Italic"
>
<Italic className="h-4 w-4" />
</Button>
<Button
type="button"
variant="ghost"
size="icon"
className={`h-8 w-8 ${editor.isActive("underline") ? "bg-muted" : ""}`}
onClick={() => editor.chain().focus().toggleUnderline().run()}
title="Underline"
>
<UnderlineIcon className="h-4 w-4" />
</Button>
<Button
type="button"
variant="ghost"
size="icon"
className={`h-8 w-8 ${editor.isActive("strike") ? "bg-muted" : ""}`}
onClick={() => editor.chain().focus().toggleStrike().run()}
title="Strikethrough"
>
<Strikethrough className="h-4 w-4" />
</Button>
</BubbleMenu>
)}
<EditorContent editor={editor} className="bg-background rounded-b-md" />
<Dialog open={isLinkDialogOpen} onOpenChange={setIsLinkDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Insert Link</DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<label htmlFor="link-text" className="text-sm font-medium">Text</label>
<Input
id="link-text"
value={linkText}
onChange={(e) => setLinkText(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Click here"
autoFocus
/>
</div>
<div className="grid gap-2">
<label htmlFor="link-url" className="text-sm font-medium">URL</label>
<Input
id="link-url"
value={linkUrl}
onChange={(e) => setLinkUrl(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="https://example.com"
/>
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setIsLinkDialogOpen(false)}>
Cancel
</Button>
<Button type="button" onClick={saveLink}>
Save
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}
+7
View File
@@ -18,6 +18,13 @@
"@hookform/resolvers": "^5.4.0", "@hookform/resolvers": "^5.4.0",
"@radix-ui/react-label": "^2.1.8", "@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-slot": "^1.2.4",
"@tailwindcss/typography": "^0.5.19",
"@tiptap/extension-bubble-menu": "^3.26.0",
"@tiptap/extension-link": "^3.26.0",
"@tiptap/extension-underline": "^3.26.0",
"@tiptap/pm": "^3.26.0",
"@tiptap/react": "^3.26.0",
"@tiptap/starter-kit": "^3.26.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"drizzle-orm": "^0.45.2", "drizzle-orm": "^0.45.2",