Refactor Admin and Settings UI to Ethereal Precision aesthetic and improve note import/export functionality
Some checks failed
Deploy to Production / Build and Deploy (push) Failing after 1m4s
Some checks failed
Deploy to Production / Build and Deploy (push) Failing after 1m4s
This commit is contained in:
@@ -1,9 +1,8 @@
|
||||
'use client'
|
||||
|
||||
import { SettingsSection } from '@/components/settings'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
import { FileText, Sparkles, MessageCircle } from 'lucide-react'
|
||||
|
||||
export default function AboutSettingsPage() {
|
||||
const { t } = useLanguage()
|
||||
@@ -11,126 +10,95 @@ export default function AboutSettingsPage() {
|
||||
const buildDate = '2026-01-17'
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold mb-2">{t('about.title')}</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
{t('about.description')}
|
||||
</p>
|
||||
<h1 className="text-2xl font-bold tracking-tight text-foreground">{t('about.title')}</h1>
|
||||
<p className="text-muted-foreground mt-1">{t('about.description')}</p>
|
||||
</div>
|
||||
|
||||
<SettingsSection
|
||||
title={t('about.appName')}
|
||||
icon={<span className="text-2xl">📝</span>}
|
||||
description={t('about.appDescription')}
|
||||
>
|
||||
<Card>
|
||||
<CardContent className="pt-6 space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="font-medium">{t('about.version')}</span>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* App info */}
|
||||
<div className="bg-card rounded-lg border border-border p-6 shadow-sm flex flex-col gap-4">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center text-primary shrink-0">
|
||||
<FileText className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-foreground">{t('about.appName')}</h3>
|
||||
<p className="text-sm text-muted-foreground">{t('about.appDescription')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3 pt-2 border-t border-border">
|
||||
<div className="flex justify-between items-center text-sm">
|
||||
<span className="text-muted-foreground">{t('about.version')}</span>
|
||||
<Badge variant="secondary">{version}</Badge>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="font-medium">{t('about.buildDate')}</span>
|
||||
<div className="flex justify-between items-center text-sm">
|
||||
<span className="text-muted-foreground">{t('about.buildDate')}</span>
|
||||
<Badge variant="outline">{buildDate}</Badge>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="font-medium">{t('about.platform')}</span>
|
||||
<div className="flex justify-between items-center text-sm">
|
||||
<span className="text-muted-foreground">{t('about.platform')}</span>
|
||||
<Badge variant="outline">{t('about.platformWeb')}</Badge>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</SettingsSection>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SettingsSection
|
||||
title={t('about.features.title')}
|
||||
icon={<span className="text-2xl">✨</span>}
|
||||
description={t('about.features.description')}
|
||||
>
|
||||
<Card>
|
||||
<CardContent className="pt-6 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-green-500">✓</span>
|
||||
<span>{t('about.features.titleSuggestions')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-green-500">✓</span>
|
||||
<span>{t('about.features.semanticSearch')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-green-500">✓</span>
|
||||
<span>{t('about.features.paragraphReformulation')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-green-500">✓</span>
|
||||
<span>{t('about.features.memoryEcho')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-green-500">✓</span>
|
||||
<span>{t('about.features.notebookOrganization')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-green-500">✓</span>
|
||||
<span>{t('about.features.dragDrop')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-green-500">✓</span>
|
||||
<span>{t('about.features.labelSystem')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-green-500">✓</span>
|
||||
<span>{t('about.features.multipleProviders')}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection
|
||||
title={t('about.technology.title')}
|
||||
icon={<span className="text-2xl">⚙️</span>}
|
||||
description={t('about.technology.description')}
|
||||
>
|
||||
<Card>
|
||||
<CardContent className="pt-6 space-y-2 text-sm">
|
||||
<div><strong>{t('about.technology.frontend')}:</strong> Next.js 16, React 19, TypeScript</div>
|
||||
<div><strong>{t('about.technology.backend')}:</strong> Next.js API Routes, Server Actions</div>
|
||||
<div><strong>{t('about.technology.database')}:</strong> SQLite (Prisma ORM)</div>
|
||||
<div><strong>{t('about.technology.authentication')}:</strong> NextAuth 5</div>
|
||||
<div><strong>{t('about.technology.ai')}:</strong> Vercel AI SDK, OpenAI, Ollama</div>
|
||||
<div><strong>{t('about.technology.ui')}:</strong> Radix UI, Tailwind CSS, Lucide Icons</div>
|
||||
<div><strong>{t('about.technology.testing')}:</strong> Playwright (E2E)</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection
|
||||
title={t('about.support.title')}
|
||||
icon={<span className="text-2xl">💬</span>}
|
||||
description={t('about.support.description')}
|
||||
>
|
||||
<Card>
|
||||
<CardContent className="pt-6 space-y-4">
|
||||
<div>
|
||||
<p className="font-medium mb-2">{t('about.support.documentation')}</p>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Check the documentation for detailed guides and tutorials.
|
||||
</p>
|
||||
{/* Features */}
|
||||
<div className="bg-card rounded-lg border border-border p-6 shadow-sm flex flex-col gap-4">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center text-primary shrink-0">
|
||||
<Sparkles className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium mb-2">{t('about.support.reportIssues')}</p>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Found a bug? Report it in the issue tracker.
|
||||
</p>
|
||||
<h3 className="font-semibold text-foreground">{t('about.features.title')}</h3>
|
||||
<p className="text-sm text-muted-foreground">{t('about.features.description')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<ul className="space-y-2 pt-2 border-t border-border">
|
||||
{[
|
||||
t('about.features.titleSuggestions'),
|
||||
t('about.features.semanticSearch'),
|
||||
t('about.features.paragraphReformulation'),
|
||||
t('about.features.memoryEcho'),
|
||||
t('about.features.notebookOrganization'),
|
||||
t('about.features.dragDrop'),
|
||||
t('about.features.labelSystem'),
|
||||
t('about.features.multipleProviders'),
|
||||
].map((feature) => (
|
||||
<li key={feature} className="flex items-center gap-2 text-sm text-foreground">
|
||||
<span className="text-primary font-bold">✓</span>
|
||||
{feature}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Support — full width */}
|
||||
<div className="md:col-span-2 bg-card rounded-lg border border-border p-6 shadow-sm">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center text-primary shrink-0">
|
||||
<MessageCircle className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium mb-2">{t('about.support.feedback')}</p>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
We value your feedback! Share your thoughts and suggestions.
|
||||
</p>
|
||||
<h3 className="font-semibold text-foreground">{t('about.support.title')}</h3>
|
||||
<p className="text-sm text-muted-foreground">{t('about.support.description')}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</SettingsSection>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 pt-4 border-t border-border">
|
||||
{[
|
||||
{ title: t('about.support.documentation'), desc: 'Guides détaillés et tutoriels.' },
|
||||
{ title: t('about.support.reportIssues'), desc: 'Signalez un bug dans le gestionnaire.' },
|
||||
{ title: t('about.support.feedback'), desc: 'Vos retours nous aident à améliorer l\'app.' },
|
||||
].map((item) => (
|
||||
<div key={item.title} className="p-3 rounded-md bg-muted">
|
||||
<p className="font-medium text-sm text-foreground">{item.title}</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">{item.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,11 +6,9 @@ export function AISettingsHeader() {
|
||||
const { t } = useLanguage()
|
||||
|
||||
return (
|
||||
<div className="mb-6">
|
||||
<h1 className="text-3xl font-bold">{t('aiSettings.title')}</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
{t('aiSettings.description')}
|
||||
</p>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-tight text-foreground">{t('aiSettings.title')}</h1>
|
||||
<p className="text-muted-foreground mt-1">{t('aiSettings.description')}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { SettingsSection, SettingSelect } from '@/components/settings'
|
||||
import { updateAISettings } from '@/app/actions/ai-settings'
|
||||
import { updateUserSettings } from '@/app/actions/user-settings'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
import { toast } from 'sonner'
|
||||
import { Palette, Type, LayoutGrid, Maximize2 } from 'lucide-react'
|
||||
|
||||
interface AppearanceSettingsClientProps {
|
||||
initialFontSize: string
|
||||
@@ -15,7 +15,13 @@ interface AppearanceSettingsClientProps {
|
||||
initialFontFamily?: string
|
||||
}
|
||||
|
||||
export function AppearanceSettingsClient({ initialFontSize, initialTheme, initialNotesViewMode, initialCardSizeMode = 'variable', initialFontFamily = 'inter' }: AppearanceSettingsClientProps) {
|
||||
export function AppearanceSettingsClient({
|
||||
initialFontSize,
|
||||
initialTheme,
|
||||
initialNotesViewMode,
|
||||
initialCardSizeMode = 'variable',
|
||||
initialFontFamily = 'inter',
|
||||
}: AppearanceSettingsClientProps) {
|
||||
const { t } = useLanguage()
|
||||
const [theme, setTheme] = useState(initialTheme || 'light')
|
||||
const [fontSize, setFontSize] = useState(initialFontSize || 'medium')
|
||||
@@ -26,11 +32,9 @@ export function AppearanceSettingsClient({ initialFontSize, initialTheme, initia
|
||||
const handleThemeChange = async (value: string) => {
|
||||
setTheme(value)
|
||||
localStorage.setItem('theme-preference', value)
|
||||
|
||||
const root = document.documentElement
|
||||
root.removeAttribute('data-theme')
|
||||
root.classList.remove('dark')
|
||||
|
||||
if (value === 'auto') {
|
||||
if (window.matchMedia('(prefers-color-scheme: dark)').matches) root.classList.add('dark')
|
||||
} else if (value === 'dark') {
|
||||
@@ -39,19 +43,14 @@ export function AppearanceSettingsClient({ initialFontSize, initialTheme, initia
|
||||
root.setAttribute('data-theme', value)
|
||||
if (['midnight'].includes(value)) root.classList.add('dark')
|
||||
}
|
||||
|
||||
await updateUserSettings({ theme: value as 'light' | 'dark' | 'auto' })
|
||||
await updateUserSettings({ theme: value as any })
|
||||
toast.success(t('settings.settingsSaved') || 'Saved')
|
||||
}
|
||||
|
||||
const handleFontSizeChange = async (value: string) => {
|
||||
setFontSize(value)
|
||||
|
||||
const fontSizeMap: Record<string, string> = {
|
||||
'small': '14px', 'medium': '16px', 'large': '18px', 'extra-large': '20px'
|
||||
}
|
||||
document.documentElement.style.setProperty('--user-font-size', fontSizeMap[value] || '16px')
|
||||
|
||||
const map: Record<string, string> = { small: '14px', medium: '16px', large: '18px', 'extra-large': '20px' }
|
||||
document.documentElement.style.setProperty('--user-font-size', map[value] || '16px')
|
||||
await updateAISettings({ fontSize: value as any })
|
||||
toast.success(t('settings.settingsSaved') || 'Saved')
|
||||
}
|
||||
@@ -76,31 +75,64 @@ export function AppearanceSettingsClient({ initialFontSize, initialTheme, initia
|
||||
setFontFamily(font)
|
||||
localStorage.setItem('font-family', font)
|
||||
const root = document.documentElement
|
||||
if (font === 'system') {
|
||||
root.classList.add('font-system')
|
||||
} else {
|
||||
root.classList.remove('font-system')
|
||||
}
|
||||
font === 'system' ? root.classList.add('font-system') : root.classList.remove('font-system')
|
||||
await updateAISettings({ fontFamily: font })
|
||||
toast.success(t('settings.settingsSaved') || 'Saved')
|
||||
}
|
||||
|
||||
const SelectCard = ({
|
||||
icon: Icon,
|
||||
title,
|
||||
description,
|
||||
value,
|
||||
options,
|
||||
onChange,
|
||||
}: {
|
||||
icon: React.ElementType
|
||||
title: string
|
||||
description: string
|
||||
value: string
|
||||
options: { value: string; label: string }[]
|
||||
onChange: (v: string) => void
|
||||
}) => (
|
||||
<div className="bg-card rounded-lg border border-border p-6 shadow-sm flex flex-col gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center text-primary shrink-0">
|
||||
<Icon className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-foreground">{title}</h3>
|
||||
<p className="text-sm text-muted-foreground">{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative mt-2">
|
||||
<select
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="w-full h-11 px-4 bg-muted border border-border rounded-lg text-foreground text-sm focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none appearance-none cursor-pointer transition-colors"
|
||||
>
|
||||
{options.map((o) => (
|
||||
<option key={o.value} value={o.value}>{o.label}</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="absolute right-4 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground">
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold mb-2">{t('appearance.title')}</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
{t('appearance.description')}
|
||||
</p>
|
||||
<h1 className="text-2xl font-bold tracking-tight text-foreground">{t('appearance.title')}</h1>
|
||||
<p className="text-muted-foreground mt-1">{t('appearance.description')}</p>
|
||||
</div>
|
||||
|
||||
<SettingsSection
|
||||
title={t('settings.theme')}
|
||||
icon={<span className="text-2xl">🎨</span>}
|
||||
description={t('settings.themeLight') + ' / ' + t('settings.themeDark')}
|
||||
>
|
||||
<SettingSelect
|
||||
label={t('settings.theme')}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<SelectCard
|
||||
icon={Palette}
|
||||
title={t('settings.theme')}
|
||||
description={t('appearance.selectTheme')}
|
||||
value={theme}
|
||||
options={[
|
||||
@@ -118,50 +150,36 @@ export function AppearanceSettingsClient({ initialFontSize, initialTheme, initia
|
||||
]}
|
||||
onChange={handleThemeChange}
|
||||
/>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection
|
||||
title={t('profile.fontSize')}
|
||||
icon={<span className="text-2xl">📝</span>}
|
||||
description={t('profile.fontSizeDescription')}
|
||||
>
|
||||
<SettingSelect
|
||||
label={t('profile.fontSize')}
|
||||
description={t('profile.selectFontSize')}
|
||||
<SelectCard
|
||||
icon={Type}
|
||||
title={t('profile.fontSize')}
|
||||
description={t('profile.fontSizeDescription')}
|
||||
value={fontSize}
|
||||
options={[
|
||||
{ value: 'small', label: t('profile.fontSizeSmall') },
|
||||
{ value: 'medium', label: t('profile.fontSizeMedium') },
|
||||
{ value: 'large', label: t('profile.fontSizeLarge') },
|
||||
{ value: 'extra-large', label: t('profile.fontSizeExtraLarge') },
|
||||
]}
|
||||
onChange={handleFontSizeChange}
|
||||
/>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection
|
||||
title={t('appearance.fontFamilyLabel') || 'Font Family'}
|
||||
icon={<span className="text-2xl">🔤</span>}
|
||||
description={t('appearance.fontFamilyDescription') || 'Choose the font used throughout the app'}
|
||||
>
|
||||
<SettingSelect
|
||||
label={t('appearance.fontFamilyLabel') || 'Font Family'}
|
||||
description={t('appearance.selectFontFamily') || 'Inter is optimized for readability, System uses your OS native font'}
|
||||
<SelectCard
|
||||
icon={Type}
|
||||
title={t('appearance.fontFamilyLabel') || 'Police'}
|
||||
description={t('appearance.fontFamilyDescription') || 'Choisissez la police de l\'application'}
|
||||
value={fontFamily}
|
||||
options={[
|
||||
{ value: 'inter', label: 'Inter' },
|
||||
{ value: 'system', label: t('appearance.fontSystem') || 'System Default' },
|
||||
{ value: 'system', label: t('appearance.fontSystem') || 'Système' },
|
||||
]}
|
||||
onChange={handleFontFamilyChange}
|
||||
/>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection
|
||||
title={t('appearance.notesViewLabel')}
|
||||
icon={<span className="text-2xl">📋</span>}
|
||||
description={t('appearance.notesViewDescription')}
|
||||
>
|
||||
<SettingSelect
|
||||
label={t('appearance.notesViewLabel')}
|
||||
<SelectCard
|
||||
icon={LayoutGrid}
|
||||
title={t('appearance.notesViewLabel')}
|
||||
description={t('appearance.notesViewDescription')}
|
||||
value={notesViewMode}
|
||||
options={[
|
||||
@@ -170,16 +188,11 @@ export function AppearanceSettingsClient({ initialFontSize, initialTheme, initia
|
||||
]}
|
||||
onChange={handleNotesViewChange}
|
||||
/>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection
|
||||
title={t('settings.cardSizeMode')}
|
||||
icon={<span className="text-2xl">📐</span>}
|
||||
description={t('settings.cardSizeModeDescription')}
|
||||
>
|
||||
<SettingSelect
|
||||
label={t('settings.cardSizeMode')}
|
||||
description={t('settings.selectCardSizeMode')}
|
||||
<SelectCard
|
||||
icon={Maximize2}
|
||||
title={t('settings.cardSizeMode')}
|
||||
description={t('settings.cardSizeModeDescription')}
|
||||
value={cardSizeMode}
|
||||
options={[
|
||||
{ value: 'variable', label: t('settings.cardSizeVariable') },
|
||||
@@ -187,7 +200,7 @@ export function AppearanceSettingsClient({ initialFontSize, initialTheme, initia
|
||||
]}
|
||||
onChange={handleCardSizeModeChange}
|
||||
/>
|
||||
</SettingsSection>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { SettingsSection } from '@/components/settings'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Download, Upload, Trash2, Loader2 } from 'lucide-react'
|
||||
import { Download, Upload, Trash2, Loader2, RefreshCw, Sparkles, Database } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
import { useRouter } from 'next/navigation'
|
||||
@@ -14,6 +13,8 @@ export default function DataSettingsPage() {
|
||||
const [isExporting, setIsExporting] = useState(false)
|
||||
const [isImporting, setIsImporting] = useState(false)
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
const [isReindexing, setIsReindexing] = useState(false)
|
||||
const [isCleaningUp, setIsCleaningUp] = useState(false)
|
||||
|
||||
const handleExport = async () => {
|
||||
setIsExporting(true)
|
||||
@@ -24,15 +25,16 @@ export default function DataSettingsPage() {
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `memento-note-export-${new Date().toISOString().split('T')[0]}.json`
|
||||
a.download = `memento-export-${new Date().toISOString().split('T')[0]}.json`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
window.URL.revokeObjectURL(url)
|
||||
toast.success(t('dataManagement.export.success'))
|
||||
} else {
|
||||
throw new Error()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Export error:', error)
|
||||
} catch {
|
||||
toast.error(t('dataManagement.export.failed'))
|
||||
} finally {
|
||||
setIsExporting(false)
|
||||
@@ -42,47 +44,73 @@ export default function DataSettingsPage() {
|
||||
const handleImport = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
setIsImporting(true)
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
const response = await fetch('/api/notes/import', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
|
||||
const response = await fetch('/api/notes/import', { method: 'POST', body: formData })
|
||||
if (response.ok) {
|
||||
const result = await response.json()
|
||||
toast.success(t('dataManagement.import.success', { count: result.count }))
|
||||
router.refresh()
|
||||
} else {
|
||||
throw new Error('Import failed')
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || 'Import failed')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Import error:', error)
|
||||
toast.error(t('dataManagement.import.failed'))
|
||||
} catch (err: any) {
|
||||
toast.error(err.message || t('dataManagement.import.failed'))
|
||||
} finally {
|
||||
setIsImporting(false)
|
||||
event.target.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteAll = async () => {
|
||||
if (!confirm(t('dataManagement.delete.confirm'))) {
|
||||
return
|
||||
const handleReindex = async () => {
|
||||
setIsReindexing(true)
|
||||
try {
|
||||
const response = await fetch('/api/notes/reindex', { method: 'POST' })
|
||||
if (response.ok) {
|
||||
const result = await response.json()
|
||||
toast.success(t('dataManagement.indexing.success', { count: result.count }))
|
||||
} else {
|
||||
throw new Error()
|
||||
}
|
||||
} catch {
|
||||
toast.error(t('dataManagement.indexing.failed'))
|
||||
} finally {
|
||||
setIsReindexing(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCleanup = async () => {
|
||||
setIsCleaningUp(true)
|
||||
try {
|
||||
const response = await fetch('/api/notes/cleanup', { method: 'POST' })
|
||||
if (response.ok) {
|
||||
const result = await response.json()
|
||||
toast.success(t('dataManagement.cleanup.success', { count: result.deletedLabels }))
|
||||
} else {
|
||||
throw new Error()
|
||||
}
|
||||
} catch {
|
||||
toast.error(t('dataManagement.cleanup.failed'))
|
||||
} finally {
|
||||
setIsCleaningUp(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteAll = async () => {
|
||||
if (!confirm(t('dataManagement.delete.confirm'))) return
|
||||
setIsDeleting(true)
|
||||
try {
|
||||
const response = await fetch('/api/notes/delete-all', { method: 'POST' })
|
||||
if (response.ok) {
|
||||
toast.success(t('dataManagement.delete.success'))
|
||||
router.refresh()
|
||||
} else {
|
||||
throw new Error()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Delete error:', error)
|
||||
} catch {
|
||||
toast.error(t('dataManagement.delete.failed'))
|
||||
} finally {
|
||||
setIsDeleting(false)
|
||||
@@ -90,102 +118,114 @@ export default function DataSettingsPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold mb-2">{t('dataManagement.title')}</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
{t('dataManagement.toolsDescription')}
|
||||
</p>
|
||||
<div className="max-w-4xl mx-auto space-y-8 p-6">
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-3xl font-bold tracking-tight text-foreground">{t('dataManagement.title')}</h1>
|
||||
<p className="text-muted-foreground">{t('dataManagement.toolsDescription')}</p>
|
||||
</div>
|
||||
|
||||
<SettingsSection
|
||||
title={`💾 ${t('dataManagement.export.title')}`}
|
||||
icon={<span className="text-2xl">💾</span>}
|
||||
description={t('dataManagement.export.description')}
|
||||
>
|
||||
<div className="flex items-center justify-between py-4">
|
||||
<div>
|
||||
<p className="font-medium">{t('dataManagement.export.title')}</p>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{t('dataManagement.export.description')}
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Export card */}
|
||||
<div className="bg-card rounded-xl border border-border p-6 shadow-sm flex flex-col justify-between transition-all hover:shadow-md">
|
||||
<div className="space-y-4">
|
||||
<div className="w-12 h-12 rounded-full bg-blue-500/10 flex items-center justify-center text-blue-600 shrink-0">
|
||||
<Download className="h-6 w-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-foreground">{t('dataManagement.export.title')}</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1 leading-relaxed">
|
||||
{t('dataManagement.export.description')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleExport}
|
||||
disabled={isExporting}
|
||||
>
|
||||
{isExporting ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
||||
) : (
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
<Button onClick={handleExport} disabled={isExporting} className="mt-6 w-full">
|
||||
{isExporting ? <Loader2 className="h-4 w-4 animate-spin mr-2" /> : <Download className="h-4 w-4 mr-2" />}
|
||||
{isExporting ? t('dataManagement.exporting') : t('dataManagement.export.button')}
|
||||
</Button>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection
|
||||
title={`📥 ${t('dataManagement.import.title')}`}
|
||||
icon={<span className="text-2xl">📥</span>}
|
||||
description={t('dataManagement.import.description')}
|
||||
>
|
||||
<div className="flex items-center justify-between py-4">
|
||||
<div>
|
||||
<p className="font-medium">{t('dataManagement.import.title')}</p>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{t('dataManagement.import.description')}
|
||||
</p>
|
||||
{/* Import card */}
|
||||
<div className="bg-card rounded-xl border border-border p-6 shadow-sm flex flex-col justify-between transition-all hover:shadow-md">
|
||||
<div className="space-y-4">
|
||||
<div className="w-12 h-12 rounded-full bg-emerald-500/10 flex items-center justify-center text-emerald-600 shrink-0">
|
||||
<Upload className="h-6 w-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-foreground">{t('dataManagement.import.title')}</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1 leading-relaxed">
|
||||
{t('dataManagement.import.description')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<input type="file" accept=".json" onChange={handleImport} disabled={isImporting} className="hidden" id="import-file" />
|
||||
<Button onClick={() => document.getElementById('import-file')?.click()} disabled={isImporting} variant="outline" className="mt-6 w-full">
|
||||
{isImporting ? <Loader2 className="h-4 w-4 animate-spin mr-2" /> : <Upload className="h-4 w-4 mr-2" />}
|
||||
{isImporting ? t('dataManagement.importing') : t('dataManagement.import.button')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Reindex card */}
|
||||
<div className="bg-card rounded-xl border border-border p-6 shadow-sm flex flex-col justify-between transition-all hover:shadow-md">
|
||||
<div className="space-y-4">
|
||||
<div className="w-12 h-12 rounded-full bg-amber-500/10 flex items-center justify-center text-amber-600 shrink-0">
|
||||
<Sparkles className="h-6 w-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-foreground">{t('dataManagement.indexing.title')}</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1 leading-relaxed">
|
||||
{t('dataManagement.indexing.description')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={handleReindex} disabled={isReindexing} variant="secondary" className="mt-6 w-full">
|
||||
{isReindexing ? <Loader2 className="h-4 w-4 animate-spin mr-2" /> : <RefreshCw className="h-4 w-4 mr-2" />}
|
||||
{isReindexing ? t('dataManagement.exporting') : t('dataManagement.indexing.button')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Cleanup card */}
|
||||
<div className="bg-card rounded-xl border border-border p-6 shadow-sm flex flex-col justify-between transition-all hover:shadow-md">
|
||||
<div className="space-y-4">
|
||||
<div className="w-12 h-12 rounded-full bg-purple-500/10 flex items-center justify-center text-purple-600 shrink-0">
|
||||
<Database className="h-6 w-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-foreground">{t('dataManagement.cleanup.title')}</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1 leading-relaxed">
|
||||
{t('dataManagement.cleanup.description')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={handleCleanup} disabled={isCleaningUp} variant="secondary" className="mt-6 w-full">
|
||||
{isCleaningUp ? <Loader2 className="h-4 w-4 animate-spin mr-2" /> : <Database className="h-4 w-4 mr-2" />}
|
||||
{isCleaningUp ? t('dataManagement.exporting') : t('dataManagement.cleanup.button')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Danger zone */}
|
||||
<div className="bg-destructive/5 rounded-xl border border-destructive/20 p-6 shadow-sm mt-12">
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<div className="w-12 h-12 rounded-full bg-destructive/10 flex items-center justify-center text-destructive shrink-0">
|
||||
<Trash2 className="h-6 w-6" />
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="file"
|
||||
accept=".json"
|
||||
onChange={handleImport}
|
||||
disabled={isImporting}
|
||||
className="hidden"
|
||||
id="import-file"
|
||||
/>
|
||||
<Button
|
||||
onClick={() => document.getElementById('import-file')?.click()}
|
||||
disabled={isImporting}
|
||||
>
|
||||
{isImporting ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
||||
) : (
|
||||
<Upload className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
{isImporting ? t('dataManagement.importing') : t('dataManagement.import.button')}
|
||||
</Button>
|
||||
<h3 className="text-xl font-bold text-destructive">{t('dataManagement.dangerZone')}</h3>
|
||||
<p className="text-sm text-muted-foreground">{t('dataManagement.dangerZoneDescription')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection
|
||||
title={`⚠️ ${t('dataManagement.dangerZone')}`}
|
||||
icon={<span className="text-2xl">⚠️</span>}
|
||||
description={t('dataManagement.dangerZoneDescription')}
|
||||
>
|
||||
<div className="flex items-center justify-between py-4 border-t border-red-200 dark:border-red-900">
|
||||
<div>
|
||||
<p className="font-medium text-red-600 dark:text-red-400">{t('dataManagement.delete.title')}</p>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{t('dataManagement.delete.description')}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between p-4 bg-background/50 rounded-lg border border-destructive/10 gap-4">
|
||||
<div className="space-y-1">
|
||||
<p className="font-semibold text-destructive">{t('dataManagement.delete.title')}</p>
|
||||
<p className="text-sm text-muted-foreground">{t('dataManagement.delete.description')}</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleDeleteAll}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
{isDeleting ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
||||
) : (
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
<Button variant="destructive" onClick={handleDeleteAll} disabled={isDeleting} className="shrink-0 w-full sm:w-auto">
|
||||
{isDeleting ? <Loader2 className="h-4 w-4 animate-spin mr-2" /> : <Trash2 className="h-4 w-4 mr-2" />}
|
||||
{isDeleting ? t('dataManagement.deleting') : t('dataManagement.delete.button')}
|
||||
</Button>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { SettingsSection, SettingToggle, SettingSelect } from '@/components/settings'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
import { updateAISettings } from '@/app/actions/ai-settings'
|
||||
import { toast } from 'sonner'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Globe, Bell } from 'lucide-react'
|
||||
|
||||
interface GeneralSettingsClientProps {
|
||||
initialSettings: {
|
||||
@@ -25,7 +25,6 @@ export function GeneralSettingsClient({ initialSettings }: GeneralSettingsClient
|
||||
const handleLanguageChange = async (value: string) => {
|
||||
setLanguage(value)
|
||||
await updateAISettings({ preferredLanguage: value as any })
|
||||
|
||||
if (value === 'auto') {
|
||||
localStorage.removeItem('user-language')
|
||||
toast.success(t('settings.languageAuto') || 'Language set to Auto')
|
||||
@@ -34,7 +33,6 @@ export function GeneralSettingsClient({ initialSettings }: GeneralSettingsClient
|
||||
setContextLanguage(value as any)
|
||||
toast.success(t('profile.languageUpdateSuccess') || 'Language updated')
|
||||
}
|
||||
|
||||
setTimeout(() => router.refresh(), 300)
|
||||
}
|
||||
|
||||
@@ -51,63 +49,102 @@ export function GeneralSettingsClient({ initialSettings }: GeneralSettingsClient
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-8">
|
||||
{/* Page title */}
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold mb-2">{t('generalSettings.title')}</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
{t('generalSettings.description')}
|
||||
</p>
|
||||
<h1 className="text-2xl font-bold tracking-tight text-foreground">{t('generalSettings.title')}</h1>
|
||||
<p className="text-muted-foreground mt-1">{t('generalSettings.description')}</p>
|
||||
</div>
|
||||
|
||||
<SettingsSection
|
||||
title={t('settings.language')}
|
||||
icon={<span className="text-2xl">🌍</span>}
|
||||
description={t('profile.languagePreferencesDescription')}
|
||||
>
|
||||
<SettingSelect
|
||||
label={t('settings.language')}
|
||||
description={t('settings.selectLanguage')}
|
||||
value={language}
|
||||
options={[
|
||||
{ value: 'auto', label: t('profile.autoDetect') },
|
||||
{ value: 'en', label: 'English' },
|
||||
{ value: 'fr', label: 'Français' },
|
||||
{ value: 'es', label: 'Español' },
|
||||
{ value: 'de', label: 'Deutsch' },
|
||||
{ value: 'fa', label: 'فارسی' },
|
||||
{ value: 'it', label: 'Italiano' },
|
||||
{ value: 'pt', label: 'Português' },
|
||||
{ value: 'ru', label: 'Русский' },
|
||||
{ value: 'zh', label: '中文' },
|
||||
{ value: 'ja', label: '日本語' },
|
||||
{ value: 'ko', label: '한국어' },
|
||||
{ value: 'ar', label: 'العربية' },
|
||||
{ value: 'hi', label: 'हिन्दी' },
|
||||
{ value: 'nl', label: 'Nederlands' },
|
||||
{ value: 'pl', label: 'Polski' },
|
||||
]}
|
||||
onChange={handleLanguageChange}
|
||||
/>
|
||||
</SettingsSection>
|
||||
{/* 2-column card grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Language card */}
|
||||
<div className="bg-card rounded-lg border border-border p-6 shadow-sm flex flex-col gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center text-primary shrink-0">
|
||||
<Globe className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-foreground">{t('settings.language')}</h3>
|
||||
<p className="text-sm text-muted-foreground">{t('settings.selectLanguage')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative mt-2">
|
||||
<select
|
||||
value={language}
|
||||
onChange={(e) => handleLanguageChange(e.target.value)}
|
||||
className="w-full h-11 px-4 bg-muted border border-border rounded-lg text-foreground text-sm focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none appearance-none cursor-pointer transition-colors"
|
||||
>
|
||||
<option value="auto">{t('profile.autoDetect')}</option>
|
||||
<option value="en">English</option>
|
||||
<option value="fr">Français</option>
|
||||
<option value="es">Español</option>
|
||||
<option value="de">Deutsch</option>
|
||||
<option value="fa">فارسی</option>
|
||||
<option value="it">Italiano</option>
|
||||
<option value="pt">Português</option>
|
||||
<option value="ru">Русский</option>
|
||||
<option value="zh">中文</option>
|
||||
<option value="ja">日本語</option>
|
||||
<option value="ko">한국어</option>
|
||||
<option value="ar">العربية</option>
|
||||
<option value="hi">हिन्दी</option>
|
||||
<option value="nl">Nederlands</option>
|
||||
<option value="pl">Polski</option>
|
||||
</select>
|
||||
<div className="absolute right-4 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground">
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SettingsSection
|
||||
title={t('settings.notifications')}
|
||||
icon={<span className="text-2xl">🔔</span>}
|
||||
description={t('settings.notificationsDesc')}
|
||||
>
|
||||
<SettingToggle
|
||||
label={t('settings.emailNotifications')}
|
||||
description={t('settings.emailNotificationsDesc')}
|
||||
checked={emailNotifications}
|
||||
onChange={handleEmailNotificationsChange}
|
||||
/>
|
||||
<SettingToggle
|
||||
label={t('settings.desktopNotifications')}
|
||||
description={t('settings.desktopNotificationsDesc')}
|
||||
checked={desktopNotifications}
|
||||
onChange={handleDesktopNotificationsChange}
|
||||
/>
|
||||
</SettingsSection>
|
||||
{/* Notifications card */}
|
||||
<div className="bg-card rounded-lg border border-border p-6 shadow-sm flex flex-col gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center text-primary shrink-0">
|
||||
<Bell className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-foreground">{t('settings.notifications')}</h3>
|
||||
<p className="text-sm text-muted-foreground">{t('settings.notificationsDesc')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 space-y-5">
|
||||
{/* Email toggle */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-foreground">{t('settings.emailNotifications')}</p>
|
||||
<p className="text-xs text-muted-foreground">{t('settings.emailNotificationsDesc')}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={emailNotifications}
|
||||
onClick={() => handleEmailNotificationsChange(!emailNotifications)}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-primary/30 ${emailNotifications ? 'bg-primary' : 'bg-muted-foreground/30'}`}
|
||||
>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform ${emailNotifications ? 'translate-x-6' : 'translate-x-1'}`} />
|
||||
</button>
|
||||
</div>
|
||||
{/* Desktop toggle */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-foreground">{t('settings.desktopNotifications')}</p>
|
||||
<p className="text-xs text-muted-foreground">{t('settings.desktopNotificationsDesc')}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={desktopNotifications}
|
||||
onClick={() => handleDesktopNotificationsChange(!desktopNotifications)}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-primary/30 ${desktopNotifications ? 'bg-primary' : 'bg-muted-foreground/30'}`}
|
||||
>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform ${desktopNotifications ? 'translate-x-6' : 'translate-x-1'}`} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,17 +8,17 @@ export default function SettingsLayout({
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<div className="container mx-auto py-10 px-4 max-w-6xl">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
||||
{/* Sidebar Navigation */}
|
||||
<aside className="lg:col-span-1">
|
||||
<SettingsNav />
|
||||
</aside>
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Horizontal Tab Navigation */}
|
||||
<header className="flex items-center gap-1 px-8 bg-background border-b border-border shrink-0">
|
||||
<SettingsNav />
|
||||
</header>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="lg:col-span-3 space-y-6">
|
||||
{/* Page Content */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className="max-w-5xl mx-auto px-8 py-8 space-y-8">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -5,10 +5,7 @@ import { listMcpKeys, getMcpServerStatus } from '@/app/actions/mcp-keys'
|
||||
|
||||
export default async function McpSettingsPage() {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user) {
|
||||
redirect('/api/auth/signin')
|
||||
}
|
||||
if (!session?.user) redirect('/api/auth/signin')
|
||||
|
||||
const [keys, serverStatus] = await Promise.all([
|
||||
listMcpKeys(),
|
||||
@@ -16,7 +13,11 @@ export default async function McpSettingsPage() {
|
||||
])
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-tight text-foreground">Paramètres MCP</h1>
|
||||
<p className="text-muted-foreground mt-1">Gérez vos clés API et serveurs MCP connectés.</p>
|
||||
</div>
|
||||
<McpSettingsPanel initialKeys={keys} serverStatus={serverStatus} />
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,179 +1,92 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
|
||||
import { updateProfile, changePassword, updateFontSize, updateShowRecentNotes } from '@/app/actions/profile'
|
||||
import { updateProfile, changePassword } from '@/app/actions/profile'
|
||||
import { toast } from 'sonner'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
|
||||
|
||||
import { User, Lock } from 'lucide-react'
|
||||
|
||||
export function ProfileForm({ user, userAISettings }: { user: any; userAISettings?: any }) {
|
||||
const router = useRouter()
|
||||
|
||||
const [fontSize, setFontSize] = useState(userAISettings?.fontSize || 'medium')
|
||||
const [isUpdatingFontSize, setIsUpdatingFontSize] = useState(false)
|
||||
const [showRecentNotes, setShowRecentNotes] = useState(userAISettings?.showRecentNotes ?? false)
|
||||
const [isUpdatingRecentNotes, setIsUpdatingRecentNotes] = useState(false)
|
||||
const { t } = useLanguage()
|
||||
|
||||
const FONT_SIZES = [
|
||||
{ value: 'small', label: t('profile.fontSizeSmall'), size: '14px' },
|
||||
{ value: 'medium', label: t('profile.fontSizeMedium'), size: '16px' },
|
||||
{ value: 'large', label: t('profile.fontSizeLarge'), size: '18px' },
|
||||
{ value: 'extra-large', label: t('profile.fontSizeExtraLarge'), size: '20px' },
|
||||
]
|
||||
|
||||
const handleFontSizeChange = async (size: string) => {
|
||||
setIsUpdatingFontSize(true)
|
||||
try {
|
||||
const result = await updateFontSize(size)
|
||||
if (result?.error) {
|
||||
toast.error(t('profile.fontSizeUpdateFailed'))
|
||||
} else {
|
||||
setFontSize(size)
|
||||
// Apply font size immediately
|
||||
applyFontSize(size)
|
||||
toast.success(t('profile.fontSizeUpdateSuccess'))
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(t('profile.fontSizeUpdateFailed'))
|
||||
} finally {
|
||||
setIsUpdatingFontSize(false)
|
||||
}
|
||||
}
|
||||
|
||||
const applyFontSize = (size: string) => {
|
||||
// Base font size in pixels (16px is standard)
|
||||
const fontSizeMap = {
|
||||
'small': '14px', // ~87% of 16px
|
||||
'medium': '16px', // 100% (standard)
|
||||
'large': '18px', // ~112% of 16px
|
||||
'extra-large': '20px' // 125% of 16px
|
||||
}
|
||||
const fontSizeFactorMap = {
|
||||
'small': 0.95,
|
||||
'medium': 1.0,
|
||||
'large': 1.1,
|
||||
'extra-large': 1.25
|
||||
}
|
||||
const fontSizeValue = fontSizeMap[size as keyof typeof fontSizeMap] || '16px'
|
||||
const fontSizeFactor = fontSizeFactorMap[size as keyof typeof fontSizeFactorMap] || 1.0
|
||||
|
||||
document.documentElement.style.setProperty('--user-font-size', fontSizeValue)
|
||||
document.documentElement.style.setProperty('--user-font-size-factor', fontSizeFactor.toString())
|
||||
localStorage.setItem('user-font-size', size)
|
||||
}
|
||||
|
||||
// Apply saved font size on mount
|
||||
useEffect(() => {
|
||||
const savedFontSize = localStorage.getItem('user-font-size') || userAISettings?.fontSize || 'medium'
|
||||
applyFontSize(savedFontSize as string)
|
||||
}, [])
|
||||
|
||||
|
||||
|
||||
const handleShowRecentNotesChange = async (enabled: boolean) => {
|
||||
setIsUpdatingRecentNotes(true)
|
||||
const previousValue = showRecentNotes
|
||||
|
||||
try {
|
||||
const result = await updateShowRecentNotes(enabled)
|
||||
if (result?.error) {
|
||||
toast.error(result.error)
|
||||
} else {
|
||||
setShowRecentNotes(enabled)
|
||||
toast.success(t('profile.recentNotesUpdateSuccess') || 'Paramètre mis à jour')
|
||||
// Force full page reload to ensure settings are reloaded
|
||||
window.location.href = '/settings/profile'
|
||||
}
|
||||
} catch (error: any) {
|
||||
setShowRecentNotes(previousValue)
|
||||
toast.error(error?.message || 'Erreur')
|
||||
} finally {
|
||||
setIsUpdatingRecentNotes(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('profile.title')}</CardTitle>
|
||||
<CardDescription>{t('profile.description')}</CardDescription>
|
||||
</CardHeader>
|
||||
<form action={async (formData) => {
|
||||
const result = await updateProfile({ name: formData.get('name') as string })
|
||||
if (result?.error) {
|
||||
toast.error(t('profile.updateFailed'))
|
||||
} else {
|
||||
toast.success(t('profile.updateSuccess'))
|
||||
}
|
||||
}}>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="name" className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">{t('profile.displayName')}</label>
|
||||
<Input id="name" name="name" defaultValue={user.name} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="email" className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">{t('profile.email')}</label>
|
||||
<Input id="email" value={user.email} disabled className="bg-muted" />
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button type="submit">{t('general.save')}</Button>
|
||||
</CardFooter>
|
||||
</form>
|
||||
</Card>
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-tight text-foreground">{t('profile.title')}</h1>
|
||||
<p className="text-muted-foreground mt-1">{t('profile.description')}</p>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('profile.changePassword')}</CardTitle>
|
||||
<CardDescription>{t('profile.changePasswordDescription')}</CardDescription>
|
||||
</CardHeader>
|
||||
<form action={async (formData) => {
|
||||
const result = await changePassword(formData)
|
||||
if (result?.error) {
|
||||
const msg = '_form' in result.error
|
||||
? result.error._form[0]
|
||||
: result.error.currentPassword?.[0] || result.error.newPassword?.[0] || result.error.confirmPassword?.[0] || t('profile.passwordChangeFailed')
|
||||
toast.error(msg)
|
||||
} else {
|
||||
toast.success(t('profile.passwordChangeSuccess'))
|
||||
// Reset form manually or redirect
|
||||
const form = document.querySelector('form#password-form') as HTMLFormElement
|
||||
form?.reset()
|
||||
}
|
||||
}} id="password-form">
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="currentPassword" className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">{t('profile.currentPassword')}</label>
|
||||
<Input id="currentPassword" name="currentPassword" type="password" required />
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Profile info card */}
|
||||
<div className="bg-card rounded-lg border border-border p-6 shadow-sm flex flex-col gap-4">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center text-primary shrink-0">
|
||||
<User className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="newPassword" className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">{t('profile.newPassword')}</label>
|
||||
<Input id="newPassword" name="newPassword" type="password" required minLength={6} />
|
||||
<div>
|
||||
<h3 className="font-semibold text-foreground">{t('profile.title')}</h3>
|
||||
<p className="text-sm text-muted-foreground">{t('profile.description')}</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="confirmPassword" className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">{t('profile.confirmPassword')}</label>
|
||||
<Input id="confirmPassword" name="confirmPassword" type="password" required minLength={6} />
|
||||
</div>
|
||||
<form action={async (formData) => {
|
||||
const result = await updateProfile({ name: formData.get('name') as string })
|
||||
if (result?.error) toast.error(t('profile.updateFailed'))
|
||||
else toast.success(t('profile.updateSuccess'))
|
||||
}} className="space-y-4">
|
||||
<div className="space-y-1.5">
|
||||
<label htmlFor="name" className="text-sm font-medium text-foreground">{t('profile.displayName')}</label>
|
||||
<Input id="name" name="name" defaultValue={user.name} className="bg-muted border-border focus:border-primary" />
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button type="submit">{t('profile.updatePassword')}</Button>
|
||||
</CardFooter>
|
||||
</form>
|
||||
</Card>
|
||||
<div className="space-y-1.5">
|
||||
<label htmlFor="email" className="text-sm font-medium text-foreground">{t('profile.email')}</label>
|
||||
<Input id="email" value={user.email} disabled className="bg-muted border-border opacity-60" />
|
||||
</div>
|
||||
<Button type="submit" className="w-full mt-2">{t('general.save')}</Button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Password card */}
|
||||
<div className="bg-card rounded-lg border border-border p-6 shadow-sm flex flex-col gap-4">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center text-primary shrink-0">
|
||||
<Lock className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-foreground">{t('profile.changePassword')}</h3>
|
||||
<p className="text-sm text-muted-foreground">{t('profile.changePasswordDescription')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<form action={async (formData) => {
|
||||
const result = await changePassword(formData)
|
||||
if (result?.error) {
|
||||
const msg = '_form' in result.error
|
||||
? result.error._form[0]
|
||||
: result.error.currentPassword?.[0] || result.error.newPassword?.[0] || result.error.confirmPassword?.[0] || t('profile.passwordChangeFailed')
|
||||
toast.error(msg)
|
||||
} else {
|
||||
toast.success(t('profile.passwordChangeSuccess'))
|
||||
const form = document.querySelector('form#password-form') as HTMLFormElement
|
||||
form?.reset()
|
||||
}
|
||||
}} id="password-form" className="space-y-4">
|
||||
<div className="space-y-1.5">
|
||||
<label htmlFor="currentPassword" className="text-sm font-medium text-foreground">{t('profile.currentPassword')}</label>
|
||||
<Input id="currentPassword" name="currentPassword" type="password" required className="bg-muted border-border focus:border-primary" />
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label htmlFor="newPassword" className="text-sm font-medium text-foreground">{t('profile.newPassword')}</label>
|
||||
<Input id="newPassword" name="newPassword" type="password" required minLength={6} className="bg-muted border-border focus:border-primary" />
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label htmlFor="confirmPassword" className="text-sm font-medium text-foreground">{t('profile.confirmPassword')}</label>
|
||||
<Input id="confirmPassword" name="confirmPassword" type="password" required minLength={6} className="bg-muted border-border focus:border-primary" />
|
||||
</div>
|
||||
<Button type="submit" className="w-full mt-2">{t('profile.updatePassword')}</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user