fix(admin-dashboard): tighten mobile layout

This commit is contained in:
2026-06-15 17:40:31 +03:30
parent 4fb44fcb4c
commit 4edf8a0736

View File

@@ -139,7 +139,7 @@ function truncateLabel(value: string, max = 18) {
} }
function chartHeight(count: number, min = 280) { function chartHeight(count: number, min = 280) {
return Math.max(min, count * 38 + 90); return Math.max(min, count * 32 + 80);
} }
function axisWidth(items: AnalyticsPointSchema[]) { function axisWidth(items: AnalyticsPointSchema[]) {
@@ -170,14 +170,14 @@ function StatCard({
}) { }) {
return ( return (
<Card className="overflow-hidden"> <Card className="overflow-hidden">
<CardContent className="flex items-center justify-between gap-4 p-5"> <CardContent className="flex items-center justify-between gap-3 p-4 sm:gap-4 sm:p-5">
<div className="space-y-2 text-right"> <div className="space-y-2 text-right">
<p className="text-sm text-muted-foreground">{title}</p> <p className="text-sm text-muted-foreground">{title}</p>
<p className="text-2xl font-black leading-tight tracking-tight">{value}</p> <p className="text-xl font-black leading-tight tracking-tight sm:text-2xl">{value}</p>
<p className="text-xs leading-6 text-muted-foreground">{description}</p> <p className="text-xs leading-6 text-muted-foreground">{description}</p>
</div> </div>
<div className="rounded-2xl p-3" style={{ backgroundColor: `${PALETTE[tone]}22`, color: PALETTE[tone] }}> <div className="rounded-2xl p-2.5 sm:p-3" style={{ backgroundColor: `${PALETTE[tone]}22`, color: PALETTE[tone] }}>
<Icon className="h-6 w-6" /> <Icon className="h-5 w-5 sm:h-6 sm:w-6" />
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@@ -202,7 +202,7 @@ function SectionLoading() {
function EmptyChart({ label = "داده‌ای برای نمایش وجود ندارد." }: { label?: string }) { function EmptyChart({ label = "داده‌ای برای نمایش وجود ندارد." }: { label?: string }) {
return ( return (
<div className="flex h-[240px] items-center justify-center rounded-xl border border-dashed text-sm text-muted-foreground"> <div className="flex h-[200px] items-center justify-center rounded-xl border border-dashed px-4 text-center text-xs text-muted-foreground sm:h-[240px] sm:text-sm">
{label} {label}
</div> </div>
); );
@@ -268,19 +268,19 @@ function FilterCard({
}) { }) {
return ( return (
<Card> <Card>
<CardHeader> <CardHeader className="p-4 sm:p-6">
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<BarChart3 className="h-5 w-5 text-primary" /> <BarChart3 className="h-5 w-5 text-primary" />
{title} {title}
</CardTitle> </CardTitle>
<CardDescription>{description}</CardDescription> <CardDescription>{description}</CardDescription>
</CardHeader> </CardHeader>
<CardContent>{children}</CardContent> <CardContent className="p-4 pt-0 sm:p-6 sm:pt-0">{children}</CardContent>
</Card> </Card>
); );
} }
function ChartViewport({ children, minWidth = 560 }: { children: React.ReactNode; minWidth?: number }) { function ChartViewport({ children, minWidth = 420 }: { children: React.ReactNode; minWidth?: number }) {
return ( return (
<div className="overflow-x-auto overflow-y-hidden pb-2" dir="ltr"> <div className="overflow-x-auto overflow-y-hidden pb-2" dir="ltr">
<div style={{ minWidth }}>{children}</div> <div style={{ minWidth }}>{children}</div>
@@ -606,7 +606,7 @@ function ValuesTable({
if (!data.length) return null; if (!data.length) return null;
return ( return (
<div className="mt-4 max-h-56 overflow-auto rounded-xl border"> <div className="mt-4 max-h-56 overflow-auto rounded-xl border">
<table className="w-full text-sm"> <table className="w-full text-xs sm:text-sm">
<tbody> <tbody>
{data.map((item, index) => ( {data.map((item, index) => (
<tr key={`${item.label}-${index}`} className="border-b last:border-b-0"> <tr key={`${item.label}-${index}`} className="border-b last:border-b-0">
@@ -644,11 +644,11 @@ function HorizontalBarCard({
const [detailsOpen, setDetailsOpen] = React.useState(false); const [detailsOpen, setDetailsOpen] = React.useState(false);
return ( return (
<Card> <Card>
<CardHeader> <CardHeader className="p-4 sm:p-6">
<CardTitle>{title}</CardTitle> <CardTitle className="text-base sm:text-lg">{title}</CardTitle>
<CardDescription>{description}</CardDescription> <CardDescription>{description}</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="p-4 pt-0 sm:p-6 sm:pt-0">
{!data.length ? ( {!data.length ? (
<EmptyChart /> <EmptyChart />
) : ( ) : (
@@ -718,19 +718,19 @@ function TrendLineCard({
}) { }) {
return ( return (
<Card> <Card>
<CardHeader> <CardHeader className="p-4 sm:p-6">
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2 text-base sm:text-lg">
<LineChartIcon className="h-5 w-5 text-primary" /> <LineChartIcon className="h-5 w-5 text-primary" />
{title} {title}
</CardTitle> </CardTitle>
<CardDescription>{description}</CardDescription> <CardDescription>{description}</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="p-4 pt-0 sm:p-6 sm:pt-0">
{!data.length ? ( {!data.length ? (
<EmptyChart /> <EmptyChart />
) : ( ) : (
<ChartViewport> <ChartViewport>
<ChartContainer config={{ value: { label: "مقدار", color } }} className="h-[300px] w-full"> <ChartContainer config={{ value: { label: "مقدار", color } }} className="h-[260px] w-full sm:h-[300px]">
<LineChart data={data} margin={{ top: 16, right: 76, bottom: 32, left: 18 }}> <LineChart data={data} margin={{ top: 16, right: 76, bottom: 32, left: 18 }}>
<CartesianGrid vertical={false} strokeDasharray="3 3" /> <CartesianGrid vertical={false} strokeDasharray="3 3" />
<XAxis <XAxis
@@ -782,17 +782,17 @@ function StatusChartCard({
}) { }) {
return ( return (
<Card> <Card>
<CardHeader> <CardHeader className="p-4 sm:p-6">
<CardTitle>{title}</CardTitle> <CardTitle className="text-base sm:text-lg">{title}</CardTitle>
<CardDescription>{description}</CardDescription> <CardDescription>{description}</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="p-4 pt-0 sm:p-6 sm:pt-0">
{!data.length ? ( {!data.length ? (
<EmptyChart /> <EmptyChart />
) : ( ) : (
<> <>
<ChartViewport minWidth={460}> <ChartViewport minWidth={400}>
<ChartContainer config={{ value: { label: "تعداد", color: PALETTE.teal } }} className="h-[300px] w-full"> <ChartContainer config={{ value: { label: "تعداد", color: PALETTE.teal } }} className="h-[260px] w-full sm:h-[300px]">
<BarChart data={data} layout="vertical" margin={{ top: 14, right: 118, bottom: 28, left: 18 }}> <BarChart data={data} layout="vertical" margin={{ top: 14, right: 118, bottom: 28, left: 18 }}>
<CartesianGrid horizontal={false} strokeDasharray="3 3" /> <CartesianGrid horizontal={false} strokeDasharray="3 3" />
<XAxis <XAxis
@@ -833,11 +833,11 @@ function StatusChartCard({
function ActivityTrendCard({ data }: { data: BlogAnalyticsSchema["activity_trend"] }) { function ActivityTrendCard({ data }: { data: BlogAnalyticsSchema["activity_trend"] }) {
return ( return (
<Card> <Card>
<CardHeader> <CardHeader className="p-4 sm:p-6">
<CardTitle>روند تعاملات بلاگ</CardTitle> <CardTitle className="text-base sm:text-lg">روند تعاملات بلاگ</CardTitle>
<CardDescription>لایک، ذخیره و کامنت در بازه انتخابی</CardDescription> <CardDescription>لایک، ذخیره و کامنت در بازه انتخابی</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="p-4 pt-0 sm:p-6 sm:pt-0">
{!data.length ? ( {!data.length ? (
<EmptyChart /> <EmptyChart />
) : ( ) : (
@@ -848,7 +848,7 @@ function ActivityTrendCard({ data }: { data: BlogAnalyticsSchema["activity_trend
saves: { label: "ذخیره", color: PALETTE.cyan }, saves: { label: "ذخیره", color: PALETTE.cyan },
comments: { label: "کامنت", color: PALETTE.amber }, comments: { label: "کامنت", color: PALETTE.amber },
}} }}
className="h-[320px] w-full" className="h-[280px] w-full sm:h-[320px]"
> >
<LineChart data={data} margin={{ top: 16, right: 64, bottom: 32, left: 18 }}> <LineChart data={data} margin={{ top: 16, right: 64, bottom: 32, left: 18 }}>
<CartesianGrid vertical={false} strokeDasharray="3 3" /> <CartesianGrid vertical={false} strokeDasharray="3 3" />
@@ -881,16 +881,16 @@ function BlogEngagementCard({ group }: { group: BlogAnalyticsSchema["post_popula
const labelAxisWidth = axisWidth(data.map((post) => ({ label: post.title, value: post.score }))); const labelAxisWidth = axisWidth(data.map((post) => ({ label: post.title, value: post.score })));
return ( return (
<Card> <Card>
<CardHeader> <CardHeader className="p-4 sm:p-6">
<CardTitle>محبوبیت نوشتهها</CardTitle> <CardTitle className="text-base sm:text-lg">محبوبیت نوشتهها</CardTitle>
<CardDescription>رتبهبندی بر اساس مجموع لایک، ذخیره و کامنت</CardDescription> <CardDescription>رتبهبندی بر اساس مجموع لایک، ذخیره و کامنت</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="p-4 pt-0 sm:p-6 sm:pt-0">
{!data.length ? ( {!data.length ? (
<EmptyChart /> <EmptyChart />
) : ( ) : (
<> <>
<ChartViewport minWidth={620}> <ChartViewport minWidth={460}>
<ChartContainer <ChartContainer
config={{ config={{
likes: { label: "لایک", color: PALETTE.rose }, likes: { label: "لایک", color: PALETTE.rose },
@@ -945,11 +945,11 @@ function BlogEngagementCard({ group }: { group: BlogAnalyticsSchema["post_popula
function TopEventsCard({ group }: { group: EventAnalyticsSchema["top_events"] }) { function TopEventsCard({ group }: { group: EventAnalyticsSchema["top_events"] }) {
return ( return (
<Card> <Card>
<CardHeader> <CardHeader className="p-4 sm:p-6">
<CardTitle>رویدادهای برتر</CardTitle> <CardTitle className="text-base sm:text-lg">رویدادهای برتر</CardTitle>
<CardDescription>بر اساس شرکتکننده تاییدشده، درآمد و زمان برگزاری</CardDescription> <CardDescription>بر اساس شرکتکننده تاییدشده، درآمد و زمان برگزاری</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-3"> <CardContent className="space-y-3 p-4 pt-0 sm:p-6 sm:pt-0">
{group.top_items.length ? ( {group.top_items.length ? (
group.top_items.map((event, index) => ( group.top_items.map((event, index) => (
<div key={event.id} className="flex items-center justify-between gap-3 rounded-xl border bg-background/70 p-3"> <div key={event.id} className="flex items-center justify-between gap-3 rounded-xl border bg-background/70 p-3">
@@ -980,11 +980,11 @@ function TopEventsCard({ group }: { group: EventAnalyticsSchema["top_events"] })
function TopPostsCard({ posts }: { posts: BlogAnalyticsSchema["top_posts"] }) { function TopPostsCard({ posts }: { posts: BlogAnalyticsSchema["top_posts"] }) {
return ( return (
<Card> <Card>
<CardHeader> <CardHeader className="p-4 sm:p-6">
<CardTitle>نوشتههای برتر</CardTitle> <CardTitle className="text-base sm:text-lg">نوشتههای برتر</CardTitle>
<CardDescription>بر اساس جمع لایک، ذخیره و کامنت</CardDescription> <CardDescription>بر اساس جمع لایک، ذخیره و کامنت</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-3"> <CardContent className="space-y-3 p-4 pt-0 sm:p-6 sm:pt-0">
{posts.length ? ( {posts.length ? (
posts.map((post, index) => ( posts.map((post, index) => (
<div key={post.id} className="flex items-center justify-between gap-3 rounded-xl border bg-background/70 p-3"> <div key={post.id} className="flex items-center justify-between gap-3 rounded-xl border bg-background/70 p-3">
@@ -1235,17 +1235,15 @@ export default function AdminDashboard() {
return ( return (
<div className="space-y-6" dir="rtl"> <div className="space-y-6" dir="rtl">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div>
<h2 className="text-2xl font-black tracking-tight">داشبورد دستاوردها</h2>
<p className="mt-1 text-sm text-muted-foreground">
گزارشهای جداگانه برای کاربران، رویدادها و بلاگ با فیلترهای مستقل و خوانا
</p>
</div>
</div>
<Tabs dir="rtl" value={activeTab} onValueChange={(value) => setActiveTab(parseTab(value))} className="space-y-6"> <Tabs dir="rtl" value={activeTab} onValueChange={(value) => setActiveTab(parseTab(value))} className="space-y-6">
<TabsList className="grid h-auto w-full grid-cols-3 rounded-2xl p-1 sm:w-fit"> <div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div>
<h2 className="text-xl font-black tracking-tight sm:text-2xl">داشبورد دستاوردها</h2>
<p className="mt-1 text-xs leading-6 text-muted-foreground sm:text-sm">
گزارشهای جداگانه برای کاربران، رویدادها و بلاگ با فیلترهای مستقل و خوانا
</p>
</div>
<TabsList className="grid h-auto w-full grid-cols-3 rounded-2xl p-1 lg:w-fit">
<TabsTrigger value="users" className="gap-2 rounded-xl py-2"> <TabsTrigger value="users" className="gap-2 rounded-xl py-2">
<UsersRound className="h-4 w-4" /> <UsersRound className="h-4 w-4" />
کاربران کاربران
@@ -1258,7 +1256,8 @@ export default function AdminDashboard() {
<Tags className="h-4 w-4" /> <Tags className="h-4 w-4" />
بلاگ بلاگ
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
</div>
<TabsContent value="users"> <TabsContent value="users">
<UsersSection filters={usersFilters} onFiltersChange={setUsersFilters} /> <UsersSection filters={usersFilters} onFiltersChange={setUsersFilters} />
</TabsContent> </TabsContent>