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>
315 lines
13 KiB
TypeScript
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}
|
|
/>
|
|
</>
|
|
)
|
|
}
|