feat: auto-save 2s + indicateur save + reminders inline actions (compléter/snooze)
Some checks failed
CI / Lint, Unit Tests & Build (push) Failing after 1m23s
CI / Deploy production (on server) (push) Has been cancelled

- Auto-save debounce 2s dans note-editor-context
- lastSavedAt state + setIsDirty(false) dans handleSave
- Indicateur toolbar: ✓ sauvegardé / ● non enregistré avec timer relatif
- Reminders sidebar: bouton ✓ compléter + bouton +1h snooze (hover inline)
- i18n: clés reminders.markDone/snooze1h + notes.savedJustNow/unsaved (15 locales)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Antigravity
2026-05-29 18:58:19 +00:00
parent 0fa8978395
commit 1b56af9743
19 changed files with 221 additions and 66 deletions

View File

@@ -41,7 +41,7 @@ import { useSearchModal } from '@/context/search-modal-context'
import { useLanguage } from '@/lib/i18n'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { applyDocumentTheme } from '@/lib/apply-document-theme'
import { getAllNotes, getTrashCount, getNotesWithReminders } from '@/app/actions/notes'
import { getAllNotes, getTrashCount, getNotesWithReminders, toggleReminderDone } from '@/app/actions/notes'
import { NOTE_CHANGE_EVENT, type NoteChangeEvent } from '@/lib/note-change-sync'
import { useNotebooks } from '@/context/notebooks-context'
import { Notebook, Note } from '@/lib/types'
@@ -183,6 +183,11 @@ function SidebarReminders({ onOpenNote }: { onOpenNote: (noteId: string, noteboo
{ id: string; title: string | null; reminder: Date | string | null; isReminderDone: boolean; notebookId: string | null }[]
>([])
const [loading, setLoading] = useState(true)
const [togglingId, setTogglingId] = useState<string | null>(null)
const reload = () => {
getNotesWithReminders().then((rows) => setReminders(rows as typeof reminders))
}
useEffect(() => {
let cancelled = false
@@ -194,11 +199,33 @@ function SidebarReminders({ onOpenNote }: { onOpenNote: (noteId: string, noteboo
.finally(() => {
if (!cancelled) setLoading(false)
})
return () => {
cancelled = true
}
return () => { cancelled = true }
}, [])
const handleComplete = async (e: React.MouseEvent, noteId: string) => {
e.stopPropagation()
setTogglingId(noteId)
await toggleReminderDone(noteId, true)
setReminders(prev => prev.map(r => r.id === noteId ? { ...r, isReminderDone: true } : r))
setTogglingId(null)
}
const handleSnooze = async (e: React.MouseEvent, noteId: string) => {
e.stopPropagation()
// Snooze = +1h
const snoozeDate = new Date(Date.now() + 60 * 60 * 1000)
setTogglingId(noteId)
try {
await fetch(`/api/notes/${noteId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ reminder: snoozeDate.toISOString(), isReminderDone: false }),
})
setReminders(prev => prev.map(r => r.id === noteId ? { ...r, reminder: snoozeDate, isReminderDone: false } : r))
} catch { reload() }
setTogglingId(null)
}
if (loading) {
return (
<div className="px-4 space-y-2">
@@ -224,25 +251,56 @@ function SidebarReminders({ onOpenNote }: { onOpenNote: (noteId: string, noteboo
}
const renderItem = (note: (typeof active)[0], overdueItem?: boolean) => (
<button
<div
key={note.id}
type="button"
onClick={() => onOpenNote(note.id, note.notebookId)}
className="w-full text-start px-4 py-2.5 rounded-xl hover:bg-brand-accent/5 transition-colors group"
className="group flex items-center gap-1 px-2 py-1.5 rounded-xl hover:bg-brand-accent/5 transition-colors"
>
<p className="text-[12px] font-medium truncate group-hover:text-brand-accent transition-colors">
{note.title || t('notes.untitled')}
</p>
<p className={cn('text-[10px] mt-0.5', overdueItem ? 'text-red-500' : 'text-muted-foreground')}>
{note.reminder &&
new Date(note.reminder).toLocaleString(undefined, {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
})}
</p>
</button>
{/* Bouton compléter */}
<button
type="button"
onClick={(e) => { void handleComplete(e, note.id) }}
disabled={togglingId === note.id}
className="flex-shrink-0 w-5 h-5 rounded-full border-2 border-concrete/30 hover:border-emerald-500 hover:bg-emerald-50 transition-all flex items-center justify-center"
title={t('reminders.markDone') || 'Marquer comme fait'}
>
{togglingId === note.id
? <div className="w-2 h-2 rounded-full bg-concrete animate-pulse" />
: null}
</button>
{/* Contenu cliquable */}
<button
type="button"
onClick={() => onOpenNote(note.id, note.notebookId)}
className="flex-1 text-start min-w-0"
>
<p className="text-[12px] font-medium truncate group-hover:text-brand-accent transition-colors">
{note.title || t('notes.untitled')}
</p>
<p className={cn('text-[10px] mt-0.5', overdueItem ? 'text-red-500' : 'text-muted-foreground')}>
{note.reminder &&
new Date(note.reminder).toLocaleString(undefined, {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
})}
</p>
</button>
{/* Actions (visibles au hover) */}
<div className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity flex gap-0.5">
<button
type="button"
onClick={(e) => { void handleSnooze(e, note.id) }}
disabled={togglingId === note.id}
className="p-1 rounded-md hover:bg-amber-100 text-amber-600 transition-colors"
title={t('reminders.snooze1h') || 'Reporter de 1h'}
>
<span className="text-[9px] font-bold">+1h</span>
</button>
</div>
</div>
)
return (