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>
This commit is contained in:
123
memento-note/components/dashboard-resume-hero.tsx
Normal file
123
memento-note/components/dashboard-resume-hero.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from 'motion/react'
|
||||
import { Clock, ChevronRight, Play } from 'lucide-react'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
import { DashboardWidgetTitleRow } from '@/components/dashboard-widget-title-row'
|
||||
|
||||
export interface ResumeNote {
|
||||
id: string
|
||||
title: string | null
|
||||
excerpt: string
|
||||
notebookName: string
|
||||
notebookColor: string
|
||||
updatedAt: string
|
||||
notebookId: string | null
|
||||
}
|
||||
|
||||
export interface DashboardResumeHeroProps {
|
||||
notes: ResumeNote[]
|
||||
loading?: boolean
|
||||
onSelect: (id: string, notebookId: string | null) => void
|
||||
formatRelativeTime: (date: string) => string
|
||||
prefersReducedMotion?: boolean
|
||||
}
|
||||
|
||||
export function DashboardResumeHero({
|
||||
notes,
|
||||
loading,
|
||||
onSelect,
|
||||
formatRelativeTime,
|
||||
prefersReducedMotion,
|
||||
}: DashboardResumeHeroProps) {
|
||||
const { t } = useLanguage()
|
||||
const hero = notes[0]
|
||||
const rest = notes.slice(1, 4)
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="rounded-2xl border border-border/30 bg-white dark:bg-zinc-900 p-5 min-h-[200px] animate-pulse">
|
||||
<div className="h-4 w-32 bg-stone-100 dark:bg-zinc-800 rounded mb-4" />
|
||||
<div className="h-6 w-3/4 bg-stone-100 dark:bg-zinc-800 rounded mb-3" />
|
||||
<div className="h-12 bg-stone-50 dark:bg-zinc-950 rounded-xl" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!hero) {
|
||||
return (
|
||||
<div className="rounded-2xl border border-dashed border-border/40 bg-white/50 dark:bg-zinc-900/50 p-8 text-center">
|
||||
<Clock size={24} className="mx-auto text-concrete/30 mb-3" strokeWidth={1.25} />
|
||||
<p className="text-xs text-concrete italic">{t('homeDashboard.noRecentNotes')}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-2xl border border-border/30 bg-white dark:bg-zinc-900 overflow-hidden">
|
||||
<div className="px-5 pt-4">
|
||||
<DashboardWidgetTitleRow
|
||||
widgetId="resume"
|
||||
icon={<Play size={11} className="text-brand-accent" fill="currentColor" />}
|
||||
title={t('homeDashboard.continue')}
|
||||
className="mb-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<motion.button
|
||||
type="button"
|
||||
whileHover={prefersReducedMotion ? undefined : { y: -1 }}
|
||||
onClick={() => onSelect(hero.id, hero.notebookId)}
|
||||
className="w-full text-start px-5 pb-5 group cursor-pointer"
|
||||
>
|
||||
<div
|
||||
className="p-4 rounded-xl border border-border/25 bg-gradient-to-br from-stone-50/80 to-white dark:from-zinc-950/50 dark:to-zinc-900/80 group-hover:border-brand-accent/35 group-hover:shadow-md transition-all"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3 mb-2">
|
||||
<span
|
||||
className="text-[8px] font-mono font-bold uppercase px-2 py-0.5 rounded text-white shrink-0"
|
||||
style={{ backgroundColor: hero.notebookColor }}
|
||||
>
|
||||
{hero.notebookName}
|
||||
</span>
|
||||
<span className="text-[9px] font-mono text-concrete/70 shrink-0">
|
||||
{formatRelativeTime(hero.updatedAt)}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="text-base sm:text-lg font-serif font-semibold text-ink dark:text-dark-ink group-hover:text-brand-accent transition-colors leading-snug mb-2 line-clamp-2">
|
||||
{hero.title || t('homeDashboard.untitled')}
|
||||
</h3>
|
||||
{hero.excerpt && (
|
||||
<p className="text-[11px] text-concrete leading-relaxed line-clamp-3">
|
||||
{hero.excerpt}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-center gap-1 mt-3 text-[9px] font-mono font-bold uppercase text-brand-accent opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
{t('homeDashboard.resumeOpen')}
|
||||
<ChevronRight size={12} />
|
||||
</div>
|
||||
</div>
|
||||
</motion.button>
|
||||
|
||||
{rest.length > 0 && (
|
||||
<div className="flex gap-2 px-5 pb-4 overflow-x-auto custom-scrollbar">
|
||||
{rest.map(note => (
|
||||
<button
|
||||
key={note.id}
|
||||
type="button"
|
||||
onClick={() => onSelect(note.id, note.notebookId)}
|
||||
className="shrink-0 max-w-[200px] p-2.5 rounded-xl border border-border/20 bg-stone-50/50 dark:bg-zinc-950/30 hover:border-brand-accent/30 transition-all text-start group"
|
||||
>
|
||||
<p className="text-[10px] font-semibold text-ink dark:text-dark-ink truncate group-hover:text-brand-accent transition-colors">
|
||||
{note.title || t('homeDashboard.untitled')}
|
||||
</p>
|
||||
<p className="text-[8px] font-mono text-concrete/60 mt-0.5">
|
||||
{formatRelativeTime(note.updatedAt)}
|
||||
</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user