'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 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; } // ───────────────────────────────────────────── // 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; } const SortableNoteItem = memo(function SortableNoteItem({ note, onEdit, onSizeChange, onDragStartNote, onDragEndNote, isDragging, isOverlay, }: 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; } const SortableGridSection = memo(function SortableGridSection({ notes, onEdit, onSizeChange, draggedNoteId, onDragStartNote, onDragEndNote, }: SortableGridSectionProps) { const ids = useMemo(() => notes.map(n => n.id), [notes]); return (
{notes.map(note => ( ))}
); }); // ───────────────────────────────────────────── // Main MasonryGrid component // ───────────────────────────────────────────── export function MasonryGrid({ notes, onEdit }: MasonryGridProps) { const { t } = useLanguage(); const [editingNote, setEditingNote] = useState<{ note: Note; readOnly?: boolean } | null>(null); const { startDrag, endDrag, draggedNoteId } = useNotebookDrag(); // Local notes state for optimistic size/order updates const [localNotes, setLocalNotes] = useState(notes); useEffect(() => { setLocalNotes(notes); }, [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)); }, []); // @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 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; setLocalNotes(prev => { const oldIndex = prev.findIndex(n => n.id === active.id); const newIndex = prev.findIndex(n => n.id === over.id); if (oldIndex === -1 || newIndex === -1) return prev; return arrayMove(prev, oldIndex, newIndex); }); // Persist new order to DB (sans revalidation pour éviter le flash) setLocalNotes(current => { const ids = current.map(n => n.id); updateFullOrderWithoutRevalidation(ids).catch(err => { console.error('Failed to persist order:', err); }); return current; }); }, [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)} /> )}
); }