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
This commit is contained in:
232
keep-notes/components/reminders-page.tsx
Normal file
232
keep-notes/components/reminders-page.tsx
Normal file
@@ -0,0 +1,232 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user