add admin
This commit is contained in:
parent
2e44c5865d
commit
b052bbedf5
@ -218,6 +218,7 @@
|
||||
},
|
||||
"admin": {
|
||||
"dashboard": "Admin Dashboard",
|
||||
"dashboardDesc": "Manage users, prompts, and system settings",
|
||||
"totalUsers": "Total Users",
|
||||
"totalPrompts": "Total Prompts",
|
||||
"sharedPrompts": "Shared Prompts",
|
||||
@ -238,7 +239,10 @@
|
||||
"approve": "Approve",
|
||||
"reject": "Reject",
|
||||
"allPrompts": "All Prompts",
|
||||
"allPromptsDesc": "Review all shared prompts"
|
||||
"allPromptsDesc": "Review all shared prompts",
|
||||
"loadingAdmin": "Loading admin panel...",
|
||||
"loadingDashboard": "Loading dashboard statistics...",
|
||||
"loadingPrompts": "Loading prompts for review..."
|
||||
},
|
||||
"errors": {
|
||||
"generic": "Something went wrong. Please try again.",
|
||||
|
@ -218,6 +218,7 @@
|
||||
},
|
||||
"admin": {
|
||||
"dashboard": "管理员后台",
|
||||
"dashboardDesc": "管理用户、提示词和系统设置",
|
||||
"totalUsers": "用户总数",
|
||||
"totalPrompts": "提示词总数",
|
||||
"sharedPrompts": "用户共享提示词",
|
||||
@ -238,7 +239,10 @@
|
||||
"approve": "通过",
|
||||
"reject": "拒绝",
|
||||
"allPrompts": "所有提示词",
|
||||
"allPromptsDesc": "审核所有共享提示词"
|
||||
"allPromptsDesc": "审核所有共享提示词",
|
||||
"loadingAdmin": "加载管理员后台中...",
|
||||
"loadingDashboard": "加载统计数据中...",
|
||||
"loadingPrompts": "加载审核提示词中..."
|
||||
},
|
||||
"errors": {
|
||||
"generic": "出现错误,请重试。",
|
||||
|
@ -4,6 +4,7 @@ import { useUser } from '@/hooks/useUser'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useEffect } from 'react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { Header } from '@/components/layout/Header'
|
||||
|
||||
export default function AdminLayout({
|
||||
children,
|
||||
@ -22,9 +23,15 @@ export default function AdminLayout({
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="flex items-center justify-center h-screen">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||
<div className="min-h-screen">
|
||||
<Header />
|
||||
<div className="max-w-7xl mx-auto px-4 py-8">
|
||||
<div className="flex items-center justify-center min-h-96">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||
<p className="text-sm text-muted-foreground">{t('loadingAdmin')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@ -35,17 +42,9 @@ export default function AdminLayout({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="border-b border-border bg-card">
|
||||
<div className="container mx-auto px-4 py-4">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{t('dashboard')}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
{children}
|
||||
</div>
|
||||
<div className="min-h-screen">
|
||||
<Header />
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -38,15 +38,22 @@ export default function AdminDashboard() {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{[...Array(4)].map((_, i) => (
|
||||
<Card key={i} className="p-6">
|
||||
<div className="animate-pulse">
|
||||
<div className="h-4 bg-muted rounded w-3/4 mb-2"></div>
|
||||
<div className="h-8 bg-muted rounded w-1/2"></div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
<div className="max-w-7xl mx-auto px-4 py-8">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-2xl sm:text-3xl font-bold text-foreground mb-2">{t('dashboard')}</h1>
|
||||
<p className="text-muted-foreground">
|
||||
{t('dashboardDesc') || 'Manage users, prompts, and system settings'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Loading State */}
|
||||
<div className="flex items-center justify-center min-h-96">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||
<p className="text-sm text-muted-foreground">{t('loadingDashboard')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -79,78 +86,112 @@ export default function AdminDashboard() {
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Stats Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{statCards.map((stat, index) => {
|
||||
const Icon = stat.icon
|
||||
return (
|
||||
<Card key={index} className="p-6 hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">
|
||||
{stat.title}
|
||||
</p>
|
||||
<p className="text-3xl font-bold text-foreground">
|
||||
{stat.value.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<Icon className={`h-8 w-8 ${stat.color}`} />
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
<div className="max-w-7xl mx-auto px-4 py-8">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-2xl sm:text-3xl font-bold text-foreground mb-2">{t('dashboard')}</h1>
|
||||
<p className="text-muted-foreground">
|
||||
{t('dashboardDesc') || 'Manage users, prompts, and system settings'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-foreground mb-4">
|
||||
{t('quickActions')}
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
<Link
|
||||
href="/admin/review"
|
||||
className="block p-3 rounded-lg border border-border hover:bg-accent hover:text-accent-foreground transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<CheckCircle className="h-5 w-5 text-primary" />
|
||||
<div>
|
||||
<div className="font-medium">
|
||||
{t('allPrompts')}
|
||||
<div className="space-y-6 lg:space-y-8">
|
||||
{/* Stats Grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 lg:gap-6">
|
||||
{statCards.map((stat, index) => {
|
||||
const Icon = stat.icon
|
||||
return (
|
||||
<Card key={index} className="p-4 lg:p-6 hover:shadow-md transition-all duration-200 border-border hover:border-primary/20">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-xs sm:text-sm font-medium text-muted-foreground mb-1 truncate">
|
||||
{stat.title}
|
||||
</p>
|
||||
<p className="text-xl sm:text-2xl lg:text-3xl font-bold text-foreground">
|
||||
{stat.value.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t('allPromptsDesc')}
|
||||
<div className="flex-shrink-0 ml-3">
|
||||
<div className="p-2 lg:p-3 rounded-full bg-muted/50">
|
||||
<Icon className={`h-5 w-5 lg:h-6 lg:w-6 ${stat.color}`} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</Card>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-foreground mb-4">
|
||||
{t('systemStatus')}
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t('databaseStatus')}
|
||||
</span>
|
||||
<span className="text-sm font-medium text-green-600">
|
||||
{t('healthy')}
|
||||
</span>
|
||||
{/* Quick Actions and System Status */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Quick Actions */}
|
||||
<Card className="p-4 lg:p-6">
|
||||
<div className="flex items-center gap-3 mb-4 lg:mb-6">
|
||||
<div className="p-2 rounded-lg bg-primary/10">
|
||||
<CheckCircle className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-foreground">
|
||||
{t('quickActions')}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t('authStatus')}
|
||||
</span>
|
||||
<span className="text-sm font-medium text-green-600">
|
||||
{t('healthy')}
|
||||
</span>
|
||||
<div className="space-y-3">
|
||||
<Link
|
||||
href="/admin/review"
|
||||
className="group block p-3 lg:p-4 rounded-lg border border-border hover:border-primary/30 hover:bg-accent/50 transition-all duration-200"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-md bg-primary/5 group-hover:bg-primary/10 transition-colors">
|
||||
<FileText className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="font-medium text-foreground group-hover:text-primary transition-colors">
|
||||
{t('allPrompts')}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground mt-0.5">
|
||||
{t('allPromptsDesc')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Card>
|
||||
|
||||
{/* System Status */}
|
||||
<Card className="p-4 lg:p-6">
|
||||
<div className="flex items-center gap-3 mb-4 lg:mb-6">
|
||||
<div className="p-2 rounded-lg bg-green-100 dark:bg-green-900/30">
|
||||
<CheckCircle className="h-5 w-5 text-green-600" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-foreground">
|
||||
{t('systemStatus')}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between p-3 rounded-lg bg-muted/30">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-green-500"></div>
|
||||
<span className="text-sm font-medium text-foreground">
|
||||
{t('databaseStatus')}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-green-600">
|
||||
{t('healthy')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-3 rounded-lg bg-muted/30">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-green-500"></div>
|
||||
<span className="text-sm font-medium text-foreground">
|
||||
{t('authStatus')}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-green-600">
|
||||
{t('healthy')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -73,104 +73,123 @@ export default function AdminReviewPage() {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{t('reviewPrompts') || 'Review Prompts'}
|
||||
</h1>
|
||||
<div className="max-w-7xl mx-auto px-4 py-8">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-4">
|
||||
<div>
|
||||
<h1 className="text-2xl sm:text-3xl font-bold text-foreground mb-2">
|
||||
{t('allPrompts')}
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
{t('reviewPromptsDesc')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<Card key={i} className="p-6">
|
||||
<div className="animate-pulse space-y-4">
|
||||
<div className="h-4 bg-muted rounded w-3/4"></div>
|
||||
<div className="h-20 bg-muted rounded"></div>
|
||||
<div className="flex gap-2">
|
||||
<div className="h-9 bg-muted rounded w-20"></div>
|
||||
<div className="h-9 bg-muted rounded w-20"></div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
{/* Loading State */}
|
||||
<div className="flex items-center justify-center min-h-96">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||
<p className="text-sm text-muted-foreground">{t('loadingPrompts')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{t('allPrompts')}
|
||||
</h1>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="secondary">
|
||||
{prompts.filter(p => !p.visibility || p.visibility === 'under_review').length} {t('pending')}
|
||||
</Badge>
|
||||
<Badge variant="outline">
|
||||
{prompts.filter(p => p.visibility === 'published').length} {t('published')}
|
||||
</Badge>
|
||||
<div className="max-w-7xl mx-auto px-4 py-8">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-4">
|
||||
<div>
|
||||
<h1 className="text-2xl sm:text-3xl font-bold text-foreground mb-2">
|
||||
{t('allPrompts')}
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
{t('reviewPromptsDesc')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="secondary" className="text-xs sm:text-sm">
|
||||
{prompts.filter(p => !p.visibility || p.visibility === 'under_review').length} {t('pending')}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="text-xs sm:text-sm">
|
||||
{prompts.filter(p => p.visibility === 'published').length} {t('published')}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
{prompts.length === 0 ? (
|
||||
<Card className="p-8 text-center">
|
||||
<CheckCircle className="h-12 w-12 text-green-500 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-semibold text-foreground mb-2">
|
||||
{t('noPromptsPending')}
|
||||
</h3>
|
||||
<p className="text-muted-foreground">
|
||||
{t('allPromptsReviewed')}
|
||||
</p>
|
||||
<Card className="p-8 lg:p-12 text-center">
|
||||
<div className="max-w-md mx-auto">
|
||||
<CheckCircle className="h-12 w-12 lg:h-16 lg:w-16 text-green-500 mx-auto mb-4" />
|
||||
<h3 className="text-lg lg:text-xl font-semibold text-foreground mb-2">
|
||||
{t('noPromptsPending')}
|
||||
</h3>
|
||||
<p className="text-muted-foreground">
|
||||
{t('allPromptsReviewed')}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4 lg:space-y-6">
|
||||
{prompts.map((prompt) => (
|
||||
<Card key={prompt.id} className="p-6">
|
||||
<Card key={prompt.id} className="p-4 lg:p-6 border-border hover:border-primary/20 transition-all duration-200">
|
||||
<div className="space-y-4">
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-lg font-semibold text-foreground">
|
||||
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3">
|
||||
<div className="space-y-1 min-w-0 flex-1">
|
||||
<h3 className="text-lg lg:text-xl font-semibold text-foreground break-words">
|
||||
{prompt.name}
|
||||
</h3>
|
||||
{prompt.description && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-sm text-muted-foreground line-clamp-2 break-words">
|
||||
{prompt.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Badge
|
||||
variant={prompt.visibility === 'published' ? 'default' : 'outline'}
|
||||
className={prompt.visibility === 'published' ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' : ''}
|
||||
>
|
||||
{prompt.visibility === 'published' ? t('published') : t('underReview')}
|
||||
</Badge>
|
||||
<div className="flex-shrink-0">
|
||||
<Badge
|
||||
variant={prompt.visibility === 'published' ? 'default' : 'outline'}
|
||||
className={`text-xs sm:text-sm ${
|
||||
prompt.visibility === 'published'
|
||||
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
{prompt.visibility === 'published' ? t('published') : t('underReview')}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Metadata */}
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4 text-sm text-muted-foreground">
|
||||
<div className="flex items-center gap-1">
|
||||
<UserIcon className="h-4 w-4" />
|
||||
<span>{prompt.user.username || prompt.user.email}</span>
|
||||
<UserIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<span className="truncate">{prompt.user.username || prompt.user.email}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Calendar className="h-4 w-4" />
|
||||
<span>
|
||||
<Calendar className="h-4 w-4 flex-shrink-0" />
|
||||
<span className="whitespace-nowrap">
|
||||
{formatDistanceToNow(new Date(prompt.createdAt), { addSuffix: true })}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content Preview */}
|
||||
<div className="border border-border rounded-lg p-4 bg-muted/50">
|
||||
<div className="border border-border rounded-lg p-3 lg:p-4 bg-muted/30">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Eye className="h-4 w-4 text-muted-foreground" />
|
||||
<Eye className="h-4 w-4 text-muted-foreground flex-shrink-0" />
|
||||
<span className="text-sm font-medium text-muted-foreground">
|
||||
{t('promptContent')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm text-foreground max-h-32 overflow-y-auto">
|
||||
<div className="text-sm text-foreground max-h-32 lg:max-h-40 overflow-y-auto break-words">
|
||||
{prompt.content.length > 500
|
||||
? `${prompt.content.slice(0, 500)}...`
|
||||
: prompt.content
|
||||
@ -179,11 +198,11 @@ export default function AdminReviewPage() {
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-3 pt-2">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-3 pt-2 border-t border-border/50">
|
||||
{prompt.visibility !== 'published' && (
|
||||
<Button
|
||||
onClick={() => handleApprove(prompt.id)}
|
||||
className="bg-green-600 hover:bg-green-700 text-white"
|
||||
className="bg-green-600 hover:bg-green-700 text-white w-full sm:w-auto"
|
||||
size="sm"
|
||||
>
|
||||
<CheckCircle className="h-4 w-4 mr-2" />
|
||||
@ -194,6 +213,7 @@ export default function AdminReviewPage() {
|
||||
onClick={() => handleReject(prompt.id)}
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
<XCircle className="h-4 w-4 mr-2" />
|
||||
{t('reject')}
|
||||
|
Loading…
Reference in New Issue
Block a user