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:
127
keep-notes/app/(main)/admin/ai/page.tsx
Normal file
127
keep-notes/app/(main)/admin/ai/page.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import { AdminMetrics } from '@/components/admin-metrics'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Zap, Settings, Activity, TrendingUp } from 'lucide-react'
|
||||
|
||||
export default async function AdminAIPage() {
|
||||
// Mock AI metrics - in a real app, these would come from analytics
|
||||
const aiMetrics = [
|
||||
{
|
||||
title: 'Total Requests',
|
||||
value: '856',
|
||||
trend: { value: 12, isPositive: true },
|
||||
icon: <Zap className="h-5 w-5 text-yellow-600 dark:text-yellow-400" />,
|
||||
},
|
||||
{
|
||||
title: 'Success Rate',
|
||||
value: '98.5%',
|
||||
trend: { value: 2, isPositive: true },
|
||||
icon: <TrendingUp className="h-5 w-5 text-green-600 dark:text-green-400" />,
|
||||
},
|
||||
{
|
||||
title: 'Avg Response Time',
|
||||
value: '1.2s',
|
||||
trend: { value: 5, isPositive: true },
|
||||
icon: <Activity className="h-5 w-5 text-blue-600 dark:text-blue-400" />,
|
||||
},
|
||||
{
|
||||
title: 'Active Features',
|
||||
value: '6',
|
||||
icon: <Settings className="h-5 w-5 text-purple-600 dark:text-purple-400" />,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
AI Management
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
Monitor and configure AI features
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline">
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
Configure
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<AdminMetrics metrics={aiMetrics} />
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-lg shadow overflow-hidden border border-gray-200 dark:border-gray-800 p-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||
Active AI Features
|
||||
</h2>
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
'Title Suggestions',
|
||||
'Semantic Search',
|
||||
'Paragraph Reformulation',
|
||||
'Memory Echo',
|
||||
'Language Detection',
|
||||
'Auto Labeling',
|
||||
].map((feature) => (
|
||||
<div
|
||||
key={feature}
|
||||
className="flex items-center justify-between p-3 bg-gray-50 dark:bg-zinc-800 rounded-lg"
|
||||
>
|
||||
<span className="text-sm text-gray-900 dark:text-white">
|
||||
{feature}
|
||||
</span>
|
||||
<span className="px-2 py-1 text-xs font-medium text-green-700 dark:text-green-400 bg-green-100 dark:bg-green-900 rounded-full">
|
||||
Active
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-lg shadow overflow-hidden border border-gray-200 dark:border-gray-800 p-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||
AI Provider Status
|
||||
</h2>
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{ name: 'OpenAI', status: 'Connected', requests: '642' },
|
||||
{ name: 'Ollama', status: 'Available', requests: '214' },
|
||||
].map((provider) => (
|
||||
<div
|
||||
key={provider.name}
|
||||
className="flex items-center justify-between p-3 bg-gray-50 dark:bg-zinc-800 rounded-lg"
|
||||
>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{provider.name}
|
||||
</p>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||
{provider.requests} requests
|
||||
</p>
|
||||
</div>
|
||||
<span
|
||||
className={`px-2 py-1 text-xs font-medium rounded-full ${
|
||||
provider.status === 'Connected'
|
||||
? 'text-green-700 dark:text-green-400 bg-green-100 dark:bg-green-900'
|
||||
: 'text-blue-700 dark:text-blue-400 bg-blue-100 dark:bg-blue-900'
|
||||
}`}
|
||||
>
|
||||
{provider.status}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-lg shadow overflow-hidden border border-gray-200 dark:border-gray-800 p-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||
Recent AI Requests
|
||||
</h2>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
Recent AI requests will be displayed here.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
23
keep-notes/app/(main)/admin/layout.tsx
Normal file
23
keep-notes/app/(main)/admin/layout.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { AdminSidebar } from '@/components/admin-sidebar'
|
||||
import { AdminContentArea } from '@/components/admin-content-area'
|
||||
import { auth } from '@/auth'
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
export default async function AdminLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const session = await auth()
|
||||
|
||||
if ((session?.user as any)?.role !== 'ADMIN') {
|
||||
redirect('/')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen bg-gray-50 dark:bg-zinc-950">
|
||||
<AdminSidebar />
|
||||
<AdminContentArea>{children}</AdminContentArea>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,39 +1,58 @@
|
||||
import { getUsers } from '@/app/actions/admin'
|
||||
import { UserList } from './user-list'
|
||||
import { CreateUserDialog } from './create-user-dialog'
|
||||
import { auth } from '@/auth'
|
||||
import { redirect } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Settings } from 'lucide-react'
|
||||
import { AdminPageHeader, SettingsButton } from '@/components/admin-page-header'
|
||||
import { AdminMetrics } from '@/components/admin-metrics'
|
||||
import { Users, Activity, Database, Zap } from 'lucide-react'
|
||||
|
||||
export default async function AdminPage() {
|
||||
const session = await auth()
|
||||
|
||||
if ((session?.user as any)?.role !== 'ADMIN') {
|
||||
redirect('/')
|
||||
}
|
||||
|
||||
const users = await getUsers()
|
||||
|
||||
// Mock metrics data - in a real app, these would come from analytics
|
||||
const metrics = [
|
||||
{
|
||||
title: 'Total Users',
|
||||
value: users.length,
|
||||
trend: { value: 12, isPositive: true },
|
||||
icon: <Users className="h-5 w-5 text-blue-600 dark:text-blue-400" />,
|
||||
},
|
||||
{
|
||||
title: 'Active Sessions',
|
||||
value: '24',
|
||||
trend: { value: 8, isPositive: true },
|
||||
icon: <Activity className="h-5 w-5 text-green-600 dark:text-green-400" />,
|
||||
},
|
||||
{
|
||||
title: 'Total Notes',
|
||||
value: '1,234',
|
||||
trend: { value: 24, isPositive: true },
|
||||
icon: <Database className="h-5 w-5 text-purple-600 dark:text-purple-400" />,
|
||||
},
|
||||
{
|
||||
title: 'AI Requests',
|
||||
value: '856',
|
||||
trend: { value: 5, isPositive: false },
|
||||
icon: <Zap className="h-5 w-5 text-yellow-600 dark:text-yellow-400" />,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-10 px-4">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<AdminPageHeader />
|
||||
<div className="flex gap-2">
|
||||
<Link href="/admin/settings">
|
||||
<Button variant="outline">
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
<SettingsButton />
|
||||
</Button>
|
||||
</Link>
|
||||
<CreateUserDialog />
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
Dashboard
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
Overview of your application metrics
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-lg shadow overflow-hidden border border-gray-200 dark:border-gray-800">
|
||||
<UserList initialUsers={users} />
|
||||
<AdminMetrics metrics={metrics} />
|
||||
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-lg shadow overflow-hidden border border-gray-200 dark:border-gray-800 p-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||
Recent Activity
|
||||
</h2>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
Recent activity will be displayed here.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import { auth } from '@/auth'
|
||||
import { redirect } from 'next/navigation'
|
||||
import { getSystemConfig } from '@/app/actions/admin-settings'
|
||||
import { AdminSettingsForm } from './admin-settings-form'
|
||||
|
||||
export default async function AdminSettingsPage() {
|
||||
const session = await auth()
|
||||
|
||||
if ((session?.user as any)?.role !== 'ADMIN') {
|
||||
redirect('/')
|
||||
}
|
||||
|
||||
const config = await getSystemConfig()
|
||||
|
||||
return (
|
||||
<div className="container max-w-4xl mx-auto py-10 px-4">
|
||||
<h1 className="text-3xl font-bold mb-8">System Configuration</h1>
|
||||
<AdminSettingsForm config={config} />
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
Settings
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
Configure application-wide settings
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-lg shadow overflow-hidden border border-gray-200 dark:border-gray-800 p-6">
|
||||
<AdminSettingsForm config={config} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
29
keep-notes/app/(main)/admin/users/page.tsx
Normal file
29
keep-notes/app/(main)/admin/users/page.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { getUsers } from '@/app/actions/admin'
|
||||
import { CreateUserDialog } from '../create-user-dialog'
|
||||
import { UserList } from '../user-list'
|
||||
import { Plus } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
export default async function AdminUsersPage() {
|
||||
const users = await getUsers()
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
Users
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
Manage application users and permissions
|
||||
</p>
|
||||
</div>
|
||||
<CreateUserDialog />
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-lg shadow overflow-hidden border border-gray-200 dark:border-gray-800">
|
||||
<UserList initialUsers={users} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { HeaderWrapper } from "@/components/header-wrapper";
|
||||
import { Sidebar } from "@/components/sidebar";
|
||||
import { ProvidersWrapper } from "@/components/providers-wrapper";
|
||||
import { auth } from "@/auth";
|
||||
import { detectUserLanguage } from "@/lib/i18n/detect-user-language";
|
||||
|
||||
export default async function MainLayout({
|
||||
children,
|
||||
@@ -8,16 +10,25 @@ export default async function MainLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const session = await auth();
|
||||
const initialLanguage = await detectUserLanguage();
|
||||
|
||||
return (
|
||||
<div className="flex h-screen flex-col">
|
||||
<HeaderWrapper user={session?.user} />
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
<Sidebar className="shrink-0 border-r overflow-y-auto" user={session?.user} />
|
||||
<main className="flex-1 overflow-y-auto">
|
||||
{children}
|
||||
</main>
|
||||
<ProvidersWrapper initialLanguage={initialLanguage}>
|
||||
<div className="bg-background-light dark:bg-background-dark font-display text-slate-900 dark:text-white overflow-hidden h-screen flex flex-col">
|
||||
{/* Top Navigation - Style Keep */}
|
||||
<HeaderWrapper user={session?.user} />
|
||||
|
||||
{/* Main Layout */}
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
{/* Sidebar Navigation - Style Keep */}
|
||||
<Sidebar className="w-64 flex-none flex-col bg-white dark:bg-[#1e2128] border-r border-slate-200 dark:border-slate-800 overflow-y-auto hidden md:flex" user={session?.user} />
|
||||
|
||||
{/* Main Content Area */}
|
||||
<main className="flex-1 overflow-y-auto bg-background-light dark:bg-background-dark p-4 scroll-smooth">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ProvidersWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,11 +20,16 @@ import { useLabels } from '@/context/LabelContext'
|
||||
import { useNoteRefresh } from '@/context/NoteRefreshContext'
|
||||
import { useReminderCheck } from '@/hooks/use-reminder-check'
|
||||
import { useAutoLabelSuggestion } from '@/hooks/use-auto-label-suggestion'
|
||||
import { useNotebooks } from '@/context/notebooks-context'
|
||||
import { Folder, Briefcase, FileText, Zap, BarChart3, Globe, Sparkles, Book, Heart, Crown, Music, Building2, Plane, ChevronRight, Plus } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { LabelFilter } from '@/components/label-filter'
|
||||
|
||||
export default function HomePage() {
|
||||
console.log('[HomePage] Component rendering')
|
||||
const searchParams = useSearchParams()
|
||||
const router = useRouter()
|
||||
// Force re-render when search params change (for filtering)
|
||||
const [notes, setNotes] = useState<Note[]>([])
|
||||
const [pinnedNotes, setPinnedNotes] = useState<Note[]>([])
|
||||
const [recentNotes, setRecentNotes] = useState<Note[]>([])
|
||||
@@ -227,21 +232,154 @@ export default function HomePage() {
|
||||
loadNotes()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [searchParams, refreshKey, showRecentNotes]) // Intentionally omit 'labels' and 'semantic' to prevent reload when adding tags or from router.push
|
||||
return (
|
||||
<main className="container mx-auto px-4 py-8 max-w-7xl">
|
||||
<NoteInput onNoteCreated={handleNoteCreated} />
|
||||
// Get notebooks context to display header
|
||||
const { notebooks } = useNotebooks()
|
||||
const currentNotebook = notebooks.find((n: any) => n.id === searchParams.get('notebook'))
|
||||
const [showNoteInput, setShowNoteInput] = useState(false)
|
||||
|
||||
{/* Batch Organization Button - Only show in Inbox with 5+ notes */}
|
||||
{isInbox && !isLoading && notes.length >= 5 && (
|
||||
<div className="mb-4 flex justify-end">
|
||||
<Button
|
||||
onClick={() => setBatchOrganizationOpen(true)}
|
||||
variant="default"
|
||||
className="gap-2"
|
||||
>
|
||||
<Wand2 className="h-4 w-4" />
|
||||
Organiser avec l'IA ({notes.length})
|
||||
</Button>
|
||||
// Get icon component for header
|
||||
const getNotebookIcon = (iconName: string) => {
|
||||
const ICON_MAP: Record<string, any> = {
|
||||
'folder': Folder,
|
||||
'briefcase': Briefcase,
|
||||
'document': FileText,
|
||||
'lightning': Zap,
|
||||
'chart': BarChart3,
|
||||
'globe': Globe,
|
||||
'sparkle': Sparkles,
|
||||
'book': Book,
|
||||
'heart': Heart,
|
||||
'crown': Crown,
|
||||
'music': Music,
|
||||
'building': Building2,
|
||||
'flight_takeoff': Plane,
|
||||
}
|
||||
return ICON_MAP[iconName] || Folder
|
||||
}
|
||||
|
||||
// Handle Note Created to close the input if desired, or keep open
|
||||
const handleNoteCreatedWrapper = (note: any) => {
|
||||
handleNoteCreated(note)
|
||||
setShowNoteInput(false)
|
||||
}
|
||||
|
||||
// Helper for Breadcrumbs
|
||||
const Breadcrumbs = ({ notebookName }: { notebookName: string }) => (
|
||||
<div className="flex items-center gap-2 text-sm text-gray-500 mb-1">
|
||||
<span>Notebooks</span>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
<span className="font-medium text-blue-600">{notebookName}</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<main className="w-full px-8 py-6 flex flex-col h-full">
|
||||
{/* Notebook Specific Header */}
|
||||
{currentNotebook ? (
|
||||
<div className="flex flex-col gap-6 mb-8 animate-in fade-in slide-in-from-top-2 duration-300">
|
||||
{/* Breadcrumbs */}
|
||||
<Breadcrumbs notebookName={currentNotebook.name} />
|
||||
|
||||
<div className="flex items-start justify-between">
|
||||
{/* Title Section */}
|
||||
<div className="flex items-center gap-5">
|
||||
<div className="p-3 bg-blue-50 dark:bg-blue-900/20 rounded-xl">
|
||||
{(() => {
|
||||
const Icon = getNotebookIcon(currentNotebook.icon || 'folder')
|
||||
return (
|
||||
<Icon
|
||||
className={cn("w-8 h-8", !currentNotebook.color && "text-blue-600 dark:text-blue-400")}
|
||||
style={currentNotebook.color ? { color: currentNotebook.color } : undefined}
|
||||
/>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold text-gray-900 dark:text-white tracking-tight">{currentNotebook.name}</h1>
|
||||
</div>
|
||||
|
||||
{/* Actions Section */}
|
||||
<div className="flex items-center gap-3">
|
||||
<LabelFilter
|
||||
selectedLabels={searchParams.get('labels')?.split(',').filter(Boolean) || []}
|
||||
onFilterChange={(newLabels) => {
|
||||
const params = new URLSearchParams(searchParams.toString())
|
||||
if (newLabels.length > 0) params.set('labels', newLabels.join(','))
|
||||
else params.delete('labels')
|
||||
router.push(`/?${params.toString()}`)
|
||||
}}
|
||||
className="border-gray-200"
|
||||
/>
|
||||
<Button
|
||||
onClick={() => setShowNoteInput(!showNoteInput)}
|
||||
className="h-10 px-6 rounded-full bg-blue-600 hover:bg-blue-700 text-white font-medium shadow-sm gap-2 transition-all"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
Add Note
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
/* Default Header for Home/Inbox */
|
||||
<div className="flex flex-col gap-6 mb-8 animate-in fade-in slide-in-from-top-2 duration-300">
|
||||
{/* Breadcrumbs Placeholder or just spacing */}
|
||||
<div className="h-5 mb-1"></div>
|
||||
|
||||
<div className="flex items-start justify-between">
|
||||
{/* Title Section */}
|
||||
<div className="flex items-center gap-5">
|
||||
<div className="p-3 bg-white border border-gray-100 dark:bg-gray-800 dark:border-gray-700 rounded-xl shadow-sm">
|
||||
<FileText className="w-8 h-8 text-blue-600" />
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold text-gray-900 dark:text-white tracking-tight">Notes</h1>
|
||||
</div>
|
||||
|
||||
{/* Actions Section */}
|
||||
<div className="flex items-center gap-3">
|
||||
<LabelFilter
|
||||
selectedLabels={searchParams.get('labels')?.split(',').filter(Boolean) || []}
|
||||
onFilterChange={(newLabels) => {
|
||||
const params = new URLSearchParams(searchParams.toString())
|
||||
if (newLabels.length > 0) params.set('labels', newLabels.join(','))
|
||||
else params.delete('labels')
|
||||
router.push(`/?${params.toString()}`)
|
||||
}}
|
||||
className="border-gray-200"
|
||||
/>
|
||||
|
||||
{/* AI Organization Button - Moved to Header */}
|
||||
{isInbox && !isLoading && notes.length >= 5 && (
|
||||
<Button
|
||||
onClick={() => setBatchOrganizationOpen(true)}
|
||||
variant="outline"
|
||||
className="h-10 px-4 rounded-full border-gray-200 text-gray-700 hover:bg-gray-50 gap-2 shadow-sm"
|
||||
title="Organiser avec l'IA"
|
||||
>
|
||||
<Wand2 className="h-4 w-4 text-purple-600" />
|
||||
<span className="hidden sm:inline">Organiser</span>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={() => setShowNoteInput(!showNoteInput)}
|
||||
className="h-10 px-6 rounded-full bg-blue-600 hover:bg-blue-700 text-white font-medium shadow-sm gap-2 transition-all"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
Add Note
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Note Input - Conditionally Visible or Always Visible on Home */}
|
||||
{/* Note Input - Conditionally Rendered */}
|
||||
{showNoteInput && (
|
||||
<div className="mb-8 animate-in fade-in slide-in-from-top-4 duration-300">
|
||||
<NoteInput
|
||||
onNoteCreated={handleNoteCreatedWrapper}
|
||||
forceExpanded={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
103
keep-notes/app/(main)/settings/appearance/appearance-form.tsx
Normal file
103
keep-notes/app/(main)/settings/appearance/appearance-form.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
25
keep-notes/app/(main)/settings/layout.tsx
Normal file
25
keep-notes/app/(main)/settings/layout.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
23
keep-notes/app/(main)/trash/page.tsx
Normal file
23
keep-notes/app/(main)/trash/page.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ArchiveHeader } from '@/components/archive-header'
|
||||
import { Trash2 } from 'lucide-react'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export default function TrashPage() {
|
||||
// Currently, we don't have soft-delete implemented, so trash is always empty.
|
||||
// This page exists to fix the 404 error and provide a placeholder.
|
||||
|
||||
return (
|
||||
<main className="container mx-auto px-4 py-8 max-w-7xl">
|
||||
<div className="flex flex-col items-center justify-center min-h-[60vh] text-center text-gray-500">
|
||||
<div className="bg-gray-100 dark:bg-gray-800 p-6 rounded-full mb-4">
|
||||
<Trash2 className="w-12 h-12 text-gray-400" />
|
||||
</div>
|
||||
<h2 className="text-xl font-medium mb-2">La corbeille est vide</h2>
|
||||
<p className="max-w-md text-sm opacity-80">
|
||||
Les notes supprimées sont actuellement effacées définitivement.
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user