diff --git a/src/app/api/seguimiento/route.ts b/src/app/api/seguimiento/route.ts index 205c863..7c1dd78 100644 --- a/src/app/api/seguimiento/route.ts +++ b/src/app/api/seguimiento/route.ts @@ -105,7 +105,17 @@ export async function POST(req: NextRequest) { : typeof body.note === 'string' ? body.note.trim() : ''; - if (!text) { + const plain = text + .replace(//gi, '\n') + .replace(/<\/(p|div|li|h[1-6])>/gi, '\n') + .replace(/<[^>]+>/g, '') + .replace(/ /gi, ' ') + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/\s+/g, ' ') + .trim(); + if (!plain) { return NextResponse.json({ error: 'Texto de la nota requerido' }, { status: 400 }); } const title = @@ -123,7 +133,7 @@ export async function POST(req: NextRequest) { entry_type: 'note', external_id: `manual-${randomUUID()}`, title, - summary: text.slice(0, 280), + summary: plain.slice(0, 280), body_md: text, occurred_at: occurredRaw, synced_at: new Date().toISOString(), diff --git a/src/components/NoteRichEditor.tsx b/src/components/NoteRichEditor.tsx new file mode 100644 index 0000000..a0f1fd3 --- /dev/null +++ b/src/components/NoteRichEditor.tsx @@ -0,0 +1,276 @@ +'use client'; + +/** + * Editor enriquecido ligero para notas de Seguimiento + * (barra tipo Google Docs: negrita, color, resaltado, listas, enlaces). + */ + +import React, { useCallback, useEffect, useRef } from 'react'; +import { + Bold, + Italic, + Underline, + List, + ListOrdered, + Link as LinkIcon, + Highlighter, + Type, + RemoveFormatting, + AlignLeft, + AlignCenter, + AlignRight, +} from 'lucide-react'; + +type Props = { + value: string; + onChange: (html: string) => void; + placeholder?: string; + className?: string; + minHeight?: string; +}; + +function run(cmd: string, value?: string) { + try { + document.execCommand(cmd, false, value); + } catch { + /* ignore */ + } +} + +function ToolbarBtn({ + title, + active, + onClick, + children, +}: { + title: string; + active?: boolean; + onClick: () => void; + children: React.ReactNode; +}) { + return ( + + ); +} + +const TEXT_COLORS = [ + { label: 'Negro', value: '#111827' }, + { label: 'Rojo', value: '#DC2626' }, + { label: 'Naranja', value: '#EA580C' }, + { label: 'Ámbar', value: '#D97706' }, + { label: 'Verde', value: '#059669' }, + { label: 'Azul', value: '#2563EB' }, + { label: 'Violeta', value: '#7C3AED' }, +]; + +const HIGHLIGHTS = [ + { label: 'Ninguno', value: 'transparent' }, + { label: 'Amarillo', value: '#FEF08A' }, + { label: 'Verde', value: '#BBF7D0' }, + { label: 'Azul', value: '#BFDBFE' }, + { label: 'Rosa', value: '#FECDD3' }, + { label: 'Naranja', value: '#FED7AA' }, +]; + +export default function NoteRichEditor({ + value, + onChange, + placeholder = 'Escribe la nota…', + className = '', + minHeight = '140px', +}: Props) { + const ref = useRef(null); + const lastHtml = useRef(value); + + useEffect(() => { + const el = ref.current; + if (!el) return; + if (value !== lastHtml.current && el.innerHTML !== value) { + el.innerHTML = value || ''; + lastHtml.current = value; + } + }, [value]); + + const emit = useCallback(() => { + const el = ref.current; + if (!el) return; + const html = el.innerHTML; + lastHtml.current = html; + onChange(html); + }, [onChange]); + + const apply = (fn: () => void) => { + ref.current?.focus(); + fn(); + emit(); + }; + + const addLink = () => { + const url = window.prompt('URL del enlace', 'https://'); + if (!url) return; + apply(() => run('createLink', url)); + }; + + return ( +
+
+ apply(() => run('bold'))}> + + + apply(() => run('italic'))}> + + + apply(() => run('underline'))} + > + + + + + + + + + + + + + + apply(() => run('insertUnorderedList'))} + > + + + apply(() => run('insertOrderedList'))} + > + + + + + + + + + apply(() => run('justifyLeft'))} + > + + + apply(() => run('justifyCenter'))} + > + + + apply(() => run('justifyRight'))} + > + + + + apply(() => run('removeFormat'))} + > + + +
+ +
+
+ ); +} + +/** True if HTML has visible text. */ +export function richTextHasContent(html: string): boolean { + const text = html + .replace(//gi, '') + .replace(/ /gi, ' ') + .replace(/<[^>]+>/g, '') + .trim(); + return text.length > 0; +} diff --git a/src/components/SeguimientoBoard.tsx b/src/components/SeguimientoBoard.tsx index 39b6d38..7da893e 100644 --- a/src/components/SeguimientoBoard.tsx +++ b/src/components/SeguimientoBoard.tsx @@ -14,6 +14,7 @@ import { Send, } from 'lucide-react'; import { PageHeader } from '@/components/ui'; +import NoteRichEditor, { richTextHasContent } from '@/components/NoteRichEditor'; type Entry = { id: string; @@ -49,7 +50,9 @@ function typeTone(t: string) { } function looksLikeHtml(s: string) { - return /<\/?(?:ul|ol|li|p|a|strong|br)\b/i.test(s); + return /<\/?(?:ul|ol|li|p|a|strong|b|em|i|u|br|span|font|div|h[1-6])\b/i.test( + s + ); } function escapeHtml(s: string) { @@ -104,7 +107,7 @@ export default function SeguimientoBoard() { }, [data, query]); async function saveNote() { - if (!noteBody.trim()) return; + if (!richTextHasContent(noteBody)) return; setNoteBusy(true); setNoteMsg(null); try { @@ -113,7 +116,7 @@ export default function SeguimientoBoard() { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: noteTitle.trim() || 'Nota de seguimiento', - body: noteBody.trim(), + body: noteBody, occurredAt: new Date().toISOString(), }), }); @@ -209,8 +212,8 @@ export default function SeguimientoBoard() { Nueva nota

- Se guarda con fecha. El histórico también incluye entradas - sincronizadas desde Superhuman Docs (docs de seguimiento). + Formato enriquecido (negrita, color, resaltado, listas, enlaces). El + histórico también incluye entradas de Superhuman / Google Doc.

-