Files
Momento/memento-note/components/dashboard-catalog-widgets.tsx
Antigravity 30da592ba2
All checks were successful
CI / Lint, Unit Tests & Build (push) Successful in 7m3s
CI / Deploy production (on server) (push) Successful in 23s
feat(dashboard): tableau de bord Second Brain configurable avec chargement progressif
Briefing granulaire, pistes rapides puis enrichissement async, layout persisté v5,
suggestions agents, intégration Gmail et navigation sidebar alignée sur /home.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-14 16:50:53 +00:00

372 lines
12 KiB
TypeScript

'use client'
import { Inbox, GraduationCap, Mail, Pin, Bot, BarChart3 } from 'lucide-react'
import { useLanguage } from '@/lib/i18n'
import { RevisionHeatmap } from '@/components/flashcards/revision-heatmap'
import { UsageMeter } from '@/components/usage-meter'
import { DashboardWidgetTitleRow } from '@/components/dashboard-widget-title-row'
import type { DashboardWidgetId } from '@/lib/dashboard/layout'
interface DashboardWidgetShellProps {
widgetId: DashboardWidgetId
icon: React.ReactNode
title: string
children: React.ReactNode
compact?: boolean
headerActions?: React.ReactNode
}
export function DashboardWidgetShell({
widgetId,
icon,
title,
children,
compact,
headerActions,
}: DashboardWidgetShellProps) {
return (
<div className={`rounded-2xl border border-border/30 bg-white dark:bg-zinc-900 h-full ${
compact ? 'p-3' : 'p-4'
}`}>
<DashboardWidgetTitleRow
widgetId={widgetId}
icon={icon}
title={title}
actions={headerActions}
className={compact ? 'mb-2' : 'mb-3'}
/>
{children}
</div>
)
}
export function DashboardInboxWidget({
count,
loading,
onOpen,
}: {
count: number
loading: boolean
onOpen: () => void
}) {
const { t } = useLanguage()
return (
<DashboardWidgetShell
widgetId="inbox"
icon={<Inbox size={12} className="text-brand-accent" />}
title={t('homeDashboard.inbox')}
compact
>
{loading ? (
<div className="h-10 rounded-lg bg-stone-50 animate-pulse" />
) : (
<button
type="button"
onClick={onOpen}
className="w-full flex items-center justify-between gap-3 p-3 rounded-xl border border-border/20 hover:border-brand-accent/30 hover:bg-brand-accent/[0.03] transition-all text-start"
>
<div>
<p className="text-2xl font-serif font-bold text-ink dark:text-dark-ink leading-none">{count}</p>
<p className="text-[10px] text-concrete mt-1">{t('homeDashboard.toOrganize')}</p>
</div>
<span className="text-[9px] font-mono uppercase font-bold text-brand-accent">{t('homeDashboard.widgetOpen')} </span>
</button>
)}
</DashboardWidgetShell>
)
}
export function DashboardRevisionWidget({
dueCount,
loading,
onOpen,
}: {
dueCount: number
loading: boolean
onOpen: () => void
}) {
const { t } = useLanguage()
return (
<DashboardWidgetShell
widgetId="revision"
icon={<GraduationCap size={12} className="text-brand-accent" />}
title={t('homeDashboard.review')}
compact
>
{loading ? (
<div className="h-10 rounded-lg bg-stone-50 animate-pulse" />
) : (
<button
type="button"
onClick={onOpen}
className="w-full flex items-center justify-between gap-3 p-3 rounded-xl border border-border/20 hover:border-brand-accent/30 hover:bg-brand-accent/[0.03] transition-all text-start"
>
<div>
<p className="text-2xl font-serif font-bold text-ink dark:text-dark-ink leading-none">{dueCount}</p>
<p className="text-[10px] text-concrete mt-1">{t('homeDashboard.cardsDue')}</p>
</div>
<span className="text-[9px] font-mono uppercase font-bold text-brand-accent">{t('homeDashboard.widgetOpen')} </span>
</button>
)}
</DashboardWidgetShell>
)
}
export function DashboardStatsWidget({
clusterCount,
bridgeCount,
noteCount,
loading,
onOpen,
}: {
clusterCount: number
bridgeCount: number
noteCount: number
loading: boolean
onOpen: () => void
}) {
const { t } = useLanguage()
return (
<DashboardWidgetShell
widgetId="stats"
icon={<BarChart3 size={12} className="text-brand-accent" />}
title={t('homeDashboard.widgets.stats')}
compact
>
{loading ? (
<div className="grid grid-cols-3 gap-2">
{[0, 1, 2].map(i => <div key={i} className="h-12 rounded-lg bg-stone-50 animate-pulse" />)}
</div>
) : (
<button type="button" onClick={onOpen} className="w-full text-start">
<div className="grid grid-cols-3 gap-2">
{[
{ label: t('homeDashboard.themes'), value: clusterCount },
{ label: t('homeDashboard.widgetStatsBridges'), value: bridgeCount },
{ label: t('homeDashboard.widgetStatsNotes'), value: noteCount },
].map(item => (
<div key={item.label} className="p-2 rounded-xl border border-border/20 bg-stone-50/50 dark:bg-zinc-950/30 text-center">
<p className="text-lg font-serif font-bold text-ink dark:text-dark-ink leading-none">{item.value}</p>
<p className="text-[8px] font-mono uppercase text-concrete mt-1 truncate">{item.label}</p>
</div>
))}
</div>
</button>
)}
</DashboardWidgetShell>
)
}
export function DashboardAgentActivityWidget({
actions,
loading,
onOpen,
}: {
actions: { id: string; agentName: string; result: string | null; createdAt: string }[]
loading: boolean
onOpen: () => void
}) {
const { t } = useLanguage()
return (
<DashboardWidgetShell
widgetId="agent-activity"
icon={<Bot size={12} className="text-brand-accent" />}
title={t('homeDashboard.widgets.agent-activity')}
compact
>
{loading ? (
<div className="space-y-2">
<div className="h-8 rounded-lg bg-stone-50 animate-pulse" />
<div className="h-8 rounded-lg bg-stone-50 animate-pulse" />
</div>
) : actions.length === 0 ? (
<p className="text-[10px] text-concrete italic py-2">{t('homeDashboard.widgetAgentActivityEmpty')}</p>
) : (
<div className="space-y-1.5">
{actions.slice(0, 3).map(a => (
<button
key={a.id}
type="button"
onClick={onOpen}
className="w-full p-2 rounded-xl border border-border/20 hover:border-brand-accent/25 text-start transition-all"
>
<p className="text-[10px] font-mono font-bold text-ink dark:text-dark-ink truncate">{a.agentName}</p>
<p className="text-[9px] text-concrete truncate mt-0.5">
{(a.result || t('homeDashboard.intelAgentNoResult')).slice(0, 80)}
</p>
</button>
))}
</div>
)}
</DashboardWidgetShell>
)
}
export function DashboardGmailWidget({
connected,
recentCaptures,
loading,
onOpen,
}: {
connected: boolean
recentCaptures: number
loading: boolean
onOpen: () => void
}) {
const { t } = useLanguage()
return (
<DashboardWidgetShell
widgetId="gmail"
icon={<Mail size={12} className="text-brand-accent" />}
title={t('homeDashboard.gmailCaptures')}
compact
>
{loading ? (
<div className="h-10 rounded-lg bg-stone-50 animate-pulse" />
) : connected ? (
<button
type="button"
onClick={onOpen}
className="w-full flex items-center justify-between gap-2 p-2.5 rounded-xl border border-border/20 hover:border-brand-accent/25 transition-all text-start"
>
<span className="text-[10px] text-ink dark:text-dark-ink">{t('homeDashboard.gmailRecent', { count: recentCaptures })}</span>
<span className="text-[8px] font-mono font-bold text-brand-accent bg-brand-accent/10 px-1.5 py-0.5 rounded shrink-0">Gmail</span>
</button>
) : (
<button
type="button"
onClick={onOpen}
className="w-full text-[10px] font-mono uppercase tracking-wider text-concrete hover:text-brand-accent transition-colors text-start py-2"
>
{t('homeDashboard.gmailConnect')}
</button>
)}
</DashboardWidgetShell>
)
}
export function DashboardPinnedWidget({
notes,
loading,
onSelect,
}: {
notes: { id: string; title: string | null; notebookId: string | null }[]
loading: boolean
onSelect: (id: string, notebookId: string | null) => void
}) {
const { t } = useLanguage()
return (
<DashboardWidgetShell
widgetId="pinned"
icon={<Pin size={12} className="text-brand-accent" />}
title={t('homeDashboard.widgets.pinned')}
compact
>
{loading ? (
<div className="space-y-2">
<div className="h-8 rounded-lg bg-stone-50 animate-pulse" />
</div>
) : notes.length === 0 ? (
<p className="text-[10px] text-concrete italic py-2">{t('homeDashboard.widgetPinnedEmpty')}</p>
) : (
<div className="space-y-1">
{notes.map(n => (
<button
key={n.id}
type="button"
onClick={() => onSelect(n.id, n.notebookId)}
className="w-full text-[10px] text-ink dark:text-dark-ink truncate text-start p-2 rounded-lg hover:bg-brand-accent/[0.04] transition-colors"
>
{n.title || t('homeDashboard.untitled')}
</button>
))}
</div>
)}
</DashboardWidgetShell>
)
}
export function DashboardActivityWidget({
data,
loading,
}: {
data: { date: string; count: number }[]
loading: boolean
}) {
const { t } = useLanguage()
return (
<DashboardWidgetShell
widgetId="activity"
icon={<BarChart3 size={12} className="text-brand-accent" />}
title={t('homeDashboard.widgets.activity')}
>
{loading ? (
<div className="h-24 rounded-lg bg-stone-50 animate-pulse" />
) : (
<RevisionHeatmap data={data} />
)}
<p className="text-[9px] text-concrete mt-2">{t('homeDashboard.widgetActivityHint')}</p>
</DashboardWidgetShell>
)
}
export function DashboardUsageWidget() {
const { t } = useLanguage()
return (
<DashboardWidgetShell
widgetId="usage"
icon={<BarChart3 size={12} className="text-brand-accent" />}
title={t('homeDashboard.widgets.usage')}
compact
>
<UsageMeter className="px-0 py-0" />
<p className="text-[9px] text-concrete mt-2">{t('homeDashboard.widgetUsageHint')}</p>
</DashboardWidgetShell>
)
}
export function DashboardFlashcardsProgressWidget({
retentionRate,
streak,
totalCards,
loading,
onOpen,
}: {
retentionRate: number
streak: number
totalCards: number
loading?: boolean
onOpen: () => void
}) {
const { t } = useLanguage()
return (
<DashboardWidgetShell
widgetId="flashcards-progress"
icon={<GraduationCap size={12} className="text-brand-accent" />}
title={t('homeDashboard.widgets.flashcards-progress')}
compact
>
{loading ? (
<div className="h-14 rounded-lg bg-stone-50 animate-pulse" />
) : (
<button type="button" onClick={onOpen} className="w-full text-start">
<div className="grid grid-cols-3 gap-2">
<div className="text-center p-2 rounded-xl border border-border/20">
<p className="text-lg font-serif font-bold text-ink dark:text-dark-ink">{retentionRate}%</p>
<p className="text-[8px] font-mono uppercase text-concrete">{t('homeDashboard.flashRetention')}</p>
</div>
<div className="text-center p-2 rounded-xl border border-border/20">
<p className="text-lg font-serif font-bold text-ink dark:text-dark-ink">{streak}</p>
<p className="text-[8px] font-mono uppercase text-concrete">{t('homeDashboard.flashStreak')}</p>
</div>
<div className="text-center p-2 rounded-xl border border-border/20">
<p className="text-lg font-serif font-bold text-ink dark:text-dark-ink">{totalCards}</p>
<p className="text-[8px] font-mono uppercase text-concrete">{t('homeDashboard.flashTotal')}</p>
</div>
</div>
</button>
)}
</DashboardWidgetShell>
)
}