'use client' import { useState, useEffect, useCallback, memo, useMemo, useRef } from 'react'; import { DndContext, DragEndEvent, DragOverlay, DragStartEvent, PointerSensor, TouchSensor, closestCenter, useSensor, useSensors, } from '@dnd-kit/core'; import { SortableContext, arrayMove, rectSortingStrategy, useSortable, } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { Note } from '@/lib/types'; import { NoteCard } from './note-card'; import { updateFullOrderWithoutRevalidation } from '@/app/actions/notes'; import { useNotebookDrag } from '@/context/notebook-drag-context'; import { useLanguage } from '@/lib/i18n'; import { useCardSizeMode } from '@/hooks/use-card-size-mode'; import dynamic from 'next/dynamic'; import './masonry-grid.css'; // Lazy-load NoteEditor — uniquement chargé au clic const NoteEditor = dynamic( () => import('./note-editor').then(m => ({ default: m.NoteEditor })), { ssr: false } ); interface MasonryGridProps { notes: Note[]; onEdit?: (note: Note, readOnly?: boolean) => void; onSizeChange?: (noteId: string, size: 'small' | 'medium' | 'large') => void; isTrashView?: boolean; } // ───────────────────────────────────────────── // Sortable Note Item // ───────────────────────────────────────────── interface SortableNoteProps { note: Note; onEdit: (note: Note, readOnly?: boolean) => void; onSizeChange: (noteId: string, newSize: 'small' | 'medium' | 'large') => void; onDragStartNote?: (noteId: string) => void; onDragEndNote?: () => void; isDragging?: boolean; isOverlay?: boolean; isTrashView?: boolean; } const SortableNoteItem = memo(function SortableNoteItem({ note, onEdit, onSizeChange, onDragStartNote, onDragEndNote, isDragging, isOverlay, isTrashView, }: SortableNoteProps) { const { attributes, listeners, setNodeRef, transform, transition, isDragging: isSortableDragging, } = useSortable({ id: note.id }); const style: React.CSSProperties = { transform: CSS.Transform.toString(transform), transition, opacity: isSortableDragging && !isOverlay ? 0.3 : 1, }; return (
onSizeChange(note.id, newSize)} />
); }) // ───────────────────────────────────────────── // Sortable Grid Section (pinned or others) // ───────────────────────────────────────────── interface SortableGridSectionProps { notes: Note[]; onEdit: (note: Note, readOnly?: boolean) => void; onSizeChange: (noteId: string, newSize: 'small' | 'medium' | 'large') => void; draggedNoteId: string | null; onDragStartNote: (noteId: string) => void; onDragEndNote: () => void; isTrashView?: boolean; } const SortableGridSection = memo(function SortableGridSection({ notes, onEdit, onSizeChange, draggedNoteId, onDragStartNote, onDragEndNote, isTrashView, }: SortableGridSectionProps) { const ids = useMemo(() => notes.map(n => n.id), [notes]); return (
{notes.map(note => ( ))}
); }); // ───────────────────────────────────────────── // Main MasonryGrid component // ───────────────────────────────────────────── export function MasonryGrid({ notes, onEdit, onSizeChange, isTrashView }: MasonryGridProps) { const { t } = useLanguage(); const [editingNote, setEditingNote] = useState<{ note: Note; readOnly?: boolean } | null>(null); const { startDrag, endDrag, draggedNoteId } = useNotebookDrag(); const cardSizeMode = useCardSizeMode(); const isUniformMode = cardSizeMode === 'uniform'; // Local notes state for optimistic size/order updates const [localNotes, setLocalNotes] = useState(notes); useEffect(() => { setLocalNotes(prev => { const prevIds = prev.map(n => n.id).join(',') const incomingIds = notes.map(n => n.id).join(',') if (prevIds === incomingIds) { const localSizeMap = new Map(prev.map(n => [n.id, n.size])) return notes.map(n => ({ ...n, size: localSizeMap.get(n.id) ?? n.size })) } // Notes added/removed: full sync but preserve local sizes const localSizeMap = new Map(prev.map(n => [n.id, n.size])) return notes.map(n => ({ ...n, size: localSizeMap.get(n.id) ?? n.size })) }) }, [notes]); const pinnedNotes = useMemo(() => localNotes.filter(n => n.isPinned), [localNotes]); const othersNotes = useMemo(() => localNotes.filter(n => !n.isPinned), [localNotes]); const [activeId, setActiveId] = useState(null); const activeNote = useMemo( () => localNotes.find(n => n.id === activeId) ?? null, [localNotes, activeId] ); const handleEdit = useCallback((note: Note, readOnly?: boolean) => { if (onEdit) { onEdit(note, readOnly); } else { setEditingNote({ note, readOnly }); } }, [onEdit]); const handleSizeChange = useCallback((noteId: string, newSize: 'small' | 'medium' | 'large') => { setLocalNotes(prev => prev.map(n => n.id === noteId ? { ...n, size: newSize } : n)); onSizeChange?.(noteId, newSize); }, [onSizeChange]); // @dnd-kit sensors — pointer (desktop) + touch (mobile) const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8 }, // Évite les activations accidentelles }), useSensor(TouchSensor, { activationConstraint: { delay: 200, tolerance: 8 }, // Long-press sur mobile }) ); const localNotesRef = useRef(localNotes) useEffect(() => { localNotesRef.current = localNotes }, [localNotes]) const handleDragStart = useCallback((event: DragStartEvent) => { setActiveId(event.active.id as string); startDrag(event.active.id as string); }, [startDrag]); const handleDragEnd = useCallback(async (event: DragEndEvent) => { const { active, over } = event; setActiveId(null); endDrag(); if (!over || active.id === over.id) return; const reordered = arrayMove( localNotesRef.current, localNotesRef.current.findIndex(n => n.id === active.id), localNotesRef.current.findIndex(n => n.id === over.id), ); if (reordered.length === 0) return; setLocalNotes(reordered); // Persist order outside of setState to avoid "setState in render" warning const ids = reordered.map(n => n.id); updateFullOrderWithoutRevalidation(ids).catch(err => { console.error('Failed to persist order:', err); }); }, [endDrag]); return (
{pinnedNotes.length > 0 && (

{t('notes.pinned')}

)} {othersNotes.length > 0 && (
{pinnedNotes.length > 0 && (

{t('notes.others')}

)}
)}
{/* DragOverlay — montre une copie flottante pendant le drag */} {activeNote ? (
handleSizeChange(activeNote.id, newSize)} />
) : null}
{editingNote && ( setEditingNote(null)} /> )}
); }