Files
Keep/keep-notes/components/reminders-page.tsx
Sepehr Ramezani fa7e166f3e feat: add reminders page, BMad skills upgrade, MCP server refactor
- Add reminders page with navigation support
- Upgrade BMad builder module to skills-based architecture
- Refactor MCP server: extract tools and auth into separate modules
- Add connections cache, custom AI provider support
- Update prisma schema and generated client
- Various UI/UX improvements and i18n updates
- Add service worker for PWA support

Made-with: Cursor
2026-04-13 21:02:53 +02:00

233 lines
8.3 KiB
TypeScript

'use client'
import { useState, useTransition } from 'react'
import { Bell, BellOff, CheckCircle2, Circle, Clock, AlertCircle, RefreshCw } from 'lucide-react'
import { Note } from '@/lib/types'
import { toggleReminderDone } from '@/app/actions/notes'
import { useLanguage } from '@/lib/i18n'
import { cn } from '@/lib/utils'
import { useRouter } from 'next/navigation'
interface RemindersPageProps {
notes: Note[]
}
function formatReminderDate(date: Date | string, t: (key: string, params?: Record<string, string | number>) => string, locale = 'fr-FR'): string {
const d = new Date(date)
const now = new Date()
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
const tomorrow = new Date(today)
tomorrow.setDate(tomorrow.getDate() + 1)
const noteDay = new Date(d.getFullYear(), d.getMonth(), d.getDate())
const timeStr = d.toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' })
if (noteDay.getTime() === today.getTime()) return t('reminders.todayAt', { time: timeStr })
if (noteDay.getTime() === tomorrow.getTime()) return t('reminders.tomorrowAt', { time: timeStr })
return d.toLocaleDateString(locale, {
weekday: 'long',
day: 'numeric',
month: 'long',
hour: '2-digit',
minute: '2-digit',
})
}
function ReminderCard({ note, onToggleDone, t }: { note: Note; onToggleDone: (id: string, done: boolean) => void; t: (key: string, params?: Record<string, string | number>) => string }) {
const now = new Date()
const reminderDate = note.reminder ? new Date(note.reminder) : null
const isOverdue = reminderDate && reminderDate < now && !note.isReminderDone
const isDone = note.isReminderDone
return (
<div
className={cn(
'group relative flex items-start gap-4 rounded-2xl border p-4 transition-all duration-200',
isDone
? 'border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800/30 opacity-60'
: isOverdue
? 'border-amber-200 dark:border-amber-800 bg-amber-50 dark:bg-amber-900/20'
: 'border-slate-200 dark:border-slate-700 bg-white dark:bg-[#1e2128] hover:shadow-md'
)}
>
{/* Done toggle */}
<button
onClick={() => onToggleDone(note.id, !isDone)}
className={cn(
'mt-0.5 flex-none transition-colors',
isDone
? 'text-green-500 hover:text-slate-400'
: 'text-slate-300 hover:text-green-500 dark:text-slate-600'
)}
title={isDone ? t('reminders.markUndone') : t('reminders.markDone')}
>
{isDone ? (
<CheckCircle2 className="w-5 h-5" />
) : (
<Circle className="w-5 h-5" />
)}
</button>
{/* Content */}
<div className="flex-1 min-w-0">
{note.title && (
<p className={cn('font-semibold text-slate-900 dark:text-white truncate', isDone && 'line-through opacity-60')}>
{note.title}
</p>
)}
<p className={cn('text-sm text-slate-600 dark:text-slate-300 line-clamp-2 mt-0.5', isDone && 'line-through opacity-60')}>
{note.content}
</p>
{/* Reminder date badge */}
{reminderDate && (
<div className={cn(
'inline-flex items-center gap-1.5 mt-2 px-2.5 py-1 rounded-full text-xs font-medium',
isDone
? 'bg-slate-100 dark:bg-slate-700 text-slate-500'
: isOverdue
? 'bg-amber-100 dark:bg-amber-900/40 text-amber-700 dark:text-amber-400'
: 'bg-primary/10 text-primary'
)}>
{isOverdue && !isDone ? (
<AlertCircle className="w-3 h-3" />
) : (
<Clock className="w-3 h-3" />
)}
{formatReminderDate(reminderDate, t)}
{note.reminderRecurrence && (
<span className="ml-1 opacity-70">· {note.reminderRecurrence}</span>
)}
</div>
)}
</div>
</div>
)
}
function SectionTitle({ icon: Icon, label, count, color }: { icon: any; label: string; count: number; color: string }) {
return (
<div className={cn('flex items-center gap-2 mb-3', color)}>
<Icon className="w-4 h-4" />
<h2 className="text-sm font-semibold uppercase tracking-wider">{label}</h2>
<span className="ml-auto text-xs font-medium bg-current/10 px-2 py-0.5 rounded-full opacity-70">{count}</span>
</div>
)
}
export function RemindersPage({ notes: initialNotes }: RemindersPageProps) {
const { t } = useLanguage()
const router = useRouter()
const [notes, setNotes] = useState(initialNotes)
const [isPending, startTransition] = useTransition()
const now = new Date()
const upcoming = notes.filter(n => !n.isReminderDone && n.reminder && new Date(n.reminder) >= now)
const overdue = notes.filter(n => !n.isReminderDone && n.reminder && new Date(n.reminder) < now)
const done = notes.filter(n => n.isReminderDone)
const handleToggleDone = (noteId: string, newDone: boolean) => {
// Optimistic update
setNotes(prev => prev.map(n => n.id === noteId ? { ...n, isReminderDone: newDone } : n))
startTransition(async () => {
await toggleReminderDone(noteId, newDone)
router.refresh()
})
}
if (notes.length === 0) {
return (
<main className="container mx-auto px-4 py-12 max-w-3xl">
<div className="flex items-center justify-between mb-8">
<h1 className="text-3xl font-bold text-slate-900 dark:text-white flex items-center gap-3">
<Bell className="w-8 h-8 text-primary" />
{t('reminders.title') || 'Rappels'}
</h1>
</div>
<div className="flex flex-col items-center justify-center min-h-[50vh] text-center text-slate-500 dark:text-slate-400">
<div className="bg-slate-100 dark:bg-slate-800 p-6 rounded-full mb-4">
<BellOff className="w-12 h-12 text-slate-400" />
</div>
<h2 className="text-xl font-semibold mb-2 text-slate-700 dark:text-slate-300">
{t('reminders.empty') || 'Aucun rappel'}
</h2>
<p className="max-w-sm text-sm opacity-80">
{t('reminders.emptyDescription') || 'Ajoutez un rappel à une note pour le retrouver ici.'}
</p>
</div>
</main>
)
}
return (
<main className="container mx-auto px-4 py-8 max-w-3xl">
<div className="flex items-center justify-between mb-8">
<h1 className="text-3xl font-bold text-slate-900 dark:text-white flex items-center gap-3">
<Bell className="w-8 h-8 text-primary" />
{t('reminders.title') || 'Rappels'}
</h1>
{isPending && (
<RefreshCw className="w-4 h-4 animate-spin text-slate-400" />
)}
</div>
<div className="space-y-8">
{/* En retard */}
{overdue.length > 0 && (
<section>
<SectionTitle
icon={AlertCircle}
label={t('reminders.overdue') || 'En retard'}
count={overdue.length}
color="text-amber-600 dark:text-amber-400"
/>
<div className="space-y-3">
{overdue.map(note => (
<ReminderCard key={note.id} note={note} onToggleDone={handleToggleDone} t={t} />
))}
</div>
</section>
)}
{/* À venir */}
{upcoming.length > 0 && (
<section>
<SectionTitle
icon={Clock}
label={t('reminders.upcoming') || 'À venir'}
count={upcoming.length}
color="text-primary"
/>
<div className="space-y-3">
{upcoming.map(note => (
<ReminderCard key={note.id} note={note} onToggleDone={handleToggleDone} t={t} />
))}
</div>
</section>
)}
{/* Terminés */}
{done.length > 0 && (
<section>
<SectionTitle
icon={CheckCircle2}
label={t('reminders.done') || 'Terminés'}
count={done.length}
color="text-green-600 dark:text-green-400"
/>
<div className="space-y-3">
{done.map(note => (
<ReminderCard key={note.id} note={note} onToggleDone={handleToggleDone} t={t} />
))}
</div>
</section>
)}
</div>
</main>
)
}