feat(ai): localize AI features
This commit is contained in:
@@ -127,7 +127,7 @@ export function AISettingsPanel({ initialSettings }: AISettingsPanelProps) {
|
||||
|
||||
<FeatureToggle
|
||||
name={t('titleSuggestions.available').replace('💡 ', '')}
|
||||
description="Suggest titles for untitled notes after 50+ words"
|
||||
description={t('aiSettings.titleSuggestionsDesc')}
|
||||
checked={settings.titleSuggestions}
|
||||
onChange={(checked) => handleToggle('titleSuggestions', checked)}
|
||||
/>
|
||||
@@ -141,7 +141,7 @@ export function AISettingsPanel({ initialSettings }: AISettingsPanelProps) {
|
||||
|
||||
<FeatureToggle
|
||||
name={t('paragraphRefactor.title')}
|
||||
description="AI-powered text improvement options"
|
||||
description={t('aiSettings.paragraphRefactorDesc')}
|
||||
checked={settings.paragraphRefactor}
|
||||
onChange={(checked) => handleToggle('paragraphRefactor', checked)}
|
||||
/>
|
||||
@@ -159,7 +159,7 @@ export function AISettingsPanel({ initialSettings }: AISettingsPanelProps) {
|
||||
{t('aiSettings.frequency')}
|
||||
</Label>
|
||||
<p className="text-xs text-gray-500 mb-3">
|
||||
How often to analyze note connections
|
||||
{t('aiSettings.frequencyDesc')}
|
||||
</p>
|
||||
<RadioGroup
|
||||
value={settings.memoryEchoFrequency}
|
||||
@@ -192,7 +192,7 @@ export function AISettingsPanel({ initialSettings }: AISettingsPanelProps) {
|
||||
<Card className="p-4">
|
||||
<Label className="text-base font-medium mb-1">{t('aiSettings.provider')}</Label>
|
||||
<p className="text-sm text-gray-500 mb-4">
|
||||
Choose your preferred AI provider
|
||||
{t('aiSettings.providerDesc')}
|
||||
</p>
|
||||
|
||||
<RadioGroup
|
||||
@@ -206,7 +206,7 @@ export function AISettingsPanel({ initialSettings }: AISettingsPanelProps) {
|
||||
{t('aiSettings.providerAuto')}
|
||||
</Label>
|
||||
<p className="text-sm text-gray-500">
|
||||
Ollama when available, OpenAI fallback
|
||||
{t('aiSettings.providerAutoDesc')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -218,7 +218,7 @@ export function AISettingsPanel({ initialSettings }: AISettingsPanelProps) {
|
||||
{t('aiSettings.providerOllama')}
|
||||
</Label>
|
||||
<p className="text-sm text-gray-500">
|
||||
100% private, runs locally on your machine
|
||||
{t('aiSettings.providerOllamaDesc')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -230,7 +230,7 @@ export function AISettingsPanel({ initialSettings }: AISettingsPanelProps) {
|
||||
{t('aiSettings.providerOpenAI')}
|
||||
</Label>
|
||||
<p className="text-sm text-gray-500">
|
||||
Most accurate, requires API key
|
||||
{t('aiSettings.providerOpenAIDesc')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -55,7 +55,10 @@ export function AutoLabelSuggestionDialog({
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ notebookId }),
|
||||
body: JSON.stringify({
|
||||
notebookId,
|
||||
language: document.documentElement.lang || 'en',
|
||||
}),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
@@ -68,7 +71,7 @@ export function AutoLabelSuggestionDialog({
|
||||
} else {
|
||||
// No suggestions is not an error - just close the dialog
|
||||
if (data.message) {
|
||||
}
|
||||
}
|
||||
onOpenChange(false)
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -113,7 +116,7 @@ export function AutoLabelSuggestionDialog({
|
||||
if (data.success) {
|
||||
toast.success(
|
||||
t('ai.autoLabels.created', { count: data.data.createdCount }) ||
|
||||
`${data.data.createdCount} labels created successfully`
|
||||
`${data.data.createdCount} labels created successfully`
|
||||
)
|
||||
onLabelsCreated()
|
||||
onOpenChange(false)
|
||||
|
||||
@@ -38,7 +38,11 @@ export function BatchOrganizationDialog({
|
||||
try {
|
||||
const response = await fetch('/api/ai/batch-organize', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
language: document.documentElement.lang || 'en'
|
||||
}),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
@@ -125,7 +129,7 @@ export function BatchOrganizationDialog({
|
||||
if (data.success) {
|
||||
toast.success(
|
||||
t('ai.batchOrganization.success', { count: data.data.movedCount }) ||
|
||||
`${data.data.movedCount} notes moved successfully`
|
||||
`${data.data.movedCount} notes moved successfully`
|
||||
)
|
||||
onNotesMoved()
|
||||
onOpenChange(false)
|
||||
@@ -306,7 +310,7 @@ export function BatchOrganizationDialog({
|
||||
) : (
|
||||
<>
|
||||
<CheckCircle2 className="h-4 w-4 mr-2" />
|
||||
{t('ai.batchOrganization.apply')}
|
||||
{t('ai.batchOrganization.apply', { count: selectedNotes.size })}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
@@ -21,13 +21,13 @@ export function DemoModeToggle({ demoMode, onToggle }: DemoModeToggleProps) {
|
||||
try {
|
||||
await onToggle(checked)
|
||||
if (checked) {
|
||||
toast.success('🧪 Demo Mode activated! Memory Echo will now work instantly.')
|
||||
toast.success(t('demoMode.activated'))
|
||||
} else {
|
||||
toast.success('Demo Mode disabled. Normal parameters restored.')
|
||||
toast.success(t('demoMode.deactivated'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error toggling demo mode:', error)
|
||||
toast.error('Failed to toggle demo mode')
|
||||
toast.error(t('demoMode.toggleFailed'))
|
||||
} finally {
|
||||
setIsPending(false)
|
||||
}
|
||||
@@ -53,14 +53,11 @@ export function DemoModeToggle({ demoMode, onToggle }: DemoModeToggleProps) {
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
🧪 Demo Mode
|
||||
🧪 {t('demoMode.title')}
|
||||
{demoMode && <Zap className="h-4 w-4 text-amber-500 animate-pulse" />}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs mt-1">
|
||||
{demoMode
|
||||
? 'Test Memory Echo instantly with relaxed parameters'
|
||||
: 'Enable instant testing of Memory Echo feature'
|
||||
}
|
||||
{t('demoMode.description')}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
@@ -77,31 +74,25 @@ export function DemoModeToggle({ demoMode, onToggle }: DemoModeToggleProps) {
|
||||
<CardContent className="pt-0 space-y-2">
|
||||
<div className="rounded-lg bg-white dark:bg-zinc-900 border border-amber-200 dark:border-amber-900/30 p-3">
|
||||
<p className="text-xs font-semibold text-gray-700 dark:text-gray-300 mb-2">
|
||||
⚡ Demo parameters active:
|
||||
{t('demoMode.parametersActive')}
|
||||
</p>
|
||||
<div className="space-y-1.5 text-xs text-gray-600 dark:text-gray-400">
|
||||
<div className="flex items-start gap-2">
|
||||
<Target className="h-3.5 w-3.5 mt-0.5 text-amber-600 flex-shrink-0" />
|
||||
<span>
|
||||
<strong>50% similarity</strong> threshold (normally 75%)
|
||||
</span>
|
||||
<span>{t('demoMode.similarityThreshold')}</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<Zap className="h-3.5 w-3.5 mt-0.5 text-amber-600 flex-shrink-0" />
|
||||
<span>
|
||||
<strong>0-day delay</strong> between notes (normally 7 days)
|
||||
</span>
|
||||
<span>{t('demoMode.delayBetweenNotes')}</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<Lightbulb className="h-3.5 w-3.5 mt-0.5 text-amber-600 flex-shrink-0" />
|
||||
<span>
|
||||
<strong>Unlimited insights</strong> (no frequency limits)
|
||||
</span>
|
||||
<span>{t('demoMode.unlimitedInsights')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-amber-700 dark:text-amber-400 text-center">
|
||||
💡 Create 2+ similar notes and see Memory Echo in action!
|
||||
💡 {t('demoMode.createNotesTip')}
|
||||
</p>
|
||||
</CardContent>
|
||||
)}
|
||||
|
||||
@@ -53,7 +53,7 @@ export function FavoritesSection({ pinnedNotes, onEdit, isLoading }: FavoritesSe
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-2xl">📌</span>
|
||||
<h2 className="text-lg font-semibold text-foreground">
|
||||
Pinned Notes
|
||||
{t('notes.pinnedNotes')}
|
||||
<span className="text-sm font-medium text-muted-foreground ml-2">
|
||||
({pinnedNotes.length})
|
||||
</span>
|
||||
|
||||
@@ -77,7 +77,7 @@ export function GhostTags({ suggestions, addedTags, isAnalyzing, onSelectTag, on
|
||||
onSelectTag(suggestion.tag);
|
||||
}}
|
||||
className={cn("flex items-center px-3 py-1 text-xs font-medium", colorClasses.text)}
|
||||
title={isNewLabel ? "Créer ce nouveau label et l'ajouter" : t('ai.clickToAddTag')}
|
||||
title={isNewLabel ? t('ai.autoLabels.createNewLabel') : t('ai.clickToAddTag')}
|
||||
>
|
||||
{isNewLabel && <Plus className="w-3 h-3 mr-1" />}
|
||||
{!isNewLabel && <Sparkles className="w-3 h-3 mr-1.5 opacity-50" />}
|
||||
|
||||
@@ -355,7 +355,7 @@ export function Header({
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => router.push('/admin')} className="cursor-pointer">
|
||||
<Shield className="mr-2 h-4 w-4" />
|
||||
<span>Admin</span>
|
||||
<span>{t('nav.adminDashboard')}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => signOut()} className="cursor-pointer text-red-600 focus:text-red-600">
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useLanguage } from '@/lib/i18n/LanguageProvider'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
@@ -33,6 +34,7 @@ interface MemoryEchoNotificationProps {
|
||||
}
|
||||
|
||||
export function MemoryEchoNotification({ onOpenNote }: MemoryEchoNotificationProps) {
|
||||
const { t } = useLanguage()
|
||||
const [insight, setInsight] = useState<MemoryEchoInsight | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [isDismissed, setIsDismissed] = useState(false)
|
||||
@@ -137,9 +139,9 @@ export function MemoryEchoNotification({ onOpenNote }: MemoryEchoNotificationPro
|
||||
<Sparkles className="h-5 w-5 text-amber-600 dark:text-amber-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold">💡 Memory Echo Discovery</h2>
|
||||
<h2 className="text-xl font-semibold">{t('memoryEcho.title')}</h2>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
These notes are connected by {similarityPercentage}% similarity
|
||||
{t('connection.similarityInfo', { similarity: similarityPercentage })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -179,7 +181,7 @@ export function MemoryEchoNotification({ onOpenNote }: MemoryEchoNotificationPro
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 line-clamp-4">
|
||||
{insight.note1.content}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-2">Click to view note →</p>
|
||||
<p className="text-xs text-gray-500 mt-2">{t('memoryEcho.clickToView')}</p>
|
||||
</div>
|
||||
|
||||
{/* Note 2 */}
|
||||
@@ -198,37 +200,35 @@ export function MemoryEchoNotification({ onOpenNote }: MemoryEchoNotificationPro
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 line-clamp-4">
|
||||
{insight.note2.content}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-2">Click to view note →</p>
|
||||
<p className="text-xs text-gray-500 mt-2">{t('memoryEcho.clickToView')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Feedback Section */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-t dark:border-zinc-700 bg-gray-50 dark:bg-zinc-800/50">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Is this connection helpful?
|
||||
{t('connection.isHelpful')}
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => handleFeedback('thumbs_up')}
|
||||
className={`px-4 py-2 rounded-lg flex items-center gap-2 transition-colors ${
|
||||
insight.feedback === 'thumbs_up'
|
||||
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
|
||||
: 'hover:bg-green-50 text-green-600 dark:hover:bg-green-950/20'
|
||||
}`}
|
||||
className={`px-4 py-2 rounded-lg flex items-center gap-2 transition-colors ${insight.feedback === 'thumbs_up'
|
||||
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
|
||||
: 'hover:bg-green-50 text-green-600 dark:hover:bg-green-950/20'
|
||||
}`}
|
||||
>
|
||||
<ThumbsUp className="h-4 w-4" />
|
||||
Helpful
|
||||
{t('memoryEcho.helpful')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleFeedback('thumbs_down')}
|
||||
className={`px-4 py-2 rounded-lg flex items-center gap-2 transition-colors ${
|
||||
insight.feedback === 'thumbs_down'
|
||||
? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'
|
||||
: 'hover:bg-red-50 text-red-600 dark:hover:bg-red-950/20'
|
||||
}`}
|
||||
className={`px-4 py-2 rounded-lg flex items-center gap-2 transition-colors ${insight.feedback === 'thumbs_down'
|
||||
? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'
|
||||
: 'hover:bg-red-50 text-red-600 dark:hover:bg-red-950/20'
|
||||
}`}
|
||||
>
|
||||
<ThumbsDown className="h-4 w-4" />
|
||||
Not Helpful
|
||||
{t('memoryEcho.notHelpful')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -248,11 +248,11 @@ export function MemoryEchoNotification({ onOpenNote }: MemoryEchoNotificationPro
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
💡 I noticed something...
|
||||
{t('memoryEcho.title')}
|
||||
<Sparkles className="h-4 w-4 text-amber-500" />
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs mt-1">
|
||||
Proactive connections between your notes
|
||||
{t('memoryEcho.description')}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
@@ -298,7 +298,7 @@ export function MemoryEchoNotification({ onOpenNote }: MemoryEchoNotificationPro
|
||||
className="flex-1 bg-amber-600 hover:bg-amber-700 text-white"
|
||||
onClick={handleView}
|
||||
>
|
||||
View Connection
|
||||
{t('memoryEcho.viewConnection')}
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center gap-1 border-l pl-2">
|
||||
@@ -307,7 +307,7 @@ export function MemoryEchoNotification({ onOpenNote }: MemoryEchoNotificationPro
|
||||
size="icon"
|
||||
className="h-8 w-8 text-green-600 hover:text-green-700 hover:bg-green-50 dark:hover:bg-green-950/20"
|
||||
onClick={() => handleFeedback('thumbs_up')}
|
||||
title="Helpful"
|
||||
title={t('memoryEcho.helpful')}
|
||||
>
|
||||
<ThumbsUp className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -316,7 +316,7 @@ export function MemoryEchoNotification({ onOpenNote }: MemoryEchoNotificationPro
|
||||
size="icon"
|
||||
className="h-8 w-8 text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-950/20"
|
||||
onClick={() => handleFeedback('thumbs_down')}
|
||||
title="Not Helpful"
|
||||
title={t('memoryEcho.notHelpful')}
|
||||
>
|
||||
<ThumbsDown className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -328,7 +328,7 @@ export function MemoryEchoNotification({ onOpenNote }: MemoryEchoNotificationPro
|
||||
className="w-full text-center text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 py-1"
|
||||
onClick={handleDismiss}
|
||||
>
|
||||
Dismiss for now
|
||||
{t('memoryEcho.dismiss')}
|
||||
</button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -1122,7 +1122,7 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
|
||||
}
|
||||
}
|
||||
|
||||
toast.success('Notes fusionnées avec succès !')
|
||||
toast.success(t('toast.notesFusionSuccess'))
|
||||
triggerRefresh()
|
||||
onClose()
|
||||
}}
|
||||
|
||||
@@ -57,7 +57,10 @@ export function NotebookSuggestionToast({
|
||||
const response = await fetch('/api/ai/suggest-notebook', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ noteContent })
|
||||
body: JSON.stringify({
|
||||
noteContent,
|
||||
language: document.documentElement.lang || 'en',
|
||||
})
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
@@ -52,7 +52,10 @@ export function NotebookSummaryDialog({
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ notebookId }),
|
||||
body: JSON.stringify({
|
||||
notebookId,
|
||||
language: document.documentElement.lang || 'en',
|
||||
}),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
@@ -82,6 +85,10 @@ export function NotebookSummaryDialog({
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader className="sr-only">
|
||||
<DialogTitle>{t('notebook.generating')}</DialogTitle>
|
||||
<DialogDescription>{t('notebook.generatingDescription') || 'Please wait...'}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col items-center justify-center py-12">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||
<p className="mt-4 text-sm text-muted-foreground">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { Note } from '@/lib/types'
|
||||
import { Clock, FileText, Tag } from 'lucide-react'
|
||||
import { Clock } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
|
||||
@@ -11,27 +11,24 @@ interface RecentNotesSectionProps {
|
||||
}
|
||||
|
||||
export function RecentNotesSection({ recentNotes, onEdit }: RecentNotesSectionProps) {
|
||||
const { language } = useLanguage()
|
||||
const { t } = useLanguage()
|
||||
|
||||
// Show only the 3 most recent notes
|
||||
const topThree = recentNotes.slice(0, 3)
|
||||
|
||||
if (topThree.length === 0) return null
|
||||
|
||||
return (
|
||||
<section data-testid="recent-notes-section" className="mb-6">
|
||||
{/* Minimalist header - matching your app style */}
|
||||
<div className="flex items-center gap-2 mb-3 px-1">
|
||||
<Clock className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
||||
{language === 'fr' ? 'Récent' : 'Recent'}
|
||||
{t('notes.recent')}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
· {topThree.length}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Compact 3-card row */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
{topThree.map((note, index) => (
|
||||
<CompactCard
|
||||
@@ -46,7 +43,6 @@ export function RecentNotesSection({ recentNotes, onEdit }: RecentNotesSectionPr
|
||||
)
|
||||
}
|
||||
|
||||
// Compact card - matching your app's clean design
|
||||
function CompactCard({
|
||||
note,
|
||||
index,
|
||||
@@ -56,9 +52,8 @@ function CompactCard({
|
||||
index: number
|
||||
onEdit?: (note: Note, readOnly?: boolean) => void
|
||||
}) {
|
||||
const { language } = useLanguage()
|
||||
// Use contentUpdatedAt - only reflects actual content changes, not property changes (size, color, etc.)
|
||||
const timeAgo = getCompactTime(note.contentUpdatedAt || note.updatedAt, language)
|
||||
const { t } = useLanguage()
|
||||
const timeAgo = getCompactTime(note.contentUpdatedAt || note.updatedAt, t)
|
||||
const isFirstNote = index === 0
|
||||
|
||||
return (
|
||||
@@ -69,7 +64,6 @@ function CompactCard({
|
||||
isFirstNote && "ring-2 ring-primary/20"
|
||||
)}
|
||||
>
|
||||
{/* Subtle left accent - colored based on recency */}
|
||||
<div className={cn(
|
||||
"absolute left-0 top-0 bottom-0 w-1 rounded-l-xl",
|
||||
isFirstNote
|
||||
@@ -79,74 +73,54 @@ function CompactCard({
|
||||
: "bg-muted dark:bg-muted/60"
|
||||
)} />
|
||||
|
||||
{/* Content with left padding for accent line */}
|
||||
<div className="pl-2">
|
||||
{/* Title */}
|
||||
<h3 className="text-sm font-semibold text-foreground line-clamp-1 mb-2">
|
||||
{note.title || (language === 'fr' ? 'Sans titre' : 'Untitled')}
|
||||
{note.title || t('notes.untitled')}
|
||||
</h3>
|
||||
|
||||
{/* Preview - 2 lines max */}
|
||||
<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 && '...'}
|
||||
</p>
|
||||
|
||||
{/* Footer with time and indicators */}
|
||||
<div className="flex items-center justify-between pt-2 border-t border-border">
|
||||
{/* Time - left */}
|
||||
<span className="text-xs text-muted-foreground flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
<span className="font-medium">{timeAgo}</span>
|
||||
</span>
|
||||
|
||||
{/* Indicators - right */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
{/* Notebook indicator */}
|
||||
{note.notebookId && (
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-primary dark:bg-primary/70" title="In notebook" />
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-primary dark:bg-primary/70" title={t('notes.inNotebook')} />
|
||||
)}
|
||||
{/* Labels indicator */}
|
||||
{note.labels && note.labels.length > 0 && (
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-emerald-500 dark:bg-emerald-400" title={`${note.labels.length} ${language === 'fr' ? 'étiquettes' : 'labels'}`} />
|
||||
<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 })} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hover indicator - top right */}
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
// Compact time display - matching your app's style
|
||||
// NOTE: Ensure dates are properly parsed from database (may come as strings)
|
||||
function getCompactTime(date: Date | string, language: string): string {
|
||||
function getCompactTime(date: Date | string, t: (key: string, params?: Record<string, any>) => string): string {
|
||||
const now = new Date()
|
||||
const then = date instanceof Date ? date : new Date(date)
|
||||
|
||||
// Validate date
|
||||
if (isNaN(then.getTime())) {
|
||||
console.warn('Invalid date provided to getCompactTime:', date)
|
||||
return language === 'fr' ? 'date invalide' : 'invalid date'
|
||||
return t('common.error')
|
||||
}
|
||||
|
||||
const seconds = Math.floor((now.getTime() - then.getTime()) / 1000)
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
const hours = Math.floor(minutes / 60)
|
||||
|
||||
if (language === 'fr') {
|
||||
if (seconds < 60) return 'à l\'instant'
|
||||
if (minutes < 60) return `il y a ${minutes}m`
|
||||
if (hours < 24) return `il y a ${hours}h`
|
||||
const days = Math.floor(hours / 24)
|
||||
return `il y a ${days}j`
|
||||
} else {
|
||||
if (seconds < 60) return 'just now'
|
||||
if (minutes < 60) return `${minutes}m ago`
|
||||
if (hours < 24) return `${hours}h ago`
|
||||
const days = Math.floor(hours / 24)
|
||||
return `${days}d ago`
|
||||
}
|
||||
if (seconds < 60) return t('time.justNow')
|
||||
if (minutes < 60) return t('time.minutesAgo', { count: minutes })
|
||||
if (hours < 24) return t('time.hoursAgo', { count: hours })
|
||||
const days = Math.floor(hours / 24)
|
||||
return t('time.daysAgo', { count: days })
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
'use client'
|
||||
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { useState, useEffect } from "react"
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
|
||||
interface ReminderDialogProps {
|
||||
open: boolean
|
||||
@@ -18,6 +21,7 @@ export function ReminderDialog({
|
||||
onSave,
|
||||
onRemove
|
||||
}: ReminderDialogProps) {
|
||||
const { t } = useLanguage()
|
||||
const [reminderDate, setReminderDate] = useState('')
|
||||
const [reminderTime, setReminderTime] = useState('')
|
||||
|
||||
@@ -51,7 +55,6 @@ export function ReminderDialog({
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent
|
||||
onInteractOutside={(event) => {
|
||||
// Prevent dialog from closing when interacting with Sonner toasts
|
||||
const target = event.target as HTMLElement;
|
||||
|
||||
const isSonnerElement =
|
||||
@@ -75,12 +78,12 @@ export function ReminderDialog({
|
||||
}}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Set Reminder</DialogTitle>
|
||||
<DialogTitle>{t('reminder.setReminder')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="reminder-date" className="text-sm font-medium">
|
||||
Date
|
||||
{t('reminder.reminderDate')}
|
||||
</label>
|
||||
<Input
|
||||
id="reminder-date"
|
||||
@@ -92,7 +95,7 @@ export function ReminderDialog({
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="reminder-time" className="text-sm font-medium">
|
||||
Time
|
||||
{t('reminder.reminderTime')}
|
||||
</label>
|
||||
<Input
|
||||
id="reminder-time"
|
||||
@@ -107,16 +110,16 @@ export function ReminderDialog({
|
||||
<div>
|
||||
{currentReminder && (
|
||||
<Button variant="outline" onClick={() => { onRemove(); onOpenChange(false); }}>
|
||||
Remove Reminder
|
||||
{t('reminder.removeReminder')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="ghost" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
{t('reminder.cancel')}
|
||||
</Button>
|
||||
<Button onClick={handleSave}>
|
||||
Set Reminder
|
||||
{t('reminder.save')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Label } from '@/components/ui/label'
|
||||
import { Loader2, Check } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { toast } from 'sonner'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
|
||||
interface SettingInputProps {
|
||||
label: string
|
||||
@@ -25,6 +26,7 @@ export function SettingInput({
|
||||
placeholder,
|
||||
disabled
|
||||
}: SettingInputProps) {
|
||||
const { t } = useLanguage()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [isSaved, setIsSaved] = useState(false)
|
||||
|
||||
@@ -35,15 +37,12 @@ export function SettingInput({
|
||||
try {
|
||||
await onChange(newValue)
|
||||
setIsSaved(true)
|
||||
toast.success('Setting saved')
|
||||
toast.success(t('toast.saved'))
|
||||
|
||||
// Clear saved indicator after 2 seconds
|
||||
setTimeout(() => setIsSaved(false), 2000)
|
||||
} catch (err) {
|
||||
console.error('Error updating setting:', err)
|
||||
toast.error('Failed to save setting', {
|
||||
description: 'Please try again'
|
||||
})
|
||||
toast.error(t('toast.saveFailed'))
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Label } from '@/components/ui/label'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { toast } from 'sonner'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
|
||||
interface SelectOption {
|
||||
value: string
|
||||
@@ -29,6 +30,7 @@ export function SettingSelect({
|
||||
onChange,
|
||||
disabled
|
||||
}: SettingSelectProps) {
|
||||
const { t } = useLanguage()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const handleChange = async (newValue: string) => {
|
||||
@@ -36,14 +38,10 @@ export function SettingSelect({
|
||||
|
||||
try {
|
||||
await onChange(newValue)
|
||||
toast.success('Setting saved', {
|
||||
description: `${label} has been updated`
|
||||
})
|
||||
toast.success(t('toast.saved'))
|
||||
} catch (err) {
|
||||
console.error('Error updating setting:', err)
|
||||
toast.error('Failed to save setting', {
|
||||
description: 'Please try again'
|
||||
})
|
||||
toast.error(t('toast.saveFailed'))
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Label } from '@/components/ui/label'
|
||||
import { Loader2, Check, X } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { toast } from 'sonner'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
|
||||
interface SettingToggleProps {
|
||||
label: string
|
||||
@@ -22,6 +23,7 @@ export function SettingToggle({
|
||||
onChange,
|
||||
disabled
|
||||
}: SettingToggleProps) {
|
||||
const { t } = useLanguage()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState(false)
|
||||
|
||||
@@ -31,15 +33,11 @@ export function SettingToggle({
|
||||
|
||||
try {
|
||||
await onChange(newChecked)
|
||||
toast.success('Setting saved', {
|
||||
description: `${label} has been ${newChecked ? 'enabled' : 'disabled'}`
|
||||
})
|
||||
toast.success(t('toast.saved'))
|
||||
} catch (err) {
|
||||
console.error('Error updating setting:', err)
|
||||
setError(true)
|
||||
toast.error('Failed to save setting', {
|
||||
description: 'Please try again'
|
||||
})
|
||||
toast.error(t('toast.saveFailed'))
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { Settings, Sparkles, Palette, User, Database, Info, Check } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
|
||||
interface SettingsSection {
|
||||
id: string
|
||||
@@ -18,41 +19,42 @@ interface SettingsNavProps {
|
||||
|
||||
export function SettingsNav({ className }: SettingsNavProps) {
|
||||
const pathname = usePathname()
|
||||
const { t } = useLanguage()
|
||||
|
||||
const sections: SettingsSection[] = [
|
||||
{
|
||||
id: 'general',
|
||||
label: 'General',
|
||||
label: t('generalSettings.title'),
|
||||
icon: <Settings className="h-5 w-5" />,
|
||||
href: '/settings/general'
|
||||
},
|
||||
{
|
||||
id: 'ai',
|
||||
label: 'AI',
|
||||
label: t('aiSettings.title'),
|
||||
icon: <Sparkles className="h-5 w-5" />,
|
||||
href: '/settings/ai'
|
||||
},
|
||||
{
|
||||
id: 'appearance',
|
||||
label: 'Appearance',
|
||||
label: t('appearance.title'),
|
||||
icon: <Palette className="h-5 w-5" />,
|
||||
href: '/settings/appearance'
|
||||
},
|
||||
{
|
||||
id: 'profile',
|
||||
label: 'Profile',
|
||||
label: t('profile.title'),
|
||||
icon: <User className="h-5 w-5" />,
|
||||
href: '/settings/profile'
|
||||
},
|
||||
{
|
||||
id: 'data',
|
||||
label: 'Data',
|
||||
label: t('dataManagement.title'),
|
||||
icon: <Database className="h-5 w-5" />,
|
||||
href: '/settings/data'
|
||||
},
|
||||
{
|
||||
id: 'about',
|
||||
label: 'About',
|
||||
label: t('about.title'),
|
||||
icon: <Info className="h-5 w-5" />,
|
||||
href: '/settings/about'
|
||||
}
|
||||
|
||||
@@ -109,11 +109,11 @@ export function Sidebar({ className, user }: { className?: string, user?: any })
|
||||
{/* Footer / Copyright / Terms */}
|
||||
<div className="mt-auto px-6 py-4 text-[10px] text-gray-400">
|
||||
<div className="flex gap-2 mb-1">
|
||||
<Link href="#" className="hover:underline">Confidentialité</Link>
|
||||
<Link href="#" className="hover:underline">{t('footer.privacy')}</Link>
|
||||
<span>•</span>
|
||||
<Link href="#" className="hover:underline">Conditions</Link>
|
||||
<Link href="#" className="hover:underline">{t('footer.terms')}</Link>
|
||||
</div>
|
||||
<p>Open Source Clone</p>
|
||||
<p>{t('footer.openSource')}</p>
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
|
||||
@@ -14,10 +14,12 @@ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { useSession, signOut } from 'next-auth/react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { LogOut, Settings, User, Shield } from 'lucide-react'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
|
||||
export function UserNav({ user }: { user?: any }) {
|
||||
const { data: session } = useSession()
|
||||
const router = useRouter()
|
||||
const { t } = useLanguage()
|
||||
|
||||
const currentUser = user || session?.user
|
||||
|
||||
@@ -51,23 +53,23 @@ export function UserNav({ user }: { user?: any }) {
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem onClick={() => router.push('/settings/profile')}>
|
||||
<User className="mr-2 h-4 w-4" />
|
||||
<span>Profile</span>
|
||||
<span>{t('nav.profile')}</span>
|
||||
</DropdownMenuItem>
|
||||
{userRole === 'ADMIN' && (
|
||||
<DropdownMenuItem onClick={() => router.push('/admin')}>
|
||||
<Shield className="mr-2 h-4 w-4" />
|
||||
<span>Admin Dashboard</span>
|
||||
<span>{t('nav.adminDashboard')}</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem onClick={() => router.push('/settings')}>
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
<span>Diagnostics</span>
|
||||
<span>{t('nav.diagnostics')}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => signOut({ callbackUrl: '/login' })}>
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
<span>Log out</span>
|
||||
<span>{t('nav.logout')}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
Reference in New Issue
Block a user