feat(dashboard): tableau de bord Second Brain configurable avec chargement progressif
All checks were successful
CI / Lint, Unit Tests & Build (push) Successful in 7m3s
CI / Deploy production (on server) (push) Successful in 23s

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:
Antigravity
2026-07-14 16:50:53 +00:00
parent d38a99586b
commit 30da592ba2
62 changed files with 7741 additions and 335 deletions

View File

@@ -35,12 +35,13 @@ import {
FileText,
Folder,
FolderOpen,
LayoutGrid,
} from 'lucide-react'
import { useSearchModal } from '@/context/search-modal-context'
import { useLanguage } from '@/lib/i18n'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { applyDocumentTheme } from '@/lib/apply-document-theme'
import { getAllNotes, getTrashCount, getNotesWithReminders, toggleReminderDone } from '@/app/actions/notes'
import { getAllNotes, getTrashCount, getNotesWithReminders, toggleReminderDone, getInboxCount } from '@/app/actions/notes'
import { NOTE_CHANGE_EVENT, type NoteChangeEvent } from '@/lib/note-change-sync'
import { useNotebooks } from '@/context/notebooks-context'
import { Notebook, Note } from '@/lib/types'
@@ -62,9 +63,10 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { performSignOut } from '@/lib/auth-client'
import { isDashboardHomeRoute } from '@/lib/dashboard/home-route'
import { useBrainstormSessions, useDeleteBrainstorm } from '@/hooks/use-brainstorm'
type NavigationView = 'notebooks' | 'agents' | 'reminders' | 'brainstorms' | 'revision' | 'insights'
type NavigationView = 'dashboard' | 'notebooks' | 'agents' | 'reminders' | 'brainstorms' | 'revision' | 'insights'
type SortOrder = 'newest' | 'oldest' | 'alpha' | 'manual'
const NOTEBOOKS_PANEL_HEIGHT_KEY = 'memento-sidebar-notebooks-height'
@@ -639,6 +641,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
const [showSortMenu, setShowSortMenu] = useState(false)
const [notebookSearchQuery, setNotebookSearchQuery] = useState('')
const [trashCount, setTrashCount] = useState(0)
const [inboxCount, setInboxCount] = useState(0)
const notebooksContainerRef = useRef<HTMLDivElement>(null)
const notebooksPanelRef = useRef<HTMLDivElement>(null)
@@ -804,8 +807,11 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
})
}, [currentNotebookId, notebooks])
const isDashboardRoute = isDashboardHomeRoute(pathname, searchParams)
const isInboxActive =
pathname === '/home' &&
searchParams.get('forceList') === '1' &&
!searchParams.get('notebook') &&
!searchParams.get('labels') &&
!searchParams.get('archived') &&
@@ -817,8 +823,9 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
else if (pathname === '/insights') setActiveView('insights')
else if (pathname.startsWith('/revision')) setActiveView('revision')
else if (searchParams.get('reminders') === '1' && pathname === '/home') setActiveView('reminders')
else if (isDashboardRoute) setActiveView('dashboard')
else if (pathname === '/home' || pathname.startsWith('/notes')) setActiveView('notebooks')
}, [pathname, searchParams])
}, [pathname, searchParams, isDashboardRoute])
const isRemindersRoute = pathname === '/home' && searchParams.get('reminders') === '1'
const isSharedRoute = pathname === '/home' && searchParams.get('shared') === '1'
@@ -848,7 +855,8 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
(pathname === '/home' || pathname.startsWith('/notes')) &&
!pathname.startsWith('/settings') &&
!isRemindersRoute &&
!isSharedRoute
!isSharedRoute &&
!isDashboardRoute
const displayName = user?.name || user?.email || ''
const initial = displayName ? displayName.charAt(0).toUpperCase() : '?'
@@ -872,6 +880,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
useEffect(() => {
let cancelled = false
getTrashCount().then(count => { if (!cancelled) setTrashCount(count) })
getInboxCount().then((count: number) => { if (!cancelled) setInboxCount(count) })
return () => { cancelled = true }
}, [])
@@ -902,6 +911,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
useEffect(() => {
const onNoteChange = (e: Event) => {
const detail = (e as CustomEvent<NoteChangeEvent>).detail
getInboxCount().then((count: number) => setInboxCount(count))
if (detail.type === 'deleted') {
setNotebookNotes((prev) => {
const next = { ...prev }
@@ -1340,6 +1350,16 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
{/* Boutons de navigation principaux */}
<div className="flex flex-col gap-2 w-full px-1.5">
{([
{
id: 'dashboard',
icon: LayoutGrid,
label: t('nav.dashboard') || 'Dashboard',
onClick: () => {
setActiveView('dashboard')
router.push('/home')
},
isActive: isDashboardRoute,
},
{
id: 'notebooks',
icon: BookOpen,
@@ -1517,7 +1537,78 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
>
<AnimatePresence mode="wait">
{activeView === 'notebooks' ? (
{activeView === 'dashboard' ? (
<motion.div
key="dashboard"
initial={{ opacity: 0, x: isRtl ? 10 : -10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: isRtl ? -10 : 10 }}
transition={{ duration: 0.2 }}
className="px-4 pt-4 space-y-4"
>
<div className="flex items-center gap-1.5">
<LayoutGrid size={14} className="text-brand-accent" />
<h3 className="text-xs font-black tracking-widest uppercase text-ink dark:text-dark-ink">
{t('nav.dashboard')}
</h3>
</div>
<p className="text-[12px] leading-relaxed text-muted-foreground">
{t('sidebar.dashboardPanelBody')}
</p>
<div className="space-y-1.5">
<button
type="button"
onClick={handleInboxClick}
className="w-full flex items-center justify-between gap-2 px-3 py-2.5 rounded-xl border border-border/40 bg-white/60 dark:bg-zinc-800/40 hover:border-brand-accent/30 hover:bg-brand-accent/5 transition-all text-[12px] font-medium text-foreground"
>
<span className="flex items-center gap-2 min-w-0">
<Inbox size={14} className="text-brand-accent shrink-0" />
{t('homeDashboard.inbox')}
</span>
{inboxCount > 0 && (
<span className="text-[10px] font-mono font-bold text-brand-accent bg-brand-accent/10 px-1.5 py-0.5 rounded shrink-0">
{inboxCount}
</span>
)}
</button>
<button
type="button"
onClick={() => router.push('/revision')}
className="w-full flex items-center gap-2 px-3 py-2.5 rounded-xl border border-border/40 bg-white/60 dark:bg-zinc-800/40 hover:border-brand-accent/30 hover:bg-brand-accent/5 transition-all text-[12px] font-medium text-foreground"
>
<GraduationCap size={14} className="text-brand-accent shrink-0" />
{t('homeDashboard.review')}
</button>
<button
type="button"
onClick={handleRemindersClick}
className="w-full flex items-center gap-2 px-3 py-2.5 rounded-xl border border-border/40 bg-white/60 dark:bg-zinc-800/40 hover:border-brand-accent/30 hover:bg-brand-accent/5 transition-all text-[12px] font-medium text-foreground"
>
<Bell size={14} className="text-brand-accent shrink-0" />
{t('homeDashboard.reminders')}
</button>
<button
type="button"
onClick={() => router.push('/insights')}
className="w-full flex items-center gap-2 px-3 py-2.5 rounded-xl border border-border/40 bg-white/60 dark:bg-zinc-800/40 hover:border-brand-accent/30 hover:bg-brand-accent/5 transition-all text-[12px] font-medium text-foreground"
>
<Sparkles size={14} className="text-brand-accent shrink-0" />
{t('homeDashboard.themes')}
</button>
<button
type="button"
onClick={() => {
setActiveView('notebooks')
router.push('/home?forceList=1')
}}
className="w-full flex items-center gap-2 px-3 py-2.5 rounded-xl border border-border/40 bg-white/60 dark:bg-zinc-800/40 hover:border-brand-accent/30 hover:bg-brand-accent/5 transition-all text-[12px] font-medium text-foreground"
>
<BookOpen size={14} className="text-brand-accent shrink-0" />
{t('sidebar.backToNotebooks')}
</button>
</div>
</motion.div>
) : activeView === 'notebooks' ? (
<motion.div
key="notebooks"
ref={notebooksContainerRef}
@@ -1641,12 +1732,22 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
</div>
<span
className={cn(
'text-[13px] font-medium truncate',
'text-[13px] font-medium truncate flex-1',
isInboxActive ? 'text-ink' : 'text-muted-ink',
)}
>
{t('sidebar.inbox')}
</span>
{inboxCount > 0 && (
<span className={cn(
'text-[10px] font-bold min-w-[18px] h-[18px] px-1 flex items-center justify-center rounded-full transition-colors',
isInboxActive
? 'bg-brand-accent text-white'
: 'bg-brand-accent/10 text-brand-accent',
)}>
{inboxCount > 99 ? '99+' : inboxCount}
</span>
)}
</button>
</div>
</div>