'use client' import { useEffect } from 'react' import { Editor } from '@tinymce/tinymce-react' import { useLocale } from 'next-intl' import { wowheadUrl, wowheadData, type WowheadType } from '@/lib/wowhead' /** * Editor TinyMCE del foro (community, self-hosted desde /tinymce — ver * scripts/copy-tinymce.mjs). Mismo editor que el foro original de FusionCMS, con su * skin oscura y su lista de plugins, adaptado a React como componente controlado: * el HTML vive en el estado del padre (value/onChange) y se envía por fetch, en vez * del `tinyMCE.triggerSave()` + jQuery del forum.js original. * * `licenseKey: 'gpl'` deja claro que es la edición community (GPL), no la de nube. * El HTML que produce se sanea SIEMPRE en el servidor con nh3 (lib/forum-sanitize), * nunca se confía en el cliente. * * Botón «Wowhead»: abre un diálogo (tipo + ID + texto) e inserta un enlace de * wowhead con su data-wowhead en el idioma de la web, para que salga el tooltip, * el color por calidad y el icono (lo pinta el script global tooltips.js). */ const WOWHEAD_TYPES: { value: WowheadType; es: string; en: string }[] = [ { value: 'item', es: 'Ítem', en: 'Item' }, { value: 'spell', es: 'Hechizo', en: 'Spell' }, { value: 'quest', es: 'Misión', en: 'Quest' }, { value: 'npc', es: 'PNJ', en: 'NPC' }, { value: 'achievement', es: 'Logro', en: 'Achievement' }, { value: 'object', es: 'Objeto', en: 'Object' }, ] function esc(s: string): string { return s.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"') } export function ForumEditor({ value, onChange, height = 400, }: { value: string onChange: (html: string) => void height?: number }) { const locale = useLocale() const es = locale !== 'en' const L = { title: 'Wowhead', tooltip: es ? 'Insertar enlace de Wowhead' : 'Insert a Wowhead link', type: es ? 'Tipo' : 'Type', id: 'ID', text: es ? 'Texto (vacío = nombre automático)' : 'Text (empty = auto name)', insert: es ? 'Insertar' : 'Insert', cancel: es ? 'Cancelar' : 'Cancel', preview: es ? 'Vista previa' : 'Preview', } // Reprocesa los enlaces de wowhead de la VISTA PREVIA (que vive en el documento // principal, no en el iframe del editor) cada vez que cambia el texto. Así el // preview se ve como el post publicado sin ensuciar lo que se guarda. useEffect(() => { const t = setTimeout(() => { try { ;(window as unknown as { $WowheadPower?: { refreshLinks?: () => void } }).$WowheadPower?.refreshLinks?.() } catch { /* no crítico */ } }, 300) return () => clearTimeout(t) }, [value]) return ( <> onChange(content)} init={{ height, menubar: false, statusbar: false, // Idioma de la interfaz del editor según el locale de la web. El inglés es el // idioma por defecto de TinyMCE (no necesita paquete); el español se carga // desde public/tinymce-langs/es.js (localización oficial de TinyMCE). language: es ? 'es' : 'en', ...(es ? { language_url: '/tinymce-langs/es.js' } : {}), skin: 'oxide-dark', content_css: 'dark', // colores de calidad de WoW dentro del editor (para el preview de wowhead) content_style: 'a{text-decoration:none}' + '.q0{color:#9d9d9d}.q1{color:#fff}.q2{color:#1eff00}.q3{color:#0070dd}' + '.q4{color:#a335ee}.q5{color:#ff8000}.q6{color:#e6cc80}.q7{color:#00ccff}', branding: false, promotion: false, mobile: { menubar: true }, plugins: 'preview searchreplace autolink directionality visualblocks visualchars fullscreen image link media codesample table charmap pagebreak nonbreaking anchor insertdatetime advlist lists wordcount emoticons', toolbar: 'bold italic strikethrough forecolor backcolor emoticons | link wowhead image media | alignleft aligncenter alignright alignjustify | numlist bullist outdent indent | removeformat', image_advtab: true, setup: (editor) => { // OJO: no se ejecuta wowhead DENTRO del editor a propósito. Si se hace, // wowhead muta el DOM editable (icono, clase icontinyl y una tabla enorme // oculta con el tooltip) y TinyMCE lo serializa al publicar, ensuciando el // post. El editor muestra el enlace con su nombre; el icono y el color de // calidad los pone wowhead en la página publicada (como recruit). editor.ui.registry.addButton('wowhead', { text: 'Wowhead', tooltip: L.tooltip, onAction: () => { editor.windowManager.open({ title: L.title, body: { type: 'panel', items: [ { type: 'listbox', name: 'wtype', label: L.type, items: WOWHEAD_TYPES.map((t) => ({ value: t.value, text: es ? t.es : t.en })), }, { type: 'input', name: 'wid', label: L.id }, { type: 'input', name: 'wtext', label: L.text }, ], }, initialData: { wtype: 'item', wid: '', wtext: '' }, buttons: [ { type: 'cancel', text: L.cancel }, { type: 'submit', text: L.insert, primary: true }, ], onSubmit: async (api) => { const data = api.getData() as { wtype: WowheadType; wid: string; wtext: string } const id = String(data.wid).trim().replace(/\D/g, '') if (!id) { api.close() return } const type = (data.wtype || 'item') as WowheadType const href = wowheadUrl(type, id, locale) const wh = wowheadData(type, id, locale) const custom = data.wtext.trim() api.close() // Resolver nombre + calidad en el servidor para insertar el enlace // YA con su nombre real y su color (clase qN), sin el parpadeo de // «item=xxx». Si falla, se cae al modo data-wh-rename-link. // Igual que recruit: se inserta un enlace PLANO con el nombre real // como texto (resuelto en el servidor, sin el parpadeo de item=xxx), // y wowhead le añade el icono (como fondo, icontinyl) y el color de // calidad al vuelo. Nada de ni clases horneadas: así no sale // doble ni descolocado. Si no hay texto y no se resuelve el nombre, // se cae a data-wh-rename-link para que wowhead lo nombre. let name = custom if (!name) { try { const res = await fetch(`/api/forum/wowhead?type=${type}&id=${id}&locale=${locale}`) const info = (await res.json()) as { name: string | null } if (info.name) name = info.name } catch { /* wowhead no disponible: fallback abajo */ } } const rename = name ? '' : ' data-wh-rename-link="true"' const label = esc(name || `${type}=${id}`) editor.insertContent( `${label} `, ) }, }) }, }) }, }} /> {value.trim() && (
{L.preview}
)} ) }