'use client' import { useState, useEffect, useRef, useCallback, memo, useMemo } from 'react'; import { Note } from '@/lib/types'; import { NoteCard } from './note-card'; import { NoteEditor } from './note-editor'; import { updateFullOrderWithoutRevalidation } from '@/app/actions/notes'; import { useResizeObserver } from '@/hooks/use-resize-observer'; import { useNotebookDrag } from '@/context/notebook-drag-context'; import { useLanguage } from '@/lib/i18n'; interface MasonryGridProps { notes: Note[]; onEdit?: (note: Note, readOnly?: boolean) => void; } interface MasonryItemProps { note: Note; onEdit: (note: Note, readOnly?: boolean) => void; onResize: () => void; onDragStart?: (noteId: string) => void; onDragEnd?: () => void; isDragging?: boolean; } function getSizeClasses(size: string = 'small') { switch (size) { case 'medium': return 'w-full sm:w-full lg:w-2/3 xl:w-2/4 2xl:w-2/5'; case 'large': return 'w-full'; case 'small': default: return 'w-full sm:w-1/2 lg:w-1/3 xl:w-1/4 2xl:w-1/5'; } } const MasonryItem = memo(function MasonryItem({ note, onEdit, onResize, onDragStart, onDragEnd, isDragging }: MasonryItemProps) { const resizeRef = useResizeObserver(onResize); const sizeClasses = getSizeClasses(note.size); return (
); }, (prev, next) => { // Custom comparison to avoid re-render on function prop changes if note data is same return prev.note.id === next.note.id && prev.note.order === next.note.order && prev.isDragging === next.isDragging; }); export function MasonryGrid({ notes, onEdit }: MasonryGridProps) { const { t } = useLanguage(); const [editingNote, setEditingNote] = useState<{ note: Note; readOnly?: boolean } | null>(null); const { startDrag, endDrag, draggedNoteId } = useNotebookDrag(); // Use external onEdit if provided, otherwise use internal state const handleEdit = useCallback((note: Note, readOnly?: boolean) => { if (onEdit) { onEdit(note, readOnly); } else { setEditingNote({ note, readOnly }); } }, [onEdit]); const pinnedGridRef = useRef(null); const othersGridRef = useRef(null); const pinnedMuuri = useRef(null); const othersMuuri = useRef(null); // Memoize filtered and sorted notes to avoid recalculation on every render const pinnedNotes = useMemo( () => notes.filter(n => n.isPinned).sort((a, b) => a.order - b.order), [notes] ); const othersNotes = useMemo( () => notes.filter(n => !n.isPinned).sort((a, b) => a.order - b.order), [notes] ); // CRITICAL: Sync editingNote when underlying note changes (e.g., after moving to notebook) // This ensures the NoteEditor gets the updated note with the new notebookId useEffect(() => { if (!editingNote) return; // Find the updated version of the currently edited note in the notes array const updatedNote = notes.find(n => n.id === editingNote.note.id); if (updatedNote) { // Check if any key properties changed (especially notebookId) const notebookIdChanged = updatedNote.notebookId !== editingNote.note.notebookId; if (notebookIdChanged) { // Update the editingNote with the new data setEditingNote(prev => prev ? { ...prev, note: updatedNote } : null); } } }, [notes, editingNote]); const handleDragEnd = useCallback(async (grid: any) => { if (!grid) return; const items = grid.getItems(); const ids = items .map((item: any) => item.getElement()?.getAttribute('data-id')) .filter((id: any): id is string => !!id); try { // Save order to database WITHOUT revalidating the page // Muuri has already updated the visual layout, so we don't need to reload await updateFullOrderWithoutRevalidation(ids); } catch (error) { console.error('Failed to persist order:', error); } }, []); const refreshLayout = useCallback(() => { // Use requestAnimationFrame for smoother updates requestAnimationFrame(() => { if (pinnedMuuri.current) { pinnedMuuri.current.refreshItems().layout(); } if (othersMuuri.current) { othersMuuri.current.refreshItems().layout(); } }); }, []); // Initialize Muuri grids once on mount and sync when needed useEffect(() => { let isMounted = true; let muuriInitialized = false; const initMuuri = async () => { // Prevent duplicate initialization if (muuriInitialized) return; muuriInitialized = true; // Import web-animations-js polyfill await import('web-animations-js'); // Dynamic import of Muuri to avoid SSR window error const MuuriClass = (await import('muuri')).default; if (!isMounted) return; // Detect if we are on a touch device (mobile behavior) const isMobile = window.matchMedia('(pointer: coarse)').matches; const layoutOptions = { dragEnabled: true, // Always use specific drag handle to avoid conflicts dragHandle: '.muuri-drag-handle', dragContainer: document.body, dragStartPredicate: { distance: 10, delay: 0, }, dragPlaceholder: { enabled: true, createElement: (item: any) => { const el = item.getElement().cloneNode(true); el.style.opacity = '0.5'; return el; }, }, dragAutoScroll: { targets: [window], speed: (item: any, target: any, intersection: any) => { return intersection * 20; }, }, }; // Initialize pinned grid if (pinnedGridRef.current && !pinnedMuuri.current) { pinnedMuuri.current = new MuuriClass(pinnedGridRef.current, layoutOptions) .on('dragEnd', () => handleDragEnd(pinnedMuuri.current)); } // Initialize others grid if (othersGridRef.current && !othersMuuri.current) { othersMuuri.current = new MuuriClass(othersGridRef.current, layoutOptions) .on('dragEnd', () => handleDragEnd(othersMuuri.current)); } }; initMuuri(); return () => { isMounted = false; pinnedMuuri.current?.destroy(); othersMuuri.current?.destroy(); pinnedMuuri.current = null; othersMuuri.current = null; }; // Only run once on mount // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Synchronize items when notes change (e.g. searching, adding) useEffect(() => { requestAnimationFrame(() => { if (pinnedMuuri.current) { pinnedMuuri.current.refreshItems().layout(); } if (othersMuuri.current) { othersMuuri.current.refreshItems().layout(); } }); }, [notes]); return (
{pinnedNotes.length > 0 && (

{t('notes.pinned')}

{pinnedNotes.map(note => ( ))}
)} {othersNotes.length > 0 && (
{pinnedNotes.length > 0 && (

{t('notes.others')}

)}
{othersNotes.map(note => ( ))}
)} {editingNote && ( setEditingNote(null)} /> )}
); }