'use client' import { Node, mergeAttributes } from '@tiptap/core' import { ReactNodeViewRenderer, NodeViewWrapper } from '@tiptap/react' import { useEffect, useRef, useState, useCallback } from 'react' import { Zap, AlertCircle, Unlink, ArrowRight } from 'lucide-react' // --------------------------------------------------------------------------- // LiveBlock Node View // --------------------------------------------------------------------------- interface LiveBlockViewProps { node: { attrs: { sourceNoteId: string blockId: string snapshotContent: string sourceNoteTitle: string } } updateAttributes: (attrs: Record) => void deleteNode: () => void } function LiveBlockView({ node, updateAttributes, deleteNode }: LiveBlockViewProps) { const { sourceNoteId, blockId, snapshotContent, sourceNoteTitle } = node.attrs const [localContent, setLocalContent] = useState(snapshotContent || '') const [isDeleted, setIsDeleted] = useState(false) const [isOffline, setIsOffline] = useState(false) const [pulse, setPulse] = useState(false) const pulseTimerRef = useRef | null>(null) // Fetch current block status on mount useEffect(() => { if (!sourceNoteId || !blockId) return fetch(`/api/blocks/${encodeURIComponent(blockId)}/status?sourceNoteId=${sourceNoteId}`) .then(r => r.json()) .then((data: { exists: boolean; content: string; sourceNoteTitle: string }) => { if (!data.exists) { setIsDeleted(true) } else { setLocalContent(data.content) updateAttributes({ snapshotContent: data.content, sourceNoteTitle: data.sourceNoteTitle }) } }) .catch(() => setIsOffline(true)) }, [sourceNoteId, blockId]) // eslint-disable-line react-hooks/exhaustive-deps // Listen for real-time block update events useEffect(() => { const handleBlockUpdate = (e: CustomEvent) => { if (e.detail?.blockId !== blockId) return setLocalContent(e.detail.content) updateAttributes({ snapshotContent: e.detail.content }) setPulse(true) if (pulseTimerRef.current) clearTimeout(pulseTimerRef.current) pulseTimerRef.current = setTimeout(() => setPulse(false), 1200) } const handleBlockDeleted = (e: CustomEvent) => { if (e.detail?.blockId !== blockId) return setIsDeleted(true) } window.addEventListener('live-block:update', handleBlockUpdate as EventListener) window.addEventListener('live-block:deleted', handleBlockDeleted as EventListener) return () => { window.removeEventListener('live-block:update', handleBlockUpdate as EventListener) window.removeEventListener('live-block:deleted', handleBlockDeleted as EventListener) if (pulseTimerRef.current) clearTimeout(pulseTimerRef.current) } }, [blockId, updateAttributes]) const handleDetach = useCallback(async () => { // Convert this node to a plain paragraph with snapshot text deleteNode() }, [deleteNode]) const handleOpenSource = useCallback(() => { window.open(`/home?openNote=${encodeURIComponent(sourceNoteId)}`, '_blank', 'noopener,noreferrer') }, [sourceNoteId]) const borderClass = isDeleted ? 'border-l-rose-500 border-y-rose-200 border-r-rose-200 bg-rose-50/20 dark:border-l-red-700 dark:border-y-red-900/40 dark:border-r-red-900/40 dark:bg-red-950/5' : isOffline ? 'border-l-amber-500 border-y-amber-200 border-r-amber-200 bg-amber-50/10 dark:border-l-amber-600 dark:border-y-amber-800/40 dark:border-r-amber-800/40' : pulse ? 'border-l-blue-500 border-y-blue-300 border-r-blue-300 bg-blue-50/20 shadow-md shadow-blue-500/15 dark:bg-blue-950/10' : 'border-l-blue-500 border-y-[#E8E6E3] border-r-[#E8E6E3] bg-blue-50/5 dark:border-y-zinc-800 dark:border-r-zinc-800 dark:bg-blue-950/5' return (
{/* Header */}
{isDeleted ? ( ) : ( )} {isDeleted ? 'Source déconnectée' : (sourceNoteTitle || 'Note connectée')} {isDeleted ? ( DÉCONNECTÉ ) : isOffline ? ( HORS-LIGNE ) : ( LIVE )}
{isDeleted ? ( ) : ( )}
{/* Content */}

{localContent || '(bloc vide)'}

) } // --------------------------------------------------------------------------- // TipTap Node Definition // --------------------------------------------------------------------------- export const LiveBlockExtension = Node.create({ name: 'liveBlock', group: 'block', atom: true, draggable: true, selectable: true, addAttributes() { return { sourceNoteId: { default: '' }, blockId: { default: '' }, snapshotContent: { default: '' }, sourceNoteTitle: { default: '' }, } }, parseHTML() { return [{ tag: 'div[data-live-block]' }] }, renderHTML({ HTMLAttributes }) { return ['div', mergeAttributes(HTMLAttributes, { 'data-live-block': 'true' })] }, addNodeView() { return ReactNodeViewRenderer(LiveBlockView) }, })