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'
|
|
|
|
|
|
|
|
|
|
|
|
interface AdminStatsProps {
|
|
|
|
|
|
categories: ForumCategory[]
|
|
|
|
|
|
topics: ForumTopic[]
|
|
|
|
|
|
posts: ForumPost[]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface Activity {
|
|
|
|
|
|
message: string
|
|
|
|
|
|
color: string
|
|
|
|
|
|
date: Date
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function AdminStats({ categories, topics, posts }: AdminStatsProps) {
|
|
|
|
|
|
const { translate } = useLocalization()
|
|
|
|
|
|
const totalCategories = categories.length
|
|
|
|
|
|
const activeCategories = categories.filter((c) => c.isActive).length
|
|
|
|
|
|
const totalTopics = topics.length
|
|
|
|
|
|
const solvedTopics = topics.filter((t) => t.isSolved).length
|
|
|
|
|
|
const totalPosts = posts.length
|
|
|
|
|
|
const acceptedAnswers = posts.filter((p) => p.isAcceptedAnswer).length
|
|
|
|
|
|
|
|
|
|
|
|
const stats = [
|
|
|
|
|
|
{
|
|
|
|
|
|
title: translate('::App.Forum.Dashboard.TotalCategories'),
|
|
|
|
|
|
value: totalCategories,
|
2026-05-13 20:03:10 +00:00
|
|
|
|
subTitle: `${activeCategories} active`,
|
|
|
|
|
|
icon: 'FaFolder' as const,
|
|
|
|
|
|
color: 'blue' as const,
|
2026-02-24 20:44:16 +00:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: translate('::App.Forum.Dashboard.TotalTopics'),
|
|
|
|
|
|
value: totalTopics,
|
2026-05-13 20:03:10 +00:00
|
|
|
|
subTitle: `${solvedTopics} solved`,
|
|
|
|
|
|
icon: 'FaCommentDots' as const,
|
|
|
|
|
|
color: 'green' as const,
|
2026-02-24 20:44:16 +00:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: translate('::App.Forum.Dashboard.TotalPosts'),
|
|
|
|
|
|
value: totalPosts,
|
2026-05-13 20:03:10 +00:00
|
|
|
|
subTitle: `${acceptedAnswers} accepted answers`,
|
|
|
|
|
|
icon: 'FaFileAlt' as const,
|
|
|
|
|
|
color: 'orange' as const,
|
2026-02-24 20:44:16 +00:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: translate('::App.Forum.Dashboard.EngagementRate'),
|
|
|
|
|
|
value: totalTopics > 0 ? Math.round((totalPosts / totalTopics) * 100) / 100 : 0,
|
2026-05-13 20:03:10 +00:00
|
|
|
|
subTitle: 'posts per topic',
|
|
|
|
|
|
icon: 'FaChartLine' as const,
|
|
|
|
|
|
color: 'purple' as const,
|
2026-02-24 20:44:16 +00:00
|
|
|
|
},
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
const recentActivities: Activity[] = []
|
|
|
|
|
|
|
|
|
|
|
|
dayjs.extend(relativeTime)
|
|
|
|
|
|
dayjs.locale('tr')
|
|
|
|
|
|
|
|
|
|
|
|
topics.forEach((topic) => {
|
|
|
|
|
|
const category = categories.find((c) => c.id === topic.categoryId)
|
|
|
|
|
|
if (topic.creationTime) {
|
|
|
|
|
|
recentActivities.push({
|
|
|
|
|
|
message: `New topic created in ${category?.name ?? 'Unknown Category'}`,
|
|
|
|
|
|
color: 'bg-blue-500',
|
|
|
|
|
|
date: new Date(topic.creationTime),
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// Posts -> "Post marked as accepted answer"
|
|
|
|
|
|
posts.forEach((post) => {
|
|
|
|
|
|
if (post.isAcceptedAnswer && post.creationTime) {
|
|
|
|
|
|
recentActivities.push({
|
|
|
|
|
|
message: 'Post marked as accepted answer',
|
|
|
|
|
|
color: 'bg-emerald-500',
|
|
|
|
|
|
date: new Date(post.creationTime),
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// Categories -> "New category created: {name}"
|
|
|
|
|
|
categories.forEach((category) => {
|
|
|
|
|
|
if (category.creationTime) {
|
|
|
|
|
|
recentActivities.push({
|
|
|
|
|
|
message: `New category created: ${category.name}`,
|
|
|
|
|
|
color: 'bg-orange-500',
|
|
|
|
|
|
date: new Date(category.creationTime),
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// Tarihe göre sırala, en güncel ilk 3 aktiviteyi al
|
|
|
|
|
|
const latestActivities = recentActivities
|
|
|
|
|
|
.sort((a, b) => b.date.getTime() - a.date.getTime())
|
|
|
|
|
|
.slice(0, 3)
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2026-05-13 20:03:10 +00:00
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
|
|
|
|
{stats.map((stat, index) => (
|
|
|
|
|
|
<Widget
|
|
|
|
|
|
key={index}
|
|
|
|
|
|
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-05-19 20:22:25 +00:00
|
|
|
|
<div className="bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
|
|
|
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
2026-02-24 20:44:16 +00:00
|
|
|
|
{translate('::App.Forum.Dashboard.RecentActivity')}
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
|
{latestActivities.map((activity, index) => (
|
2026-05-19 20:22:25 +00:00
|
|
|
|
<div key={index} className="flex items-start space-x-3 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
2026-02-24 20:44:16 +00:00
|
|
|
|
<div className={`w-2 h-2 ${activity.color} rounded-full mt-2`} />
|
|
|
|
|
|
<div>
|
2026-05-19 20:22:25 +00:00
|
|
|
|
<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).fromNow()}</p>
|
2026-02-24 20:44:16 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|