fix: i18n system overhaul and sidebar UI bugs
- Fix LanguageProvider: add RTL support (ar/fa), translation caching, prevent blank flash during load, browser language detection - Fix detect-user-language: extend whitelist from 5 to all 15 languages - Remove hardcoded initialLanguage="fr" from auth layout - Complete fr.json translation (all sections translated from English) - Add missing admin.ai keys to all 13 non-English locales - Translate ai.autoLabels, ai.batchOrganization, memoryEcho sections for all locales - Remove duplicate top-level autoLabels/batchOrganization from en.json - Fix notebook creation: replace window.location.reload() with createNotebookOptimistic + router.refresh() - Fix notebook name truncation in sidebar with min-w-0 - Remove redundant router.refresh() after note creation in page.tsx Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,18 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useTransition, useOptimistic } from 'react'
|
||||
import { Note } from '@/lib/types'
|
||||
import { Clock } from 'lucide-react'
|
||||
import { Clock, Pin, FolderOpen, Trash2, Folder, X } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
import { Button } from './ui/button'
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from './ui/dropdown-menu'
|
||||
import { togglePin, deleteNote, dismissFromRecent } from '@/app/actions/notes'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useNotebooks } from '@/context/notebooks-context'
|
||||
import { useNoteRefresh } from '@/context/NoteRefreshContext'
|
||||
import { toast } from 'sonner'
|
||||
import { StickyNote } from 'lucide-react'
|
||||
|
||||
interface RecentNotesSectionProps {
|
||||
recentNotes: Note[]
|
||||
@@ -53,16 +62,89 @@ function CompactCard({
|
||||
onEdit?: (note: Note, readOnly?: boolean) => void
|
||||
}) {
|
||||
const { t } = useLanguage()
|
||||
const timeAgo = getCompactTime(note.contentUpdatedAt || note.updatedAt, t)
|
||||
const router = useRouter()
|
||||
const { notebooks, moveNoteToNotebookOptimistic } = useNotebooks()
|
||||
const { triggerRefresh } = useNoteRefresh()
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
const [showNotebookMenu, setShowNotebookMenu] = useState(false)
|
||||
const [, startTransition] = useTransition()
|
||||
|
||||
// Optimistic UI state
|
||||
const [optimisticNote, addOptimisticNote] = useOptimistic(
|
||||
note,
|
||||
(state: Note, newProps: Partial<Note>) => ({ ...state, ...newProps })
|
||||
)
|
||||
|
||||
const timeAgo = getCompactTime(optimisticNote.contentUpdatedAt || optimisticNote.updatedAt, t)
|
||||
const isFirstNote = index === 0
|
||||
|
||||
const handleTogglePin = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
startTransition(async () => {
|
||||
const newPinnedState = !optimisticNote.isPinned
|
||||
addOptimisticNote({ isPinned: newPinnedState })
|
||||
await togglePin(note.id, newPinnedState)
|
||||
|
||||
// Trigger global refresh to update lists
|
||||
triggerRefresh()
|
||||
router.refresh()
|
||||
|
||||
if (newPinnedState) {
|
||||
toast.success(t('notes.pinned') || 'Note pinned')
|
||||
} else {
|
||||
toast.info(t('notes.unpinned') || 'Note unpinned')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleMoveToNotebook = async (notebookId: string | null) => {
|
||||
await moveNoteToNotebookOptimistic(note.id, notebookId)
|
||||
setShowNotebookMenu(false)
|
||||
triggerRefresh()
|
||||
}
|
||||
|
||||
const handleDelete = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
if (confirm(t('notes.confirmDelete'))) {
|
||||
setIsDeleting(true)
|
||||
try {
|
||||
await deleteNote(note.id)
|
||||
triggerRefresh()
|
||||
router.refresh()
|
||||
} catch (error) {
|
||||
console.error('Failed to delete note:', error)
|
||||
setIsDeleting(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleDismiss = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
// Optimistic removal
|
||||
setIsDeleting(true)
|
||||
try {
|
||||
await dismissFromRecent(note.id)
|
||||
// Don't refresh list to prevent immediate replacement
|
||||
// triggerRefresh()
|
||||
// router.refresh()
|
||||
toast.success(t('notes.dismissed') || 'Note dismissed from recent')
|
||||
} catch (error) {
|
||||
console.error('Failed to dismiss note:', error)
|
||||
setIsDeleting(false)
|
||||
toast.error(t('common.error') || 'Failed to dismiss')
|
||||
}
|
||||
}
|
||||
|
||||
if (isDeleting) return null
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => onEdit?.(note)}
|
||||
<div
|
||||
className={cn(
|
||||
"group relative text-left p-4 bg-card border rounded-xl shadow-sm hover:shadow-md transition-all duration-200 min-h-[44px]",
|
||||
isFirstNote && "ring-2 ring-primary/20"
|
||||
"group relative flex flex-col p-4 bg-card border rounded-xl shadow-sm hover:shadow-md transition-all duration-200 min-h-[44px]",
|
||||
isFirstNote && "ring-2 ring-primary/20",
|
||||
"cursor-pointer"
|
||||
)}
|
||||
onClick={() => onEdit?.(note)}
|
||||
>
|
||||
<div className={cn(
|
||||
"absolute left-0 top-0 bottom-0 w-1 rounded-l-xl",
|
||||
@@ -73,14 +155,83 @@ function CompactCard({
|
||||
: "bg-muted dark:bg-muted/60"
|
||||
)} />
|
||||
|
||||
{/* Action Buttons Overlay - Ensure visible on hover and touch devices often handle this with tap */}
|
||||
<div className="absolute top-2 right-2 flex items-center gap-1 opacity-100 md:opacity-0 group-hover:opacity-100 transition-opacity z-10">
|
||||
{/* Pin Button */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0 bg-background/50 backdrop-blur-sm border shadow-sm md:shadow-none md:border-none md:bg-transparent"
|
||||
onClick={handleTogglePin}
|
||||
title={optimisticNote.isPinned ? t('notes.unpin') : t('notes.pin')}
|
||||
>
|
||||
<Pin className={cn("h-3.5 w-3.5", optimisticNote.isPinned ? "fill-current text-primary" : "text-muted-foreground")} />
|
||||
</Button>
|
||||
|
||||
{/* Move to Notebook */}
|
||||
<DropdownMenu open={showNotebookMenu} onOpenChange={setShowNotebookMenu}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0 bg-background/50 backdrop-blur-sm border shadow-sm md:shadow-none md:border-none md:bg-transparent"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
title={t('notebookSuggestion.moveToNotebook')}
|
||||
>
|
||||
<FolderOpen className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-56" onClick={(e) => e.stopPropagation()}>
|
||||
<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)}
|
||||
>
|
||||
<Folder className="h-4 w-4 mr-2" />
|
||||
{notebook.name}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* Dismiss Button */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0 hover:text-destructive bg-background/50 backdrop-blur-sm border shadow-sm md:shadow-none md:border-none md:bg-transparent"
|
||||
onClick={handleDismiss}
|
||||
title={t('common.close') || 'Dismiss'}
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
|
||||
{/* Delete Button */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0 hover:text-destructive bg-background/50 backdrop-blur-sm border shadow-sm md:shadow-none md:border-none md:bg-transparent"
|
||||
onClick={handleDelete}
|
||||
title={t('notes.delete')}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="pl-2">
|
||||
<h3 className="text-sm font-semibold text-foreground line-clamp-1 mb-2">
|
||||
{note.title || t('notes.untitled')}
|
||||
{optimisticNote.title || t('notes.untitled')}
|
||||
</h3>
|
||||
|
||||
<p className="text-xs text-muted-foreground line-clamp-2 mb-3 min-h-[2.5rem]">
|
||||
{note.content?.substring(0, 80) || ''}
|
||||
{note.content && note.content.length > 80 && '...'}
|
||||
{(optimisticNote.content && typeof optimisticNote.content === 'string') ? optimisticNote.content.substring(0, 80) : ''}
|
||||
{optimisticNote.content && typeof optimisticNote.content === 'string' && optimisticNote.content.length > 80 && '...'}
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-between pt-2 border-t border-border">
|
||||
@@ -90,18 +241,19 @@ function CompactCard({
|
||||
</span>
|
||||
|
||||
<div className="flex items-center gap-1.5">
|
||||
{note.notebookId && (
|
||||
{optimisticNote.isPinned && (
|
||||
<Pin className="w-3 h-3 text-primary fill-current" />
|
||||
)}
|
||||
{optimisticNote.notebookId && (
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-primary dark:bg-primary/70" title={t('notes.inNotebook')} />
|
||||
)}
|
||||
{note.labels && note.labels.length > 0 && (
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-emerald-500 dark:bg-emerald-400" title={t('labels.count', { count: note.labels.length })} />
|
||||
{optimisticNote.labels && optimisticNote.labels.length > 0 && (
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-emerald-500 dark:bg-emerald-400" title={t('labels.count', { count: optimisticNote.labels.length })} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute top-3 right-3 w-2 h-2 rounded-full bg-primary opacity-0 group-hover:opacity-100 transition-opacity duration-200" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user