feat: smart note history with manual/auto modes, delete entries, i18n fixes
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 1m16s

- Add noteHistoryMode setting (manual default / auto) with DB migration
- Manual mode: commit button in editor toolbar creates snapshots on demand
- Auto mode: smart snapshots with 20-char diff threshold + 5min cooldown,
  structural changes (color, pin, archive, labels) bypass cooldown
- Add delete individual history entries from history modal
- Fix sidebar: Notes nav no longer active on notebook pages
- Fix sidebar icon: replace filled Lightbulb with outlined FileText
- Fix title suggestions: change from amber to sky blue color scheme
- Fix hydration mismatch: add suppressHydrationWarning on locale dates
- Complete i18n: add history, sort, and AI chat translations for all 16 languages
- Translate French AI assistant section (40+ keys) from English to French
- Update README with new features and stack info

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-28 21:05:55 +02:00
parent ed807d3b2a
commit 69ea064ca8
40 changed files with 2110 additions and 250 deletions

View File

@@ -0,0 +1,51 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { getNoteHistory, restoreNoteVersion } from '@/app/actions/notes'
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
}
try {
const { id } = await params
const limitRaw = request.nextUrl.searchParams.get('limit')
const limit = limitRaw ? Number.parseInt(limitRaw, 10) : 30
const entries = await getNoteHistory(id, Number.isNaN(limit) ? 30 : limit)
return NextResponse.json({ success: true, data: entries })
} catch (error) {
console.error('Error fetching note history:', error)
return NextResponse.json({ success: false, error: 'Failed to fetch history' }, { status: 500 })
}
}
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
}
try {
const { id } = await params
const body = await request.json()
const historyEntryId = body?.historyEntryId
if (!historyEntryId || typeof historyEntryId !== 'string') {
return NextResponse.json({ success: false, error: 'historyEntryId is required' }, { status: 400 })
}
const restoredNote = await restoreNoteVersion(id, historyEntryId)
return NextResponse.json({ success: true, data: restoredNote })
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to restore history version'
console.error('Error restoring note history:', error)
return NextResponse.json({ success: false, error: message }, { status: 500 })
}
}

View File

@@ -2,6 +2,13 @@ import { NextRequest, NextResponse } from 'next/server'
import prisma from '@/lib/prisma'
import { auth } from '@/auth'
import { parseNote } from '@/lib/utils'
import {
createNoteHistorySnapshot,
getNoteHistoryMode,
isNoteHistoryEnabledForUser,
shouldCaptureHistorySnapshot,
shouldCreateAutoSnapshot,
} from '@/lib/note-history'
// GET /api/notes/[id] - Get a single note
export async function GET(
@@ -134,6 +141,32 @@ export async function PUT(
data: updateData,
})
try {
const historyEnabled = await isNoteHistoryEnabledForUser(session.user.id)
if (historyEnabled && shouldCaptureHistorySnapshot(updateData)) {
const mode = await getNoteHistoryMode(session.user.id)
if (mode === 'auto') {
const shouldAuto = await shouldCreateAutoSnapshot({
noteId: id,
userId: session.user.id,
updateData,
existingContent: existingNote.content ?? '',
existingTitle: existingNote.title ?? null,
})
if (shouldAuto) {
await createNoteHistorySnapshot({
noteId: id,
userId: session.user.id,
reason: 'api:update',
})
}
}
// manual mode: no auto-snapshot
}
} catch (snapshotError) {
console.error('[HISTORY] Failed to create snapshot from /api/notes/[id] PUT:', snapshotError)
}
return NextResponse.json({
success: true,
data: parseNote(note)