feat: auto-save 2s + indicateur save + reminders inline actions (compléter/snooze)
- 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:
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user