Some checks failed
Deploy to Production / Build and Deploy (push) Failing after 1m7s
Replaced ~100+ hardcoded French and English text strings across 30+ components with proper i18n t() calls. Added 57 new translation keys to all 15 locale files (ar, de, en, es, fa, fr, hi, it, ja, ko, nl, pl, pt, ru, zh). Key changes: - contextual-ai-chat.tsx: 30 French strings → t() (actions, toasts, labels, placeholders) - ai-chat.tsx: 15 French/English strings → t() (header, tabs, welcome, insights, history) - note-inline-editor.tsx: 20 French fallbacks removed (toolbar, save status, checklist) - lab-skeleton.tsx: French loading text → t() - admin-header.tsx, header.tsx, editor-connections-section.tsx: French fallbacks removed - New AI chat component, agent cards, sidebar, settings panel i18n cleanup Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
209 lines
6.7 KiB
TypeScript
209 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 {
|
|
Lightbulb,
|
|
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')
|
|
}
|
|
|
|
// 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={Lightbulb}
|
|
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>
|
|
)
|
|
}
|