From 4fb44fcb4cfd9b7ea5df4823c916cd9ba9b7b7a1 Mon Sep 17 00:00:00 2001 From: Amirhossein Khalili Date: Mon, 15 Jun 2026 17:38:11 +0330 Subject: [PATCH] fix(admin-dashboard): improve blog engagement visuals --- src/views/AdminDashboard.tsx | 237 +++++++++++++++++++++++++++++------ 1 file changed, 197 insertions(+), 40 deletions(-) diff --git a/src/views/AdminDashboard.tsx b/src/views/AdminDashboard.tsx index 12fb2f6..63c2526 100644 --- a/src/views/AdminDashboard.tsx +++ b/src/views/AdminDashboard.tsx @@ -30,11 +30,8 @@ import { Cell, Line, LineChart, - Scatter, - ScatterChart, XAxis, YAxis, - ZAxis, } from "recharts"; import AsyncSearchableCombobox from "@/components/AsyncSearchableCombobox"; import { Badge } from "@/components/ui/badge"; @@ -436,6 +433,167 @@ function DashboardPointDetailModal({ ); } +type PostEngagementDatum = AnalyticsPostPopularitySchema & { + label: string; + score: number; +}; + +function postScore(post: AnalyticsPostPopularitySchema) { + return Number(post.likes || 0) + Number(post.saves || 0) + Number(post.comments || 0); +} + +function toPostEngagementData(posts: AnalyticsPostPopularitySchema[]): PostEngagementDatum[] { + return posts.map((post) => ({ + ...post, + label: post.title, + score: postScore(post), + })); +} + +function PostEngagementTooltip({ + active, + payload, +}: { + active?: boolean; + payload?: Array<{ payload?: PostEngagementDatum }>; +}) { + if (!active || !payload?.length || !payload[0].payload) return null; + const item = payload[0].payload; + return ( +
+

{item.title}

+
+

لایک: {formatNumberPersian(item.likes)}

+

ذخیره: {formatNumberPersian(item.saves)}

+

کامنت: {formatNumberPersian(item.comments)}

+

تعامل کل: {formatNumberPersian(item.score)}

+
+
+ ); +} + +function BlogPostEngagementModal({ + open, + onOpenChange, + posts, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + posts: AnalyticsPostPopularitySchema[]; +}) { + const [search, setSearch] = React.useState(""); + const [sortBy, setSortBy] = React.useState<"score" | "likes" | "saves" | "comments" | "title">("score"); + const data = React.useMemo(() => { + const normalizedSearch = search.trim().toLocaleLowerCase("fa-IR"); + const filtered = normalizedSearch + ? posts.filter((post) => post.title.toLocaleLowerCase("fa-IR").includes(normalizedSearch)) + : posts; + return toPostEngagementData(filtered).sort((a, b) => { + if (sortBy === "title") return a.title.localeCompare(b.title, "fa"); + return Number(b[sortBy]) - Number(a[sortBy]) || a.title.localeCompare(b.title, "fa"); + }); + }, [posts, search, sortBy]); + + return ( + + + + محبوبیت نوشته‌ها +

نمای کامل تعامل نوشته‌ها با امکان جستجو و مرتب‌سازی

+
+
+ setSearch(event.target.value)} + placeholder="جستجو در عنوان نوشته..." + className="text-right" + /> + {[ + ["score", "تعامل کل"], + ["likes", "لایک"], + ["saves", "ذخیره"], + ["comments", "کامنت"], + ["title", "عنوان"], + ].map(([key, label]) => ( + + ))} +
+ {!data.length ? ( + + ) : ( + <> + + + ({ label: post.title, value: post.score }))) + 16, bottom: 24, left: 20 }} + > + + formatNumberPersian(Number(value))} /> + ({ label: post.title, value: post.score })))} + tickLine={false} + axisLine={false} + tickMargin={10} + tickFormatter={(value) => truncateLabel(String(value), 26)} + /> + } /> + + + + + + +
+ + + + + + + + + + + + {data.map((post) => ( + + + + + + + + ))} + +
نوشتهلایکذخیرهکامنتتعامل کل
{post.title}{formatNumberPersian(post.likes)}{formatNumberPersian(post.saves)}{formatNumberPersian(post.comments)}{formatNumberPersian(post.score)}
+
+ + )} +
+
+ ); +} + function ValuesTable({ data, unit, @@ -716,13 +874,16 @@ function ActivityTrendCard({ data }: { data: BlogAnalyticsSchema["activity_trend ); } -function BlogScatterCard({ group }: { group: BlogAnalyticsSchema["post_popularity"] }) { - const data = group.top_items; +function BlogEngagementCard({ group }: { group: BlogAnalyticsSchema["post_popularity"] }) { + const data = toPostEngagementData(group.top_items); + const allPosts = group.items?.length ? group.items : group.top_items; + const [detailsOpen, setDetailsOpen] = React.useState(false); + const labelAxisWidth = axisWidth(data.map((post) => ({ label: post.title, value: post.score }))); return ( محبوبیت نوشته‌ها - لایک در برابر ذخیره؛ اندازه نقطه بر اساس تعداد کامنت + رتبه‌بندی بر اساس مجموع لایک، ذخیره و کامنت {!data.length ? ( @@ -732,52 +893,48 @@ function BlogScatterCard({ group }: { group: BlogAnalyticsSchema["post_popularit - - + + formatNumberPersian(Number(value))} /> formatNumberPersian(Number(value))} + dataKey="label" + type="category" + orientation="right" + width={labelAxisWidth} + tickLine={false} + axisLine={false} + tickMargin={10} + tickFormatter={(value) => truncateLabel(String(value), 22)} /> - - { - if (!active || !payload?.length) return null; - const item = payload[0].payload as AnalyticsPostPopularitySchema; - return ( -
-

{item.title}

-
-

لایک: {formatNumberPersian(item.likes)}

-

ذخیره: {formatNumberPersian(item.saves)}

-

کامنت: {formatNumberPersian(item.comments)}

-
-
- ); - }} - /> - -
+ } /> + + + +
{group.total_count > data.length ? ( -

- نمایش {formatNumberPersian(data.length)} نوشته برتر از {formatNumberPersian(group.total_count)} نوشته دارای تعامل. -

+
+ نمودار خلاصه {formatNumberPersian(data.length)} نوشته اول را نشان می‌دهد. + +
) : null} + )}
@@ -1033,7 +1190,7 @@ function BlogContent({ data }: { data: BlogAnalyticsSchema }) {
- +