2026-08-15 05:57:47 +00:00
|
|
|
|
import { useMemo } from 'react'
|
2026-05-13 20:03:10 +00:00
|
|
|
|
import Widget from '@/components/ui/Widget/Widget'
|
2026-02-24 20:44:16 +00:00
|
|
|
|
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'
|
|
|
|
|
|
|
2026-08-15 05:57:47 +00:00
|
|
|
|
// Plugin kaydı modül seviyesinde yapılır; her render'da tekrarlanması gereksiz iştir.
|
|
|
|
|
|
dayjs.extend(relativeTime)
|
|
|
|
|
|
|
|
|
|
|
|
const MAX_ACTIVITIES = 5
|
|
|
|
|
|
|
2026-02-24 20:44:16 +00:00
|
|
|
|
interface AdminStatsProps {
|
|
|
|
|
|
categories: ForumCategory[]
|
|
|
|
|
|
topics: ForumTopic[]
|
|
|
|
|
|
posts: ForumPost[]
|
2026-07-11 20:35:32 +00:00
|
|
|
|
totalCounts: { categories: number; topics: number; posts: number }
|
2026-02-24 20:44:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface Activity {
|
2026-08-15 05:57:47 +00:00
|
|
|
|
id: string
|
2026-02-24 20:44:16 +00:00
|
|
|
|
message: string
|
|
|
|
|
|
color: string
|
|
|
|
|
|
date: Date
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-11 20:35:32 +00:00
|
|
|
|
export function AdminStats({ categories, topics, posts, totalCounts }: AdminStatsProps) {
|
2026-08-15 05:57:47 +00:00
|
|
|
|
const { translate, cultureName } = useLocalization()
|
|
|
|
|
|
|
2026-02-24 20:44:16 +00:00
|
|
|
|
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 = [
|
|
|
|
|
|
{
|
2026-08-15 05:57:47 +00:00
|
|
|
|
key: 'categories',
|
2026-08-14 14:03:34 +00:00
|
|
|
|
title: translate('::App.ForumDashboard.TotalCategories'),
|
2026-08-15 05:57:47 +00:00
|
|
|
|
value: totalCounts.categories,
|
|
|
|
|
|
subTitle: translate('::App.ForumDashboard.ActiveCategories', { 0: activeCategories }),
|
2026-05-13 20:03:10 +00:00
|
|
|
|
icon: 'FaFolder' as const,
|
|
|
|
|
|
color: 'blue' as const,
|
2026-02-24 20:44:16 +00:00
|
|
|
|
},
|
|
|
|
|
|
{
|
2026-08-15 05:57:47 +00:00
|
|
|
|
key: 'topics',
|
2026-08-14 14:03:34 +00:00
|
|
|
|
title: translate('::App.ForumDashboard.TotalTopics'),
|
2026-08-15 05:57:47 +00:00
|
|
|
|
value: totalCounts.topics,
|
|
|
|
|
|
subTitle: translate('::App.ForumDashboard.SolvedTopics', { 0: solvedTopics }),
|
2026-05-13 20:03:10 +00:00
|
|
|
|
icon: 'FaCommentDots' as const,
|
|
|
|
|
|
color: 'green' as const,
|
2026-02-24 20:44:16 +00:00
|
|
|
|
},
|
|
|
|
|
|
{
|
2026-08-15 05:57:47 +00:00
|
|
|
|
key: 'posts',
|
2026-08-14 14:03:34 +00:00
|
|
|
|
title: translate('::App.ForumDashboard.TotalPosts'),
|
2026-08-15 05:57:47 +00:00
|
|
|
|
value: totalCounts.posts,
|
|
|
|
|
|
subTitle: translate('::App.ForumDashboard.AcceptedAnswers', { 0: acceptedAnswers }),
|
2026-05-13 20:03:10 +00:00
|
|
|
|
icon: 'FaFileAlt' as const,
|
|
|
|
|
|
color: 'orange' as const,
|
2026-02-24 20:44:16 +00:00
|
|
|
|
},
|
|
|
|
|
|
{
|
2026-08-15 05:57:47 +00:00
|
|
|
|
key: 'engagement',
|
2026-08-14 14:03:34 +00:00
|
|
|
|
title: translate('::App.ForumDashboard.EngagementRate'),
|
2026-08-15 05:57:47 +00:00
|
|
|
|
value:
|
|
|
|
|
|
totalCounts.topics > 0
|
|
|
|
|
|
? Math.round((totalCounts.posts / totalCounts.topics) * 100) / 100
|
|
|
|
|
|
: 0,
|
|
|
|
|
|
subTitle: translate('::App.ForumDashboard.PostsPerTopic'),
|
2026-05-13 20:03:10 +00:00
|
|
|
|
icon: 'FaChartLine' as const,
|
|
|
|
|
|
color: 'purple' as const,
|
2026-02-24 20:44:16 +00:00
|
|
|
|
},
|
|
|
|
|
|
]
|
|
|
|
|
|
|
2026-08-15 05:57:47 +00:00
|
|
|
|
const latestActivities = useMemo<Activity[]>(() => {
|
|
|
|
|
|
const categoryNameById = new Map(categories.map((c) => [c.id, c.name]))
|
|
|
|
|
|
const activities: Activity[] = []
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
2026-08-15 05:57:47 +00:00
|
|
|
|
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')
|
|
|
|
|
|
}`,
|
2026-02-24 20:44:16 +00:00
|
|
|
|
color: 'bg-blue-500',
|
|
|
|
|
|
date: new Date(topic.creationTime),
|
|
|
|
|
|
})
|
2026-08-15 05:57:47 +00:00
|
|
|
|
})
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
2026-08-15 05:57:47 +00:00
|
|
|
|
posts.forEach((post) => {
|
|
|
|
|
|
if (!post.isAcceptedAnswer || !post.creationTime) return
|
|
|
|
|
|
activities.push({
|
|
|
|
|
|
id: `post-${post.id}`,
|
|
|
|
|
|
message: translate('::App.ForumDashboard.Postmarked'),
|
2026-02-24 20:44:16 +00:00
|
|
|
|
color: 'bg-emerald-500',
|
|
|
|
|
|
date: new Date(post.creationTime),
|
|
|
|
|
|
})
|
2026-08-15 05:57:47 +00:00
|
|
|
|
})
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
2026-08-15 05:57:47 +00:00
|
|
|
|
categories.forEach((category) => {
|
|
|
|
|
|
if (!category.creationTime) return
|
|
|
|
|
|
activities.push({
|
|
|
|
|
|
id: `category-${category.id}`,
|
|
|
|
|
|
message: `${translate('::App.ForumDashboard.NewCategoryCreated')} — ${category.name}`,
|
2026-02-24 20:44:16 +00:00
|
|
|
|
color: 'bg-orange-500',
|
|
|
|
|
|
date: new Date(category.creationTime),
|
|
|
|
|
|
})
|
2026-08-15 05:57:47 +00:00
|
|
|
|
})
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
2026-08-15 05:57:47 +00:00
|
|
|
|
return activities
|
|
|
|
|
|
.sort((a, b) => b.date.getTime() - a.date.getTime())
|
|
|
|
|
|
.slice(0, MAX_ACTIVITIES)
|
|
|
|
|
|
}, [categories, posts, topics, translate])
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
|
|
|
|
|
return (
|
2026-05-13 20:03:10 +00:00
|
|
|
|
<div className="space-y-4">
|
2026-08-15 05:57:47 +00:00
|
|
|
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
|
|
|
|
|
|
{stats.map((stat) => (
|
2026-05-13 20:03:10 +00:00
|
|
|
|
<Widget
|
2026-08-15 05:57:47 +00:00
|
|
|
|
key={stat.key}
|
2026-05-13 20:03:10 +00:00
|
|
|
|
title={stat.title}
|
|
|
|
|
|
value={stat.value}
|
|
|
|
|
|
subTitle={stat.subTitle}
|
|
|
|
|
|
icon={stat.icon}
|
|
|
|
|
|
color={stat.color}
|
|
|
|
|
|
/>
|
|
|
|
|
|
))}
|
2026-02-24 20:44:16 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Recent Activity */}
|
2026-08-15 05:57:47 +00:00
|
|
|
|
<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">
|
2026-08-14 14:03:34 +00:00
|
|
|
|
{translate('::App.ForumDashboard.RecentActivity')}
|
2026-02-24 20:44:16 +00:00
|
|
|
|
</h3>
|
|
|
|
|
|
<div className="space-y-4">
|
2026-08-15 05:57:47 +00:00
|
|
|
|
{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>
|
2026-02-24 20:44:16 +00:00
|
|
|
|
</div>
|
2026-08-15 05:57:47 +00:00
|
|
|
|
))
|
|
|
|
|
|
)}
|
2026-02-24 20:44:16 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|