feat: RTL/i18n, AI translate+undo, no-refresh saves, settings perf

- RTL: force dir=rtl on LabelFilter, NotesViewToggle, LabelManagementDialog
- i18n: add missing keys (notifications, privacy, edit/preview, AI translate/undo)
- Settings pages: convert to Server Components (general, appearance) + loading skeleton
- AI menu: add Translate option (10 languages) + Undo AI button in toolbar
- Fix: saveInline uses REST API instead of Server Action → eliminates all implicit refreshes in list mode
- Fix: NotesTabsView notes sync effect preserves selected note on content changes
- Fix: auto-tag suggestions now filter already-assigned labels
- Fix: color change in card view uses local state (no refresh)
- Fix: nav links use <Link> for prefetching (Settings, Admin)
- Fix: suppress duplicate label suggestions already on note
- Route: add /api/ai/translate endpoint
This commit is contained in:
Sepehr Ramezani
2026-04-15 23:48:28 +02:00
parent 39671c6472
commit b6a548acd8
68 changed files with 5014 additions and 485 deletions

View File

@@ -385,9 +385,9 @@ export async function createNote(data: {
reminder?: Date | null
isMarkdown?: boolean
size?: 'small' | 'medium' | 'large'
sharedWith?: string[]
autoGenerated?: boolean
notebookId?: string | undefined // Assign note to a notebook if provided
skipRevalidation?: boolean // Option to prevent full page refresh for smooth optimistic UI updates
}) {
const session = await auth();
if (!session?.user?.id) throw new Error('Unauthorized');
@@ -421,8 +421,10 @@ export async function createNote(data: {
await syncLabels(session.user.id, data.labels, data.notebookId ?? null)
}
// Revalidate main page (handles both inbox and notebook views via query params)
revalidatePath('/')
if (!data.skipRevalidation) {
// Revalidate main page (handles both inbox and notebook views via query params)
revalidatePath('/')
}
// Fire-and-forget: run AI operations in background without blocking the response
const userId = session.user.id
@@ -470,7 +472,9 @@ export async function createNote(data: {
data: { labels: JSON.stringify(appliedLabels) }
})
await syncLabels(userId, appliedLabels, notebookId ?? null)
revalidatePath('/')
if (!data.skipRevalidation) {
revalidatePath('/')
}
}
}
} catch (error) {
@@ -503,7 +507,7 @@ export async function updateNote(id: string, data: {
size?: 'small' | 'medium' | 'large'
autoGenerated?: boolean | null
notebookId?: string | null
}) {
}, options?: { skipContentTimestamp?: boolean; skipRevalidation?: boolean }) {
const session = await auth();
if (!session?.user?.id) throw new Error('Unauthorized');
@@ -556,9 +560,10 @@ export async function updateNote(id: string, data: {
// Only update contentUpdatedAt for actual content changes, NOT for property changes
// (size, color, isPinned, isArchived are properties, not content)
// skipContentTimestamp=true is used by the inline editor to avoid bumping "Récent" on every auto-save
const contentFields = ['title', 'content', 'checkItems', 'images', 'links']
const isContentChange = contentFields.some(field => field in data)
if (isContentChange) {
if (isContentChange && !options?.skipContentTimestamp) {
updateData.contentUpdatedAt = new Date()
}
@@ -582,7 +587,7 @@ export async function updateNote(id: string, data: {
const structuralFields = ['isPinned', 'isArchived', 'labels', 'notebookId']
const isStructuralChange = structuralFields.some(field => field in data)
if (isStructuralChange) {
if (isStructuralChange && !options?.skipRevalidation) {
revalidatePath('/')
revalidatePath(`/note/${id}`)