Files
Momento/memento-note/components/dashboard-next-paths.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

166 lines
6.5 KiB
TypeScript

'use client'
import { motion } from 'motion/react'
import { Loader2 } from 'lucide-react'
import {
ArrowRight, GitBranch, Lightbulb, Link2, BookOpen, Inbox,
GraduationCap, Compass, Plus, Sparkles, PenLine,
} from 'lucide-react'
import type { LucideIcon } from 'lucide-react'
import { useLanguage } from '@/lib/i18n'
import { DashboardWidgetTitleRow } from '@/components/dashboard-widget-title-row'
import type { DashboardPath, DashboardPathType } from '@/lib/dashboard/path-types'
const TYPE_META: Record<DashboardPathType, { Icon: LucideIcon; accent: string }> = {
continue: { Icon: PenLine, accent: 'text-ink' },
connect: { Icon: Link2, accent: 'text-brand-accent' },
'add-link': { Icon: Plus, accent: 'text-emerald-600' },
bridge: { Icon: GitBranch, accent: 'text-violet-600' },
research: { Icon: Sparkles, accent: 'text-sky-600' },
explore: { Icon: Compass, accent: 'text-amber-600' },
organize: { Icon: Inbox, accent: 'text-brand-accent' },
review: { Icon: GraduationCap, accent: 'text-brand-accent' },
resurface: { Icon: Lightbulb, accent: 'text-brand-accent' },
daily: { Icon: BookOpen, accent: 'text-concrete' },
}
export interface DashboardNextPathsProps {
paths: DashboardPath[]
loading?: boolean
enriching?: boolean
focusNoteTitle?: string | null
onAction: (path: DashboardPath) => void
prefersReducedMotion?: boolean
}
export function DashboardNextPaths({
paths,
loading,
enriching,
focusNoteTitle,
onAction,
prefersReducedMotion,
}: DashboardNextPathsProps) {
const { t } = useLanguage()
if (loading) {
return (
<div className="rounded-2xl border border-brand-accent/20 bg-gradient-to-br from-white via-white to-brand-accent/[0.04] dark:from-zinc-900 dark:via-zinc-900 dark:to-brand-accent/[0.06] shadow-sm overflow-hidden">
<div className="px-5 pt-4 pb-3 border-b border-border/15">
<DashboardWidgetTitleRow
widgetId="next-paths"
title={t('homeDashboard.pathsTitle')}
className="mb-0"
/>
{focusNoteTitle && (
<p className="text-[11px] text-concrete mt-1">
{t('homeDashboard.pathsFromNote', { title: focusNoteTitle })}
</p>
)}
</div>
<div className="flex flex-col items-center justify-center gap-3 px-5 py-10">
<Loader2 size={22} className="text-brand-accent animate-spin" strokeWidth={2} />
<p className="text-[11px] text-concrete text-center leading-relaxed max-w-[240px]">
{t('homeDashboard.pathsLoading')}
</p>
</div>
</div>
)
}
if (paths.length === 0) {
return (
<div className="rounded-2xl border border-border/25 bg-white/60 dark:bg-zinc-900/60 p-5">
<DashboardWidgetTitleRow
widgetId="next-paths"
title={t('homeDashboard.pathsTitle')}
className="mb-2"
/>
<p className="text-[10px] text-concrete italic">{t('homeDashboard.pathsEmpty')}</p>
</div>
)
}
const hero = paths[0]
const rest = paths.slice(1, 5)
const HeroIcon = TYPE_META[hero.type].Icon
return (
<div className="rounded-2xl border border-brand-accent/20 bg-gradient-to-br from-white via-white to-brand-accent/[0.04] dark:from-zinc-900 dark:via-zinc-900 dark:to-brand-accent/[0.06] shadow-sm overflow-hidden">
<div className="px-5 pt-4 pb-3 border-b border-border/15">
<DashboardWidgetTitleRow
widgetId="next-paths"
title={t('homeDashboard.pathsTitle')}
className="mb-0"
actions={enriching ? (
<span className="inline-flex items-center gap-1.5 text-[8px] font-mono uppercase text-concrete">
<Loader2 size={10} className="text-brand-accent animate-spin" />
{t('homeDashboard.pathsEnriching')}
</span>
) : undefined}
/>
{focusNoteTitle && (
<p className="text-[11px] text-concrete mt-1">
{t('homeDashboard.pathsFromNote', { title: focusNoteTitle })}
</p>
)}
</div>
<motion.button
type="button"
onClick={() => onAction(hero)}
whileHover={prefersReducedMotion ? undefined : { y: -1 }}
className="w-full text-start px-5 py-4 hover:bg-brand-accent/[0.03] transition-colors border-b border-border/10"
>
<div className="flex items-start gap-3">
<div className={`w-9 h-9 rounded-xl bg-brand-accent/10 flex items-center justify-center shrink-0 ${TYPE_META[hero.type].accent}`}>
<HeroIcon size={16} strokeWidth={1.75} />
</div>
<div className="min-w-0 flex-1">
<p className="text-[8px] font-mono font-bold uppercase tracking-wider text-concrete mb-0.5">
{t(`homeDashboard.pathTypes.${hero.type}`)}
{hero.score ? ` · ${hero.score}%` : ''}
</p>
<p className="text-sm font-serif font-semibold text-ink dark:text-dark-ink leading-snug">
{hero.title}
</p>
<p className="text-[11px] text-concrete leading-relaxed mt-1 line-clamp-2">
{hero.description}
</p>
</div>
<span className="shrink-0 inline-flex items-center gap-1 text-[9px] font-mono font-bold uppercase text-brand-accent mt-1">
{t(`homeDashboard.pathActions.${hero.actionKey}`)}
<ArrowRight size={10} />
</span>
</div>
</motion.button>
{rest.length > 0 && (
<div className="divide-y divide-border/10">
{rest.map(path => {
const meta = TYPE_META[path.type]
const Icon = meta.Icon
return (
<button
key={path.id}
type="button"
onClick={() => onAction(path)}
className="w-full flex items-center gap-3 px-5 py-3 text-start hover:bg-stone-50/80 dark:hover:bg-zinc-950/40 transition-colors"
>
<Icon size={13} className={`shrink-0 ${meta.accent}`} />
<div className="min-w-0 flex-1">
<p className="text-[11px] font-medium text-ink dark:text-dark-ink truncate">{path.title}</p>
<p className="text-[9px] text-concrete truncate">{path.description}</p>
</div>
<span className="text-[8px] font-mono uppercase text-brand-accent shrink-0">
{t(`homeDashboard.pathActions.${path.actionKey}`)}
</span>
</button>
)
})}
</div>
)}
</div>
)
}