- 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
233 lines
8.3 KiB
TypeScript
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>
|
|
)
|
|
}
|