diff --git a/memento-note/app/actions/notes.ts b/memento-note/app/actions/notes.ts index f7f2825..180e6ab 100644 --- a/memento-note/app/actions/notes.ts +++ b/memento-note/app/actions/notes.ts @@ -319,6 +319,31 @@ export async function toggleReminderDone(noteId: string, done: boolean) { } } +// Clear completed reminders (set reminder to null for done reminders) +export async function clearCompletedReminders() { + const session = await auth(); + if (!session?.user?.id) return { error: 'Unauthorized' } + + try { + await prisma.note.updateMany({ + where: { + userId: session.user.id, + isReminderDone: true, + reminder: { not: null }, + }, + data: { + reminder: null, + isReminderDone: false, + }, + }) + revalidatePath('/reminders') + return { success: true } + } catch (error) { + console.error('Error clearing completed reminders:', error) + return { error: 'Failed to clear reminders' } + } +} + // Get archived notes only export async function getArchivedNotes() { const session = await auth(); diff --git a/memento-note/components/note-actions.tsx b/memento-note/components/note-actions.tsx index c39f492..74d56dc 100644 --- a/memento-note/components/note-actions.tsx +++ b/memento-note/components/note-actions.tsx @@ -9,6 +9,7 @@ import { import { Archive, ArchiveRestore, + Bell, MoreVertical, Palette, Pin, @@ -22,6 +23,8 @@ import { import { cn } from "@/lib/utils" import { NOTE_COLORS } from "@/lib/types" import { useLanguage } from "@/lib/i18n" +import { ReminderDialog } from "@/components/reminder-dialog" +import { useState } from "react" interface NoteActionsProps { isPinned: boolean @@ -41,6 +44,9 @@ interface NoteActionsProps { onPermanentDelete?: () => void onOpenHistory?: () => void historyEnabled?: boolean + noteId?: string + currentReminder?: Date | null + onUpdateReminder?: (noteId: string, reminder: Date | null) => void className?: string } @@ -62,9 +68,13 @@ export function NoteActions({ onPermanentDelete, onOpenHistory, historyEnabled = false, + noteId, + currentReminder, + onUpdateReminder, className }: NoteActionsProps) { const { t } = useLanguage() + const [showReminder, setShowReminder] = useState(false) // Trash view: show only Restore and Permanent Delete if (isTrashView) { @@ -105,6 +115,36 @@ export function NoteActions({ className={cn("flex items-center justify-end gap-1", className)} onClick={(e) => e.stopPropagation()} > + {/* Reminder */} + {noteId && onUpdateReminder && ( + <> + +
e.stopPropagation()}> + { + onUpdateReminder(noteId, date) + setShowReminder(false) + }} + onRemove={() => { + onUpdateReminder(noteId, null) + setShowReminder(false) + }} + /> +
+ + )} + {/* Color Palette */} diff --git a/memento-note/components/note-card.tsx b/memento-note/components/note-card.tsx index e4987c8..862c069 100644 --- a/memento-note/components/note-card.tsx +++ b/memento-note/components/note-card.tsx @@ -177,6 +177,24 @@ export const NoteCard = memo(function NoteCard({ const [comparisonNotes, setComparisonNotes] = useState(null) const [fusionNotes, setFusionNotes] = useState>>([]) const [showNotebookMenu, setShowNotebookMenu] = useState(false) + const [reminderDate, setReminderDate] = useState(note.reminder ? new Date(note.reminder) : null) + + const handleUpdateReminder = async (noteId: string, reminder: Date | null) => { + startTransition(async () => { + try { + await updateNote(noteId, { reminder }) + setReminderDate(reminder) + triggerRefresh() + if (reminder) { + toast.success(t('notes.reminderSet', { datetime: reminder.toLocaleString() })) + } else { + toast.info(t('reminder.removeReminder')) + } + } catch { + toast.error(t('general.error')) + } + }) + } // Move note to a notebook const handleMoveToNotebook = async (notebookId: string | null) => { @@ -653,6 +671,9 @@ export const NoteCard = memo(function NoteCard({ onPermanentDelete={handlePermanentDelete} onOpenHistory={() => onOpenHistory?.(note)} historyEnabled={noteHistoryEnabled} + noteId={note.id} + currentReminder={reminderDate} + onUpdateReminder={handleUpdateReminder} className="absolute bottom-0 left-0 right-0 p-2 opacity-100 md:opacity-0 group-hover:opacity-100 transition-opacity" /> )} diff --git a/memento-note/components/notes-tabs-view.tsx b/memento-note/components/notes-tabs-view.tsx index 7087af8..23cfa70 100644 --- a/memento-note/components/notes-tabs-view.tsx +++ b/memento-note/components/notes-tabs-view.tsx @@ -50,8 +50,10 @@ import { History, PanelRightClose, PanelRightOpen, + Bell, } from 'lucide-react' import { Button } from '@/components/ui/button' +import { ReminderDialog } from '@/components/reminder-dialog' import { Dialog, DialogContent, @@ -385,17 +387,20 @@ function NoteMetaSidebar({ onArchive, onOpenHistory, onEnableHistory, + onUpdateReminder, }: { note: Note onPinToggle: (note: Note) => void onArchive: (note: Note) => void onOpenHistory?: (note: Note) => void onEnableHistory?: (noteId: string) => Promise + onUpdateReminder?: (noteId: string, reminder: Date | null) => void }) { const { t } = useLanguage() const { notebooks, moveNoteToNotebookOptimistic } = useNotebooks() const [moveOpen, setMoveOpen] = useState(false) const [isMoving, setIsMoving] = useState(false) + const [showReminder, setShowReminder] = useState(false) // t() returns the key itself when not found — use this wrapper for safe fallbacks const ts = (key: string, fallback: string) => { @@ -571,6 +576,32 @@ function NoteMetaSidebar({ onClick={() => onArchive(note)} /> + {/* Reminder */} + {onUpdateReminder && ( + <> + } + label={t('reminder.setReminder')} + onClick={() => setShowReminder(true)} + /> +
e.stopPropagation()}> + { + onUpdateReminder(note.id, date) + setShowReminder(false) + }} + onRemove={() => { + onUpdateReminder(note.id, null) + setShowReminder(false) + }} + /> +
+ + )} + {/* History */} } @@ -928,6 +959,22 @@ export function NotesTabsView({ onArchive={handleArchive} onOpenHistory={onOpenHistory} onEnableHistory={onEnableHistory} + onUpdateReminder={async (noteId, reminder) => { + try { + await updateNote(noteId, { reminder }) + setItems((prev) => + prev.map((n) => (n.id === noteId ? { ...n, reminder } : n)) + ) + if (reminder) { + toast.success(t('notes.reminderSet', { datetime: reminder.toLocaleString() })) + } else { + toast.info(t('reminder.removeReminder')) + } + triggerRefresh() + } catch { + toast.error(t('general.error')) + } + }} /> )} diff --git a/memento-note/components/notification-panel.tsx b/memento-note/components/notification-panel.tsx index 83288fb..92426c3 100644 --- a/memento-note/components/notification-panel.tsx +++ b/memento-note/components/notification-panel.tsx @@ -3,17 +3,18 @@ import { useState, useEffect, useCallback } from 'react' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' -import { Bell, Check, X, Clock } from 'lucide-react' +import { Bell, Check, X, Clock, AlertCircle, CheckCircle2, Circle, Share2 } from 'lucide-react' import { Popover, PopoverContent, PopoverTrigger, } from '@/components/ui/popover' -import { getPendingShareRequests, respondToShareRequest } from '@/app/actions/notes' +import { getPendingShareRequests, respondToShareRequest, getNotesWithReminders, toggleReminderDone } from '@/app/actions/notes' import { toast } from 'sonner' import { useNoteRefreshOptional } from '@/context/NoteRefreshContext' import { cn } from '@/lib/utils' import { useLanguage } from '@/lib/i18n' +import { formatDistanceToNow } from 'date-fns' interface ShareRequest { id: string @@ -35,34 +36,52 @@ interface ShareRequest { } } +interface ReminderNote { + id: string + title: string | null + content: string + reminder: Date | string | null + isReminderDone: boolean +} + export function NotificationPanel() { const { triggerRefresh } = useNoteRefreshOptional() const { t } = useLanguage() const [requests, setRequests] = useState([]) + const [reminders, setReminders] = useState([]) const [isLoading, setIsLoading] = useState(false) const [open, setOpen] = useState(false) - const loadRequests = useCallback(async () => { + const loadData = useCallback(async () => { try { - const data = await getPendingShareRequests() - setRequests(data as any) + const [shareData, reminderData] = await Promise.all([ + getPendingShareRequests(), + getNotesWithReminders(), + ]) + setRequests(shareData as any) + setReminders((reminderData as any) || []) } catch (error: any) { - console.error('Failed to load share requests:', error) + console.error('Failed to load notifications:', error) } }, []) useEffect(() => { - loadRequests() - const interval = setInterval(loadRequests, 10000) - const onFocus = () => loadRequests() + loadData() + const interval = setInterval(loadData, 30000) + const onFocus = () => loadData() window.addEventListener('focus', onFocus) return () => { clearInterval(interval) window.removeEventListener('focus', onFocus) } - }, [loadRequests]) + }, [loadData]) - const pendingCount = requests.length + const now = new Date() + const activeReminders = reminders.filter(r => !r.isReminderDone && r.reminder) + const overdueReminders = activeReminders.filter(r => new Date(r.reminder!) < now) + const upcomingReminders = activeReminders.filter(r => new Date(r.reminder!) >= now) + + const pendingCount = requests.length + overdueReminders.length const handleAccept = async (shareId: string) => { try { @@ -92,6 +111,18 @@ export function NotificationPanel() { } } + const handleToggleReminder = async (noteId: string, done: boolean) => { + try { + await toggleReminderDone(noteId, done) + setReminders(prev => prev.map(r => r.id === noteId ? { ...r, isReminderDone: done } : r)) + triggerRefresh() + } catch { + toast.error(t('general.error')) + } + } + + const hasContent = requests.length > 0 || activeReminders.length > 0 + return ( @@ -130,20 +161,72 @@ export function NotificationPanel() {
- ) : requests.length === 0 ? ( + ) : !hasContent ? (

{t('notification.noNotifications') || 'No new notifications'}

) : (
+ {/* Overdue reminders */} + {overdueReminders.map((note) => ( +
+
+ +
+
+ + + {t('reminders.overdue')} + +
+

{note.title || t('notification.untitled')}

+
+ + {note.reminder && formatDistanceToNow(new Date(note.reminder), { addSuffix: true })} +
+
+
+
+ ))} + + {/* Upcoming reminders */} + {upcomingReminders.slice(0, 5).map((note) => ( +
+
+ +
+

{note.title || t('notification.untitled')}

+
+ {note.reminder && new Date(note.reminder).toLocaleDateString(undefined, { + month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' + })} +
+
+
+
+ ))} + + {/* Share requests */} {requests.map((request) => (
-
-
+
+
{(request.sharer.name || request.sharer.email)[0].toUpperCase()}
@@ -156,47 +239,54 @@ export function NotificationPanel() {
-
+
- -
- - {new Date(request.createdAt).toLocaleDateString()} -
))}
)} + + {/* Footer link to reminders page */} + {activeReminders.length > 0 && ( + + )} ) diff --git a/memento-note/components/reminders-page.tsx b/memento-note/components/reminders-page.tsx index 65f31f1..b1ba439 100644 --- a/memento-note/components/reminders-page.tsx +++ b/memento-note/components/reminders-page.tsx @@ -1,9 +1,9 @@ 'use client' import { useState, useTransition } from 'react' -import { Bell, BellOff, CheckCircle2, Circle, Clock, AlertCircle, RefreshCw } from 'lucide-react' +import { Bell, BellOff, CheckCircle2, Circle, Clock, AlertCircle, RefreshCw, Trash2 } from 'lucide-react' import { Note } from '@/lib/types' -import { toggleReminderDone } from '@/app/actions/notes' +import { toggleReminderDone, clearCompletedReminders } from '@/app/actions/notes' import { useLanguage } from '@/lib/i18n' import { cn } from '@/lib/utils' import { useRouter } from 'next/navigation' @@ -213,12 +213,27 @@ export function RemindersPage({ notes: initialNotes }: RemindersPageProps) { {/* Terminés */} {done.length > 0 && (
- +
+ + +
{done.map(note => ( diff --git a/memento-note/components/sidebar.tsx b/memento-note/components/sidebar.tsx index 63f07b1..aec96f0 100644 --- a/memento-note/components/sidebar.tsx +++ b/memento-note/components/sidebar.tsx @@ -23,6 +23,7 @@ import { import { useLanguage } from '@/lib/i18n' import { NotebooksList } from './notebooks-list' import { useHomeViewOptional } from '@/context/home-view-context' +import { useNoteRefreshOptional } from '@/context/NoteRefreshContext' import { useEffect, useState } from 'react' import { getTrashCount } from '@/app/actions/notes' @@ -34,6 +35,7 @@ export function Sidebar({ className, user }: { className?: string, user?: any }) const router = useRouter() const { t } = useLanguage() const homeBridge = useHomeViewOptional() + const { refreshKey } = useNoteRefreshOptional() const [trashCount, setTrashCount] = useState(0) const searchKey = searchParams.toString() @@ -44,7 +46,7 @@ export function Sidebar({ className, user }: { className?: string, user?: any }) useEffect(() => { if (HIDDEN_ROUTES.some(r => pathname.startsWith(r))) return getTrashCount().then(setTrashCount) - }, [pathname, searchKey]) + }, [pathname, searchKey, refreshKey]) // Hide sidebar on Agents, Chat IA and Lab routes if (HIDDEN_ROUTES.some(r => pathname.startsWith(r))) return null diff --git a/memento-note/locales/en.json b/memento-note/locales/en.json index fdddfca..265ee6a 100644 --- a/memento-note/locales/en.json +++ b/memento-note/locales/en.json @@ -740,7 +740,9 @@ "markDone": "Mark as done", "markUndone": "Mark as undone", "todayAt": "Today at {time}", - "tomorrowAt": "Tomorrow at {time}" + "tomorrowAt": "Tomorrow at {time}", + "clearCompleted": "Clear completed", + "viewAll": "View all reminders" }, "notebook": { "create": "Create Notebook", diff --git a/memento-note/locales/fr.json b/memento-note/locales/fr.json index 1054b9b..8fa1177 100644 --- a/memento-note/locales/fr.json +++ b/memento-note/locales/fr.json @@ -740,7 +740,9 @@ "markDone": "Marquer comme terminé", "markUndone": "Marquer comme non terminé", "todayAt": "Aujourd'hui à {time}", - "tomorrowAt": "Demain à {time}" + "tomorrowAt": "Demain à {time}", + "clearCompleted": "Effacer les terminés", + "viewAll": "Voir tous les rappels" }, "notebook": { "create": "Créer un carnet",