## 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>
232 lines
8.4 KiB
TypeScript
232 lines
8.4 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { useRouter } from 'next/navigation'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import { Bell, Check, X, Clock, User } from 'lucide-react'
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuTrigger,
|
|
} from '@/components/ui/dropdown-menu'
|
|
import { getPendingShareRequests, respondToShareRequest, removeSharedNoteFromView } from '@/app/actions/notes'
|
|
import { toast } from 'sonner'
|
|
import { useNoteRefresh } from '@/context/NoteRefreshContext'
|
|
import { cn } from '@/lib/utils'
|
|
import { useLanguage } from '@/lib/i18n'
|
|
|
|
interface ShareRequest {
|
|
id: string
|
|
status: string
|
|
permission: string
|
|
createdAt: Date
|
|
note: {
|
|
id: string
|
|
title: string | null
|
|
content: string
|
|
color: string
|
|
createdAt: Date
|
|
}
|
|
sharer: {
|
|
id: string
|
|
name: string | null
|
|
email: string
|
|
image: string | null
|
|
}
|
|
}
|
|
|
|
export function NotificationPanel() {
|
|
const router = useRouter()
|
|
const { triggerRefresh } = useNoteRefresh()
|
|
const { t } = useLanguage()
|
|
const [requests, setRequests] = useState<ShareRequest[]>([])
|
|
const [isLoading, setIsLoading] = useState(false)
|
|
const [pendingCount, setPendingCount] = useState(0)
|
|
|
|
const loadRequests = async () => {
|
|
setIsLoading(true)
|
|
try {
|
|
const data = await getPendingShareRequests()
|
|
setRequests(data)
|
|
setPendingCount(data.length)
|
|
} catch (error: any) {
|
|
console.error('Failed to load share requests:', error)
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
loadRequests()
|
|
const interval = setInterval(loadRequests, 10000)
|
|
return () => clearInterval(interval)
|
|
}, [])
|
|
|
|
const handleAccept = async (shareId: string) => {
|
|
try {
|
|
await respondToShareRequest(shareId, 'accept')
|
|
router.refresh()
|
|
triggerRefresh()
|
|
setRequests(prev => prev.filter(r => r.id !== shareId))
|
|
setPendingCount(prev => prev - 1)
|
|
toast.success(t('notes.noteCreated'), {
|
|
description: t('collaboration.nowHasAccess', { name: 'Note' }),
|
|
duration: 3000,
|
|
})
|
|
} catch (error: any) {
|
|
console.error('[NOTIFICATION] Error:', error)
|
|
toast.error(error.message || t('general.error'))
|
|
}
|
|
}
|
|
|
|
const handleDecline = async (shareId: string) => {
|
|
try {
|
|
await respondToShareRequest(shareId, 'decline')
|
|
router.refresh()
|
|
triggerRefresh()
|
|
setRequests(prev => prev.filter(r => r.id !== shareId))
|
|
setPendingCount(prev => prev - 1)
|
|
toast.info(t('general.operationFailed'))
|
|
} catch (error: any) {
|
|
console.error('[NOTIFICATION] Error:', error)
|
|
toast.error(error.message || t('general.error'))
|
|
}
|
|
}
|
|
|
|
const handleRemove = async (shareId: string) => {
|
|
try {
|
|
await removeSharedNoteFromView(shareId)
|
|
router.refresh()
|
|
triggerRefresh()
|
|
setRequests(prev => prev.filter(r => r.id !== shareId))
|
|
toast.info(t('general.operationFailed'))
|
|
} catch (error: any) {
|
|
toast.error(error.message || t('general.error'))
|
|
}
|
|
}
|
|
|
|
return (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="relative h-9 w-9 p-0 hover:bg-accent/50 transition-all duration-200"
|
|
>
|
|
<Bell className="h-4 w-4 transition-transform duration-200 hover:scale-110" />
|
|
{pendingCount > 0 && (
|
|
<Badge
|
|
variant="destructive"
|
|
className="absolute -top-1 -right-1 h-5 w-5 flex items-center justify-center p-0 text-xs animate-pulse shadow-lg"
|
|
>
|
|
{pendingCount > 9 ? '9+' : pendingCount}
|
|
</Badge>
|
|
)}
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="w-80">
|
|
<div className="px-4 py-3 border-b bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-950/20 dark:to-indigo-950/20">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<Bell className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
|
<span className="font-semibold text-sm">{t('nav.aiSettings')}</span>
|
|
</div>
|
|
{pendingCount > 0 && (
|
|
<Badge className="bg-blue-600 hover:bg-blue-700 text-white shadow-md">
|
|
{pendingCount}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{isLoading ? (
|
|
<div className="p-6 text-center text-sm text-muted-foreground">
|
|
<div className="animate-spin h-6 w-6 border-2 border-blue-600 border-t-transparent rounded-full mx-auto mb-2" />
|
|
{t('general.loading')}
|
|
</div>
|
|
) : requests.length === 0 ? (
|
|
<div className="p-6 text-center text-sm text-muted-foreground">
|
|
<Bell className="h-10 w-10 mx-auto mb-3 opacity-30" />
|
|
<p className="font-medium">{t('search.noResults')}</p>
|
|
</div>
|
|
) : (
|
|
<div className="max-h-96 overflow-y-auto">
|
|
{requests.map((request) => (
|
|
<div
|
|
key={request.id}
|
|
className="p-4 border-b last:border-0 hover:bg-accent/50 transition-colors duration-150"
|
|
>
|
|
<div className="flex items-start gap-3 mb-3">
|
|
<div className="h-8 w-8 rounded-full bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center text-white font-semibold text-xs shadow-md">
|
|
{(request.sharer.name || request.sharer.email)[0].toUpperCase()}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm font-semibold truncate">
|
|
{request.sharer.name || request.sharer.email}
|
|
</p>
|
|
<p className="text-xs text-muted-foreground truncate mt-0.5">
|
|
shared "{request.note.title || 'Untitled'}"
|
|
</p>
|
|
</div>
|
|
<Badge
|
|
variant="secondary"
|
|
className="text-xs capitalize bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300 border-0"
|
|
>
|
|
{request.permission}
|
|
</Badge>
|
|
</div>
|
|
|
|
<div className="flex gap-2 mt-3">
|
|
<button
|
|
onClick={() => handleAccept(request.id)}
|
|
className={cn(
|
|
"flex-1 h-9 px-4 text-xs font-semibold rounded-lg",
|
|
"bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700",
|
|
"text-white shadow-md hover:shadow-lg",
|
|
"transition-all duration-200",
|
|
"flex items-center justify-center gap-1.5",
|
|
"active:scale-95"
|
|
)}
|
|
>
|
|
<Check className="h-3.5 w-3.5" />
|
|
{t('general.confirm')}
|
|
</button>
|
|
<button
|
|
onClick={() => handleDecline(request.id)}
|
|
className={cn(
|
|
"flex-1 h-9 px-4 text-xs font-semibold rounded-lg",
|
|
"bg-white dark:bg-gray-800",
|
|
"border-2 border-gray-200 dark:border-gray-700",
|
|
"text-gray-700 dark:text-gray-300",
|
|
"hover:bg-gray-50 dark:hover:bg-gray-700",
|
|
"hover:border-gray-300 dark:hover:border-gray-600",
|
|
"transition-all duration-200",
|
|
"flex items-center justify-center gap-1.5",
|
|
"active:scale-95"
|
|
)}
|
|
>
|
|
<X className="h-3.5 w-3.5" />
|
|
{t('general.cancel')}
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-1.5 mt-3 text-xs text-muted-foreground">
|
|
<Clock className="h-3 w-3" />
|
|
<span>{new Date(request.createdAt).toLocaleDateString()}</span>
|
|
<button
|
|
onClick={() => handleRemove(request.id)}
|
|
className="ml-auto text-muted-foreground hover:text-foreground transition-colors duration-150"
|
|
>
|
|
{t('general.close')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
)
|
|
}
|