'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)}
/>
)}
);
}