feat: reminder button in list/tabs view, notifications show reminders, trash count live update
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 43s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 43s
- Add reminder action to NoteActions (masonry view) and NoteMetaSidebar (tabs view) - NotificationPanel now fetches and displays upcoming/overdue reminders alongside share requests - Badge count includes overdue reminders; overdue items show mark-as-done toggle - Sidebar trash count refreshes on NoteRefreshContext trigger (no more manual refresh) - Add "Clear completed" button on reminders page with clearCompletedReminders action - Add i18n keys for new features (en/fr) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
// Get archived notes only
|
||||||
export async function getArchivedNotes() {
|
export async function getArchivedNotes() {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
Archive,
|
Archive,
|
||||||
ArchiveRestore,
|
ArchiveRestore,
|
||||||
|
Bell,
|
||||||
MoreVertical,
|
MoreVertical,
|
||||||
Palette,
|
Palette,
|
||||||
Pin,
|
Pin,
|
||||||
@@ -22,6 +23,8 @@ import {
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { NOTE_COLORS } from "@/lib/types"
|
import { NOTE_COLORS } from "@/lib/types"
|
||||||
import { useLanguage } from "@/lib/i18n"
|
import { useLanguage } from "@/lib/i18n"
|
||||||
|
import { ReminderDialog } from "@/components/reminder-dialog"
|
||||||
|
import { useState } from "react"
|
||||||
|
|
||||||
interface NoteActionsProps {
|
interface NoteActionsProps {
|
||||||
isPinned: boolean
|
isPinned: boolean
|
||||||
@@ -41,6 +44,9 @@ interface NoteActionsProps {
|
|||||||
onPermanentDelete?: () => void
|
onPermanentDelete?: () => void
|
||||||
onOpenHistory?: () => void
|
onOpenHistory?: () => void
|
||||||
historyEnabled?: boolean
|
historyEnabled?: boolean
|
||||||
|
noteId?: string
|
||||||
|
currentReminder?: Date | null
|
||||||
|
onUpdateReminder?: (noteId: string, reminder: Date | null) => void
|
||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,9 +68,13 @@ export function NoteActions({
|
|||||||
onPermanentDelete,
|
onPermanentDelete,
|
||||||
onOpenHistory,
|
onOpenHistory,
|
||||||
historyEnabled = false,
|
historyEnabled = false,
|
||||||
|
noteId,
|
||||||
|
currentReminder,
|
||||||
|
onUpdateReminder,
|
||||||
className
|
className
|
||||||
}: NoteActionsProps) {
|
}: NoteActionsProps) {
|
||||||
const { t } = useLanguage()
|
const { t } = useLanguage()
|
||||||
|
const [showReminder, setShowReminder] = useState(false)
|
||||||
|
|
||||||
// Trash view: show only Restore and Permanent Delete
|
// Trash view: show only Restore and Permanent Delete
|
||||||
if (isTrashView) {
|
if (isTrashView) {
|
||||||
@@ -105,6 +115,36 @@ export function NoteActions({
|
|||||||
className={cn("flex items-center justify-end gap-1", className)}
|
className={cn("flex items-center justify-end gap-1", className)}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
|
{/* Reminder */}
|
||||||
|
{noteId && onUpdateReminder && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className={cn("h-8 w-8 p-0", currentReminder && "text-primary")}
|
||||||
|
title={t('reminder.setReminder')}
|
||||||
|
onClick={() => setShowReminder(true)}
|
||||||
|
>
|
||||||
|
<Bell className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
|
<ReminderDialog
|
||||||
|
open={showReminder}
|
||||||
|
onOpenChange={setShowReminder}
|
||||||
|
currentReminder={currentReminder || null}
|
||||||
|
onSave={(date) => {
|
||||||
|
onUpdateReminder(noteId, date)
|
||||||
|
setShowReminder(false)
|
||||||
|
}}
|
||||||
|
onRemove={() => {
|
||||||
|
onUpdateReminder(noteId, null)
|
||||||
|
setShowReminder(false)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Color Palette */}
|
{/* Color Palette */}
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
|
|||||||
@@ -177,6 +177,24 @@ export const NoteCard = memo(function NoteCard({
|
|||||||
const [comparisonNotes, setComparisonNotes] = useState<string[] | null>(null)
|
const [comparisonNotes, setComparisonNotes] = useState<string[] | null>(null)
|
||||||
const [fusionNotes, setFusionNotes] = useState<Array<Partial<Note>>>([])
|
const [fusionNotes, setFusionNotes] = useState<Array<Partial<Note>>>([])
|
||||||
const [showNotebookMenu, setShowNotebookMenu] = useState(false)
|
const [showNotebookMenu, setShowNotebookMenu] = useState(false)
|
||||||
|
const [reminderDate, setReminderDate] = useState<Date | null>(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
|
// Move note to a notebook
|
||||||
const handleMoveToNotebook = async (notebookId: string | null) => {
|
const handleMoveToNotebook = async (notebookId: string | null) => {
|
||||||
@@ -653,6 +671,9 @@ export const NoteCard = memo(function NoteCard({
|
|||||||
onPermanentDelete={handlePermanentDelete}
|
onPermanentDelete={handlePermanentDelete}
|
||||||
onOpenHistory={() => onOpenHistory?.(note)}
|
onOpenHistory={() => onOpenHistory?.(note)}
|
||||||
historyEnabled={noteHistoryEnabled}
|
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"
|
className="absolute bottom-0 left-0 right-0 p-2 opacity-100 md:opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -50,8 +50,10 @@ import {
|
|||||||
History,
|
History,
|
||||||
PanelRightClose,
|
PanelRightClose,
|
||||||
PanelRightOpen,
|
PanelRightOpen,
|
||||||
|
Bell,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { ReminderDialog } from '@/components/reminder-dialog'
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -385,17 +387,20 @@ function NoteMetaSidebar({
|
|||||||
onArchive,
|
onArchive,
|
||||||
onOpenHistory,
|
onOpenHistory,
|
||||||
onEnableHistory,
|
onEnableHistory,
|
||||||
|
onUpdateReminder,
|
||||||
}: {
|
}: {
|
||||||
note: Note
|
note: Note
|
||||||
onPinToggle: (note: Note) => void
|
onPinToggle: (note: Note) => void
|
||||||
onArchive: (note: Note) => void
|
onArchive: (note: Note) => void
|
||||||
onOpenHistory?: (note: Note) => void
|
onOpenHistory?: (note: Note) => void
|
||||||
onEnableHistory?: (noteId: string) => Promise<void>
|
onEnableHistory?: (noteId: string) => Promise<void>
|
||||||
|
onUpdateReminder?: (noteId: string, reminder: Date | null) => void
|
||||||
}) {
|
}) {
|
||||||
const { t } = useLanguage()
|
const { t } = useLanguage()
|
||||||
const { notebooks, moveNoteToNotebookOptimistic } = useNotebooks()
|
const { notebooks, moveNoteToNotebookOptimistic } = useNotebooks()
|
||||||
const [moveOpen, setMoveOpen] = useState(false)
|
const [moveOpen, setMoveOpen] = useState(false)
|
||||||
const [isMoving, setIsMoving] = 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
|
// t() returns the key itself when not found — use this wrapper for safe fallbacks
|
||||||
const ts = (key: string, fallback: string) => {
|
const ts = (key: string, fallback: string) => {
|
||||||
@@ -571,6 +576,32 @@ function NoteMetaSidebar({
|
|||||||
onClick={() => onArchive(note)}
|
onClick={() => onArchive(note)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Reminder */}
|
||||||
|
{onUpdateReminder && (
|
||||||
|
<>
|
||||||
|
<SidebarActionBtn
|
||||||
|
icon={<Bell className={cn("h-3.5 w-3.5", note.reminder && "text-primary")} />}
|
||||||
|
label={t('reminder.setReminder')}
|
||||||
|
onClick={() => setShowReminder(true)}
|
||||||
|
/>
|
||||||
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
|
<ReminderDialog
|
||||||
|
open={showReminder}
|
||||||
|
onOpenChange={setShowReminder}
|
||||||
|
currentReminder={note.reminder ? new Date(note.reminder) : null}
|
||||||
|
onSave={(date) => {
|
||||||
|
onUpdateReminder(note.id, date)
|
||||||
|
setShowReminder(false)
|
||||||
|
}}
|
||||||
|
onRemove={() => {
|
||||||
|
onUpdateReminder(note.id, null)
|
||||||
|
setShowReminder(false)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* History */}
|
{/* History */}
|
||||||
<SidebarActionBtn
|
<SidebarActionBtn
|
||||||
icon={<History className="h-3.5 w-3.5" />}
|
icon={<History className="h-3.5 w-3.5" />}
|
||||||
@@ -928,6 +959,22 @@ export function NotesTabsView({
|
|||||||
onArchive={handleArchive}
|
onArchive={handleArchive}
|
||||||
onOpenHistory={onOpenHistory}
|
onOpenHistory={onOpenHistory}
|
||||||
onEnableHistory={onEnableHistory}
|
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'))
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,17 +3,18 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Badge } from '@/components/ui/badge'
|
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 {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from '@/components/ui/popover'
|
} 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 { toast } from 'sonner'
|
||||||
import { useNoteRefreshOptional } from '@/context/NoteRefreshContext'
|
import { useNoteRefreshOptional } from '@/context/NoteRefreshContext'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { useLanguage } from '@/lib/i18n'
|
import { useLanguage } from '@/lib/i18n'
|
||||||
|
import { formatDistanceToNow } from 'date-fns'
|
||||||
|
|
||||||
interface ShareRequest {
|
interface ShareRequest {
|
||||||
id: string
|
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() {
|
export function NotificationPanel() {
|
||||||
const { triggerRefresh } = useNoteRefreshOptional()
|
const { triggerRefresh } = useNoteRefreshOptional()
|
||||||
const { t } = useLanguage()
|
const { t } = useLanguage()
|
||||||
const [requests, setRequests] = useState<ShareRequest[]>([])
|
const [requests, setRequests] = useState<ShareRequest[]>([])
|
||||||
|
const [reminders, setReminders] = useState<ReminderNote[]>([])
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
const loadRequests = useCallback(async () => {
|
const loadData = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const data = await getPendingShareRequests()
|
const [shareData, reminderData] = await Promise.all([
|
||||||
setRequests(data as any)
|
getPendingShareRequests(),
|
||||||
|
getNotesWithReminders(),
|
||||||
|
])
|
||||||
|
setRequests(shareData as any)
|
||||||
|
setReminders((reminderData as any) || [])
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Failed to load share requests:', error)
|
console.error('Failed to load notifications:', error)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadRequests()
|
loadData()
|
||||||
const interval = setInterval(loadRequests, 10000)
|
const interval = setInterval(loadData, 30000)
|
||||||
const onFocus = () => loadRequests()
|
const onFocus = () => loadData()
|
||||||
window.addEventListener('focus', onFocus)
|
window.addEventListener('focus', onFocus)
|
||||||
return () => {
|
return () => {
|
||||||
clearInterval(interval)
|
clearInterval(interval)
|
||||||
window.removeEventListener('focus', onFocus)
|
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) => {
|
const handleAccept = async (shareId: string) => {
|
||||||
try {
|
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 (
|
return (
|
||||||
<Popover open={open} onOpenChange={setOpen}>
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
@@ -130,20 +161,72 @@ export function NotificationPanel() {
|
|||||||
<div className="p-6 text-center text-sm text-muted-foreground">
|
<div className="p-6 text-center text-sm text-muted-foreground">
|
||||||
<div className="animate-spin h-6 w-6 border-2 border-primary border-t-transparent rounded-full mx-auto mb-2" />
|
<div className="animate-spin h-6 w-6 border-2 border-primary border-t-transparent rounded-full mx-auto mb-2" />
|
||||||
</div>
|
</div>
|
||||||
) : requests.length === 0 ? (
|
) : !hasContent ? (
|
||||||
<div className="p-6 text-center text-sm text-muted-foreground">
|
<div className="p-6 text-center text-sm text-muted-foreground">
|
||||||
<Bell className="h-10 w-10 mx-auto mb-3 opacity-30" />
|
<Bell className="h-10 w-10 mx-auto mb-3 opacity-30" />
|
||||||
<p className="font-medium">{t('notification.noNotifications') || 'No new notifications'}</p>
|
<p className="font-medium">{t('notification.noNotifications') || 'No new notifications'}</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="max-h-96 overflow-y-auto">
|
<div className="max-h-96 overflow-y-auto">
|
||||||
|
{/* Overdue reminders */}
|
||||||
|
{overdueReminders.map((note) => (
|
||||||
|
<div
|
||||||
|
key={note.id}
|
||||||
|
className="p-3 border-b last:border-0 hover:bg-accent/50 transition-colors duration-150"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => handleToggleReminder(note.id, true)}
|
||||||
|
className="mt-0.5 flex-none text-amber-500 hover:text-green-500 transition-colors"
|
||||||
|
title={t('reminders.markDone')}
|
||||||
|
>
|
||||||
|
<Circle className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-1.5 mb-0.5">
|
||||||
|
<AlertCircle className="w-3 h-3 text-amber-500" />
|
||||||
|
<span className="text-[10px] font-semibold uppercase tracking-wider text-amber-600 dark:text-amber-400">
|
||||||
|
{t('reminders.overdue')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm font-medium truncate">{note.title || t('notification.untitled')}</p>
|
||||||
|
<div className="flex items-center gap-1 mt-1 text-xs text-muted-foreground">
|
||||||
|
<Clock className="w-3 h-3" />
|
||||||
|
{note.reminder && formatDistanceToNow(new Date(note.reminder), { addSuffix: true })}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Upcoming reminders */}
|
||||||
|
{upcomingReminders.slice(0, 5).map((note) => (
|
||||||
|
<div
|
||||||
|
key={note.id}
|
||||||
|
className="p-3 border-b last:border-0 hover:bg-accent/50 transition-colors duration-150"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Clock className="w-4 h-4 mt-0.5 flex-none text-primary" />
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm font-medium truncate">{note.title || t('notification.untitled')}</p>
|
||||||
|
<div className="text-xs text-muted-foreground mt-0.5">
|
||||||
|
{note.reminder && new Date(note.reminder).toLocaleDateString(undefined, {
|
||||||
|
month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Share requests */}
|
||||||
{requests.map((request) => (
|
{requests.map((request) => (
|
||||||
<div
|
<div
|
||||||
key={request.id}
|
key={request.id}
|
||||||
className="p-4 border-b last:border-0 hover:bg-accent/50 transition-colors duration-150"
|
className="p-3 border-b last:border-0 hover:bg-accent/50 transition-colors duration-150"
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-3 mb-3">
|
<div className="flex items-start gap-3 mb-2">
|
||||||
<div className="h-8 w-8 rounded-full bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center text-white font-semibold text-xs shadow-md shrink-0">
|
<div className="h-7 w-7 rounded-full bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center text-white font-semibold text-[10px] shadow-md shrink-0">
|
||||||
{(request.sharer.name || request.sharer.email)[0].toUpperCase()}
|
{(request.sharer.name || request.sharer.email)[0].toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
@@ -156,47 +239,54 @@ export function NotificationPanel() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-2 mt-3">
|
<div className="flex gap-2 mt-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleDecline(request.id)}
|
onClick={() => handleDecline(request.id)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex-1 h-9 px-4 text-xs font-semibold rounded-lg",
|
"flex-1 h-7 px-3 text-[11px] font-semibold rounded-md",
|
||||||
"border border-border bg-background",
|
"border border-border bg-background",
|
||||||
"text-muted-foreground",
|
"text-muted-foreground",
|
||||||
"hover:bg-muted hover:text-foreground",
|
"hover:bg-muted hover:text-foreground",
|
||||||
"transition-all duration-200",
|
"transition-all duration-200",
|
||||||
"flex items-center justify-center gap-1.5",
|
"flex items-center justify-center gap-1",
|
||||||
"active:scale-95"
|
"active:scale-95"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<X className="h-3.5 w-3.5" />
|
<X className="h-3 w-3" />
|
||||||
{t('notification.decline') || t('general.cancel')}
|
{t('notification.decline') || t('general.cancel')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleAccept(request.id)}
|
onClick={() => handleAccept(request.id)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex-1 h-9 px-4 text-xs font-semibold rounded-lg",
|
"flex-1 h-7 px-3 text-[11px] font-semibold rounded-md",
|
||||||
"bg-primary text-primary-foreground",
|
"bg-primary text-primary-foreground",
|
||||||
"hover:bg-primary/90",
|
"hover:bg-primary/90",
|
||||||
"shadow-sm hover:shadow",
|
"shadow-sm",
|
||||||
"transition-all duration-200",
|
"transition-all duration-200",
|
||||||
"flex items-center justify-center gap-1.5",
|
"flex items-center justify-center gap-1",
|
||||||
"active:scale-95"
|
"active:scale-95"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Check className="h-3.5 w-3.5" />
|
<Check className="h-3 w-3" />
|
||||||
{t('notification.accept') || t('general.confirm')}
|
{t('notification.accept') || t('general.confirm')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-1.5 mt-3 text-xs text-muted-foreground">
|
|
||||||
<Clock className="h-3 w-3" />
|
|
||||||
<span>{new Date(request.createdAt).toLocaleDateString()}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Footer link to reminders page */}
|
||||||
|
{activeReminders.length > 0 && (
|
||||||
|
<div className="px-4 py-2 border-t bg-muted/30">
|
||||||
|
<a
|
||||||
|
href="/reminders"
|
||||||
|
className="text-[11px] font-medium text-primary hover:underline"
|
||||||
|
>
|
||||||
|
{t('reminders.viewAll') || t('reminders.title') || 'Voir tous les rappels'}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState, useTransition } from 'react'
|
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 { Note } from '@/lib/types'
|
||||||
import { toggleReminderDone } from '@/app/actions/notes'
|
import { toggleReminderDone, clearCompletedReminders } from '@/app/actions/notes'
|
||||||
import { useLanguage } from '@/lib/i18n'
|
import { useLanguage } from '@/lib/i18n'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
@@ -213,12 +213,27 @@ export function RemindersPage({ notes: initialNotes }: RemindersPageProps) {
|
|||||||
{/* Terminés */}
|
{/* Terminés */}
|
||||||
{done.length > 0 && (
|
{done.length > 0 && (
|
||||||
<section>
|
<section>
|
||||||
<SectionTitle
|
<div className="flex items-center justify-between mb-3">
|
||||||
icon={CheckCircle2}
|
<SectionTitle
|
||||||
label={t('reminders.done') || 'Terminés'}
|
icon={CheckCircle2}
|
||||||
count={done.length}
|
label={t('reminders.done') || 'Terminés'}
|
||||||
color="text-green-600 dark:text-green-400"
|
count={done.length}
|
||||||
/>
|
color="text-green-600 dark:text-green-400"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
startTransition(async () => {
|
||||||
|
await clearCompletedReminders()
|
||||||
|
setNotes(prev => prev.filter(n => !n.isReminderDone))
|
||||||
|
router.refresh()
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground hover:text-red-500 transition-colors"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-3 h-3" />
|
||||||
|
{t('reminders.clearCompleted') || 'Effacer'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{done.map(note => (
|
{done.map(note => (
|
||||||
<ReminderCard key={note.id} note={note} onToggleDone={handleToggleDone} t={t} />
|
<ReminderCard key={note.id} note={note} onToggleDone={handleToggleDone} t={t} />
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
import { useLanguage } from '@/lib/i18n'
|
import { useLanguage } from '@/lib/i18n'
|
||||||
import { NotebooksList } from './notebooks-list'
|
import { NotebooksList } from './notebooks-list'
|
||||||
import { useHomeViewOptional } from '@/context/home-view-context'
|
import { useHomeViewOptional } from '@/context/home-view-context'
|
||||||
|
import { useNoteRefreshOptional } from '@/context/NoteRefreshContext'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { getTrashCount } from '@/app/actions/notes'
|
import { getTrashCount } from '@/app/actions/notes'
|
||||||
|
|
||||||
@@ -34,6 +35,7 @@ export function Sidebar({ className, user }: { className?: string, user?: any })
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { t } = useLanguage()
|
const { t } = useLanguage()
|
||||||
const homeBridge = useHomeViewOptional()
|
const homeBridge = useHomeViewOptional()
|
||||||
|
const { refreshKey } = useNoteRefreshOptional()
|
||||||
const [trashCount, setTrashCount] = useState(0)
|
const [trashCount, setTrashCount] = useState(0)
|
||||||
|
|
||||||
const searchKey = searchParams.toString()
|
const searchKey = searchParams.toString()
|
||||||
@@ -44,7 +46,7 @@ export function Sidebar({ className, user }: { className?: string, user?: any })
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (HIDDEN_ROUTES.some(r => pathname.startsWith(r))) return
|
if (HIDDEN_ROUTES.some(r => pathname.startsWith(r))) return
|
||||||
getTrashCount().then(setTrashCount)
|
getTrashCount().then(setTrashCount)
|
||||||
}, [pathname, searchKey])
|
}, [pathname, searchKey, refreshKey])
|
||||||
|
|
||||||
// Hide sidebar on Agents, Chat IA and Lab routes
|
// Hide sidebar on Agents, Chat IA and Lab routes
|
||||||
if (HIDDEN_ROUTES.some(r => pathname.startsWith(r))) return null
|
if (HIDDEN_ROUTES.some(r => pathname.startsWith(r))) return null
|
||||||
|
|||||||
@@ -740,7 +740,9 @@
|
|||||||
"markDone": "Mark as done",
|
"markDone": "Mark as done",
|
||||||
"markUndone": "Mark as undone",
|
"markUndone": "Mark as undone",
|
||||||
"todayAt": "Today at {time}",
|
"todayAt": "Today at {time}",
|
||||||
"tomorrowAt": "Tomorrow at {time}"
|
"tomorrowAt": "Tomorrow at {time}",
|
||||||
|
"clearCompleted": "Clear completed",
|
||||||
|
"viewAll": "View all reminders"
|
||||||
},
|
},
|
||||||
"notebook": {
|
"notebook": {
|
||||||
"create": "Create Notebook",
|
"create": "Create Notebook",
|
||||||
|
|||||||
@@ -740,7 +740,9 @@
|
|||||||
"markDone": "Marquer comme terminé",
|
"markDone": "Marquer comme terminé",
|
||||||
"markUndone": "Marquer comme non terminé",
|
"markUndone": "Marquer comme non terminé",
|
||||||
"todayAt": "Aujourd'hui à {time}",
|
"todayAt": "Aujourd'hui à {time}",
|
||||||
"tomorrowAt": "Demain à {time}"
|
"tomorrowAt": "Demain à {time}",
|
||||||
|
"clearCompleted": "Effacer les terminés",
|
||||||
|
"viewAll": "Voir tous les rappels"
|
||||||
},
|
},
|
||||||
"notebook": {
|
"notebook": {
|
||||||
"create": "Créer un carnet",
|
"create": "Créer un carnet",
|
||||||
|
|||||||
Reference in New Issue
Block a user