feat: Complete internationalization and code cleanup
## Translation Files - Add 11 new language files (es, de, pt, ru, zh, ja, ko, ar, hi, nl, pl) - Add 100+ missing translation keys across all 15 languages - New sections: notebook, pagination, ai.batchOrganization, ai.autoLabels - Update nav section with workspace, quickAccess, myLibrary keys ## Component Updates - Update 15+ components to use translation keys instead of hardcoded text - Components: notebook dialogs, sidebar, header, note-input, ghost-tags, etc. - Replace 80+ hardcoded English/French strings with t() calls - Ensure consistent UI across all supported languages ## Code Quality - Remove 77+ console.log statements from codebase - Clean up API routes, components, hooks, and services - Keep only essential error handling (no debugging logs) ## UI/UX Improvements - Update Keep logo to yellow post-it style (from-yellow-400 to-amber-500) - Change selection colors to #FEF3C6 (notebooks) and #EFB162 (nav items) - Make "+" button permanently visible in notebooks section - Fix grammar and syntax errors in multiple components ## Bug Fixes - Fix JSON syntax errors in it.json, nl.json, pl.json, zh.json - Fix syntax errors in notebook-suggestion-toast.tsx - Fix syntax errors in use-auto-tagging.ts - Fix syntax errors in paragraph-refactor.service.ts - Fix duplicate "fusion" section in nl.json 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> Ou une version plus courte si vous préférez : feat(i18n): Add 15 languages, remove logs, update UI components - Create 11 new translation files (es, de, pt, ru, zh, ja, ko, ar, hi, nl, pl) - Add 100+ translation keys: notebook, pagination, AI features - Update 15+ components to use translations (80+ strings) - Remove 77+ console.log statements from codebase - Fix JSON syntax errors in 4 translation files - Fix component syntax errors (toast, hooks, services) - Update logo to yellow post-it style - Change selection colors (#FEF3C6, #EFB162) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,14 +3,21 @@
|
||||
import { Note, NOTE_COLORS, NoteColor } from '@/lib/types'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Pin, Bell, GripVertical, X } from 'lucide-react'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { Pin, Bell, GripVertical, X, Link2, FolderOpen, StickyNote } from 'lucide-react'
|
||||
import { useState, useEffect, useTransition, useOptimistic } from 'react'
|
||||
import { useSession } from 'next-auth/react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { deleteNote, toggleArchive, togglePin, updateColor, updateNote, getNoteAllUsers, leaveSharedNote } from '@/app/actions/notes'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { deleteNote, toggleArchive, togglePin, updateColor, updateNote, updateSize, getNoteAllUsers, leaveSharedNote, removeFusedBadge } from '@/app/actions/notes'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { formatDistanceToNow } from 'date-fns'
|
||||
import { fr } from 'date-fns/locale'
|
||||
import { formatDistanceToNow, Locale } from 'date-fns'
|
||||
import * as dateFnsLocales from 'date-fns/locale'
|
||||
import { MarkdownContent } from './markdown-content'
|
||||
import { LabelBadge } from './label-badge'
|
||||
import { NoteImages } from './note-images'
|
||||
@@ -18,26 +25,106 @@ import { NoteChecklist } from './note-checklist'
|
||||
import { NoteActions } from './note-actions'
|
||||
import { CollaboratorDialog } from './collaborator-dialog'
|
||||
import { CollaboratorAvatars } from './collaborator-avatars'
|
||||
import { ConnectionsBadge } from './connections-badge'
|
||||
import { ConnectionsOverlay } from './connections-overlay'
|
||||
import { ComparisonModal } from './comparison-modal'
|
||||
import { useConnectionsCompare } from '@/hooks/use-connections-compare'
|
||||
import { useLabels } from '@/context/LabelContext'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
import { useNotebooks } from '@/context/notebooks-context'
|
||||
|
||||
// Mapping of supported languages to date-fns locales
|
||||
const localeMap: Record<string, Locale> = {
|
||||
en: dateFnsLocales.enUS,
|
||||
fr: dateFnsLocales.fr,
|
||||
es: dateFnsLocales.es,
|
||||
de: dateFnsLocales.de,
|
||||
fa: dateFnsLocales.faIR,
|
||||
it: dateFnsLocales.it,
|
||||
pt: dateFnsLocales.pt,
|
||||
ru: dateFnsLocales.ru,
|
||||
zh: dateFnsLocales.zhCN,
|
||||
ja: dateFnsLocales.ja,
|
||||
ko: dateFnsLocales.ko,
|
||||
ar: dateFnsLocales.ar,
|
||||
hi: dateFnsLocales.hi,
|
||||
nl: dateFnsLocales.nl,
|
||||
pl: dateFnsLocales.pl,
|
||||
}
|
||||
|
||||
function getDateLocale(language: string): Locale {
|
||||
return localeMap[language] || dateFnsLocales.enUS
|
||||
}
|
||||
|
||||
interface NoteCardProps {
|
||||
note: Note
|
||||
onEdit?: (note: Note, readOnly?: boolean) => void
|
||||
isDragging?: boolean
|
||||
isDragOver?: boolean
|
||||
onDragStart?: (noteId: string) => void
|
||||
onDragEnd?: () => void
|
||||
}
|
||||
|
||||
export function NoteCard({ note, onEdit, isDragging, isDragOver }: NoteCardProps) {
|
||||
// Helper function to get initials from name
|
||||
function getInitials(name: string): string {
|
||||
if (!name) return '??'
|
||||
const trimmedName = name.trim()
|
||||
const parts = trimmedName.split(' ')
|
||||
if (parts.length === 1) {
|
||||
return trimmedName.substring(0, 2).toUpperCase()
|
||||
}
|
||||
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase()
|
||||
}
|
||||
|
||||
// Helper function to get avatar color based on name hash
|
||||
function getAvatarColor(name: string): string {
|
||||
const colors = [
|
||||
'bg-blue-500',
|
||||
'bg-purple-500',
|
||||
'bg-green-500',
|
||||
'bg-orange-500',
|
||||
'bg-pink-500',
|
||||
'bg-teal-500',
|
||||
'bg-red-500',
|
||||
'bg-indigo-500',
|
||||
]
|
||||
|
||||
const hash = name.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)
|
||||
return colors[hash % colors.length]
|
||||
}
|
||||
|
||||
export function NoteCard({ note, onEdit, isDragging, isDragOver, onDragStart, onDragEnd }: NoteCardProps) {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const { refreshLabels } = useLabels()
|
||||
const { data: session } = useSession()
|
||||
const { t, language } = useLanguage()
|
||||
const { notebooks, moveNoteToNotebookOptimistic } = useNotebooks()
|
||||
const [isPending, startTransition] = useTransition()
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
const [showCollaboratorDialog, setShowCollaboratorDialog] = useState(false)
|
||||
const [collaborators, setCollaborators] = useState<any[]>([])
|
||||
const [owner, setOwner] = useState<any>(null)
|
||||
const [showConnectionsOverlay, setShowConnectionsOverlay] = useState(false)
|
||||
const [comparisonNotes, setComparisonNotes] = useState<string[] | null>(null)
|
||||
const [showNotebookMenu, setShowNotebookMenu] = useState(false)
|
||||
|
||||
// Move note to a notebook
|
||||
const handleMoveToNotebook = async (notebookId: string | null) => {
|
||||
await moveNoteToNotebookOptimistic(note.id, notebookId)
|
||||
setShowNotebookMenu(false)
|
||||
router.refresh()
|
||||
}
|
||||
const colorClasses = NOTE_COLORS[note.color as NoteColor] || NOTE_COLORS.default
|
||||
|
||||
// Check if this note is currently open in the editor
|
||||
const isNoteOpenInEditor = searchParams.get('note') === note.id
|
||||
|
||||
// Only fetch comparison notes when we have IDs to compare
|
||||
const { notes: comparisonNotesData, isLoading: isLoadingComparison } = useConnectionsCompare(
|
||||
comparisonNotes && comparisonNotes.length > 0 ? comparisonNotes : null
|
||||
)
|
||||
|
||||
// Optimistic UI state for instant feedback
|
||||
const [optimisticNote, addOptimisticNote] = useOptimistic(
|
||||
note,
|
||||
@@ -71,7 +158,7 @@ export function NoteCard({ note, onEdit, isDragging, isDragOver }: NoteCardProps
|
||||
}, [note.id, note.userId])
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (confirm('Are you sure you want to delete this note?')) {
|
||||
if (confirm(t('notes.confirmDelete'))) {
|
||||
setIsDeleting(true)
|
||||
try {
|
||||
await deleteNote(note.id)
|
||||
@@ -111,8 +198,7 @@ export function NoteCard({ note, onEdit, isDragging, isDragOver }: NoteCardProps
|
||||
const handleSizeChange = async (size: 'small' | 'medium' | 'large') => {
|
||||
startTransition(async () => {
|
||||
addOptimisticNote({ size })
|
||||
await updateNote(note.id, { size })
|
||||
router.refresh()
|
||||
await updateSize(note.id, size)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -130,7 +216,7 @@ export function NoteCard({ note, onEdit, isDragging, isDragOver }: NoteCardProps
|
||||
}
|
||||
|
||||
const handleLeaveShare = async () => {
|
||||
if (confirm('Are you sure you want to leave this shared note?')) {
|
||||
if (confirm(t('notes.confirmLeaveShare'))) {
|
||||
try {
|
||||
await leaveSharedNote(note.id)
|
||||
setIsDeleting(true) // Hide the note from view
|
||||
@@ -140,6 +226,15 @@ export function NoteCard({ note, onEdit, isDragging, isDragOver }: NoteCardProps
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemoveFusedBadge = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation() // Prevent opening the note editor
|
||||
startTransition(async () => {
|
||||
addOptimisticNote({ autoGenerated: null })
|
||||
await removeFusedBadge(note.id)
|
||||
router.refresh()
|
||||
})
|
||||
}
|
||||
|
||||
if (isDeleting) return null
|
||||
|
||||
return (
|
||||
@@ -151,8 +246,7 @@ export function NoteCard({ note, onEdit, isDragging, isDragOver }: NoteCardProps
|
||||
colorClasses.bg,
|
||||
colorClasses.card,
|
||||
colorClasses.hover,
|
||||
isDragging && 'opacity-30',
|
||||
isDragOver && 'ring-2 ring-blue-500'
|
||||
isDragging && 'opacity-30'
|
||||
)}
|
||||
onClick={(e) => {
|
||||
// Only trigger edit if not clicking on buttons
|
||||
@@ -163,24 +257,51 @@ export function NoteCard({ note, onEdit, isDragging, isDragOver }: NoteCardProps
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* Drag Handle - Visible only on mobile/touch devices */}
|
||||
<div className="absolute top-2 left-2 z-20 md:hidden cursor-grab active:cursor-grabbing drag-handle touch-none">
|
||||
<GripVertical className="h-4 w-4 text-gray-400 dark:text-gray-500" />
|
||||
{/* Move to Notebook Dropdown Menu */}
|
||||
<div onClick={(e) => e.stopPropagation()} className="absolute top-2 right-2 z-20">
|
||||
<DropdownMenu open={showNotebookMenu} onOpenChange={setShowNotebookMenu}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0 bg-blue-100 dark:bg-blue-900/30 hover:bg-blue-200 dark:hover:bg-blue-900/50 text-blue-600 dark:text-blue-400"
|
||||
title={t('notebookSuggestion.moveToNotebook')}
|
||||
>
|
||||
<FolderOpen className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-56">
|
||||
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">
|
||||
{t('notebookSuggestion.moveToNotebook')}
|
||||
</div>
|
||||
<DropdownMenuItem onClick={() => handleMoveToNotebook(null)}>
|
||||
<StickyNote className="h-4 w-4 mr-2" />
|
||||
{t('notebookSuggestion.generalNotes')}
|
||||
</DropdownMenuItem>
|
||||
{notebooks.map((notebook: any) => (
|
||||
<DropdownMenuItem
|
||||
key={notebook.id}
|
||||
onClick={() => handleMoveToNotebook(notebook.id)}
|
||||
>
|
||||
<span className="text-lg mr-2">{notebook.icon || '📁'}</span>
|
||||
{notebook.name}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
{/* Pin Button - Visible on hover or if pinned, always accessible */}
|
||||
{/* Pin Button - Visible on hover or if pinned */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={cn(
|
||||
"absolute top-2 right-2 z-20 h-8 w-8 p-0 rounded-full transition-opacity",
|
||||
optimisticNote.isPinned ? "opacity-100" : "opacity-0 group-hover:opacity-100",
|
||||
"md:flex", // On desktop follow hover logic
|
||||
"flex" // Ensure it's a flex container for the icon
|
||||
"absolute top-2 right-12 z-20 h-8 w-8 p-0 rounded-full transition-opacity",
|
||||
optimisticNote.isPinned ? "opacity-100" : "opacity-0 group-hover:opacity-100"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleTogglePin();
|
||||
e.stopPropagation()
|
||||
handleTogglePin()
|
||||
}}
|
||||
>
|
||||
<Pin
|
||||
@@ -190,11 +311,41 @@ export function NoteCard({ note, onEdit, isDragging, isDragOver }: NoteCardProps
|
||||
|
||||
{/* Reminder Icon - Move slightly if pin button is there */}
|
||||
{note.reminder && new Date(note.reminder) > new Date() && (
|
||||
<Bell
|
||||
<Bell
|
||||
className="absolute top-3 right-10 h-4 w-4 text-blue-600 dark:text-blue-400"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Memory Echo Badges - Fusion + Connections (BEFORE Title) */}
|
||||
<div className="flex flex-wrap gap-1 mb-2">
|
||||
{/* Fusion Badge with remove button */}
|
||||
{note.autoGenerated && (
|
||||
<div className="px-1.5 py-0.5 rounded text-[10px] font-medium bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-400 border border-purple-200 dark:border-purple-800 flex items-center gap-1 group/badge relative">
|
||||
<Link2 className="h-2.5 w-2.5" />
|
||||
{t('memoryEcho.fused')}
|
||||
<button
|
||||
onClick={handleRemoveFusedBadge}
|
||||
className="ml-1 opacity-0 group-hover/badge:opacity-100 hover:opacity-100 transition-opacity"
|
||||
title={t('notes.remove') || 'Remove'}
|
||||
>
|
||||
<X className="h-2.5 w-2.5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Connections Badge */}
|
||||
<ConnectionsBadge
|
||||
noteId={note.id}
|
||||
onClick={() => {
|
||||
// Only open overlay if note is NOT open in editor
|
||||
// (to avoid having 2 Dialogs with 2 close buttons)
|
||||
if (!isNoteOpenInEditor) {
|
||||
setShowConnectionsOverlay(true)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
{optimisticNote.title && (
|
||||
<h3 className="text-base font-medium mb-2 pr-10 text-gray-900 dark:text-gray-100">
|
||||
@@ -202,11 +353,26 @@ export function NoteCard({ note, onEdit, isDragging, isDragOver }: NoteCardProps
|
||||
</h3>
|
||||
)}
|
||||
|
||||
{/* Search Match Type Badge */}
|
||||
{optimisticNote.matchType && (
|
||||
<Badge
|
||||
variant={optimisticNote.matchType === 'exact' ? 'default' : 'secondary'}
|
||||
className={cn(
|
||||
'mb-2 text-xs',
|
||||
optimisticNote.matchType === 'exact'
|
||||
? 'bg-green-100 text-green-800 border-green-200 dark:bg-green-900/30 dark:text-green-300 dark:border-green-800'
|
||||
: 'bg-blue-100 text-blue-800 border-blue-200 dark:bg-blue-900/30 dark:text-blue-300 dark:border-blue-800'
|
||||
)}
|
||||
>
|
||||
{t(`semanticSearch.${optimisticNote.matchType === 'exact' ? 'exactMatch' : 'related'}`)}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{/* Shared badge */}
|
||||
{isSharedNote && owner && (
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-xs text-blue-600 dark:text-blue-400 font-medium">
|
||||
Shared by {owner.name || owner.email}
|
||||
{t('notes.sharedBy')} {owner.name || owner.email}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -218,7 +384,7 @@ export function NoteCard({ note, onEdit, isDragging, isDragOver }: NoteCardProps
|
||||
}}
|
||||
>
|
||||
<X className="h-3 w-3 mr-1" />
|
||||
Leave
|
||||
{t('notes.leaveShare')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
@@ -265,8 +431,8 @@ export function NoteCard({ note, onEdit, isDragging, isDragOver }: NoteCardProps
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Labels */}
|
||||
{optimisticNote.labels && optimisticNote.labels.length > 0 && (
|
||||
{/* Labels - ONLY show if note belongs to a notebook (labels are contextual per PRD) */}
|
||||
{optimisticNote.notebookId && optimisticNote.labels && optimisticNote.labels.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mt-3">
|
||||
{optimisticNote.labels.map((label) => (
|
||||
<LabelBadge key={label} label={label} />
|
||||
@@ -274,19 +440,28 @@ export function NoteCard({ note, onEdit, isDragging, isDragOver }: NoteCardProps
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Collaborators */}
|
||||
{optimisticNote.userId && collaborators.length > 0 && (
|
||||
<CollaboratorAvatars
|
||||
collaborators={collaborators}
|
||||
ownerId={optimisticNote.userId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Creation Date */}
|
||||
<div className="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
{formatDistanceToNow(new Date(note.createdAt), { addSuffix: true, locale: fr })}
|
||||
{/* Footer with Date only */}
|
||||
<div className="mt-3 flex items-center justify-end">
|
||||
{/* Creation Date */}
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{formatDistanceToNow(new Date(note.createdAt), { addSuffix: true, locale: getDateLocale(language) })}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Owner Avatar - Aligned with action buttons at bottom */}
|
||||
{owner && (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute bottom-2 left-2 z-20",
|
||||
"w-6 h-6 rounded-full text-white text-[10px] font-semibold flex items-center justify-center",
|
||||
getAvatarColor(owner.name || owner.email || 'Unknown')
|
||||
)}
|
||||
title={owner.name || owner.email || 'Unknown'}
|
||||
>
|
||||
{getInitials(owner.name || owner.email || '??')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action Bar Component - Only for owner */}
|
||||
{isOwner && (
|
||||
<NoteActions
|
||||
@@ -316,6 +491,39 @@ export function NoteCard({ note, onEdit, isDragging, isDragOver }: NoteCardProps
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Connections Overlay */}
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<ConnectionsOverlay
|
||||
isOpen={showConnectionsOverlay}
|
||||
onClose={() => setShowConnectionsOverlay(false)}
|
||||
noteId={note.id}
|
||||
onOpenNote={(noteId) => {
|
||||
// Find the note and open it
|
||||
onEdit?.(note, false)
|
||||
}}
|
||||
onCompareNotes={(noteIds) => {
|
||||
setComparisonNotes(noteIds)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Comparison Modal */}
|
||||
{comparisonNotes && comparisonNotesData.length > 0 && (
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<ComparisonModal
|
||||
isOpen={!!comparisonNotes}
|
||||
onClose={() => setComparisonNotes(null)}
|
||||
notes={comparisonNotesData}
|
||||
onOpenNote={(noteId) => {
|
||||
const foundNote = comparisonNotesData.find(n => n.id === noteId)
|
||||
if (foundNote) {
|
||||
onEdit?.(foundNote, false)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user