From 021bee944486e6d78f62a559e2d5badd4743f129 Mon Sep 17 00:00:00 2001 From: Amirhossein Khalili Date: Mon, 15 Jun 2026 21:34:59 +0330 Subject: [PATCH] fix(admin-dashboard): prevent mobile chart overflow --- src/views/AdminDashboard.tsx | 143 ++++++++++++++++++++++------------- 1 file changed, 91 insertions(+), 52 deletions(-) diff --git a/src/views/AdminDashboard.tsx b/src/views/AdminDashboard.tsx index f421ded..374031e 100644 --- a/src/views/AdminDashboard.tsx +++ b/src/views/AdminDashboard.tsx @@ -42,6 +42,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/u import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { useIsMobile } from "@/hooks/use-mobile"; import { api } from "@/lib/api"; import type { AnalyticsPointGroupSchema, @@ -138,13 +139,13 @@ function truncateLabel(value: string, max = 18) { return normalized.length > max ? `${normalized.slice(0, max - 1)}…` : normalized; } -function chartHeight(count: number, min = 280) { - return Math.max(min, count * 32 + 80); +function chartHeight(count: number, min = 280, compact = false) { + return Math.max(compact ? Math.min(min, 240) : min, count * (compact ? 26 : 32) + (compact ? 64 : 80)); } -function axisWidth(items: AnalyticsPointSchema[]) { +function axisWidth(items: AnalyticsPointSchema[], compact = false) { const maxLength = Math.max(...items.map((item) => String(item.label).length), 10); - return Math.min(190, Math.max(90, maxLength * 7)); + return Math.min(compact ? 92 : 190, Math.max(compact ? 64 : 90, maxLength * (compact ? 4.5 : 7))); } function dataWithOtherNotice(group: AnalyticsPointGroupSchema) { @@ -280,10 +281,23 @@ function FilterCard({ ); } -function ChartViewport({ children, minWidth = 420 }: { children: React.ReactNode; minWidth?: number }) { +function ChartViewport({ + children, + minWidth = 420, + scrollable = false, +}: { + children: React.ReactNode; + minWidth?: number; + scrollable?: boolean; +}) { + const isMobile = useIsMobile(); + const shouldScroll = scrollable && !isMobile; + return ( -
-
{children}
+
+
+ {children} +
); } @@ -354,6 +368,7 @@ function DashboardPointDetailModal({ }) { const [search, setSearch] = React.useState(""); const [sortBy, setSortBy] = React.useState<"value" | "label">("value"); + const isMobile = useIsMobile(); const filteredData = React.useMemo(() => { const normalizedSearch = search.trim().toLocaleLowerCase("fa-IR"); const filtered = normalizedSearch @@ -390,16 +405,16 @@ function DashboardPointDetailModal({ ) : ( <> - + truncateLabel(String(value), 24)} + tickMargin={isMobile ? 4 : 10} + tickFormatter={(value) => truncateLabel(String(value), isMobile ? 12 : 24)} /> } /> @@ -484,6 +499,7 @@ function BlogPostEngagementModal({ }) { const [search, setSearch] = React.useState(""); const [sortBy, setSortBy] = React.useState<"score" | "likes" | "saves" | "comments" | "title">("score"); + const isMobile = useIsMobile(); const data = React.useMemo(() => { const normalizedSearch = search.trim().toLocaleLowerCase("fa-IR"); const filtered = normalizedSearch @@ -530,7 +546,7 @@ function BlogPostEngagementModal({ ) : ( <> - + ({ label: post.title, value: post.score })))} + width={axisWidth(data.map((post) => ({ label: post.title, value: post.score })), isMobile)} tickLine={false} axisLine={false} - tickMargin={10} - tickFormatter={(value) => truncateLabel(String(value), 26)} + tickMargin={isMobile ? 4 : 10} + tickFormatter={(value) => truncateLabel(String(value), isMobile ? 12 : 26)} /> } /> @@ -651,8 +667,9 @@ function HorizontalBarCard({ const data = dataWithOtherNotice(group); const allData = fullGroupItems(group); const [detailsOpen, setDetailsOpen] = React.useState(false); + const isMobile = useIsMobile(); return ( - + {title} {description} @@ -666,9 +683,13 @@ function HorizontalBarCard({ - + truncateLabel(String(value))} + tickMargin={isMobile ? 4 : 10} + tickFormatter={(value) => truncateLabel(String(value), isMobile ? 10 : 18)} /> } /> @@ -726,8 +747,9 @@ function TrendLineCard({ color?: string; valueFormatter?: (value: number) => string; }) { + const isMobile = useIsMobile(); return ( - + @@ -740,24 +762,27 @@ function TrendLineCard({ ) : ( - - + + valueFormatter(Number(value))} /> ; }) { + const isMobile = useIsMobile(); return ( - + {title} {description} @@ -802,9 +828,13 @@ function StatusChartCard({ ) : ( <> - - - + + + truncateLabel(String(value), 14)} + tickMargin={isMobile ? 4 : 10} + tickFormatter={(value) => truncateLabel(String(value), isMobile ? 9 : 14)} /> } /> @@ -843,8 +873,9 @@ function StatusChartCard({ } function ActivityTrendCard({ data }: { data: BlogAnalyticsSchema["activity_trend"] }) { + const isMobile = useIsMobile(); return ( - + روند تعاملات بلاگ لایک، ذخیره و کامنت در بازه انتخابی @@ -860,17 +891,20 @@ function ActivityTrendCard({ data }: { data: BlogAnalyticsSchema["activity_trend saves: { label: "ذخیره", color: PALETTE.cyan }, comments: { label: "کامنت", color: PALETTE.amber }, }} - className="h-[280px] w-full sm:h-[320px]" + className="h-[230px] w-full sm:h-[320px]" > - + - + formatNumberPersian(Number(value))} /> } /> @@ -890,9 +924,10 @@ function BlogEngagementCard({ group }: { group: BlogAnalyticsSchema["post_popula 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 }))); + const isMobile = useIsMobile(); + const labelAxisWidth = axisWidth(data.map((post) => ({ label: post.title, value: post.score })), isMobile); return ( - + محبوبیت نوشته‌ها رتبه‌بندی بر اساس مجموع لایک، ذخیره و کامنت @@ -902,7 +937,7 @@ function BlogEngagementCard({ group }: { group: BlogAnalyticsSchema["post_popula ) : ( <> - + - + truncateLabel(String(value), 22)} + tickMargin={isMobile ? 4 : 10} + tickFormatter={(value) => truncateLabel(String(value), isMobile ? 10 : 22)} /> } />