fix: Add debounced Undo/Redo system to avoid character-by-character history
- Add debounced state updates for title and content (500ms delay) - Immediate UI updates with delayed history saving - Prevent one-letter-per-undo issue - Add cleanup for debounce timers on unmount
This commit is contained in:
235
keep-notes/app/actions/notes.ts
Normal file
235
keep-notes/app/actions/notes.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
'use server'
|
||||
|
||||
import { revalidatePath } from 'next/cache'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { Note, CheckItem } from '@/lib/types'
|
||||
|
||||
// Helper function to parse JSON strings from database
|
||||
function parseNote(dbNote: any): Note {
|
||||
return {
|
||||
...dbNote,
|
||||
checkItems: dbNote.checkItems ? JSON.parse(dbNote.checkItems) : null,
|
||||
labels: dbNote.labels ? JSON.parse(dbNote.labels) : null,
|
||||
images: dbNote.images ? JSON.parse(dbNote.images) : null,
|
||||
}
|
||||
}
|
||||
|
||||
// Get all notes (non-archived by default)
|
||||
export async function getNotes(includeArchived = false) {
|
||||
try {
|
||||
const notes = await prisma.note.findMany({
|
||||
where: includeArchived ? {} : { isArchived: false },
|
||||
orderBy: [
|
||||
{ isPinned: 'desc' },
|
||||
{ updatedAt: 'desc' }
|
||||
]
|
||||
})
|
||||
|
||||
return notes.map(parseNote)
|
||||
} catch (error) {
|
||||
console.error('Error fetching notes:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// Get archived notes only
|
||||
export async function getArchivedNotes() {
|
||||
try {
|
||||
const notes = await prisma.note.findMany({
|
||||
where: { isArchived: true },
|
||||
orderBy: { updatedAt: 'desc' }
|
||||
})
|
||||
|
||||
return notes.map(parseNote)
|
||||
} catch (error) {
|
||||
console.error('Error fetching archived notes:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// Search notes
|
||||
export async function searchNotes(query: string) {
|
||||
try {
|
||||
if (!query.trim()) {
|
||||
return await getNotes()
|
||||
}
|
||||
|
||||
const notes = await prisma.note.findMany({
|
||||
where: {
|
||||
isArchived: false,
|
||||
OR: [
|
||||
{ title: { contains: query, mode: 'insensitive' } },
|
||||
{ content: { contains: query, mode: 'insensitive' } }
|
||||
]
|
||||
},
|
||||
orderBy: [
|
||||
{ isPinned: 'desc' },
|
||||
{ updatedAt: 'desc' }
|
||||
]
|
||||
})
|
||||
|
||||
return notes.map(parseNote)
|
||||
} catch (error) {
|
||||
console.error('Error searching notes:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new note
|
||||
export async function createNote(data: {
|
||||
title?: string
|
||||
content: string
|
||||
color?: string
|
||||
type?: 'text' | 'checklist'
|
||||
checkItems?: CheckItem[]
|
||||
labels?: string[]
|
||||
images?: string[]
|
||||
isArchived?: boolean
|
||||
}) {
|
||||
try {
|
||||
const note = await prisma.note.create({
|
||||
data: {
|
||||
title: data.title || null,
|
||||
content: data.content,
|
||||
color: data.color || 'default',
|
||||
type: data.type || 'text',
|
||||
checkItems: data.checkItems ? JSON.stringify(data.checkItems) : null,
|
||||
labels: data.labels ? JSON.stringify(data.labels) : null,
|
||||
images: data.images ? JSON.stringify(data.images) : null,
|
||||
isArchived: data.isArchived || false,
|
||||
}
|
||||
})
|
||||
|
||||
revalidatePath('/')
|
||||
return parseNote(note)
|
||||
} catch (error) {
|
||||
console.error('Error creating note:', error)
|
||||
throw new Error('Failed to create note')
|
||||
}
|
||||
}
|
||||
|
||||
// Update a note
|
||||
export async function updateNote(id: string, data: {
|
||||
title?: string | null
|
||||
content?: string
|
||||
color?: string
|
||||
isPinned?: boolean
|
||||
isArchived?: boolean
|
||||
type?: 'text' | 'checklist'
|
||||
checkItems?: CheckItem[] | null
|
||||
labels?: string[] | null
|
||||
images?: string[] | null
|
||||
}) {
|
||||
try {
|
||||
// Stringify JSON fields if they exist
|
||||
const updateData: any = { ...data }
|
||||
if ('checkItems' in data) {
|
||||
updateData.checkItems = data.checkItems ? JSON.stringify(data.checkItems) : null
|
||||
}
|
||||
if ('labels' in data) {
|
||||
updateData.labels = data.labels ? JSON.stringify(data.labels) : null
|
||||
}
|
||||
if ('images' in data) {
|
||||
updateData.images = data.images ? JSON.stringify(data.images) : null
|
||||
}
|
||||
updateData.updatedAt = new Date()
|
||||
|
||||
const note = await prisma.note.update({
|
||||
where: { id },
|
||||
data: updateData
|
||||
})
|
||||
|
||||
revalidatePath('/')
|
||||
return parseNote(note)
|
||||
} catch (error) {
|
||||
console.error('Error updating note:', error)
|
||||
throw new Error('Failed to update note')
|
||||
}
|
||||
}
|
||||
|
||||
// Delete a note
|
||||
export async function deleteNote(id: string) {
|
||||
try {
|
||||
await prisma.note.delete({
|
||||
where: { id }
|
||||
})
|
||||
|
||||
revalidatePath('/')
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
console.error('Error deleting note:', error)
|
||||
throw new Error('Failed to delete note')
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle pin status
|
||||
export async function togglePin(id: string, isPinned: boolean) {
|
||||
return updateNote(id, { isPinned })
|
||||
}
|
||||
|
||||
// Toggle archive status
|
||||
export async function toggleArchive(id: string, isArchived: boolean) {
|
||||
return updateNote(id, { isArchived })
|
||||
}
|
||||
|
||||
// Update note color
|
||||
export async function updateColor(id: string, color: string) {
|
||||
return updateNote(id, { color })
|
||||
}
|
||||
|
||||
// Update note labels
|
||||
export async function updateLabels(id: string, labels: string[]) {
|
||||
return updateNote(id, { labels })
|
||||
}
|
||||
|
||||
// Get all unique labels
|
||||
export async function getAllLabels() {
|
||||
try {
|
||||
const notes = await prisma.note.findMany({
|
||||
select: { labels: true }
|
||||
})
|
||||
|
||||
const labelsSet = new Set<string>()
|
||||
notes.forEach(note => {
|
||||
const labels = note.labels ? JSON.parse(note.labels) : null
|
||||
if (labels) {
|
||||
labels.forEach((label: string) => labelsSet.add(label))
|
||||
}
|
||||
})
|
||||
|
||||
return Array.from(labelsSet).sort()
|
||||
} catch (error) {
|
||||
console.error('Error fetching labels:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// Reorder notes (drag and drop)
|
||||
export async function reorderNotes(draggedId: string, targetId: string) {
|
||||
try {
|
||||
const draggedNote = await prisma.note.findUnique({ where: { id: draggedId } })
|
||||
const targetNote = await prisma.note.findUnique({ where: { id: targetId } })
|
||||
|
||||
if (!draggedNote || !targetNote) {
|
||||
throw new Error('Notes not found')
|
||||
}
|
||||
|
||||
// Swap the order values
|
||||
await prisma.$transaction([
|
||||
prisma.note.update({
|
||||
where: { id: draggedId },
|
||||
data: { order: targetNote.order }
|
||||
}),
|
||||
prisma.note.update({
|
||||
where: { id: targetId },
|
||||
data: { order: draggedNote.order }
|
||||
})
|
||||
])
|
||||
|
||||
revalidatePath('/')
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
console.error('Error reordering notes:', error)
|
||||
throw new Error('Failed to reorder notes')
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user