chore: clean up repo for public release

- Remove BMAD framework, IDE configs, dev screenshots, test files,
  internal docs, and backup files
- Rename keep-notes/ to memento-note/
- Update all references from keep-notes to memento-note
- Add Apache 2.0 license with Commons Clause (non-commercial restriction)
- Add clean .gitignore and .env.docker.example
This commit is contained in:
Sepehr Ramezani
2026-04-20 22:48:06 +02:00
parent 402e88b788
commit e4d4e23dc7
3981 changed files with 407 additions and 530622 deletions

View File

@@ -0,0 +1,259 @@
'use client'
import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { useState, useEffect } from 'react'
import { toast } from 'sonner'
import { Loader2, CheckCircle2, XCircle, Clock, Zap, Info } from 'lucide-react'
import { useLanguage } from '@/lib/i18n'
interface TestResult {
success: boolean
provider?: string
model?: string
responseTime?: number
tags?: Array<{ tag: string; confidence: number }>
embeddingLength?: number
firstValues?: number[]
error?: string
details?: any
}
export function AI_TESTER({ type }: { type: 'tags' | 'embeddings' }) {
const { t } = useLanguage()
const [isLoading, setIsLoading] = useState(false)
const [result, setResult] = useState<TestResult | null>(null)
const [config, setConfig] = useState<any>(null)
useEffect(() => {
fetchConfig()
}, [])
const fetchConfig = async () => {
try {
const response = await fetch('/api/ai/config')
const data = await response.json()
setConfig(data)
if (data.previousTest) {
setResult(data.previousTest[type] || null)
}
} catch (error) {
console.error('Failed to fetch config:', error)
}
}
const runTest = async () => {
setIsLoading(true)
const startTime = Date.now()
try {
const response = await fetch(`/api/ai/test-${type}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
})
const endTime = Date.now()
const data = await response.json()
setResult({
...data,
responseTime: endTime - startTime
})
if (data.success) {
toast.success(
`${t('admin.aiTest.testSuccessToast', { type: type === 'tags' ? 'Tags' : 'Embeddings' })}`,
{
description: `Provider: ${data.provider} | Time: ${endTime - startTime}ms`
}
)
} else {
toast.error(
`${t('admin.aiTest.testFailedToast', { type: type === 'tags' ? 'Tags' : 'Embeddings' })}`,
{
description: data.error || 'Unknown error'
}
)
}
} catch (error: any) {
const endTime = Date.now()
const errorResult = {
success: false,
error: error.message || 'Network error',
responseTime: endTime - startTime
}
setResult(errorResult)
toast.error(t('admin.aiTest.testError', { error: error.message }))
} finally {
setIsLoading(false)
}
}
const getProviderInfo = () => {
if (!config) return { provider: t('admin.aiTest.testing'), model: t('admin.aiTest.testing') }
if (type === 'tags') {
return {
provider: config.AI_PROVIDER_TAGS || 'ollama',
model: config.AI_MODEL_TAGS || 'granite4:latest'
}
} else {
return {
provider: config.AI_PROVIDER_EMBEDDING || 'ollama',
model: config.AI_MODEL_EMBEDDING || 'embeddinggemma:latest'
}
}
}
const providerInfo = getProviderInfo()
return (
<div className="space-y-4">
{/* Provider Info */}
<div className="space-y-3 p-4 bg-muted/50 rounded-lg">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">{t('admin.aiTest.provider')}</span>
<Badge variant="outline" className="text-xs">
{providerInfo.provider.toUpperCase()}
</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-sm font-medium">{t('admin.aiTest.model')}</span>
<span className="text-sm text-muted-foreground font-mono">
{providerInfo.model}
</span>
</div>
</div>
{/* Test Button */}
<Button
onClick={runTest}
disabled={isLoading}
className="w-full"
variant={result?.success ? 'default' : result?.success === false ? 'destructive' : 'default'}
>
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
{t('admin.aiTest.testing')}
</>
) : (
<>
<Zap className="mr-2 h-4 w-4" />
{t('admin.aiTest.runTest')}
</>
)}
</Button>
{/* Results */}
{result && (
<Card className={result.success ? 'border-green-200 dark:border-green-900' : 'border-red-200 dark:border-red-900'}>
<CardContent className="pt-6">
{/* Status */}
<div className="flex items-center gap-2 mb-4">
{result.success ? (
<>
<CheckCircle2 className="h-5 w-5 text-green-600" />
<span className="font-semibold text-green-600 dark:text-green-400">{t('admin.aiTest.testPassed')}</span>
</>
) : (
<>
<XCircle className="h-5 w-5 text-red-600" />
<span className="font-semibold text-red-600 dark:text-red-400">{t('admin.aiTest.testFailed')}</span>
</>
)}
</div>
{/* Response Time */}
{result.responseTime && (
<div className="flex items-center gap-2 text-sm text-muted-foreground mb-4">
<Clock className="h-4 w-4" />
<span>{t('admin.aiTest.responseTime', { time: result.responseTime })}</span>
</div>
)}
{/* Tags Results */}
{type === 'tags' && result.success && result.tags && (
<div className="space-y-3">
<div className="flex items-center gap-2">
<Info className="h-4 w-4 text-primary" />
<span className="text-sm font-medium">{t('admin.aiTest.generatedTags')}</span>
</div>
<div className="flex flex-wrap gap-2">
{result.tags.map((tag, idx) => (
<Badge
key={idx}
variant="secondary"
className="text-sm"
>
{tag.tag}
<span className="ml-1 text-xs opacity-70">
({Math.round(tag.confidence * 100)}%)
</span>
</Badge>
))}
</div>
</div>
)}
{/* Embeddings Results */}
{type === 'embeddings' && result.success && result.embeddingLength && (
<div className="space-y-3">
<div className="flex items-center gap-2">
<Info className="h-4 w-4 text-green-600" />
<span className="text-sm font-medium">{t('admin.aiTest.embeddingDimensions')}</span>
</div>
<div className="p-3 bg-muted rounded-lg">
<div className="text-2xl font-bold text-center">
{result.embeddingLength}
</div>
<div className="text-xs text-center text-muted-foreground mt-1">
{t('admin.aiTest.vectorDimensions')}
</div>
</div>
{result.firstValues && result.firstValues.length > 0 && (
<div className="space-y-1">
<span className="text-xs font-medium">{t('admin.aiTest.first5Values')}</span>
<div className="p-2 bg-muted rounded font-mono text-xs">
[{result.firstValues.slice(0, 5).map((v, i) => v.toFixed(4)).join(', ')}]
</div>
</div>
)}
</div>
)}
{/* Error Details */}
{!result.success && result.error && (
<div className="mt-4 p-3 bg-red-50 dark:bg-red-950/20 rounded-lg border border-red-200 dark:border-red-900">
<p className="text-sm font-medium text-red-900 dark:text-red-100">{t('admin.aiTest.error')}</p>
<p className="text-sm text-red-700 dark:text-red-300 mt-1">{result.error}</p>
{result.details && (
<details className="mt-2">
<summary className="text-xs cursor-pointer text-red-600 dark:text-red-400">
{t('admin.aiTest.technicalDetails')}
</summary>
<pre className="mt-2 text-xs overflow-auto p-2 bg-red-100 dark:bg-red-900/30 rounded">
{JSON.stringify(result.details, null, 2)}
</pre>
</details>
)}
</div>
)}
</CardContent>
</Card>
)}
{/* Loading State */}
{isLoading && (
<div className="text-center py-4">
<Loader2 className="h-8 w-8 animate-spin mx-auto text-primary" />
<p className="text-sm text-muted-foreground mt-2">
{t('admin.aiTest.testingType', { type: type === 'tags' ? 'tags generation' : 'embeddings' })}
</p>
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,102 @@
'use client'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import Link from 'next/link'
import { ArrowLeft, TestTube } from 'lucide-react'
import { AI_TESTER } from './ai-tester'
import { useLanguage } from '@/lib/i18n'
export default function AITestPage() {
const { t } = useLanguage()
return (
<div className="container mx-auto py-10 px-4 max-w-6xl">
<div className="flex justify-between items-center mb-8">
<div className="flex items-center gap-3">
<Link href="/admin/settings">
<Button variant="outline" size="icon">
<ArrowLeft className="h-4 w-4" />
</Button>
</Link>
<div>
<h1 className="text-3xl font-bold flex items-center gap-2">
<TestTube className="h-8 w-8" />
{t('admin.aiTest.title')}
</h1>
<p className="text-muted-foreground mt-1">
{t('admin.aiTest.description')}
</p>
</div>
</div>
</div>
<div className="grid gap-6 md:grid-cols-2">
{/* Tags Provider Test */}
<Card className="border-primary/20 dark:border-primary/30">
<CardHeader className="bg-primary/5 dark:bg-primary/10">
<CardTitle className="flex items-center gap-2">
<span className="text-2xl">🏷</span>
{t('admin.aiTest.tagsTestTitle')}
</CardTitle>
<CardDescription>
{t('admin.aiTest.tagsTestDescription')}
</CardDescription>
</CardHeader>
<CardContent className="pt-6">
<AI_TESTER type="tags" />
</CardContent>
</Card>
{/* Embeddings Provider Test */}
<Card className="border-green-200 dark:border-green-900">
<CardHeader className="bg-green-50/50 dark:bg-green-950/20">
<CardTitle className="flex items-center gap-2">
<span className="text-2xl">🔍</span>
{t('admin.aiTest.embeddingsTestTitle')}
</CardTitle>
<CardDescription>
{t('admin.aiTest.embeddingsTestDescription')}
</CardDescription>
</CardHeader>
<CardContent className="pt-6">
<AI_TESTER type="embeddings" />
</CardContent>
</Card>
</div>
{/* Info Section */}
<Card className="mt-6">
<CardHeader>
<CardTitle> {t('admin.aiTest.howItWorksTitle')}</CardTitle>
</CardHeader>
<CardContent className="space-y-4 text-sm">
<div>
<h4 className="font-semibold mb-2">{t('admin.aiTest.tagsGenerationTest')}</h4>
<ul className="list-disc list-inside space-y-1 text-muted-foreground">
<li>{t('admin.aiTest.tagsStep1')}</li>
<li>{t('admin.aiTest.tagsStep2')}</li>
<li>{t('admin.aiTest.tagsStep3')}</li>
<li>{t('admin.aiTest.tagsStep4')}</li>
</ul>
</div>
<div>
<h4 className="font-semibold mb-2">{t('admin.aiTest.embeddingsTestLabel')}</h4>
<ul className="list-disc list-inside space-y-1 text-muted-foreground">
<li>{t('admin.aiTest.embeddingsStep1')}</li>
<li>{t('admin.aiTest.embeddingsStep2')}</li>
<li>{t('admin.aiTest.embeddingsStep3')}</li>
<li>{t('admin.aiTest.embeddingsStep4')}</li>
</ul>
</div>
<div className="bg-amber-50 dark:bg-amber-950/20 p-4 rounded-lg border border-amber-200 dark:border-amber-900">
<p className="font-semibold text-amber-900 dark:text-amber-100">💡 {t('admin.aiTest.tipTitle')}</p>
<p className="text-amber-800 dark:text-amber-200 mt-1">
{t('admin.aiTest.tipContent')}
</p>
</div>
</CardContent>
</Card>
</div>
)
}

View File

@@ -0,0 +1,147 @@
import { AdminMetrics } from '@/components/admin-metrics'
import { Button } from '@/components/ui/button'
import { Zap, Settings, Activity, TrendingUp } from 'lucide-react'
import Link from 'next/link'
import { getSystemConfig } from '@/lib/config'
export default async function AdminAIPage() {
const config = await getSystemConfig()
// Determine provider status based on config
const openaiKey = config.OPENAI_API_KEY
const ollamaUrl = config.OLLAMA_BASE_URL || config.OLLAMA_BASE_URL_TAGS || config.OLLAMA_BASE_URL_EMBEDDING
const providers = [
{
name: 'OpenAI',
status: openaiKey ? 'Connected' : 'Not Configured',
requests: 'N/A' // We don't track request counts yet
},
{
name: 'Ollama',
status: ollamaUrl ? 'Available' : 'Not Configured',
requests: 'N/A'
},
]
// Mock AI metrics - in a real app, these would come from analytics
// TODO: Implement real analytics tracking
const aiMetrics = [
{
title: 'Total Requests',
value: '—',
trend: { value: 0, isPositive: true },
icon: <Zap className="h-5 w-5 text-yellow-600 dark:text-yellow-400" />,
},
{
title: 'Success Rate',
value: '100%',
trend: { value: 0, isPositive: true },
icon: <TrendingUp className="h-5 w-5 text-green-600 dark:text-green-400" />,
},
{
title: 'Avg Response Time',
value: '—',
trend: { value: 0, isPositive: true },
icon: <Activity className="h-5 w-5 text-primary dark:text-primary-foreground" />,
},
{
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>
<Link href="/admin/settings">
<Button variant="outline">
<Settings className="mr-2 h-4 w-4" />
Configure
</Button>
</Link>
</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">
{providers.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' || provider.status === 'Available'
? 'text-green-700 dark:text-green-400 bg-green-100 dark:bg-green-900'
: 'text-gray-600 dark:text-gray-400 bg-gray-100 dark:bg-gray-800'
}`}
>
{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. (Coming Soon)
</p>
</div>
</div>
)
}

View File

@@ -0,0 +1,80 @@
'use client'
import { useState } from 'react'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogFooter,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Plus } from 'lucide-react'
import { createUser } from '@/app/actions/admin'
import { toast } from 'sonner'
import { useLanguage } from '@/lib/i18n'
export function CreateUserDialog() {
const [open, setOpen] = useState(false)
const { t } = useLanguage()
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>
<Plus className="mr-2 h-4 w-4" /> {t('admin.users.addUser')}
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>{t('admin.users.createUser')}</DialogTitle>
<DialogDescription>
{t('admin.users.createUserDescription')}
</DialogDescription>
</DialogHeader>
<form
action={async (formData) => {
const result = await createUser(formData)
if (result?.error) {
toast.error(t('admin.users.createFailed'))
} else {
toast.success(t('admin.users.createSuccess'))
setOpen(false)
}
}}
className="grid gap-4 py-4"
>
<div className="grid gap-2">
<label htmlFor="name">{t('admin.users.name')}</label>
<Input id="name" name="name" required />
</div>
<div className="grid gap-2">
<label htmlFor="email">{t('admin.users.email')}</label>
<Input id="email" name="email" type="email" required />
</div>
<div className="grid gap-2">
<label htmlFor="password">{t('admin.users.password')}</label>
<Input id="password" name="password" type="password" required minLength={6} />
</div>
<div className="grid gap-2">
<label htmlFor="role">{t('admin.users.role')}</label>
<select
id="role"
name="role"
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
>
<option value="USER">{t('admin.users.roles.user')}</option>
<option value="ADMIN">{t('admin.users.roles.admin')}</option>
</select>
</div>
<DialogFooter>
<Button type="submit">{t('admin.users.createUser')}</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}

View 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 h-full bg-gray-50 dark:bg-zinc-950">
<AdminSidebar />
<AdminContentArea>{children}</AdminContentArea>
</div>
)
}

View File

@@ -0,0 +1,20 @@
export default function AdminLoading() {
return (
<div className="space-y-6 animate-pulse">
<div>
<div className="h-9 w-48 bg-muted rounded-md mb-2" />
<div className="h-4 w-72 bg-muted rounded-md" />
</div>
{[1, 2, 3].map((i) => (
<div key={i} className="rounded-lg border border-border bg-white dark:bg-zinc-900 p-6 space-y-4">
<div className="h-5 w-40 bg-muted rounded" />
<div className="h-px bg-border" />
<div className="space-y-3">
<div className="h-4 w-full bg-muted rounded" />
<div className="h-4 w-3/4 bg-muted rounded" />
</div>
</div>
))}
</div>
)
}

View File

@@ -0,0 +1,59 @@
import { getUsers } from '@/app/actions/admin'
import { AdminMetrics } from '@/components/admin-metrics'
import { Users, Activity, Database, Zap } from 'lucide-react'
export default async function AdminPage() {
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-primary dark:text-primary-foreground" />,
},
{
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="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>
<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>
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,23 @@
import { getSystemConfig } from '@/app/actions/admin-settings'
import { AdminSettingsForm } from './admin-settings-form'
export default async function AdminSettingsPage() {
const config = await getSystemConfig()
return (
<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>
)
}

View File

@@ -0,0 +1,83 @@
'use client'
import { useState } from 'react'
import { Button } from '@/components/ui/button'
import { deleteUser, updateUserRole } from '@/app/actions/admin'
import { toast } from 'sonner'
import { Trash2, Shield, ShieldOff } from 'lucide-react'
import { format } from 'date-fns'
import { useLanguage } from '@/lib/i18n'
export function UserList({ initialUsers }: { initialUsers: any[] }) {
const { t } = useLanguage()
const handleDelete = async (id: string) => {
if (!confirm(t('admin.users.confirmDelete'))) return
try {
await deleteUser(id)
toast.success(t('admin.users.deleteSuccess'))
} catch (e) {
toast.error(t('admin.users.deleteFailed'))
}
}
const handleRoleToggle = async (user: any) => {
const newRole = user.role === 'ADMIN' ? 'USER' : 'ADMIN'
try {
await updateUserRole(user.id, newRole)
toast.success(t('admin.users.roleUpdateSuccess', { role: newRole }))
} catch (e) {
toast.error(t('admin.users.roleUpdateFailed'))
}
}
return (
<div className="w-full overflow-auto">
<table className="w-full caption-bottom text-sm text-left">
<thead className="[&_tr]:border-b">
<tr className="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted">
<th className="h-12 px-4 align-middle font-medium text-muted-foreground">{t('admin.users.table.name')}</th>
<th className="h-12 px-4 align-middle font-medium text-muted-foreground">{t('admin.users.table.email')}</th>
<th className="h-12 px-4 align-middle font-medium text-muted-foreground">{t('admin.users.table.role')}</th>
<th className="h-12 px-4 align-middle font-medium text-muted-foreground">{t('admin.users.table.createdAt')}</th>
<th className="h-12 px-4 align-middle font-medium text-muted-foreground text-right">{t('admin.users.table.actions')}</th>
</tr>
</thead>
<tbody className="[&_tr:last-child]:border-0">
{initialUsers.map((user) => (
<tr key={user.id} className="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted">
<td className="p-4 align-middle font-medium">{user.name || t('common.notAvailable')}</td>
<td className="p-4 align-middle">{user.email}</td>
<td className="p-4 align-middle">
<span className={`inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 ${user.role === 'ADMIN' ? 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80' : 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80'}`}>
{user.role === 'ADMIN' ? t('admin.users.roles.admin') : t('admin.users.roles.user')}
</span>
</td>
<td className="p-4 align-middle">{format(new Date(user.createdAt), 'PP')}</td>
<td className="p-4 align-middle text-right">
<div className="flex justify-end gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => handleRoleToggle(user)}
title={user.role === 'ADMIN' ? t('admin.users.demote') : t('admin.users.promote')}
>
{user.role === 'ADMIN' ? <ShieldOff className="h-4 w-4" /> : <Shield className="h-4 w-4" />}
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDelete(user.id)}
className="text-red-600 hover:text-red-900"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)
}

View 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>
)
}