Fix tests and add changelog
This commit is contained in:
@@ -2,93 +2,257 @@
|
||||
|
||||
import { Note } from '@/lib/types'
|
||||
import { NoteCard } from './note-card'
|
||||
import { useState } from 'react'
|
||||
import { useState, useMemo, useEffect } from 'react'
|
||||
import { NoteEditor } from './note-editor'
|
||||
import { reorderNotes } from '@/app/actions/notes'
|
||||
import { reorderNotes, getNotes } from '@/app/actions/notes'
|
||||
import {
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
DragOverlay,
|
||||
DragStartEvent,
|
||||
MouseSensor,
|
||||
TouchSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
closestCenter,
|
||||
PointerSensor,
|
||||
} from '@dnd-kit/core'
|
||||
import {
|
||||
SortableContext,
|
||||
rectSortingStrategy,
|
||||
useSortable,
|
||||
arrayMove,
|
||||
} from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
interface NoteGridProps {
|
||||
notes: Note[]
|
||||
}
|
||||
|
||||
function SortableNote({ note, onEdit }: { note: Note; onEdit: (note: Note) => void }) {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id: note.id })
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
zIndex: isDragging ? 1000 : 1,
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
data-note-id={note.id}
|
||||
data-draggable="true"
|
||||
>
|
||||
<NoteCard note={note} onEdit={onEdit} isDragging={isDragging} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function NoteGrid({ notes }: NoteGridProps) {
|
||||
const router = useRouter()
|
||||
const [editingNote, setEditingNote] = useState<Note | null>(null)
|
||||
const [draggedNote, setDraggedNote] = useState<Note | null>(null)
|
||||
const [dragOverNote, setDragOverNote] = useState<Note | null>(null)
|
||||
const [activeId, setActiveId] = useState<string | null>(null)
|
||||
const [localPinnedNotes, setLocalPinnedNotes] = useState<Note[]>([])
|
||||
const [localUnpinnedNotes, setLocalUnpinnedNotes] = useState<Note[]>([])
|
||||
|
||||
const pinnedNotes = notes.filter(note => note.isPinned).sort((a, b) => a.order - b.order)
|
||||
const unpinnedNotes = notes.filter(note => !note.isPinned).sort((a, b) => a.order - b.order)
|
||||
// Sync local state with props
|
||||
useEffect(() => {
|
||||
setLocalPinnedNotes(notes.filter(note => note.isPinned).sort((a, b) => a.order - b.order))
|
||||
setLocalUnpinnedNotes(notes.filter(note => !note.isPinned).sort((a, b) => a.order - b.order))
|
||||
}, [notes])
|
||||
|
||||
const handleDragStart = (note: Note) => {
|
||||
setDraggedNote(note)
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: {
|
||||
distance: 8,
|
||||
},
|
||||
}),
|
||||
useSensor(MouseSensor, {
|
||||
activationConstraint: {
|
||||
distance: 8,
|
||||
},
|
||||
}),
|
||||
useSensor(TouchSensor, {
|
||||
activationConstraint: {
|
||||
delay: 200,
|
||||
tolerance: 6,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
const handleDragStart = (event: DragStartEvent) => {
|
||||
console.log('[DND-DEBUG] Drag started:', {
|
||||
activeId: event.active.id,
|
||||
activeData: event.active.data.current
|
||||
})
|
||||
setActiveId(event.active.id as string)
|
||||
}
|
||||
|
||||
const handleDragEnd = async () => {
|
||||
if (draggedNote && dragOverNote && draggedNote.id !== dragOverNote.id) {
|
||||
// Reorder notes
|
||||
const sourceIndex = notes.findIndex(n => n.id === draggedNote.id)
|
||||
const targetIndex = notes.findIndex(n => n.id === dragOverNote.id)
|
||||
const handleDragEnd = async (event: DragEndEvent) => {
|
||||
const { active, over } = event
|
||||
console.log('[DND-DEBUG] Drag ended:', {
|
||||
activeId: active.id,
|
||||
overId: over?.id,
|
||||
hasOver: !!over
|
||||
})
|
||||
setActiveId(null)
|
||||
|
||||
if (!over || active.id === over.id) {
|
||||
console.log('[DND-DEBUG] Drag cancelled: no valid drop target or same element')
|
||||
return
|
||||
}
|
||||
|
||||
const activeIdStr = active.id as string
|
||||
const overIdStr = over.id as string
|
||||
|
||||
// Determine which section the dragged note belongs to
|
||||
const isInPinned = localPinnedNotes.some(n => n.id === activeIdStr)
|
||||
const targetIsInPinned = localPinnedNotes.some(n => n.id === overIdStr)
|
||||
|
||||
console.log('[DND-DEBUG] Section check:', {
|
||||
activeIdStr,
|
||||
overIdStr,
|
||||
isInPinned,
|
||||
targetIsInPinned,
|
||||
pinnedNotesCount: localPinnedNotes.length,
|
||||
unpinnedNotesCount: localUnpinnedNotes.length
|
||||
})
|
||||
|
||||
// Only allow reordering within the same section
|
||||
if (isInPinned !== targetIsInPinned) {
|
||||
console.log('[DND-DEBUG] Drag cancelled: crossing sections (pinned/unpinned)')
|
||||
return
|
||||
}
|
||||
|
||||
if (isInPinned) {
|
||||
// Reorder pinned notes
|
||||
const oldIndex = localPinnedNotes.findIndex(n => n.id === activeIdStr)
|
||||
const newIndex = localPinnedNotes.findIndex(n => n.id === overIdStr)
|
||||
|
||||
await reorderNotes(draggedNote.id, dragOverNote.id)
|
||||
console.log('[DND-DEBUG] Pinned reorder:', { oldIndex, newIndex })
|
||||
|
||||
if (oldIndex !== -1 && newIndex !== -1) {
|
||||
const newOrder = arrayMove(localPinnedNotes, oldIndex, newIndex)
|
||||
setLocalPinnedNotes(newOrder)
|
||||
console.log('[DND-DEBUG] Calling reorderNotes for pinned notes')
|
||||
await reorderNotes(activeIdStr, overIdStr)
|
||||
|
||||
// Refresh notes from server to sync state
|
||||
console.log('[DND-DEBUG] Refreshing notes from server after reorder')
|
||||
await refreshNotesFromServer()
|
||||
} else {
|
||||
console.log('[DND-DEBUG] Invalid indices for pinned reorder')
|
||||
}
|
||||
} else {
|
||||
// Reorder unpinned notes
|
||||
const oldIndex = localUnpinnedNotes.findIndex(n => n.id === activeIdStr)
|
||||
const newIndex = localUnpinnedNotes.findIndex(n => n.id === overIdStr)
|
||||
|
||||
console.log('[DND-DEBUG] Unpinned reorder:', { oldIndex, newIndex })
|
||||
|
||||
if (oldIndex !== -1 && newIndex !== -1) {
|
||||
const newOrder = arrayMove(localUnpinnedNotes, oldIndex, newIndex)
|
||||
setLocalUnpinnedNotes(newOrder)
|
||||
console.log('[DND-DEBUG] Calling reorderNotes for unpinned notes')
|
||||
await reorderNotes(activeIdStr, overIdStr)
|
||||
|
||||
// Refresh notes from server to sync state
|
||||
console.log('[DND-DEBUG] Refreshing notes from server after reorder')
|
||||
await refreshNotesFromServer()
|
||||
} else {
|
||||
console.log('[DND-DEBUG] Invalid indices for unpinned reorder')
|
||||
}
|
||||
}
|
||||
setDraggedNote(null)
|
||||
setDragOverNote(null)
|
||||
}
|
||||
|
||||
const handleDragOver = (note: Note) => {
|
||||
if (draggedNote && draggedNote.id !== note.id) {
|
||||
setDragOverNote(note)
|
||||
}
|
||||
// Function to refresh notes from server without full page reload
|
||||
const refreshNotesFromServer = async () => {
|
||||
console.log('[DND-DEBUG] Fetching fresh notes from server...')
|
||||
const freshNotes = await getNotes()
|
||||
console.log('[DND-DEBUG] Received fresh notes:', freshNotes.length)
|
||||
|
||||
// Update local state with fresh data
|
||||
const pinned = freshNotes.filter(note => note.isPinned).sort((a, b) => a.order - b.order)
|
||||
const unpinned = freshNotes.filter(note => !note.isPinned).sort((a, b) => a.order - b.order)
|
||||
|
||||
setLocalPinnedNotes(pinned)
|
||||
setLocalUnpinnedNotes(unpinned)
|
||||
|
||||
console.log('[DND-DEBUG] Local state updated with fresh server data')
|
||||
}
|
||||
|
||||
// Find active note from either section
|
||||
const activeNote = activeId
|
||||
? localPinnedNotes.find(n => n.id === activeId) || localUnpinnedNotes.find(n => n.id === activeId)
|
||||
: null
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-8">
|
||||
{pinnedNotes.length > 0 && (
|
||||
<div>
|
||||
<h2 className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-3 px-2">
|
||||
Pinned
|
||||
</h2>
|
||||
<div className="columns-1 sm:columns-2 lg:columns-3 xl:columns-4 2xl:columns-5 gap-4 space-y-4">
|
||||
{pinnedNotes.map(note => (
|
||||
<div key={note.id} className="break-inside-avoid mb-4">
|
||||
<NoteCard
|
||||
note={note}
|
||||
onEdit={setEditingNote}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragOver={handleDragOver}
|
||||
isDragging={draggedNote?.id === note.id}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{unpinnedNotes.length > 0 && (
|
||||
<div>
|
||||
{pinnedNotes.length > 0 && (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
{localPinnedNotes.length > 0 && (
|
||||
<div>
|
||||
<h2 className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-3 px-2">
|
||||
Others
|
||||
Pinned
|
||||
</h2>
|
||||
)}
|
||||
<div className="columns-1 sm:columns-2 lg:columns-3 xl:columns-4 2xl:columns-5 gap-4 space-y-4">
|
||||
{unpinnedNotes.map(note => (
|
||||
<div key={note.id} className="break-inside-avoid mb-4">
|
||||
<NoteCard
|
||||
note={note}
|
||||
onEdit={setEditingNote}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragOver={handleDragOver}
|
||||
isDragging={draggedNote?.id === note.id}
|
||||
/>
|
||||
<SortableContext items={localPinnedNotes.map(n => n.id)} strategy={rectSortingStrategy}>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4 auto-rows-max">
|
||||
{localPinnedNotes.map((note) => (
|
||||
<SortableNote key={note.id} note={note} onEdit={setEditingNote} />
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</SortableContext>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{localUnpinnedNotes.length > 0 && (
|
||||
<div>
|
||||
{localPinnedNotes.length > 0 && (
|
||||
<h2 className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-3 px-2">
|
||||
Others
|
||||
</h2>
|
||||
)}
|
||||
<SortableContext items={localUnpinnedNotes.map(n => n.id)} strategy={rectSortingStrategy}>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4 auto-rows-max">
|
||||
{localUnpinnedNotes.map((note) => (
|
||||
<SortableNote key={note.id} note={note} onEdit={setEditingNote} />
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DragOverlay>
|
||||
{activeNote ? (
|
||||
<div className="opacity-90 rotate-2 scale-105 shadow-2xl">
|
||||
<NoteCard
|
||||
note={activeNote}
|
||||
onEdit={() => {}}
|
||||
isDragging={true}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
|
||||
{notes.length === 0 && (
|
||||
<div className="text-center py-16">
|
||||
<p className="text-gray-500 dark:text-gray-400 text-lg">No notes yet</p>
|
||||
@@ -98,10 +262,7 @@ export function NoteGrid({ notes }: NoteGridProps) {
|
||||
</div>
|
||||
|
||||
{editingNote && (
|
||||
<NoteEditor
|
||||
note={editingNote}
|
||||
onClose={() => setEditingNote(null)}
|
||||
/>
|
||||
<NoteEditor note={editingNote} onClose={() => setEditingNote(null)} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user