fix: unify theme system - fix theme switching persistence

- Unified localStorage key to 'theme-preference' across all components
- Fixed header.tsx using wrong localStorage key ('theme' instead of 'theme-preference')
- Added localStorage hybrid persistence for instant theme changes
- Removed router.refresh() which was causing stale data revert
- Replaced Blue theme with Sepia
- Consolidated auth() calls to prevent race conditions
- Updated UserSettingsData types to include all themes
This commit is contained in:
2026-01-18 22:33:41 +01:00
parent ef60dafd73
commit ddb67ba9e5
306 changed files with 59580 additions and 6063 deletions

View File

@@ -9,136 +9,126 @@ export default function AboutSettingsPage() {
const buildDate = '2026-01-17'
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>
{/* Main Content */}
<main className="lg:col-span-3 space-y-6">
<div>
<h1 className="text-3xl font-bold mb-2">About</h1>
<p className="text-gray-600 dark:text-gray-400">
Information about the application
</p>
</div>
<SettingsSection
title="Keep Notes"
icon={<span className="text-2xl">📝</span>}
description="A powerful note-taking application with AI-powered features"
>
<Card>
<CardContent className="pt-6 space-y-4">
<div className="flex justify-between items-center">
<span className="font-medium">Version</span>
<Badge variant="secondary">{version}</Badge>
</div>
<div className="flex justify-between items-center">
<span className="font-medium">Build Date</span>
<Badge variant="outline">{buildDate}</Badge>
</div>
<div className="flex justify-between items-center">
<span className="font-medium">Platform</span>
<Badge variant="outline">Web</Badge>
</div>
</CardContent>
</Card>
</SettingsSection>
<SettingsSection
title="Features"
icon={<span className="text-2xl"></span>}
description="AI-powered capabilities"
>
<Card>
<CardContent className="pt-6 space-y-2">
<div className="flex items-center gap-2">
<span className="text-green-500"></span>
<span>AI-powered title suggestions</span>
</div>
<div className="flex items-center gap-2">
<span className="text-green-500"></span>
<span>Semantic search with embeddings</span>
</div>
<div className="flex items-center gap-2">
<span className="text-green-500"></span>
<span>Paragraph reformulation</span>
</div>
<div className="flex items-center gap-2">
<span className="text-green-500"></span>
<span>Memory Echo daily insights</span>
</div>
<div className="flex items-center gap-2">
<span className="text-green-500"></span>
<span>Notebook organization</span>
</div>
<div className="flex items-center gap-2">
<span className="text-green-500"></span>
<span>Drag & drop note management</span>
</div>
<div className="flex items-center gap-2">
<span className="text-green-500"></span>
<span>Label system</span>
</div>
<div className="flex items-center gap-2">
<span className="text-green-500"></span>
<span>Multiple AI providers (OpenAI, Ollama)</span>
</div>
</CardContent>
</Card>
</SettingsSection>
<SettingsSection
title="Technology Stack"
icon={<span className="text-2xl"></span>}
description="Built with modern technologies"
>
<Card>
<CardContent className="pt-6 space-y-2 text-sm">
<div><strong>Frontend:</strong> Next.js 16, React 19, TypeScript</div>
<div><strong>Backend:</strong> Next.js API Routes, Server Actions</div>
<div><strong>Database:</strong> SQLite (Prisma ORM)</div>
<div><strong>Authentication:</strong> NextAuth 5</div>
<div><strong>AI:</strong> Vercel AI SDK, OpenAI, Ollama</div>
<div><strong>UI:</strong> Radix UI, Tailwind CSS, Lucide Icons</div>
<div><strong>Testing:</strong> Playwright (E2E)</div>
</CardContent>
</Card>
</SettingsSection>
<SettingsSection
title="Support"
icon={<span className="text-2xl">💬</span>}
description="Get help and feedback"
>
<Card>
<CardContent className="pt-6 space-y-4">
<div>
<p className="font-medium mb-2">Documentation</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
Check the documentation for detailed guides and tutorials.
</p>
</div>
<div>
<p className="font-medium mb-2">Report Issues</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
Found a bug? Report it in the issue tracker.
</p>
</div>
<div>
<p className="font-medium mb-2">Feedback</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
We value your feedback! Share your thoughts and suggestions.
</p>
</div>
</CardContent>
</Card>
</SettingsSection>
</main>
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold mb-2">About</h1>
<p className="text-gray-600 dark:text-gray-400">
Information about the application
</p>
</div>
<SettingsSection
title="Keep Notes"
icon={<span className="text-2xl">📝</span>}
description="A powerful note-taking application with AI-powered features"
>
<Card>
<CardContent className="pt-6 space-y-4">
<div className="flex justify-between items-center">
<span className="font-medium">Version</span>
<Badge variant="secondary">{version}</Badge>
</div>
<div className="flex justify-between items-center">
<span className="font-medium">Build Date</span>
<Badge variant="outline">{buildDate}</Badge>
</div>
<div className="flex justify-between items-center">
<span className="font-medium">Platform</span>
<Badge variant="outline">Web</Badge>
</div>
</CardContent>
</Card>
</SettingsSection>
<SettingsSection
title="Features"
icon={<span className="text-2xl"></span>}
description="AI-powered capabilities"
>
<Card>
<CardContent className="pt-6 space-y-2">
<div className="flex items-center gap-2">
<span className="text-green-500"></span>
<span>AI-powered title suggestions</span>
</div>
<div className="flex items-center gap-2">
<span className="text-green-500"></span>
<span>Semantic search with embeddings</span>
</div>
<div className="flex items-center gap-2">
<span className="text-green-500"></span>
<span>Paragraph reformulation</span>
</div>
<div className="flex items-center gap-2">
<span className="text-green-500"></span>
<span>Memory Echo daily insights</span>
</div>
<div className="flex items-center gap-2">
<span className="text-green-500"></span>
<span>Notebook organization</span>
</div>
<div className="flex items-center gap-2">
<span className="text-green-500"></span>
<span>Drag & drop note management</span>
</div>
<div className="flex items-center gap-2">
<span className="text-green-500"></span>
<span>Label system</span>
</div>
<div className="flex items-center gap-2">
<span className="text-green-500"></span>
<span>Multiple AI providers (OpenAI, Ollama)</span>
</div>
</CardContent>
</Card>
</SettingsSection>
<SettingsSection
title="Technology Stack"
icon={<span className="text-2xl"></span>}
description="Built with modern technologies"
>
<Card>
<CardContent className="pt-6 space-y-2 text-sm">
<div><strong>Frontend:</strong> Next.js 16, React 19, TypeScript</div>
<div><strong>Backend:</strong> Next.js API Routes, Server Actions</div>
<div><strong>Database:</strong> SQLite (Prisma ORM)</div>
<div><strong>Authentication:</strong> NextAuth 5</div>
<div><strong>AI:</strong> Vercel AI SDK, OpenAI, Ollama</div>
<div><strong>UI:</strong> Radix UI, Tailwind CSS, Lucide Icons</div>
<div><strong>Testing:</strong> Playwright (E2E)</div>
</CardContent>
</Card>
</SettingsSection>
<SettingsSection
title="Support"
icon={<span className="text-2xl">💬</span>}
description="Get help and feedback"
>
<Card>
<CardContent className="pt-6 space-y-4">
<div>
<p className="font-medium mb-2">Documentation</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
Check the documentation for detailed guides and tutorials.
</p>
</div>
<div>
<p className="font-medium mb-2">Report Issues</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
Found a bug? Report it in the issue tracker.
</p>
</div>
<div>
<p className="font-medium mb-2">Feedback</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
We value your feedback! Share your thoughts and suggestions.
</p>
</div>
</CardContent>
</Card>
</SettingsSection>
</div>
)
}

View File

@@ -14,7 +14,7 @@ export default async function AISettingsPage() {
const settings = await getAISettings()
return (
<div className="container mx-auto py-8 max-w-4xl">
<div className="space-y-6">
<AISettingsHeader />
<AISettingsPanel initialSettings={settings} />
</div>

View File

@@ -0,0 +1,103 @@
'use client'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
import { SettingsSection, SettingSelect } from '@/components/settings'
// Import actions directly
import { updateAISettings as updateAI } from '@/app/actions/ai-settings'
import { updateUserSettings as updateUser } from '@/app/actions/user-settings'
interface AppearanceSettingsFormProps {
initialTheme: string
initialFontSize: string
}
export function AppearanceSettingsForm({ initialTheme, initialFontSize }: AppearanceSettingsFormProps) {
const router = useRouter()
const [theme, setTheme] = useState(initialTheme)
const [fontSize, setFontSize] = useState(initialFontSize)
const handleThemeChange = async (value: string) => {
setTheme(value)
localStorage.setItem('theme-preference', value)
// Instant visual update
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') {
root.classList.add('dark')
} else {
root.setAttribute('data-theme', value)
if (['midnight'].includes(value)) root.classList.add('dark')
}
// Save to DB (no need for router.refresh - localStorage handles immediate visuals)
await updateUser({ theme: value as 'light' | 'dark' | 'auto' | 'sepia' | 'midnight' })
}
const handleFontSizeChange = async (value: string) => {
setFontSize(value)
// Instant visual update
const fontSizeMap: Record<string, string> = {
'small': '14px', 'medium': '16px', 'large': '18px', 'extra-large': '20px'
}
const root = document.documentElement
root.style.setProperty('--user-font-size', fontSizeMap[value] || '16px')
await updateAI({ fontSize: value as any })
}
return (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold mb-2">Appearance</h1>
<p className="text-gray-600 dark:text-gray-400">
Customize look and feel of application
</p>
</div>
<SettingsSection
title="Theme"
icon={<span className="text-2xl">🎨</span>}
description="Choose your preferred color scheme"
>
<SettingSelect
label="Color Scheme"
description="Select app's visual theme"
value={theme}
options={[
{ value: 'light', label: 'Light' },
{ value: 'dark', label: 'Dark' },
{ value: 'sepia', label: 'Sepia' },
{ value: 'midnight', label: 'Midnight' },
{ value: 'auto', label: 'Auto (system)' },
]}
onChange={handleThemeChange}
/>
</SettingsSection>
<SettingsSection
title="Typography"
icon={<span className="text-2xl">📝</span>}
description="Adjust text size for better readability"
>
<SettingSelect
label="Font Size"
description="Adjust size of text throughout app"
value={fontSize}
options={[
{ value: 'small', label: 'Small' },
{ value: 'medium', label: 'Medium' },
{ value: 'large', label: 'Large' },
]}
onChange={handleFontSizeChange}
/>
</SettingsSection>
</div>
)
}

View File

@@ -1,79 +1,111 @@
'use client'
import { useState } from 'react'
import { useState, useEffect } from 'react'
import { SettingsNav, SettingsSection, SettingSelect } from '@/components/settings'
import { updateAISettings } from '@/app/actions/ai-settings'
import { updateAISettings, getAISettings } from '@/app/actions/ai-settings'
import { updateUserSettings, getUserSettings } from '@/app/actions/user-settings'
export default function AppearanceSettingsPage() {
const [theme, setTheme] = useState('auto')
const [fontSize, setFontSize] = useState('medium')
// Load settings on mount
useEffect(() => {
async function loadSettings() {
try {
const [aiSettings, userSettings] = await Promise.all([
getAISettings(),
getUserSettings()
])
if (aiSettings.fontSize) setFontSize(aiSettings.fontSize)
if (userSettings.theme) setTheme(userSettings.theme)
} catch (error) {
console.error('Error loading settings:', error)
}
}
loadSettings()
}, [])
const handleThemeChange = async (value: string) => {
setTheme(value)
// TODO: Implement theme persistence
console.log('Theme:', value)
localStorage.setItem('theme-preference', value)
// Instant visual update
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') {
root.classList.add('dark')
} else {
root.setAttribute('data-theme', value)
if (['midnight'].includes(value)) root.classList.add('dark')
}
await updateUserSettings({ theme: value as 'light' | 'dark' | 'auto' })
}
const handleFontSizeChange = async (value: string) => {
setFontSize(value)
// TODO: Implement font size persistence
// Instant visual update
const fontSizeMap: Record<string, string> = {
'small': '14px', 'medium': '16px', 'large': '18px', 'extra-large': '20px'
}
const root = document.documentElement
root.style.setProperty('--user-font-size', fontSizeMap[value] || '16px')
await updateAISettings({ fontSize: value as any })
}
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>
{/* Main Content */}
<main className="lg:col-span-3 space-y-6">
<div>
<h1 className="text-3xl font-bold mb-2">Appearance</h1>
<p className="text-gray-600 dark:text-gray-400">
Customize the look and feel of the application
</p>
</div>
<SettingsSection
title="Theme"
icon={<span className="text-2xl">🎨</span>}
description="Choose your preferred color scheme"
>
<SettingSelect
label="Color Scheme"
description="Select the app's visual theme"
value={theme}
options={[
{ value: 'light', label: 'Light' },
{ value: 'dark', label: 'Dark' },
{ value: 'auto', label: 'Auto (system)' },
]}
onChange={handleThemeChange}
/>
</SettingsSection>
<SettingsSection
title="Typography"
icon={<span className="text-2xl">📝</span>}
description="Adjust text size for better readability"
>
<SettingSelect
label="Font Size"
description="Adjust the size of text throughout the app"
value={fontSize}
options={[
{ value: 'small', label: 'Small' },
{ value: 'medium', label: 'Medium' },
{ value: 'large', label: 'Large' },
]}
onChange={handleFontSizeChange}
/>
</SettingsSection>
</main>
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold mb-2">Appearance</h1>
<p className="text-gray-600 dark:text-gray-400">
Customize look and feel of application
</p>
</div>
<SettingsSection
title="Theme"
icon={<span className="text-2xl">🎨</span>}
description="Choose your preferred color scheme"
>
<SettingSelect
label="Color Scheme"
description="Select app's visual theme"
value={theme}
options={[
{ value: 'light', label: 'Light' },
{ value: 'dark', label: 'Dark' },
{ value: 'sepia', label: 'Sepia' },
{ value: 'midnight', label: 'Midnight' },
{ value: 'auto', label: 'Auto (system)' },
]}
onChange={handleThemeChange}
/>
</SettingsSection>
<SettingsSection
title="Typography"
icon={<span className="text-2xl">📝</span>}
description="Adjust text size for better readability"
>
<SettingSelect
label="Font Size"
description="Adjust size of text throughout app"
value={fontSize}
options={[
{ value: 'small', label: 'Small' },
{ value: 'medium', label: 'Medium' },
{ value: 'large', label: 'Large' },
]}
onChange={handleFontSizeChange}
/>
</SettingsSection>
</div>
)
}

View File

@@ -89,112 +89,102 @@ export default function DataSettingsPage() {
}
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="space-y-6">
<div>
<h1 className="text-3xl font-bold mb-2">Data Management</h1>
<p className="text-gray-600 dark:text-gray-400">
Export, import, or manage your data
</p>
</div>
{/* Main Content */}
<main className="lg:col-span-3 space-y-6">
<SettingsSection
title="Export Data"
icon={<span className="text-2xl">💾</span>}
description="Download your notes as a JSON file"
>
<div className="flex items-center justify-between py-4">
<div>
<h1 className="text-3xl font-bold mb-2">Data Management</h1>
<p className="text-gray-600 dark:text-gray-400">
Export, import, or manage your data
<p className="font-medium">Export All Notes</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
Download all your notes in JSON format
</p>
</div>
<SettingsSection
title="Export Data"
icon={<span className="text-2xl">💾</span>}
description="Download your notes as a JSON file"
<Button
onClick={handleExport}
disabled={isExporting}
>
<div className="flex items-center justify-between py-4">
<div>
<p className="font-medium">Export All Notes</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
Download all your notes in JSON format
</p>
</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" />
)}
{isExporting ? 'Exporting...' : 'Export'}
</Button>
</div>
</SettingsSection>
{isExporting ? (
<Loader2 className="h-4 w-4 animate-spin mr-2" />
) : (
<Download className="h-4 w-4 mr-2" />
)}
{isExporting ? 'Exporting...' : 'Export'}
</Button>
</div>
</SettingsSection>
<SettingsSection
title="Import Data"
icon={<span className="text-2xl">📥</span>}
description="Import notes from a JSON file"
>
<div className="flex items-center justify-between py-4">
<div>
<p className="font-medium">Import Notes</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
Upload a JSON file to import notes
</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}
>
{isImporting ? (
<Loader2 className="h-4 w-4 animate-spin mr-2" />
) : (
<Upload className="h-4 w-4 mr-2" />
)}
{isImporting ? 'Importing...' : 'Import'}
</Button>
</div>
</div>
</SettingsSection>
<SettingsSection
title="Import Data"
icon={<span className="text-2xl">📥</span>}
description="Import notes from a JSON file"
>
<div className="flex items-center justify-between py-4">
<div>
<p className="font-medium">Import Notes</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
Upload a JSON file to import notes
</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}
>
{isImporting ? (
<Loader2 className="h-4 w-4 animate-spin mr-2" />
) : (
<Upload className="h-4 w-4 mr-2" />
)}
{isImporting ? 'Importing...' : 'Import'}
</Button>
</div>
</div>
</SettingsSection>
<SettingsSection
title="Danger Zone"
icon={<span className="text-2xl"></span>}
description="Permanently delete your data"
<SettingsSection
title="Danger Zone"
icon={<span className="text-2xl"></span>}
description="Permanently delete your data"
>
<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">Delete All Notes</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
This action cannot be undone
</p>
</div>
<Button
variant="destructive"
onClick={handleDeleteAll}
disabled={isDeleting}
>
<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">Delete All Notes</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
This action cannot be undone
</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" />
)}
{isDeleting ? 'Deleting...' : 'Delete All'}
</Button>
</div>
</SettingsSection>
</main>
</div>
{isDeleting ? (
<Loader2 className="h-4 w-4 animate-spin mr-2" />
) : (
<Trash2 className="h-4 w-4 mr-2" />
)}
{isDeleting ? 'Deleting...' : 'Delete All'}
</Button>
</div>
</SettingsSection>
</div>
)
}

View File

@@ -1,107 +1,124 @@
'use client'
import { useState } from 'react'
import { SettingsNav, SettingsSection, SettingToggle, SettingSelect, SettingsSearch } from '@/components/settings'
import { useState, useEffect } from 'react'
import { SettingsNav, SettingsSection, SettingToggle, SettingSelect } from '@/components/settings'
import { useLanguage } from '@/lib/i18n'
import { updateAISettings } from '@/app/actions/ai-settings'
import { updateAISettings, getAISettings } from '@/app/actions/ai-settings'
export default function GeneralSettingsPage() {
const { t } = useLanguage()
const [language, setLanguage] = useState('auto')
const [emailNotifications, setEmailNotifications] = useState(false)
const [desktopNotifications, setDesktopNotifications] = useState(false)
const [anonymousAnalytics, setAnonymousAnalytics] = useState(false)
// Load settings on mount
useEffect(() => {
async function loadSettings() {
try {
const settings = await getAISettings()
if (settings.preferredLanguage) setLanguage(settings.preferredLanguage)
if (settings.emailNotifications !== undefined) setEmailNotifications(settings.emailNotifications)
if (settings.desktopNotifications !== undefined) setDesktopNotifications(settings.desktopNotifications)
if (settings.anonymousAnalytics !== undefined) setAnonymousAnalytics(settings.anonymousAnalytics)
} catch (error) {
console.error('Error loading settings:', error)
}
}
loadSettings()
}, [])
const handleLanguageChange = async (value: string) => {
setLanguage(value)
await updateAISettings({ preferredLanguage: value as any })
}
const handleNotificationsChange = async (enabled: boolean) => {
// TODO: Implement notifications setting
console.log('Notifications:', enabled)
const handleEmailNotificationsChange = async (enabled: boolean) => {
setEmailNotifications(enabled)
await updateAISettings({ emailNotifications: enabled })
}
const handleDesktopNotificationsChange = async (enabled: boolean) => {
setDesktopNotifications(enabled)
await updateAISettings({ desktopNotifications: enabled })
}
const handleAnonymousAnalyticsChange = async (enabled: boolean) => {
setAnonymousAnalytics(enabled)
await updateAISettings({ anonymousAnalytics: enabled })
}
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>
{/* Main Content */}
<main className="lg:col-span-3 space-y-6">
<div>
<h1 className="text-3xl font-bold mb-2">General Settings</h1>
<p className="text-gray-600 dark:text-gray-400">
Configure basic application preferences
</p>
</div>
<SettingsSearch onSearch={(query) => console.log('Search:', query)} />
<SettingsSection
title="Language & Region"
icon={<span className="text-2xl">🌍</span>}
description="Choose your preferred language and regional settings"
>
<SettingSelect
label="Language"
description="Select the interface language"
value={language}
options={[
{ value: 'auto', label: 'Auto-detect' },
{ 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>
<SettingsSection
title="Notifications"
icon={<span className="text-2xl">🔔</span>}
description="Manage how and when you receive notifications"
>
<SettingToggle
label="Email Notifications"
description="Receive email updates about your notes"
checked={false}
onChange={handleNotificationsChange}
/>
<SettingToggle
label="Desktop Notifications"
description="Show notifications in your browser"
checked={false}
onChange={handleNotificationsChange}
/>
</SettingsSection>
<SettingsSection
title="Privacy"
icon={<span className="text-2xl">🔒</span>}
description="Control your privacy settings"
>
<SettingToggle
label="Anonymous Analytics"
description="Help improve the app with anonymous usage data"
checked={false}
onChange={handleNotificationsChange}
/>
</SettingsSection>
</main>
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold mb-2">General Settings</h1>
<p className="text-gray-600 dark:text-gray-400">
Configure basic application preferences
</p>
</div>
<SettingsSection
title="Language & Region"
icon={<span className="text-2xl">🌍</span>}
description="Choose your preferred language and regional settings"
>
<SettingSelect
label="Language"
description="Select interface language"
value={language}
options={[
{ value: 'auto', label: 'Auto-detect' },
{ 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>
<SettingsSection
title="Notifications"
icon={<span className="text-2xl">🔔</span>}
description="Manage how and when you receive notifications"
>
<SettingToggle
label="Email Notifications"
description="Receive email updates about your notes"
checked={emailNotifications}
onChange={handleEmailNotificationsChange}
/>
<SettingToggle
label="Desktop Notifications"
description="Show notifications in your browser"
checked={desktopNotifications}
onChange={handleDesktopNotificationsChange}
/>
</SettingsSection>
<SettingsSection
title="Privacy"
icon={<span className="text-2xl">🔒</span>}
description="Control your privacy settings"
>
<SettingToggle
label="Anonymous Analytics"
description="Help improve app with anonymous usage data"
checked={anonymousAnalytics}
onChange={handleAnonymousAnalyticsChange}
/>
</SettingsSection>
</div>
)
}

View File

@@ -0,0 +1,25 @@
'use client'
import { SettingsNav } from '@/components/settings'
export default function SettingsLayout({
children,
}: {
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>
{/* Main Content */}
<main className="lg:col-span-3 space-y-6">
{children}
</main>
</div>
</div>
)
}

View File

@@ -78,146 +78,136 @@ export default function SettingsPage() {
}
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>
{/* Main Content */}
<main className="lg:col-span-3 space-y-6">
<div>
<h1 className="text-3xl font-bold mb-2">Settings</h1>
<p className="text-gray-600 dark:text-gray-400">
Configure your application settings
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold mb-2">Settings</h1>
<p className="text-gray-600 dark:text-gray-400">
Configure your application settings
</p>
</div>
{/* Quick Links */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Link href="/settings/ai">
<div className="p-4 border rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors cursor-pointer">
<BrainCircuit className="h-6 w-6 text-purple-500 mb-2" />
<h3 className="font-semibold">AI Settings</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">
Configure AI features and provider
</p>
</div>
{/* Quick Links */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Link href="/settings/ai">
<div className="p-4 border rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors cursor-pointer">
<BrainCircuit className="h-6 w-6 text-purple-500 mb-2" />
<h3 className="font-semibold">AI Settings</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">
Configure AI features and provider
</p>
</div>
</Link>
<Link href="/settings/profile">
<div className="p-4 border rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors cursor-pointer">
<RefreshCw className="h-6 w-6 text-blue-500 mb-2" />
<h3 className="font-semibold">Profile Settings</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">
Manage your account and preferences
</p>
</div>
</Link>
</Link>
<Link href="/settings/profile">
<div className="p-4 border rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors cursor-pointer">
<RefreshCw className="h-6 w-6 text-blue-500 mb-2" />
<h3 className="font-semibold">Profile Settings</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">
Manage your account and preferences
</p>
</div>
</Link>
</div>
{/* AI Diagnostics */}
<SettingsSection
title="AI Diagnostics"
icon={<span className="text-2xl">🔍</span>}
description="Check your AI provider connection status"
>
<div className="grid grid-cols-2 gap-4 py-4">
<div className="p-4 rounded-lg bg-secondary/50">
<p className="text-sm font-medium text-muted-foreground mb-1">Configured Provider</p>
<p className="text-lg font-mono">{config?.provider || '...'}</p>
</div>
<div className="p-4 rounded-lg bg-secondary/50">
<p className="text-sm font-medium text-muted-foreground mb-1">API Status</p>
<div className="flex items-center gap-2">
{status === 'success' && <CheckCircle className="text-green-500 w-5 h-5" />}
{status === 'error' && <XCircle className="text-red-500 w-5 h-5" />}
<span className={`text-sm font-medium ${
status === 'success' ? 'text-green-600 dark:text-green-400' :
status === 'error' ? 'text-red-600 dark:text-red-400' :
'text-gray-600'
}`}>
{status === 'success' ? 'Operational' :
status === 'error' ? 'Error' :
'Checking...'}
</span>
</div>
</div>
{/* AI Diagnostics */}
<SettingsSection
title="AI Diagnostics"
icon={<span className="text-2xl">🔍</span>}
description="Check your AI provider connection status"
>
<div className="grid grid-cols-2 gap-4 py-4">
<div className="p-4 rounded-lg bg-secondary/50">
<p className="text-sm font-medium text-muted-foreground mb-1">Configured Provider</p>
<p className="text-lg font-mono">{config?.provider || '...'}</p>
</div>
<div className="p-4 rounded-lg bg-secondary/50">
<p className="text-sm font-medium text-muted-foreground mb-1">API Status</p>
<div className="flex items-center gap-2">
{status === 'success' && <CheckCircle className="text-green-500 w-5 h-5" />}
{status === 'error' && <XCircle className="text-red-500 w-5 h-5" />}
<span className={`text-sm font-medium ${status === 'success' ? 'text-green-600 dark:text-green-400' :
status === 'error' ? 'text-red-600 dark:text-red-400' :
'text-gray-600'
}`}>
{status === 'success' ? 'Operational' :
status === 'error' ? 'Error' :
'Checking...'}
</span>
</div>
</div>
</div>
{result && (
<div className="space-y-2 mt-4">
<h3 className="text-sm font-medium">Test Details:</h3>
<div className={`p-4 rounded-md font-mono text-xs overflow-x-auto ${status === 'error'
? 'bg-red-50 text-red-900 border border-red-200 dark:bg-red-950 dark:text-red-100 dark:border-red-900'
: 'bg-slate-950 text-slate-50'
}`}>
<pre>{JSON.stringify(result, null, 2)}</pre>
</div>
{result && (
<div className="space-y-2 mt-4">
<h3 className="text-sm font-medium">Test Details:</h3>
<div className={`p-4 rounded-md font-mono text-xs overflow-x-auto ${
status === 'error'
? 'bg-red-50 text-red-900 border border-red-200 dark:bg-red-950 dark:text-red-100 dark:border-red-900'
: 'bg-slate-950 text-slate-50'
}`}>
<pre>{JSON.stringify(result, null, 2)}</pre>
</div>
{status === 'error' && (
<div className="text-sm text-red-600 dark:text-red-400 mt-2">
<p className="font-bold">Troubleshooting Tips:</p>
<ul className="list-disc list-inside mt-1 space-y-1">
<li>Check that Ollama is running (<code className="bg-red-100 dark:bg-red-900 px-1 rounded">ollama list</code>)</li>
<li>Check URL (http://localhost:11434)</li>
<li>Verify model (e.g., granite4:latest) is downloaded</li>
<li>Check Next.js server terminal for more logs</li>
</ul>
</div>
)}
{status === 'error' && (
<div className="text-sm text-red-600 dark:text-red-400 mt-2">
<p className="font-bold">Troubleshooting Tips:</p>
<ul className="list-disc list-inside mt-1 space-y-1">
<li>Check that Ollama is running (<code className="bg-red-100 dark:bg-red-900 px-1 rounded">ollama list</code>)</li>
<li>Check URL (http://localhost:11434)</li>
<li>Verify model (e.g., granite4:latest) is downloaded</li>
<li>Check Next.js server terminal for more logs</li>
</ul>
</div>
)}
</div>
)}
<div className="mt-4 flex justify-end">
<Button variant="outline" size="sm" onClick={checkConnection} disabled={loading}>
{loading ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : <RefreshCw className="w-4 h-4 mr-2" />}
Test Connection
</Button>
<div className="mt-4 flex justify-end">
<Button variant="outline" size="sm" onClick={checkConnection} disabled={loading}>
{loading ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : <RefreshCw className="w-4 h-4 mr-2" />}
Test Connection
</Button>
</div>
</SettingsSection>
{/* Maintenance */}
<SettingsSection
title="Maintenance"
icon={<span className="text-2xl">🔧</span>}
description="Tools to maintain your database health"
>
<div className="space-y-4 py-4">
<div className="flex items-center justify-between p-4 border rounded-lg">
<div>
<h3 className="font-medium flex items-center gap-2">
Clean Orphan Tags
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">
Remove tags that are no longer used by any notes
</p>
</div>
</SettingsSection>
<Button variant="secondary" onClick={handleCleanup} disabled={cleanupLoading}>
{cleanupLoading ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : <Database className="w-4 h-4 mr-2" />}
Clean
</Button>
</div>
{/* Maintenance */}
<SettingsSection
title="Maintenance"
icon={<span className="text-2xl">🔧</span>}
description="Tools to maintain your database health"
>
<div className="space-y-4 py-4">
<div className="flex items-center justify-between p-4 border rounded-lg">
<div>
<h3 className="font-medium flex items-center gap-2">
Clean Orphan Tags
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">
Remove tags that are no longer used by any notes
</p>
</div>
<Button variant="secondary" onClick={handleCleanup} disabled={cleanupLoading}>
{cleanupLoading ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : <Database className="w-4 h-4 mr-2" />}
Clean
</Button>
</div>
<div className="flex items-center justify-between p-4 border rounded-lg">
<div>
<h3 className="font-medium flex items-center gap-2">
Semantic Indexing
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">
Generate vectors for all notes to enable intent-based search
</p>
</div>
<Button variant="secondary" onClick={handleSync} disabled={syncLoading}>
{syncLoading ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : <BrainCircuit className="w-4 h-4 mr-2" />}
Index All
</Button>
</div>
<div className="flex items-center justify-between p-4 border rounded-lg">
<div>
<h3 className="font-medium flex items-center gap-2">
Semantic Indexing
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">
Generate vectors for all notes to enable intent-based search
</p>
</div>
</SettingsSection>
</main>
</div>
<Button variant="secondary" onClick={handleSync} disabled={syncLoading}>
{syncLoading ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : <BrainCircuit className="w-4 h-4 mr-2" />}
Index All
</Button>
</div>
</div>
</SettingsSection>
</div>
)
}

View File

@@ -23,29 +23,25 @@ export default async function ProfilePage() {
redirect('/login')
}
// Get user AI settings for language preference and recent notes setting
// Get user AI settings
let userAISettings = { preferredLanguage: 'auto', showRecentNotes: false }
try {
const result = await prisma.$queryRaw<Array<{ preferredLanguage: string | null; showRecentNotes: number | null }>>`
SELECT preferredLanguage, showRecentNotes FROM UserAISettings WHERE userId = ${session.user.id}
`
if (result && result[0]) {
// Handle NULL values - if showRecentNotes is NULL, default to false
const showRecentNotesValue = result[0].showRecentNotes !== null && result[0].showRecentNotes !== undefined
? result[0].showRecentNotes === 1
: false
const aiSettings = await prisma.userAISettings.findUnique({
where: { userId: session.user.id }
})
if (aiSettings) {
userAISettings = {
preferredLanguage: result[0].preferredLanguage || 'auto',
showRecentNotes: showRecentNotesValue
preferredLanguage: aiSettings.preferredLanguage || 'auto',
showRecentNotes: aiSettings.showRecentNotes ?? false
}
}
} catch (error) {
// Record doesn't exist, use defaults
console.error('Error fetching AI settings:', error)
}
return (
<div className="container max-w-2xl mx-auto py-10 px-4">
<div className="max-w-2xl">
<ProfilePageHeader />
<ProfileForm user={user} userAISettings={userAISettings} />

View File

@@ -125,7 +125,7 @@ export function ProfileForm({ user, userAISettings }: { user: any; userAISetting
const handleShowRecentNotesChange = async (enabled: boolean) => {
setIsUpdatingRecentNotes(true)
const previousValue = showRecentNotes
try {
const result = await updateShowRecentNotes(enabled)
if (result?.error) {
@@ -209,56 +209,7 @@ export function ProfileForm({ user, userAISettings }: { user: any; userAISetting
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>{t('profile.displaySettings')}</CardTitle>
<CardDescription>{t('profile.displaySettingsDescription')}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="fontSize">{t('profile.fontSize')}</Label>
<Select
value={fontSize}
onValueChange={handleFontSizeChange}
disabled={isUpdatingFontSize}
>
<SelectTrigger id="fontSize">
<SelectValue placeholder={t('profile.selectFontSize')} />
</SelectTrigger>
<SelectContent>
{FONT_SIZES.map((size) => (
<SelectItem key={size.value} value={size.value}>
<span className="flex items-center gap-2">
<span>{size.label}</span>
<span className="text-xs text-muted-foreground">({size.size})</span>
</span>
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
{t('profile.fontSizeDescription')}
</p>
</div>
<div className="flex items-center justify-between pt-4 border-t">
<div className="space-y-0.5">
<Label htmlFor="showRecentNotes" className="text-base font-medium">
{t('profile.showRecentNotes') || 'Afficher la section Récent'}
</Label>
<p className="text-sm text-muted-foreground">
{t('profile.showRecentNotesDescription') || 'Afficher les notes récentes (7 derniers jours) sur la page principale'}
</p>
</div>
<Switch
id="showRecentNotes"
checked={showRecentNotes}
onCheckedChange={handleShowRecentNotesChange}
disabled={isUpdatingRecentNotes}
/>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>