refactor(ux): consolidate BMAD skills, update design system, and clean up Prisma generated client
This commit is contained in:
292
keep-notes/.backup-keep/masonry-grid.tsx
Normal file
292
keep-notes/.backup-keep/masonry-grid.tsx
Normal file
@@ -0,0 +1,292 @@
|
||||
'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;
|
||||
onSizeChange?: (noteId: string, size: 'small' | 'medium' | 'large') => 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 (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
className="masonry-sortable-item"
|
||||
data-id={note.id}
|
||||
data-size={note.size}
|
||||
>
|
||||
<NoteCard
|
||||
note={note}
|
||||
onEdit={onEdit}
|
||||
onDragStart={onDragStartNote}
|
||||
onDragEnd={onDragEndNote}
|
||||
isDragging={isDragging}
|
||||
onSizeChange={(newSize) => onSizeChange(note.id, newSize)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 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 (
|
||||
<SortableContext items={ids} strategy={rectSortingStrategy}>
|
||||
<div className="masonry-css-grid">
|
||||
{notes.map(note => (
|
||||
<SortableNoteItem
|
||||
key={note.id}
|
||||
note={note}
|
||||
onEdit={onEdit}
|
||||
onSizeChange={onSizeChange}
|
||||
onDragStartNote={onDragStartNote}
|
||||
onDragEndNote={onDragEndNote}
|
||||
isDragging={draggedNoteId === note.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
);
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Main MasonryGrid component
|
||||
// ─────────────────────────────────────────────
|
||||
export function MasonryGrid({ notes, onEdit, onSizeChange }: 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<Note[]>(notes);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalNotes(prev => {
|
||||
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<string | null>(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<Note[]>(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 (
|
||||
<DndContext
|
||||
id="masonry-dnd"
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<div className="masonry-container">
|
||||
{pinnedNotes.length > 0 && (
|
||||
<div className="mb-8">
|
||||
<h2 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3 px-2">
|
||||
{t('notes.pinned')}
|
||||
</h2>
|
||||
<SortableGridSection
|
||||
notes={pinnedNotes}
|
||||
onEdit={handleEdit}
|
||||
onSizeChange={handleSizeChange}
|
||||
draggedNoteId={draggedNoteId}
|
||||
onDragStartNote={startDrag}
|
||||
onDragEndNote={endDrag}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{othersNotes.length > 0 && (
|
||||
<div>
|
||||
{pinnedNotes.length > 0 && (
|
||||
<h2 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3 px-2">
|
||||
{t('notes.others')}
|
||||
</h2>
|
||||
)}
|
||||
<SortableGridSection
|
||||
notes={othersNotes}
|
||||
onEdit={handleEdit}
|
||||
onSizeChange={handleSizeChange}
|
||||
draggedNoteId={draggedNoteId}
|
||||
onDragStartNote={startDrag}
|
||||
onDragEndNote={endDrag}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* DragOverlay — montre une copie flottante pendant le drag */}
|
||||
<DragOverlay>
|
||||
{activeNote ? (
|
||||
<div className="masonry-sortable-item masonry-drag-overlay" data-size={activeNote.size}>
|
||||
<NoteCard
|
||||
note={activeNote}
|
||||
onEdit={handleEdit}
|
||||
isDragging={true}
|
||||
onSizeChange={(newSize) => handleSizeChange(activeNote.id, newSize)}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</DragOverlay>
|
||||
|
||||
{editingNote && (
|
||||
<NoteEditor
|
||||
note={editingNote.note}
|
||||
readOnly={editingNote.readOnly}
|
||||
onClose={() => setEditingNote(null)}
|
||||
/>
|
||||
)}
|
||||
</DndContext>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user