161 lines
5.3 KiB
TypeScript
161 lines
5.3 KiB
TypeScript
import { useMemo } from 'react'
|
||
import Widget from '@/components/ui/Widget/Widget'
|
||
import { ForumCategory, ForumPost, ForumTopic } from '@/proxy/forum/forum'
|
||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||
import dayjs from 'dayjs'
|
||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||
import 'dayjs/locale/tr'
|
||
|
||
// Plugin kaydı modül seviyesinde yapılır; her render'da tekrarlanması gereksiz iştir.
|
||
dayjs.extend(relativeTime)
|
||
|
||
const MAX_ACTIVITIES = 5
|
||
|
||
interface AdminStatsProps {
|
||
categories: ForumCategory[]
|
||
topics: ForumTopic[]
|
||
posts: ForumPost[]
|
||
totalCounts: { categories: number; topics: number; posts: number }
|
||
}
|
||
|
||
interface Activity {
|
||
id: string
|
||
message: string
|
||
color: string
|
||
date: Date
|
||
}
|
||
|
||
export function AdminStats({ categories, topics, posts, totalCounts }: AdminStatsProps) {
|
||
const { translate, cultureName } = useLocalization()
|
||
|
||
const activeCategories = categories.filter((c) => c.isActive).length
|
||
const solvedTopics = topics.filter((t) => t.isSolved).length
|
||
const acceptedAnswers = posts.filter((p) => p.isAcceptedAnswer).length
|
||
|
||
const stats = [
|
||
{
|
||
key: 'categories',
|
||
title: translate('::App.ForumDashboard.TotalCategories'),
|
||
value: totalCounts.categories,
|
||
subTitle: translate('::App.ForumDashboard.ActiveCategories', { 0: activeCategories }),
|
||
icon: 'FaFolder' as const,
|
||
color: 'blue' as const,
|
||
},
|
||
{
|
||
key: 'topics',
|
||
title: translate('::App.ForumDashboard.TotalTopics'),
|
||
value: totalCounts.topics,
|
||
subTitle: translate('::App.ForumDashboard.SolvedTopics', { 0: solvedTopics }),
|
||
icon: 'FaCommentDots' as const,
|
||
color: 'green' as const,
|
||
},
|
||
{
|
||
key: 'posts',
|
||
title: translate('::App.ForumDashboard.TotalPosts'),
|
||
value: totalCounts.posts,
|
||
subTitle: translate('::App.ForumDashboard.AcceptedAnswers', { 0: acceptedAnswers }),
|
||
icon: 'FaFileAlt' as const,
|
||
color: 'orange' as const,
|
||
},
|
||
{
|
||
key: 'engagement',
|
||
title: translate('::App.ForumDashboard.EngagementRate'),
|
||
value:
|
||
totalCounts.topics > 0
|
||
? Math.round((totalCounts.posts / totalCounts.topics) * 100) / 100
|
||
: 0,
|
||
subTitle: translate('::App.ForumDashboard.PostsPerTopic'),
|
||
icon: 'FaChartLine' as const,
|
||
color: 'purple' as const,
|
||
},
|
||
]
|
||
|
||
const latestActivities = useMemo<Activity[]>(() => {
|
||
const categoryNameById = new Map(categories.map((c) => [c.id, c.name]))
|
||
const activities: Activity[] = []
|
||
|
||
topics.forEach((topic) => {
|
||
if (!topic.creationTime) return
|
||
activities.push({
|
||
id: `topic-${topic.id}`,
|
||
message: `${translate('::App.ForumTopicManagement.NewTopic')} — ${
|
||
categoryNameById.get(topic.categoryId) ?? translate('::App.Platform.Unknown')
|
||
}`,
|
||
color: 'bg-blue-500',
|
||
date: new Date(topic.creationTime),
|
||
})
|
||
})
|
||
|
||
posts.forEach((post) => {
|
||
if (!post.isAcceptedAnswer || !post.creationTime) return
|
||
activities.push({
|
||
id: `post-${post.id}`,
|
||
message: translate('::App.ForumDashboard.Postmarked'),
|
||
color: 'bg-emerald-500',
|
||
date: new Date(post.creationTime),
|
||
})
|
||
})
|
||
|
||
categories.forEach((category) => {
|
||
if (!category.creationTime) return
|
||
activities.push({
|
||
id: `category-${category.id}`,
|
||
message: `${translate('::App.ForumDashboard.NewCategoryCreated')} — ${category.name}`,
|
||
color: 'bg-orange-500',
|
||
date: new Date(category.creationTime),
|
||
})
|
||
})
|
||
|
||
return activities
|
||
.sort((a, b) => b.date.getTime() - a.date.getTime())
|
||
.slice(0, MAX_ACTIVITIES)
|
||
}, [categories, posts, topics, translate])
|
||
|
||
return (
|
||
<div className="space-y-4">
|
||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
|
||
{stats.map((stat) => (
|
||
<Widget
|
||
key={stat.key}
|
||
title={stat.title}
|
||
value={stat.value}
|
||
subTitle={stat.subTitle}
|
||
icon={stat.icon}
|
||
color={stat.color}
|
||
/>
|
||
))}
|
||
</div>
|
||
|
||
{/* Recent Activity */}
|
||
<div className="rounded-xl border border-gray-200 bg-white p-6 shadow-sm dark:border-gray-700 dark:bg-gray-900">
|
||
<h3 className="mb-4 text-lg font-semibold text-gray-900 dark:text-white">
|
||
{translate('::App.ForumDashboard.RecentActivity')}
|
||
</h3>
|
||
<div className="space-y-4">
|
||
{latestActivities.length === 0 ? (
|
||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||
{translate('::App.Platform.NoDataFound')}
|
||
</p>
|
||
) : (
|
||
latestActivities.map((activity) => (
|
||
<div
|
||
key={activity.id}
|
||
className="flex items-start space-x-3 rounded-lg bg-gray-50 p-3 dark:bg-gray-800"
|
||
>
|
||
<div className={`mt-2 h-2 w-2 ${activity.color} rounded-full`} />
|
||
<div>
|
||
<p className="text-sm text-gray-900 dark:text-white">{activity.message}</p>
|
||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||
{dayjs(activity.date)
|
||
.locale(cultureName?.startsWith('tr') ? 'tr' : 'en')
|
||
.fromNow()}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
))
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|