Files
Momento/memento-note/components/sidebar.tsx
sepehr 69ea064ca8
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 1m16s
feat: smart note history with manual/auto modes, delete entries, i18n fixes
- Add noteHistoryMode setting (manual default / auto) with DB migration
- Manual mode: commit button in editor toolbar creates snapshots on demand
- Auto mode: smart snapshots with 20-char diff threshold + 5min cooldown,
  structural changes (color, pin, archive, labels) bypass cooldown
- Add delete individual history entries from history modal
- Fix sidebar: Notes nav no longer active on notebook pages
- Fix sidebar icon: replace filled Lightbulb with outlined FileText
- Fix title suggestions: change from amber to sky blue color scheme
- Fix hydration mismatch: add suppressHydrationWarning on locale dates
- Complete i18n: add history, sort, and AI chat translations for all 16 languages
- Translate French AI assistant section (40+ keys) from English to French
- Update README with new features and stack info

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 21:05:55 +02:00

210 lines
6.7 KiB
TypeScript

'use client'
import Link from 'next/link'
import { usePathname, useSearchParams, useRouter } from 'next/navigation'
import { cn } from '@/lib/utils'
import {
FileText,
Bell,
Archive,
Trash2,
Plus,
Sparkles,
X,
Tag,
} from 'lucide-react'
import { Button } from '@/components/ui/button'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip'
import { useLanguage } from '@/lib/i18n'
import { NotebooksList } from './notebooks-list'
import { useHomeViewOptional } from '@/context/home-view-context'
import { useEffect, useState } from 'react'
import { getTrashCount } from '@/app/actions/notes'
const HIDDEN_ROUTES = ['/agents', '/chat', '/lab', '/admin']
export function Sidebar({ className, user }: { className?: string, user?: any }) {
const pathname = usePathname()
const searchParams = useSearchParams()
const router = useRouter()
const { t } = useLanguage()
const homeBridge = useHomeViewOptional()
const [trashCount, setTrashCount] = useState(0)
const searchKey = searchParams.toString()
// Fetch trash count — skip for hidden/admin routes to avoid dispatching a
// Server Action during an ongoing App Router navigation transition, which
// would increment React's nested-update counter and trigger Error #310.
useEffect(() => {
if (HIDDEN_ROUTES.some(r => pathname.startsWith(r))) return
getTrashCount().then(setTrashCount)
}, [pathname, searchKey])
// Hide sidebar on Agents, Chat IA and Lab routes
if (HIDDEN_ROUTES.some(r => pathname.startsWith(r))) return null
// Active label filter
const activeLabel = searchParams.get('label')
const activeLabels = searchParams.get('labels')?.split(',').filter(Boolean) || []
const clearLabelFilter = () => {
const params = new URLSearchParams(searchParams)
params.delete('label')
router.push(`/?${params.toString()}`)
}
const clearLabelsFilter = (labelToRemove?: string) => {
const params = new URLSearchParams(searchParams)
if (labelToRemove) {
const remaining = activeLabels.filter(l => l !== labelToRemove)
if (remaining.length > 0) {
params.set('labels', remaining.join(','))
} else {
params.delete('labels')
}
} else {
params.delete('labels')
}
router.push(`/?${params.toString()}`)
}
// Helper to determine if a link is active
const isActive = (href: string, exact = false) => {
if (href === '/') {
// Home is active only if no special filters are applied
return pathname === '/' &&
!searchParams.get('label') &&
!searchParams.get('archived') &&
!searchParams.get('trashed') &&
!searchParams.get('notebook')
}
// For labels
if (href.startsWith('/?label=')) {
const labelParam = searchParams.get('label')
// Extract label from href
const labelFromHref = href.split('=')[1]
return labelParam === labelFromHref
}
// For other routes
return pathname === href
}
const NavItem = ({ href, icon: Icon, label, active, badge }: any) => (
<Link
href={href}
className={cn(
"flex items-center gap-4 px-6 py-3 rounded-e-full me-2 transition-colors",
"text-[15px] font-medium tracking-wide",
active
? "bg-primary/10 text-primary dark:bg-primary/20 dark:text-primary-foreground"
: "text-muted-foreground hover:bg-muted/50 dark:hover:bg-muted/30"
)}
>
<Icon className={cn("w-5 h-5", active ? "fill-current" : "")} />
<span className="truncate">{label}</span>
{badge > 0 && (
<span className={cn(
"ms-auto text-[10px] font-semibold px-1.5 py-0.5 rounded-full min-w-[20px] text-center",
active
? "bg-primary/20 text-primary"
: "bg-muted text-muted-foreground"
)}>
{badge}
</span>
)}
</Link>
)
return (
<aside className={cn(
"w-[280px] flex-none flex-col bg-white dark:bg-[#1e2128] overflow-y-auto hidden md:flex py-2",
className
)}>
{/* Main Navigation */}
<div className="flex flex-col gap-1 px-3">
<NavItem
href="/"
icon={FileText}
label={t('sidebar.notes') || 'Notes'}
active={isActive('/')}
/>
</div>
{/* Notebooks Section */}
<div className="flex flex-col mt-2">
<NotebooksList />
</div>
{/* Active Label Filter Chips */}
{pathname === '/' && (activeLabel || activeLabels.length > 0) && (
<div className="px-4 pt-2 flex flex-col gap-1">
{activeLabel && (
<div className="flex items-center gap-2 ps-2 pe-1 py-1.5 rounded-e-full me-2 bg-primary/10 dark:bg-primary/20 text-primary dark:text-primary-foreground">
<Tag className="w-3.5 h-3.5 shrink-0" />
<span className="text-xs font-medium truncate flex-1">{activeLabel}</span>
<button
type="button"
onClick={clearLabelFilter}
className="shrink-0 p-0.5 rounded-full hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors"
title={t('sidebar.clearFilter') || 'Remove filter'}
>
<X className="w-3 h-3" />
</button>
</div>
)}
{activeLabels.map((label) => (
<div
key={label}
className="flex items-center gap-2 ps-2 pe-1 py-1.5 rounded-e-full me-2 bg-primary/10 dark:bg-primary/20 text-primary dark:text-primary-foreground"
>
<Tag className="w-3.5 h-3.5 shrink-0" />
<span className="text-xs font-medium truncate flex-1">{label}</span>
<button
type="button"
onClick={() => clearLabelsFilter(label)}
className="shrink-0 p-0.5 rounded-full hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors"
title={t('sidebar.clearFilter') || 'Remove filter'}
>
<X className="w-3 h-3" />
</button>
</div>
))}
</div>
)}
{/* Archive & Trash */}
<div className="flex flex-col mt-auto pb-4 border-t border-transparent">
<NavItem
href="/reminders"
icon={Bell}
label={t('sidebar.reminders') || 'Rappels'}
active={isActive('/reminders')}
/>
<NavItem
href="/archive"
icon={Archive}
label={t('sidebar.archive') || 'Archives'}
active={pathname === '/archive'}
/>
<NavItem
href="/trash"
icon={Trash2}
label={t('sidebar.trash') || 'Corbeille'}
active={pathname === '/trash'}
badge={trashCount}
/>
</div>
</aside>
)
}