'use client'; import { useEditor, EditorContent } from '@tiptap/react'; import { Node, mergeAttributes } from '@tiptap/core'; import StarterKit from '@tiptap/starter-kit'; import Underline from '@tiptap/extension-underline'; import Link from '@tiptap/extension-link'; import { TextStyle } from '@tiptap/extension-text-style'; import { Color } from '@tiptap/extension-color'; import TextAlign from '@tiptap/extension-text-align'; import Highlight from '@tiptap/extension-highlight'; import Image from '@tiptap/extension-image'; import Youtube from '@tiptap/extension-youtube'; import { Bold, Italic, Underline as UnderlineIcon, Link as LinkIcon, AlignLeft, AlignCenter, AlignRight, AlignJustify, Highlighter, Type, List, ListOrdered, Quote, Undo, Redo, ImageIcon, Video, Youtube as YoutubeIcon, Palette } from 'lucide-react'; import { useEffect, useState } from 'react'; import { MediaGridSelector } from './MediaGridSelector'; interface RichTextEditorProps { value: string; onChange: (value: string) => void; label: string; } const VideoNode = Node.create({ name: 'videoNode', group: 'block', selectable: true, draggable: true, atom: true, addAttributes() { return { src: { default: null }, controls: { default: 'true' }, } }, parseHTML() { return [{ tag: 'video' }] }, renderHTML({ HTMLAttributes }) { if (HTMLAttributes.src && typeof HTMLAttributes.src === 'string') { HTMLAttributes.src = HTMLAttributes.src.replace('/preview?', '/view?'); } return ['video', mergeAttributes(HTMLAttributes, { class: 'w-full aspect-video rounded-xl shadow-lg my-6', controls: 'true', preload: 'metadata' })] }, }); const MenuBar = ({ editor }: { editor: any }) => { const [isMediaModalOpen, setIsMediaModalOpen] = useState(false); if (!editor) return null; const setLink = () => { const url = window.prompt('URL del enlace:'); if (url) { editor.chain().focus().setLink({ href: url }).run(); } }; const addYoutubeVideo = () => { const url = window.prompt('URL de YouTube:'); if (url) { editor.commands.setYoutubeVideo({ src: url, width: 640, height: 480, }); } }; const handleMediaInsert = (fileId: string, url?: string, tipo?: string) => { if (url) { if (tipo === 'video') { const videoUrl = url.replace('/preview?', '/view?'); editor.chain().focus().insertContent(``).run(); } else { editor.chain().focus().setImage({ src: url }).run(); } } setIsMediaModalOpen(false); }; const setColor = (e: React.ChangeEvent) => { editor.chain().focus().setColor(e.target.value).run(); }; return ( <> editor.chain().focus().toggleBold().run()} className={`p-2 rounded hover:bg-slate-800 transition-colors ${editor.isActive('bold') ? 'text-cyan-400 bg-slate-800' : 'text-slate-400'}`} title="Negrita" > editor.chain().focus().toggleItalic().run()} className={`p-2 rounded hover:bg-slate-800 transition-colors ${editor.isActive('italic') ? 'text-cyan-400 bg-slate-800' : 'text-slate-400'}`} title="Cursiva" > editor.chain().focus().toggleUnderline().run()} className={`p-2 rounded hover:bg-slate-800 transition-colors ${editor.isActive('underline') ? 'text-cyan-400 bg-slate-800' : 'text-slate-400'}`} title="Subrayado" > editor.chain().focus().setTextAlign('left').run()} className={`p-2 rounded hover:bg-slate-800 transition-colors ${editor.isActive({ textAlign: 'left' }) ? 'text-cyan-400 bg-slate-800' : 'text-slate-400'}`} > editor.chain().focus().setTextAlign('center').run()} className={`p-2 rounded hover:bg-slate-800 transition-colors ${editor.isActive({ textAlign: 'center' }) ? 'text-cyan-400 bg-slate-800' : 'text-slate-400'}`} > editor.chain().focus().setTextAlign('right').run()} className={`p-2 rounded hover:bg-slate-800 transition-colors ${editor.isActive({ textAlign: 'right' }) ? 'text-cyan-400 bg-slate-800' : 'text-slate-400'}`} > editor.chain().focus().setTextAlign('justify').run()} className={`p-2 rounded hover:bg-slate-800 transition-colors ${editor.isActive({ textAlign: 'justify' }) ? 'text-cyan-400 bg-slate-800' : 'text-slate-400'}`} > editor.chain().focus().toggleBulletList().run()} className={`p-2 rounded hover:bg-slate-800 transition-colors ${editor.isActive('bulletList') ? 'text-cyan-400 bg-slate-800' : 'text-slate-400'}`} > editor.chain().focus().toggleOrderedList().run()} className={`p-2 rounded hover:bg-slate-800 transition-colors ${editor.isActive('orderedList') ? 'text-cyan-400 bg-slate-800' : 'text-slate-400'}`} > editor.chain().focus().toggleHighlight().run()} className={`p-2 rounded hover:bg-slate-800 transition-colors ${editor.isActive('highlight') ? 'text-yellow-400 bg-slate-800' : 'text-slate-400'}`} > setIsMediaModalOpen(true)} className="p-2 rounded hover:bg-slate-800 text-slate-400 hover:text-cyan-400 transition-colors" title="Insertar Imagen de Biblioteca" > setIsMediaModalOpen(true)} className="p-2 rounded hover:bg-slate-800 text-slate-400 hover:text-cyan-400 transition-colors" title="Insertar Video de Biblioteca" > editor.chain().focus().undo().run()} className="p-2 text-slate-500 hover:text-white transition-colors" > editor.chain().focus().redo().run()} className="p-2 text-slate-500 hover:text-white transition-colors" > setIsMediaModalOpen(false)} onChange={handleMediaInsert} /> > ); }; export const RichTextEditor = ({ value, onChange, label }: RichTextEditorProps) => { const editor = useEditor({ extensions: [ StarterKit, Underline, Link.configure({ openOnClick: false, HTMLAttributes: { class: 'text-cyan-400 underline decoration-cyan-400/50 hover:text-cyan-300 transition-colors', }, }), TextStyle, Color, Image.configure({ inline: true, allowBase64: true, HTMLAttributes: { class: 'rounded-xl shadow-lg max-w-full h-auto object-cover my-6' } }), VideoNode, Youtube.configure({ controls: true, HTMLAttributes: { class: 'w-full aspect-video rounded-xl shadow-lg my-6' } }), TextAlign.configure({ types: ['heading', 'paragraph'], alignments: ['left', 'center', 'right', 'justify'], }), Highlight.configure({ multicolor: true }), ], content: value, onUpdate: ({ editor }) => { onChange(editor.getHTML()); }, editorProps: { attributes: { class: 'prose prose-invert max-w-none focus:outline-none min-h-[300px] p-6 text-slate-200 leading-relaxed custom-scrollbar whitespace-pre-wrap break-words prose-p:empty:before:content-["\\00a0"]', }, }, }); const [mounted, setMounted] = useState(false); useEffect(() => { setMounted(true); }, []); // Keep content synced if value changes externally useEffect(() => { if (editor && value && value !== editor.getHTML()) { const fixedValue = value .replace(/(]*src=["'])([^"']*)\/preview\?([^"']*["'])/gi, '$1$2/view?$3') .replace(/(]*src=["'])([^"']*)\/preview\?([^"']*["'])/gi, '$1$2/view?$3'); editor.commands.setContent(fixedValue); } }, [value, editor]); if (!mounted) { return ( {label} ); } return ( {label} {/* Hidden input for form submission if needed, though we use controlled value */} ); };