From 1b56af97438b5afaec99738156e6b678cbc6a874 Mon Sep 17 00:00:00 2001 From: Antigravity Date: Fri, 29 May 2026 18:58:19 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20auto-save=202s=20+=20indicateur=20save?= =?UTF-8?q?=20+=20reminders=20inline=20actions=20(compl=C3=A9ter/snooze)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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> --- .../note-editor/note-editor-context.tsx | 24 ++++- .../note-editor/note-editor-toolbar.tsx | 57 +++++++--- memento-note/components/note-editor/types.ts | 1 + memento-note/components/sidebar.tsx | 100 ++++++++++++++---- memento-note/locales/ar.json | 7 +- memento-note/locales/de.json | 7 +- memento-note/locales/en.json | 7 +- memento-note/locales/es.json | 7 +- memento-note/locales/fa.json | 7 +- memento-note/locales/fr.json | 7 +- memento-note/locales/hi.json | 7 +- memento-note/locales/it.json | 7 +- memento-note/locales/ja.json | 7 +- memento-note/locales/ko.json | 7 +- memento-note/locales/nl.json | 7 +- memento-note/locales/pl.json | 7 +- memento-note/locales/pt.json | 7 +- memento-note/locales/ru.json | 7 +- memento-note/locales/zh.json | 7 +- 19 files changed, 221 insertions(+), 66 deletions(-) diff --git a/memento-note/components/note-editor/note-editor-context.tsx b/memento-note/components/note-editor/note-editor-context.tsx index 73cf535..575be76 100644 --- a/memento-note/components/note-editor/note-editor-context.tsx +++ b/memento-note/components/note-editor/note-editor-context.tsx @@ -81,6 +81,7 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o const [color, setColor] = useState(note.color) const [size, setSize] = useState(note.size || 'small') const [isSaving, setIsSaving] = useState(false) + const [lastSavedAt, setLastSavedAt] = useState(null) const [removedImageUrls, setRemovedImageUrls] = useState([]) const [isMarkdown, setIsMarkdown] = useState(note.type === 'markdown') const [showMarkdownPreview, setShowMarkdownPreview] = useState(note.type === 'markdown') @@ -691,6 +692,8 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o queryClient.invalidateQueries({ queryKey: queryKeys.note(note.id) }) queryClient.invalidateQueries({ queryKey: queryKeys.notes(note.notebookId) }) emitNoteChange({ type: 'updated', note: result }) + setIsDirty(false) + setLastSavedAt(new Date()) toast.success(t('notes.saved') || 'Note sauvegardée !') } catch (error) { console.error('[SAVE] updateNote failed:', error) @@ -816,6 +819,7 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o queryClient.invalidateQueries({ queryKey: queryKeys.notes(note.notebookId) }) emitNoteChange({ type: 'updated', note: result }) setIsDirty(false) + setLastSavedAt(new Date()) toast.success(t('notes.saved') || 'Saved') } catch (error) { console.error('[SAVE] updateNote failed:', error) @@ -830,6 +834,23 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o const handleSaveRef = useRef(handleSave) handleSaveRef.current = handleSave + // Auto-save : 2s après le dernier changement si isDirty + const autoSaveTimerRef = useRef | null>(null) + useEffect(() => { + if (!isDirty || isSaving || readOnly) return + if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current) + autoSaveTimerRef.current = setTimeout(() => { + if (fullPage) { + void handleSaveInPlaceRef.current() + } else { + void handleSaveRef.current() + } + }, 2000) + return () => { + if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current) + } + }, [isDirty, isSaving, readOnly, fullPage]) + useEffect(() => { if (!fullPage) return const handler = (e: KeyboardEvent) => { @@ -870,6 +891,7 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o removedImageUrls, isSaving, isDirty, + lastSavedAt, isProcessingAI, aiOpen, infoOpen, @@ -894,7 +916,7 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o quotaExceededFeature, }), [ title, content, checkItems, labels, images, links, newLabel, color, size, - showMarkdownPreview, removedImageUrls, isSaving, isDirty, isProcessingAI, aiOpen, infoOpen, + showMarkdownPreview, removedImageUrls, isSaving, isDirty, lastSavedAt, isProcessingAI, aiOpen, infoOpen, isGeneratingTitles, titleSuggestions, dismissedTitleSuggestions, isReformulating, reformulationModal, previousContentForCopilot, showReminderDialog, currentReminder, showLinkDialog, linkUrl, comparisonNotes, fusionNotes, dismissedTags, filteredSuggestions, diff --git a/memento-note/components/note-editor/note-editor-toolbar.tsx b/memento-note/components/note-editor/note-editor-toolbar.tsx index 4f0518c..5e4b772 100644 --- a/memento-note/components/note-editor/note-editor-toolbar.tsx +++ b/memento-note/components/note-editor/note-editor-toolbar.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState, useRef, useCallback } from 'react' +import { useState, useRef, useCallback, useEffect } from 'react' import { useNoteEditorContext } from './note-editor-context' import { LabelManager } from '@/components/label-manager' import { LabelBadge } from '@/components/label-badge' @@ -46,6 +46,21 @@ export function NoteEditorToolbar({ mode, onClose, onToggleAttachments, attachme const [isConverting, setIsConverting] = useState(false) const [shareOpen, setShareOpen] = useState(false) const [flashcardsOpen, setFlashcardsOpen] = useState(false) + const [relativeTime, setRelativeTime] = useState(null) + + // Mise à jour du temps relatif depuis la dernière sauvegarde + useEffect(() => { + if (!state.lastSavedAt) { setRelativeTime(null); return } + const update = () => { + const secs = Math.floor((Date.now() - state.lastSavedAt!.getTime()) / 1000) + if (secs < 10) setRelativeTime(t('notes.savedJustNow') || 'Sauvegardé') + else if (secs < 60) setRelativeTime(`${secs}s`) + else setRelativeTime(`${Math.floor(secs / 60)}min`) + } + update() + const interval = setInterval(update, 10000) + return () => clearInterval(interval) + }, [state.lastSavedAt, t]) const notebookName = notebooks.find(nb => nb.id === note.notebookId)?.name || null @@ -294,20 +309,34 @@ export function NoteEditorToolbar({ mode, onClose, onToggleAttachments, attachme )} {!readOnly && ( - + {state.isDirty && !state.isSaving && ( + + {t('notes.unsaved') || '●'} + + )} + + )} {!readOnly && ( diff --git a/memento-note/components/note-editor/types.ts b/memento-note/components/note-editor/types.ts index 7eda3e7..fd96406 100644 --- a/memento-note/components/note-editor/types.ts +++ b/memento-note/components/note-editor/types.ts @@ -19,6 +19,7 @@ export interface NoteEditorState { removedImageUrls: string[] isSaving: boolean isDirty: boolean + lastSavedAt: Date | null isProcessingAI: boolean aiOpen: boolean diff --git a/memento-note/components/sidebar.tsx b/memento-note/components/sidebar.tsx index 1bd37ca..d98df26 100644 --- a/memento-note/components/sidebar.tsx +++ b/memento-note/components/sidebar.tsx @@ -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(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 (
@@ -224,25 +251,56 @@ function SidebarReminders({ onOpenNote }: { onOpenNote: (noteId: string, noteboo } const renderItem = (note: (typeof active)[0], overdueItem?: boolean) => ( - + {/* Bouton compléter */} + + + {/* Contenu cliquable */} + + + {/* Actions (visibles au hover) */} +
+ +
+
) return ( diff --git a/memento-note/locales/ar.json b/memento-note/locales/ar.json index c922ff1..8b8e20f 100644 --- a/memento-note/locales/ar.json +++ b/memento-note/locales/ar.json @@ -280,7 +280,9 @@ "saveFailed": "فشل الحفظ", "search": "بحث", "unarchived": "إلغاء الأرشفة", - "uploading": "جاري الرفع..." + "uploading": "جاري الرفع...", + "savedJustNow": "Saved", + "unsaved": "Unsaved changes" }, "pagination": { "previous": "←", @@ -1011,7 +1013,8 @@ "todayAt": "اليوم في {time}", "tomorrowAt": "غداً في {time}", "clearCompleted": "مسح المكتملة", - "viewAll": "عرض جميع التذكيرات" + "viewAll": "عرض جميع التذكيرات", + "snooze1h": "Snooze 1 hour" }, "notebook": { "create": "إنشاء دفتر", diff --git a/memento-note/locales/de.json b/memento-note/locales/de.json index edbfd34..52f027a 100644 --- a/memento-note/locales/de.json +++ b/memento-note/locales/de.json @@ -280,7 +280,9 @@ "saveFailed": "Speichern fehlgeschlagen", "search": "Suchen", "unarchived": "Entarchiviert", - "uploading": "Wird hochgeladen..." + "uploading": "Wird hochgeladen...", + "savedJustNow": "Saved", + "unsaved": "Unsaved changes" }, "pagination": { "previous": "←", @@ -1011,7 +1013,8 @@ "todayAt": "Heute um {time}", "tomorrowAt": "Morgen um {time}", "clearCompleted": "Klar abgeschlossen", - "viewAll": "Alle Erinnerungen anzeigen" + "viewAll": "Alle Erinnerungen anzeigen", + "snooze1h": "Snooze 1 hour" }, "notebook": { "create": "Notizbuch erstellen", diff --git a/memento-note/locales/en.json b/memento-note/locales/en.json index 657489f..9ef75fd 100644 --- a/memento-note/locales/en.json +++ b/memento-note/locales/en.json @@ -310,7 +310,9 @@ "generateIllustration": "Generate illustration", "saveFailed": "Failed to save", "createFirst": "Create your first note", - "unarchived": "Unarchived" + "unarchived": "Unarchived", + "savedJustNow": "Saved", + "unsaved": "Unsaved changes" }, "pagination": { "previous": "←", @@ -1148,7 +1150,8 @@ "todayAt": "Today at {time}", "tomorrowAt": "Tomorrow at {time}", "clearCompleted": "Clear completed", - "viewAll": "View all reminders" + "viewAll": "View all reminders", + "snooze1h": "Snooze 1 hour" }, "notebook": { "create": "Create Notebook", diff --git a/memento-note/locales/es.json b/memento-note/locales/es.json index b9a3e12..5f230b7 100644 --- a/memento-note/locales/es.json +++ b/memento-note/locales/es.json @@ -280,7 +280,9 @@ "saveFailed": "Error al guardar", "search": "Buscar", "unarchived": "Desarchivado", - "uploading": "Subiendo..." + "uploading": "Subiendo...", + "savedJustNow": "Saved", + "unsaved": "Unsaved changes" }, "pagination": { "previous": "←", @@ -1011,7 +1013,8 @@ "todayAt": "Hoy a las {time}", "tomorrowAt": "Mañana a las {time}", "clearCompleted": "Borrar completado", - "viewAll": "Ver todos los recordatorios" + "viewAll": "Ver todos los recordatorios", + "snooze1h": "Snooze 1 hour" }, "notebook": { "create": "Crear cuaderno", diff --git a/memento-note/locales/fa.json b/memento-note/locales/fa.json index f47857a..45d6862 100644 --- a/memento-note/locales/fa.json +++ b/memento-note/locales/fa.json @@ -280,7 +280,9 @@ "saveFailed": "ذخیره سازی ناموفق", "search": "جستجو", "unarchived": "خارج بایگانی", - "uploading": "در حال آپلود..." + "uploading": "در حال آپلود...", + "savedJustNow": "Saved", + "unsaved": "Unsaved changes" }, "pagination": { "previous": "←", @@ -1044,7 +1046,8 @@ "todayAt": "امروز ساعت {time}", "tomorrowAt": "فردا ساعت {time}", "clearCompleted": "پاک کردن تکمیل شده‌ها", - "viewAll": "مشاهده همه یادآوری‌ها" + "viewAll": "مشاهده همه یادآوری‌ها", + "snooze1h": "Snooze 1 hour" }, "notebook": { "create": "ایجاد دفترچه", diff --git a/memento-note/locales/fr.json b/memento-note/locales/fr.json index 8c2bc1f..bc1a018 100644 --- a/memento-note/locales/fr.json +++ b/memento-note/locales/fr.json @@ -316,7 +316,9 @@ "generateIllustration": "Générer une illustration", "saveFailed": "Échec de la sauvegarde", "createFirst": "Créez votre première note", - "unarchived": "Désarchivée" + "unarchived": "Désarchivée", + "savedJustNow": "Sauvegardé", + "unsaved": "Non sauvegardé" }, "pagination": { "previous": "←", @@ -1154,7 +1156,8 @@ "todayAt": "Aujourd'hui à {time}", "tomorrowAt": "Demain à {time}", "clearCompleted": "Effacer les terminés", - "viewAll": "Voir tous les rappels" + "viewAll": "Voir tous les rappels", + "snooze1h": "Reporter de 1h" }, "notebook": { "create": "Créer un carnet", diff --git a/memento-note/locales/hi.json b/memento-note/locales/hi.json index 73f857d..4184671 100644 --- a/memento-note/locales/hi.json +++ b/memento-note/locales/hi.json @@ -280,7 +280,9 @@ "saveFailed": "सहेजने में विफल", "search": "खोजें", "unarchived": "अनार्काइव नहीं", - "uploading": "अपलोड हो रहा है..." + "uploading": "अपलोड हो रहा है...", + "savedJustNow": "Saved", + "unsaved": "Unsaved changes" }, "pagination": { "previous": "←", @@ -1011,7 +1013,8 @@ "todayAt": "आज {time} बजे", "tomorrowAt": "कल {time} बजे", "clearCompleted": "स्पष्टतः पूरा", - "viewAll": "सभी अनुस्मारक देखें" + "viewAll": "सभी अनुस्मारक देखें", + "snooze1h": "Snooze 1 hour" }, "notebook": { "create": "नोटबुक बनाएं", diff --git a/memento-note/locales/it.json b/memento-note/locales/it.json index 5109b16..1935847 100644 --- a/memento-note/locales/it.json +++ b/memento-note/locales/it.json @@ -280,7 +280,9 @@ "saveFailed": "Salvataggio non riuscito", "search": "Cerca", "unarchived": "Dearchiviato", - "uploading": "Caricamento..." + "uploading": "Caricamento...", + "savedJustNow": "Saved", + "unsaved": "Unsaved changes" }, "pagination": { "previous": "←", @@ -1011,7 +1013,8 @@ "todayAt": "Oggi alle {time}", "tomorrowAt": "Domani alle {time}", "clearCompleted": "Cancella completato", - "viewAll": "Visualizza tutti i promemoria" + "viewAll": "Visualizza tutti i promemoria", + "snooze1h": "Snooze 1 hour" }, "notebook": { "create": "Crea notebook", diff --git a/memento-note/locales/ja.json b/memento-note/locales/ja.json index c2c6100..240b091 100644 --- a/memento-note/locales/ja.json +++ b/memento-note/locales/ja.json @@ -280,7 +280,9 @@ "saveFailed": "保存に失敗しました", "search": "検索", "unarchived": "アーカイブ解除", - "uploading": "アップロード中..." + "uploading": "アップロード中...", + "savedJustNow": "Saved", + "unsaved": "Unsaved changes" }, "pagination": { "previous": "←", @@ -1011,7 +1013,8 @@ "todayAt": "今日 {time}", "tomorrowAt": "明日 {time}", "clearCompleted": "クリア完了", - "viewAll": "すべてのリマインダーを表示" + "viewAll": "すべてのリマインダーを表示", + "snooze1h": "Snooze 1 hour" }, "notebook": { "create": "ノートブックを作成", diff --git a/memento-note/locales/ko.json b/memento-note/locales/ko.json index 12b0090..41bdf8f 100644 --- a/memento-note/locales/ko.json +++ b/memento-note/locales/ko.json @@ -280,7 +280,9 @@ "saveFailed": "저장 실패", "search": "검색", "unarchived": "보관 해제됨", - "uploading": "업로드 중..." + "uploading": "업로드 중...", + "savedJustNow": "Saved", + "unsaved": "Unsaved changes" }, "pagination": { "previous": "←", @@ -1011,7 +1013,8 @@ "todayAt": "오늘 {time}", "tomorrowAt": "내일 {time}", "clearCompleted": "클리어 완료", - "viewAll": "모든 알림 보기" + "viewAll": "모든 알림 보기", + "snooze1h": "Snooze 1 hour" }, "notebook": { "create": "노트북 만들기", diff --git a/memento-note/locales/nl.json b/memento-note/locales/nl.json index e5d45d5..042638b 100644 --- a/memento-note/locales/nl.json +++ b/memento-note/locales/nl.json @@ -280,7 +280,9 @@ "saveFailed": "Opslaan mislukt", "search": "Zoeken", "unarchived": "Gearchiveerd", - "uploading": "Uploaden..." + "uploading": "Uploaden...", + "savedJustNow": "Saved", + "unsaved": "Unsaved changes" }, "pagination": { "previous": "←", @@ -1011,7 +1013,8 @@ "todayAt": "Vandaag om {time}", "tomorrowAt": "Morgen om {time}", "clearCompleted": "Duidelijk voltooid", - "viewAll": "Bekijk alle herinneringen" + "viewAll": "Bekijk alle herinneringen", + "snooze1h": "Snooze 1 hour" }, "notebook": { "create": "Notitieboek maken", diff --git a/memento-note/locales/pl.json b/memento-note/locales/pl.json index b0640c9..b5ba6dc 100644 --- a/memento-note/locales/pl.json +++ b/memento-note/locales/pl.json @@ -280,7 +280,9 @@ "saveFailed": "Nie udało się zapisać", "search": "Szukaj", "unarchived": "Wycofane z archiwum", - "uploading": "Przesyłanie..." + "uploading": "Przesyłanie...", + "savedJustNow": "Saved", + "unsaved": "Unsaved changes" }, "pagination": { "previous": "←", @@ -1011,7 +1013,8 @@ "todayAt": "Dzisiaj o {time}", "tomorrowAt": "Jutro o {time}", "clearCompleted": "Wyczyść zakończone", - "viewAll": "Wyświetl wszystkie przypomnienia" + "viewAll": "Wyświetl wszystkie przypomnienia", + "snooze1h": "Snooze 1 hour" }, "notebook": { "create": "Utwórz notatnik", diff --git a/memento-note/locales/pt.json b/memento-note/locales/pt.json index 0e8f2f9..4623ac5 100644 --- a/memento-note/locales/pt.json +++ b/memento-note/locales/pt.json @@ -280,7 +280,9 @@ "saveFailed": "Falha ao salvar", "search": "Pesquisar", "unarchived": "Desarquivado", - "uploading": "Enviando..." + "uploading": "Enviando...", + "savedJustNow": "Saved", + "unsaved": "Unsaved changes" }, "pagination": { "previous": "←", @@ -1011,7 +1013,8 @@ "todayAt": "Hoje às {time}", "tomorrowAt": "Amanhã às {time}", "clearCompleted": "Limpeza concluída", - "viewAll": "Ver todos os lembretes" + "viewAll": "Ver todos os lembretes", + "snooze1h": "Snooze 1 hour" }, "notebook": { "create": "Criar caderno", diff --git a/memento-note/locales/ru.json b/memento-note/locales/ru.json index 5f90fb1..3b87a82 100644 --- a/memento-note/locales/ru.json +++ b/memento-note/locales/ru.json @@ -280,7 +280,9 @@ "saveFailed": "Не удалось сохранить", "search": "Поиск", "unarchived": "Разархивировано", - "uploading": "Загрузка..." + "uploading": "Загрузка...", + "savedJustNow": "Saved", + "unsaved": "Unsaved changes" }, "pagination": { "previous": "←", @@ -1011,7 +1013,8 @@ "todayAt": "Сегодня в {time}", "tomorrowAt": "Завтра в {time}", "clearCompleted": "Очистить завершено", - "viewAll": "Просмотреть все напоминания" + "viewAll": "Просмотреть все напоминания", + "snooze1h": "Snooze 1 hour" }, "notebook": { "create": "Создать блокнот", diff --git a/memento-note/locales/zh.json b/memento-note/locales/zh.json index a2f8d8c..b7b8d8e 100644 --- a/memento-note/locales/zh.json +++ b/memento-note/locales/zh.json @@ -280,7 +280,9 @@ "saveFailed": "保存失败", "search": "搜索", "unarchived": "已取消归档", - "uploading": "上传中..." + "uploading": "上传中...", + "savedJustNow": "Saved", + "unsaved": "Unsaved changes" }, "pagination": { "previous": "←", @@ -1011,7 +1013,8 @@ "todayAt": "今天 {time}", "tomorrowAt": "明天 {time}", "clearCompleted": "清除完成", - "viewAll": "查看所有提醒" + "viewAll": "查看所有提醒", + "snooze1h": "Snooze 1 hour" }, "notebook": { "create": "创建笔记本",