Files
Momento/memento-note/components/notebooks-list.tsx
sepehr 153c921960
Some checks failed
Deploy to Production / Build and Deploy (push) Failing after 1m7s
fix: comprehensive i18n — replace hardcoded French/English strings with t() calls
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>
2026-04-26 21:14:45 +02:00

315 lines
13 KiB
TypeScript

'use client'
import { useState, useCallback } from 'react'
import Link from 'next/link'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import { cn } from '@/lib/utils'
import { StickyNote, Plus, Tag, Folder, ChevronDown, ChevronRight } from 'lucide-react'
import { useNotebooks } from '@/context/notebooks-context'
import { useNotebookDrag } from '@/context/notebook-drag-context'
import { Button } from '@/components/ui/button'
import { CreateNotebookDialog } from './create-notebook-dialog'
import { NotebookActions } from './notebook-actions'
import { DeleteNotebookDialog } from './delete-notebook-dialog'
import { EditNotebookDialog } from './edit-notebook-dialog'
import { NotebookSummaryDialog } from './notebook-summary-dialog'
import { useLanguage } from '@/lib/i18n'
import { useLabels } from '@/context/LabelContext'
import { LabelManagementDialog } from '@/components/label-management-dialog'
import { Notebook } from '@/lib/types'
import { getNotebookIcon } from '@/lib/notebook-icon'
export function NotebooksList() {
const pathname = usePathname()
const searchParams = useSearchParams()
const router = useRouter()
const { t, language } = useLanguage()
const { notebooks, currentNotebook, deleteNotebook, moveNoteToNotebookOptimistic, isLoading } = useNotebooks()
const { draggedNoteId, dragOverNotebookId, dragOver } = useNotebookDrag()
const { labels } = useLabels()
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
const [editingNotebook, setEditingNotebook] = useState<Notebook | null>(null)
const [deletingNotebook, setDeletingNotebook] = useState<Notebook | null>(null)
const [summaryNotebook, setSummaryNotebook] = useState<Notebook | null>(null)
const [expandedNotebook, setExpandedNotebook] = useState<string | null>(null)
const [labelsDialogOpen, setLabelsDialogOpen] = useState(false)
const currentNotebookId = searchParams.get('notebook')
// Handle drop on a notebook
const handleDrop = useCallback(async (e: React.DragEvent, notebookId: string | null) => {
e.preventDefault()
e.stopPropagation()
const noteId = e.dataTransfer.getData('text/plain')
if (noteId) {
await moveNoteToNotebookOptimistic(noteId, notebookId)
}
dragOver(null)
}, [moveNoteToNotebookOptimistic, dragOver])
// Handle drag over a notebook
const handleDragOver = useCallback((e: React.DragEvent, notebookId: string | null) => {
e.preventDefault()
dragOver(notebookId)
}, [dragOver])
// Handle drag leave
const handleDragLeave = useCallback(() => {
dragOver(null)
}, [dragOver])
const handleSelectNotebook = (notebookId: string | null) => {
const params = new URLSearchParams(searchParams)
if (notebookId) {
params.set('notebook', notebookId)
} else {
params.delete('notebook')
}
// Clear other filters
params.delete('labels')
params.delete('search')
router.push(`/?${params.toString()}`)
}
const handleToggleExpand = (notebookId: string) => {
setExpandedNotebook((prev) => (prev === notebookId ? null : notebookId))
}
const handleLabelFilter = (labelName: string, notebookId: string) => {
const params = new URLSearchParams(searchParams)
const currentLabels = params.get('labels')?.split(',').filter(Boolean) || []
if (currentLabels.includes(labelName)) {
params.set('labels', currentLabels.filter((l: string) => l !== labelName).join(','))
} else {
params.set('labels', [...currentLabels, labelName].join(','))
}
params.set('notebook', notebookId)
router.push(`/?${params.toString()}`)
}
if (isLoading) {
return (
<div className="my-2">
<div className="px-4 py-2">
<div className="text-xs text-gray-500">{t('common.loading')}</div>
</div>
</div>
)
}
return (
<>
<LabelManagementDialog open={labelsDialogOpen} onOpenChange={setLabelsDialogOpen} />
<div className="flex flex-col pt-1">
{/* Header with Add Button */}
<div className="flex items-center justify-between px-6 py-2 mt-2 group cursor-pointer text-gray-500 hover:text-gray-800 dark:hover:text-gray-300">
<span className="text-xs font-semibold uppercase tracking-wider">{t('nav.notebooks') || 'NOTEBOOKS'}</span>
<button
onClick={() => setIsCreateDialogOpen(true)}
className="p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-full transition-colors"
title={t('notebooks.create') || 'Create notebook'}
>
<Plus className="w-4 h-4" />
</button>
</div>
{/* Notebooks Loop */}
{notebooks.map((notebook: Notebook) => {
const isActive = currentNotebookId === notebook.id
const isExpanded = expandedNotebook === notebook.id
const isDragOver = dragOverNotebookId === notebook.id
// Get icon component
const NotebookIcon = getNotebookIcon(notebook.icon || 'folder')
return (
<div key={notebook.id} className="group flex flex-col">
{isActive ? (
// Active notebook with expanded labels
<div
onDrop={(e) => handleDrop(e, notebook.id)}
onDragOver={(e) => handleDragOver(e, notebook.id)}
onDragLeave={handleDragLeave}
className={cn(
"flex flex-col me-2 rounded-e-full transition-all relative",
!notebook.color && "bg-primary/10 dark:bg-primary/20",
isDragOver && "ring-2 ring-primary ring-dashed"
)}
style={notebook.color ? { backgroundColor: `${notebook.color}20` } : undefined}
>
{/* Header */}
<div className="pointer-events-auto flex items-center justify-between px-6 py-3">
<div className="flex items-center gap-4 min-w-0 flex-1">
<NotebookIcon
className={cn("w-5 h-5 flex-shrink-0 fill-current", !notebook.color && "text-primary dark:text-primary-foreground")}
style={notebook.color ? { color: notebook.color } : undefined}
/>
<span
className={cn("text-[15px] font-medium tracking-wide truncate min-w-0", !notebook.color && "text-primary dark:text-primary-foreground")}
style={notebook.color ? { color: notebook.color } : undefined}
>
{notebook.name}
</span>
</div>
<div className="flex items-center gap-1 flex-shrink-0">
{/* Actions menu for active notebook */}
<NotebookActions
notebook={notebook}
onEdit={() => setEditingNotebook(notebook)}
onDelete={() => setDeletingNotebook(notebook)}
onSummary={() => setSummaryNotebook(notebook)}
/>
<button
type="button"
onClick={(e) => {
e.stopPropagation()
handleToggleExpand(notebook.id)
}}
className={cn(
"shrink-0 rounded-full p-1 transition-colors",
!notebook.color &&
"text-primary hover:text-primary/80 dark:text-primary-foreground dark:hover:text-primary-foreground/80"
)}
style={notebook.color ? { color: notebook.color } : undefined}
aria-expanded={isExpanded}
>
<ChevronDown className={cn("h-4 w-4 transition-transform", isExpanded && "rotate-180")} />
</button>
</div>
</div>
{/* Contextual Labels Tree */}
{isExpanded && (
<div className="flex flex-col pb-2">
{labels.length === 0 ? (
<p className="pointer-events-none ps-12 pe-4 py-2 text-xs text-muted-foreground">
{t('sidebar.noLabelsInNotebook')}
</p>
) : (
labels.map((label: any) => (
<button
key={label.id}
type="button"
onClick={() => handleLabelFilter(label.name, notebook.id)}
className={cn(
'pointer-events-auto flex items-center gap-4 ps-12 pe-4 py-2 rounded-e-full me-2 transition-colors',
'hover:bg-accent/60 text-muted-foreground hover:text-foreground',
searchParams.get('labels')?.includes(label.name) &&
'font-semibold text-foreground'
)}
>
<Tag className="h-4 w-4 shrink-0" />
<span className="text-xs font-medium truncate">{label.name}</span>
</button>
))
)}
<button
type="button"
onClick={() => setLabelsDialogOpen(true)}
className="pointer-events-auto flex items-center gap-2 ps-12 pe-4 py-2 mt-1 rounded-e-full me-2 transition-colors text-muted-foreground hover:text-foreground hover:bg-accent/60 group/label"
>
<Plus className="h-3 w-3 shrink-0 group-hover/label:scale-110 transition-transform" />
<span className="text-xs font-medium">{t('sidebar.editLabels')}</span>
</button>
</div>
)}
</div>
) : (
// Inactive notebook
<div
onDrop={(e) => handleDrop(e, notebook.id)}
onDragOver={(e) => handleDragOver(e, notebook.id)}
onDragLeave={handleDragLeave}
className={cn(
"flex items-center relative",
isDragOver && "ring-2 ring-blue-500 ring-dashed rounded-e-full me-2"
)}
>
<button
onClick={() => handleSelectNotebook(notebook.id)}
className={cn(
"pointer-events-auto flex items-center gap-4 px-6 py-3 rounded-e-full me-2 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800/50 transition-colors w-full pe-24",
isDragOver && "opacity-50"
)}
>
<NotebookIcon className="w-5 h-5 flex-shrink-0" />
<span className="text-[15px] font-medium tracking-wide truncate min-w-0 text-start">{notebook.name}</span>
{(notebook as any).notesCount > 0 && (
<span className="text-xs text-gray-400 ms-2 flex-shrink-0">({new Intl.NumberFormat(language).format((notebook as any).notesCount)})</span>
)}
</button>
{/* Actions + expand on the right — always rendered, visible on hover */}
<div className="absolute end-3 top-1/2 -translate-y-1/2 flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity z-10">
<NotebookActions
notebook={notebook}
onEdit={() => setEditingNotebook(notebook)}
onDelete={() => setDeletingNotebook(notebook)}
onSummary={() => setSummaryNotebook(notebook)}
/>
<button
onClick={(e) => { e.stopPropagation(); handleToggleExpand(notebook.id); }}
className={cn(
"p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 transition-colors rounded-full hover:bg-gray-200 dark:hover:bg-gray-700",
expandedNotebook === notebook.id && "rotate-180"
)}
>
<ChevronDown className="w-4 h-4" />
</button>
</div>
</div>
)}
</div>
)
})}
</div>
{/* Create Notebook Dialog */}
<CreateNotebookDialog
open={isCreateDialogOpen}
onOpenChange={setIsCreateDialogOpen}
/>
{/* Edit Notebook Dialog */}
{editingNotebook && (
<EditNotebookDialog
notebook={editingNotebook}
open={!!editingNotebook}
onOpenChange={(open) => {
if (!open) setEditingNotebook(null)
}}
/>
)}
{/* Delete Confirmation Dialog */}
{deletingNotebook && (
<DeleteNotebookDialog
notebook={deletingNotebook}
open={!!deletingNotebook}
onOpenChange={(open) => {
if (!open) setDeletingNotebook(null)
}}
/>
)}
{/* Notebook Summary Dialog */}
<NotebookSummaryDialog
open={!!summaryNotebook}
onOpenChange={(open) => {
if (!open) setSummaryNotebook(null)
}}
notebookId={summaryNotebook?.id ?? null}
notebookName={summaryNotebook?.name}
/>
</>
)
}