@@ -42,6 +42,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/u
import { Input } from "@/components/ui/input" ;
import { Input } from "@/components/ui/input" ;
import { Label } from "@/components/ui/label" ;
import { Label } from "@/components/ui/label" ;
import { Tabs , TabsContent , TabsList , TabsTrigger } from "@/components/ui/tabs" ;
import { Tabs , TabsContent , TabsList , TabsTrigger } from "@/components/ui/tabs" ;
import { useIsMobile } from "@/hooks/use-mobile" ;
import { api } from "@/lib/api" ;
import { api } from "@/lib/api" ;
import type {
import type {
AnalyticsPointGroupSchema ,
AnalyticsPointGroupSchema ,
@@ -138,13 +139,13 @@ function truncateLabel(value: string, max = 18) {
return normalized . length > max ? ` ${ normalized . slice ( 0 , max - 1 ) } … ` : normalized ;
return normalized . length > max ? ` ${ normalized . slice ( 0 , max - 1 ) } … ` : normalized ;
}
}
function chartHeight ( count : number , min = 280 ) {
function chartHeight ( count : number , min = 280 , compact = false ) {
return Math . max ( min , count * 32 + 80 ) ;
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 ) ;
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 ) {
function dataWithOtherNotice ( group : AnalyticsPointGroupSchema ) {
@@ -212,13 +213,17 @@ function DateRangeFilter({
value ,
value ,
onChange ,
onChange ,
onReset ,
onReset ,
resetDisabled ,
showReset = true ,
} : {
} : {
value : DateRangeState ;
value : DateRangeState ;
onChange : ( next : DateRangeState ) = > void ;
onChange : ( next : DateRangeState ) = > void ;
onReset : ( ) = > void ;
onReset : ( ) = > void ;
resetDisabled? : boolean ;
showReset? : boolean ;
} ) {
} ) {
return (
return (
< div className = "grid gap-3 md:grid-cols-[1fr_1fr_auto]" >
< div className = { cn ( "grid gap-3" , showReset ? "md:grid-cols-[1fr_1fr_auto]" : "md:grid-cols-2" ) } >
< div className = "space-y-2" >
< div className = "space-y-2" >
< Label > ا ز ت ا ر ی خ < / Label >
< Label > ا ز ت ا ر ی خ < / Label >
< DatePicker
< DatePicker
@@ -247,16 +252,31 @@ function DateRangeFilter({
containerClassName = "w-full"
containerClassName = "w-full"
/ >
/ >
< / div >
< / div >
< div className = "flex items-end" >
{ showReset ? (
< Button variant = "destructive" className = "w-full gap-2 md:w-auto" onClick = { onReset } >
< div className = "flex items-end" >
< Eraser className = "h-4 w-4" / >
< FilterResetButton disabled = { resetDisabled ? ? ( ! value . from && ! value . to ) } onClick = { onReset } / >
پ ا ک ک ر د ن
< / div >
< / Button >
) : null }
< / div >
< / div >
< / div >
) ;
) ;
}
}
function FilterResetButton ( { disabled , onClick } : { disabled : boolean ; onClick : ( ) = > void } ) {
return (
< Button
variant = "destructive"
className = "w-full gap-2 md:w-10 md:px-0"
onClick = { onClick }
disabled = { disabled }
title = "پاککردن"
aria-label = "پاککردن فیلترها"
>
< Eraser className = "h-4 w-4" / >
< span className = "md:sr-only" > پ ا ک ک ر د ن < / span >
< / Button >
) ;
}
function FilterCard ( {
function FilterCard ( {
title ,
title ,
description ,
description ,
@@ -280,10 +300,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 (
return (
< div className = "overflow-x-auto overflow-y -hidden pb-2 " dir = "ltr" >
< div className = { cn ( "min-w-0 max-w-full overflow-y-hidden pb-2" , shouldScroll ? "overflow-x-auto" : " overflow-x -hidden") } dir = "ltr" >
< div style = { { minWidth } } > { children } < / div >
< div className = "min-w-0 max-w-full" style = { { width : "100%" , minWidth : shouldScroll ? minWidth : undefined } } >
{ children }
< / div >
< / div >
< / div >
) ;
) ;
}
}
@@ -354,6 +387,7 @@ function DashboardPointDetailModal({
} ) {
} ) {
const [ search , setSearch ] = React . useState ( "" ) ;
const [ search , setSearch ] = React . useState ( "" ) ;
const [ sortBy , setSortBy ] = React . useState < "value" | "label" > ( "value" ) ;
const [ sortBy , setSortBy ] = React . useState < "value" | "label" > ( "value" ) ;
const isMobile = useIsMobile ( ) ;
const filteredData = React . useMemo ( ( ) = > {
const filteredData = React . useMemo ( ( ) = > {
const normalizedSearch = search . trim ( ) . toLocaleLowerCase ( "fa-IR" ) ;
const normalizedSearch = search . trim ( ) . toLocaleLowerCase ( "fa-IR" ) ;
const filtered = normalizedSearch
const filtered = normalizedSearch
@@ -390,16 +424,16 @@ function DashboardPointDetailModal({
< EmptyChart label = "موردی با این جستجو پیدا نشد." / >
< EmptyChart label = "موردی با این جستجو پیدا نشد." / >
) : (
) : (
< >
< >
< ChartViewport minWidth = { 680 } >
< ChartViewport minWidth = { 680 } scrollable >
< ChartContainer
< ChartContainer
config = { { value : { label : "مقدار" , color } } }
config = { { value : { label : "مقدار" , color } } }
className = "w-full"
className = "w-full"
style = { { height : Math.min ( Math . max ( filteredData . length * 34 + 100 , 360) , 780 ) } }
style = { { height : Math.min ( Math . max ( filteredData . length * ( isMobile ? 28 : 34 ) + 100 , isMobile ? 300 : 360) , 780 ) } }
>
>
< BarChart
< BarChart
data = { filteredData }
data = { filteredData }
layout = "vertical"
layout = "vertical"
margin = { { top : 12 , right : 8 , bottom : 24 , left : 32 } }
margin = { isMobile ? { top : 8 , right : 2 , bottom : 22 , left : 20 } : { top : 12 , right : 8 , bottom : 24 , left : 32 } }
>
>
< CartesianGrid horizontal = { false } strokeDasharray = "3 3" / >
< CartesianGrid horizontal = { false } strokeDasharray = "3 3" / >
< XAxis
< XAxis
@@ -415,11 +449,11 @@ function DashboardPointDetailModal({
dataKey = "label"
dataKey = "label"
type = "category"
type = "category"
orientation = "right"
orientation = "right"
width = { axisWidth ( filteredData ) }
width = { axisWidth ( filteredData , isMobile )}
tickLine = { false }
tickLine = { false }
axisLine = { false }
axisLine = { false }
tickMargin = { 10}
tickMargin = { isMobile ? 4 : 10}
tickFormatter = { ( value ) = > truncateLabel ( String ( value ) , 24) }
tickFormatter = { ( value ) = > truncateLabel ( String ( value ) , isMobile ? 12 : 24) }
/ >
/ >
< ChartTooltip content = { < CustomValueTooltip unit = { unit } formatter = { formatter } / > } / >
< ChartTooltip content = { < CustomValueTooltip unit = { unit } formatter = { formatter } / > } / >
< Bar dataKey = "value" radius = { [ 8 , 8 , 8 , 8 ] } fill = "var(--color-value)" / >
< Bar dataKey = "value" radius = { [ 8 , 8 , 8 , 8 ] } fill = "var(--color-value)" / >
@@ -484,6 +518,7 @@ function BlogPostEngagementModal({
} ) {
} ) {
const [ search , setSearch ] = React . useState ( "" ) ;
const [ search , setSearch ] = React . useState ( "" ) ;
const [ sortBy , setSortBy ] = React . useState < "score" | "likes" | "saves" | "comments" | "title" > ( "score" ) ;
const [ sortBy , setSortBy ] = React . useState < "score" | "likes" | "saves" | "comments" | "title" > ( "score" ) ;
const isMobile = useIsMobile ( ) ;
const data = React . useMemo ( ( ) = > {
const data = React . useMemo ( ( ) = > {
const normalizedSearch = search . trim ( ) . toLocaleLowerCase ( "fa-IR" ) ;
const normalizedSearch = search . trim ( ) . toLocaleLowerCase ( "fa-IR" ) ;
const filtered = normalizedSearch
const filtered = normalizedSearch
@@ -530,7 +565,7 @@ function BlogPostEngagementModal({
< EmptyChart label = "نوشتهای با این جستجو پیدا نشد." / >
< EmptyChart label = "نوشتهای با این جستجو پیدا نشد." / >
) : (
) : (
< >
< >
< ChartViewport minWidth = { 760 } >
< ChartViewport minWidth = { 760 } scrollable >
< ChartContainer
< ChartContainer
config = { {
config = { {
likes : { label : "لایک" , color : PALETTE.rose } ,
likes : { label : "لایک" , color : PALETTE.rose } ,
@@ -538,12 +573,12 @@ function BlogPostEngagementModal({
comments : { label : "کامنت" , color : PALETTE.amber } ,
comments : { label : "کامنت" , color : PALETTE.amber } ,
} }
} }
className = "w-full"
className = "w-full"
style = { { height : Math.min ( Math . max ( data . length * 34 + 100 , 380) , 820 ) } }
style = { { height : Math.min ( Math . max ( data . length * ( isMobile ? 28 : 34 ) + 100 , isMobile ? 320 : 380) , 820 ) } }
>
>
< BarChart
< BarChart
data = { data }
data = { data }
layout = "vertical"
layout = "vertical"
margin = { { top : 12 , right : 8 , bottom : 24 , left : 32 } }
margin = { isMobile ? { top : 8 , right : 2 , bottom : 22 , left : 20 } : { top : 12 , right : 8 , bottom : 24 , left : 32 } }
>
>
< CartesianGrid horizontal = { false } strokeDasharray = "3 3" / >
< CartesianGrid horizontal = { false } strokeDasharray = "3 3" / >
< XAxis
< XAxis
@@ -559,11 +594,11 @@ function BlogPostEngagementModal({
dataKey = "label"
dataKey = "label"
type = "category"
type = "category"
orientation = "right"
orientation = "right"
width = { axisWidth ( data . map ( ( post ) = > ( { label : post.title , value : post.score } ) ) ) }
width = { axisWidth ( data . map ( ( post ) = > ( { label : post.title , value : post.score } ) ) , isMobile )}
tickLine = { false }
tickLine = { false }
axisLine = { false }
axisLine = { false }
tickMargin = { 10}
tickMargin = { isMobile ? 4 : 10}
tickFormatter = { ( value ) = > truncateLabel ( String ( value ) , 26) }
tickFormatter = { ( value ) = > truncateLabel ( String ( value ) , isMobile ? 12 : 26) }
/ >
/ >
< ChartTooltip content = { < PostEngagementTooltip / > } / >
< ChartTooltip content = { < PostEngagementTooltip / > } / >
< Bar dataKey = "likes" stackId = "engagement" fill = "var(--color-likes)" radius = { [ 0 , 0 , 0 , 0 ] } / >
< Bar dataKey = "likes" stackId = "engagement" fill = "var(--color-likes)" radius = { [ 0 , 0 , 0 , 0 ] } / >
@@ -651,8 +686,9 @@ function HorizontalBarCard({
const data = dataWithOtherNotice ( group ) ;
const data = dataWithOtherNotice ( group ) ;
const allData = fullGroupItems ( group ) ;
const allData = fullGroupItems ( group ) ;
const [ detailsOpen , setDetailsOpen ] = React . useState ( false ) ;
const [ detailsOpen , setDetailsOpen ] = React . useState ( false ) ;
const isMobile = useIsMobile ( ) ;
return (
return (
< Card >
< Card className = "min-w-0 overflow-hidden" >
< CardHeader className = "p-4 sm:p-6" >
< CardHeader className = "p-4 sm:p-6" >
< CardTitle className = "text-base sm:text-lg" > { title } < / CardTitle >
< CardTitle className = "text-base sm:text-lg" > { title } < / CardTitle >
< CardDescription > { description } < / CardDescription >
< CardDescription > { description } < / CardDescription >
@@ -666,9 +702,13 @@ function HorizontalBarCard({
< ChartContainer
< ChartContainer
config = { { value : { label : "مقدار" , color } } }
config = { { value : { label : "مقدار" , color } } }
className = "w-full"
className = "w-full"
style = { { height : chartHeight ( data . length ) } }
style = { { height : chartHeight ( data . length , 280 , isMobile ) } }
>
>
< BarChart data = { data } layout = "vertical" margin = { { top : 12 , right : 8 , bottom : 24 , left : 32 } } >
< BarChart
data = { data }
layout = "vertical"
margin = { isMobile ? { top : 8 , right : 2 , bottom : 22 , left : 18 } : { top : 12 , right : 8 , bottom : 24 , left : 32 } }
>
< CartesianGrid horizontal = { false } strokeDasharray = "3 3" / >
< CartesianGrid horizontal = { false } strokeDasharray = "3 3" / >
< XAxis
< XAxis
type = "number"
type = "number"
@@ -683,11 +723,11 @@ function HorizontalBarCard({
dataKey = "label"
dataKey = "label"
type = "category"
type = "category"
orientation = "right"
orientation = "right"
width = { axisWidth ( data ) }
width = { axisWidth ( data , isMobile )}
tickLine = { false }
tickLine = { false }
axisLine = { false }
axisLine = { false }
tickMargin = { 10}
tickMargin = { isMobile ? 4 : 10}
tickFormatter = { ( value ) = > truncateLabel ( String ( value ) ) }
tickFormatter = { ( value ) = > truncateLabel ( String ( value ) , isMobile ? 10 : 18 )}
/ >
/ >
< ChartTooltip content = { < CustomValueTooltip unit = { unit } formatter = { valueFormatter } / > } / >
< ChartTooltip content = { < CustomValueTooltip unit = { unit } formatter = { valueFormatter } / > } / >
< Bar dataKey = "value" radius = { [ 8 , 8 , 8 , 8 ] } fill = "var(--color-value)" / >
< Bar dataKey = "value" radius = { [ 8 , 8 , 8 , 8 ] } fill = "var(--color-value)" / >
@@ -726,8 +766,9 @@ function TrendLineCard({
color? : string ;
color? : string ;
valueFormatter ? : ( value : number ) = > string ;
valueFormatter ? : ( value : number ) = > string ;
} ) {
} ) {
const isMobile = useIsMobile ( ) ;
return (
return (
< Card >
< Card className = "min-w-0 overflow-hidden" >
< CardHeader className = "p-4 sm:p-6" >
< CardHeader className = "p-4 sm:p-6" >
< CardTitle className = "flex items-center gap-2 text-base sm:text-lg" >
< 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" / >
@@ -740,24 +781,27 @@ function TrendLineCard({
< EmptyChart / >
< EmptyChart / >
) : (
) : (
< ChartViewport >
< ChartViewport >
< ChartContainer config = { { value : { label : "مقدار" , color } } } className = "h-[26 0px] w-full sm:h-[300px]" >
< ChartContainer config = { { value : { label : "مقدار" , color } } } className = "h-[22 0px] w-full sm:h-[300px]" >
< LineChart data = { data } margin = { { top : 16 , right : 12 , bottom : 32 , left : 18 } } >
< LineChart
data = { data }
margin = { isMobile ? { top : 12 , right : 4 , bottom : 26 , left : 8 } : { top : 16 , right : 12 , bottom : 32 , left : 18 } }
>
< CartesianGrid vertical = { false } strokeDasharray = "3 3" / >
< CartesianGrid vertical = { false } strokeDasharray = "3 3" / >
< XAxis
< XAxis
dataKey = "date"
dataKey = "date"
reversed
reversed
tickLine = { false }
tickLine = { false }
axisLine = { false }
axisLine = { false }
minTickGap = { 34}
minTickGap = { isMobile ? 18 : 34}
tickMargin = { 10}
tickMargin = { isMobile ? 6 : 10}
tickFormatter = { formatJalaliTick }
tickFormatter = { formatJalaliTick }
/ >
/ >
< YAxis
< YAxis
orientation = "right"
orientation = "right"
width = { 76}
width = { isMobile ? 46 : 76}
tickLine = { false }
tickLine = { false }
axisLine = { false }
axisLine = { false }
tickMargin = { 10}
tickMargin = { isMobile ? 4 : 10}
tickFormatter = { ( value ) = > valueFormatter ( Number ( value ) ) }
tickFormatter = { ( value ) = > valueFormatter ( Number ( value ) ) }
/ >
/ >
< ChartTooltip
< ChartTooltip
@@ -791,8 +835,9 @@ function StatusChartCard({
description : string ;
description : string ;
data : Array < { status : string ; label : string ; value : number } > ;
data : Array < { status : string ; label : string ; value : number } > ;
} ) {
} ) {
const isMobile = useIsMobile ( ) ;
return (
return (
< Card >
< Card className = "min-w-0 overflow-hidden" >
< CardHeader className = "p-4 sm:p-6" >
< CardHeader className = "p-4 sm:p-6" >
< CardTitle className = "text-base sm:text-lg" > { title } < / CardTitle >
< CardTitle className = "text-base sm:text-lg" > { title } < / CardTitle >
< CardDescription > { description } < / CardDescription >
< CardDescription > { description } < / CardDescription >
@@ -802,9 +847,13 @@ function StatusChartCard({
< EmptyChart / >
< EmptyChart / >
) : (
) : (
< >
< >
< ChartViewport minWidth = { 400 } >
< ChartViewport >
< ChartContainer config = { { value : { label : "تعداد" , color : PALETTE.teal } } } className = "h-[26 0px] w-full sm:h-[300px]" >
< ChartContainer config = { { value : { label : "تعداد" , color : PALETTE.teal } } } className = "h-[22 0px] w-full sm:h-[300px]" >
< BarChart data = { data } layout = "vertical" margin = { { top : 14 , right : 8 , bottom : 28 , left : 32 } } >
< BarChart
data = { data }
layout = "vertical"
margin = { isMobile ? { top : 10 , right : 2 , bottom : 22 , left : 18 } : { top : 14 , right : 8 , bottom : 28 , left : 32 } }
>
< CartesianGrid horizontal = { false } strokeDasharray = "3 3" / >
< CartesianGrid horizontal = { false } strokeDasharray = "3 3" / >
< XAxis
< XAxis
type = "number"
type = "number"
@@ -819,11 +868,11 @@ function StatusChartCard({
dataKey = "label"
dataKey = "label"
type = "category"
type = "category"
orientation = "right"
orientation = "right"
width = { 108}
width = { isMobile ? 74 : 108}
tickLine = { false }
tickLine = { false }
axisLine = { false }
axisLine = { false }
tickMargin = { 10}
tickMargin = { isMobile ? 4 : 10}
tickFormatter = { ( value ) = > truncateLabel ( String ( value ) , 14) }
tickFormatter = { ( value ) = > truncateLabel ( String ( value ) , isMobile ? 9 : 14) }
/ >
/ >
< ChartTooltip content = { < CustomValueTooltip / > } / >
< ChartTooltip content = { < CustomValueTooltip / > } / >
< Bar dataKey = "value" radius = { [ 8 , 8 , 8 , 8 ] } >
< Bar dataKey = "value" radius = { [ 8 , 8 , 8 , 8 ] } >
@@ -843,8 +892,9 @@ function StatusChartCard({
}
}
function ActivityTrendCard ( { data } : { data : BlogAnalyticsSchema [ "activity_trend" ] } ) {
function ActivityTrendCard ( { data } : { data : BlogAnalyticsSchema [ "activity_trend" ] } ) {
const isMobile = useIsMobile ( ) ;
return (
return (
< Card >
< Card className = "min-w-0 overflow-hidden" >
< CardHeader className = "p-4 sm:p-6" >
< CardHeader className = "p-4 sm:p-6" >
< CardTitle className = "text-base sm:text-lg" > ر و ن د ت ع ا م ل ا ت ب ل ا گ < / CardTitle >
< CardTitle className = "text-base sm:text-lg" > ر و ن د ت ع ا م ل ا ت ب ل ا گ < / CardTitle >
< CardDescription > ل ا ی ک ، ذ خ ی ر ه و ک ا م ن ت د ر ب ا ز ه ا ن ت خ ا ب ی < / CardDescription >
< CardDescription > ل ا ی ک ، ذ خ ی ر ه و ک ا م ن ت د ر ب ا ز ه ا ن ت خ ا ب ی < / CardDescription >
@@ -860,17 +910,20 @@ 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-[28 0px] w-full sm:h-[320px]"
className = "h-[23 0px] w-full sm:h-[320px]"
>
>
< LineChart data = { data } margin = { { top : 16 , right : 12 , bottom : 32 , left : 18 } } >
< LineChart
data = { data }
margin = { isMobile ? { top : 12 , right : 4 , bottom : 26 , left : 8 } : { top : 16 , right : 12 , bottom : 32 , left : 18 } }
>
< CartesianGrid vertical = { false } strokeDasharray = "3 3" / >
< CartesianGrid vertical = { false } strokeDasharray = "3 3" / >
< XAxis dataKey = "date" reversed tickLine = { false } axisLine = { false } tickFormatter = { formatJalaliTick } minTickGap = { 34} tickMargin = { 10} / >
< XAxis dataKey = "date" reversed tickLine = { false } axisLine = { false } tickFormatter = { formatJalaliTick } minTickGap = { isMobile ? 18 : 34} tickMargin = { isMobile ? 6 : 10} / >
< YAxis
< YAxis
orientation = "right"
orientation = "right"
width = { 64}
width = { isMobile ? 44 : 64}
tickLine = { false }
tickLine = { false }
axisLine = { false }
axisLine = { false }
tickMargin = { 10}
tickMargin = { isMobile ? 4 : 10}
tickFormatter = { ( value ) = > formatNumberPersian ( Number ( value ) ) }
tickFormatter = { ( value ) = > formatNumberPersian ( Number ( value ) ) }
/ >
/ >
< ChartTooltip content = { < ChartTooltipContent / > } / >
< ChartTooltip content = { < ChartTooltipContent / > } / >
@@ -890,9 +943,10 @@ function BlogEngagementCard({ group }: { group: BlogAnalyticsSchema["post_popula
const data = toPostEngagementData ( group . top_items ) ;
const data = toPostEngagementData ( group . top_items ) ;
const allPosts = group . items ? . length ? group.items : group.top_items ;
const allPosts = group . items ? . length ? group.items : group.top_items ;
const [ detailsOpen , setDetailsOpen ] = React . useState ( false ) ;
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 (
return (
< Card >
< Card className = "min-w-0 overflow-hidden" >
< CardHeader className = "p-4 sm:p-6" >
< CardHeader className = "p-4 sm:p-6" >
< CardTitle className = "text-base sm:text-lg" > م ح ب و ب ی ت ن و ش ت ه ه ا < / CardTitle >
< CardTitle className = "text-base sm:text-lg" > م ح ب و ب ی ت ن و ش ت ه ه ا < / CardTitle >
< CardDescription > ر ت ب ه ب ن د ی ب ر ا س ا س م ج م و ع ل ا ی ک ، ذ خ ی ر ه و ک ا م ن ت < / CardDescription >
< CardDescription > ر ت ب ه ب ن د ی ب ر ا س ا س م ج م و ع ل ا ی ک ، ذ خ ی ر ه و ک ا م ن ت < / CardDescription >
@@ -902,7 +956,7 @@ function BlogEngagementCard({ group }: { group: BlogAnalyticsSchema["post_popula
< EmptyChart / >
< EmptyChart / >
) : (
) : (
< >
< >
< ChartViewport minWidth = { 460 } >
< ChartViewport >
< ChartContainer
< ChartContainer
config = { {
config = { {
likes : { label : "لایک" , color : PALETTE.rose } ,
likes : { label : "لایک" , color : PALETTE.rose } ,
@@ -910,9 +964,13 @@ function BlogEngagementCard({ group }: { group: BlogAnalyticsSchema["post_popula
comments : { label : "کامنت" , color : PALETTE.amber } ,
comments : { label : "کامنت" , color : PALETTE.amber } ,
} }
} }
className = "w-full"
className = "w-full"
style = { { height : chartHeight ( data . length , 320 ) } }
style = { { height : chartHeight ( data . length , 320 , isMobile ) } }
>
>
< BarChart data = { data } layout = "vertical" margin = { { top : 12 , right : 8 , bottom : 24 , left : 32 } } >
< BarChart
data = { data }
layout = "vertical"
margin = { isMobile ? { top : 8 , right : 2 , bottom : 22 , left : 18 } : { top : 12 , right : 8 , bottom : 24 , left : 32 } }
>
< CartesianGrid horizontal = { false } strokeDasharray = "3 3" / >
< CartesianGrid horizontal = { false } strokeDasharray = "3 3" / >
< XAxis
< XAxis
type = "number"
type = "number"
@@ -929,8 +987,8 @@ function BlogEngagementCard({ group }: { group: BlogAnalyticsSchema["post_popula
width = { labelAxisWidth }
width = { labelAxisWidth }
tickLine = { false }
tickLine = { false }
axisLine = { false }
axisLine = { false }
tickMargin = { 10}
tickMargin = { isMobile ? 4 : 10}
tickFormatter = { ( value ) = > truncateLabel ( String ( value ) , 22) }
tickFormatter = { ( value ) = > truncateLabel ( String ( value ) , isMobile ? 10 : 22) }
/ >
/ >
< ChartTooltip content = { < PostEngagementTooltip / > } / >
< ChartTooltip content = { < PostEngagementTooltip / > } / >
< Bar dataKey = "likes" stackId = "engagement" fill = "var(--color-likes)" radius = { [ 0 , 0 , 0 , 0 ] } / >
< Bar dataKey = "likes" stackId = "engagement" fill = "var(--color-likes)" radius = { [ 0 , 0 , 0 , 0 ] } / >
@@ -965,7 +1023,13 @@ function TopEventsCard({ group }: { group: EventAnalyticsSchema["top_events"] })
< CardContent className = "space-y-3 p-4 pt-0 sm:p-6 sm:pt-0" >
< 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" >
< a
key = { event . id }
href = { ` /events/ ${ event . slug } ` }
target = "_blank"
rel = "noopener noreferrer"
className = "flex items-center justify-between gap-3 rounded-xl border bg-background/70 p-3 transition hover:border-primary/50 hover:bg-muted/40"
>
< div className = "min-w-0 text-right" >
< div className = "min-w-0 text-right" >
< p className = "truncate font-medium" > { event . title } < / p >
< p className = "truncate font-medium" > { event . title } < / p >
< p className = "mt-1 text-xs text-muted-foreground" >
< p className = "mt-1 text-xs text-muted-foreground" >
@@ -975,7 +1039,7 @@ function TopEventsCard({ group }: { group: EventAnalyticsSchema["top_events"] })
< / p >
< / p >
< / div >
< / div >
< Badge variant = "secondary" > { toPersianDigits ( index + 1 ) } < / Badge >
< Badge variant = "secondary" > { toPersianDigits ( index + 1 ) } < / Badge >
< / div >
< / a >
) )
) )
) : (
) : (
< p className = "text-sm text-muted-foreground" > د ا د ه ا ی و ج و د ن د ا ر د . < / p >
< p className = "text-sm text-muted-foreground" > د ا د ه ا ی و ج و د ن د ا ر د . < / p >
@@ -1000,7 +1064,13 @@ function TopPostsCard({ posts }: { posts: BlogAnalyticsSchema["top_posts"] }) {
< CardContent className = "space-y-3 p-4 pt-0 sm:p-6 sm:pt-0" >
< 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" >
< a
key = { post . id }
href = { ` /blog/ ${ post . slug } ` }
target = "_blank"
rel = "noopener noreferrer"
className = "flex items-center justify-between gap-3 rounded-xl border bg-background/70 p-3 transition hover:border-primary/50 hover:bg-muted/40"
>
< div className = "min-w-0 text-right" >
< div className = "min-w-0 text-right" >
< p className = "truncate font-medium" > { post . title } < / p >
< p className = "truncate font-medium" > { post . title } < / p >
< p className = "mt-1 text-xs text-muted-foreground" >
< p className = "mt-1 text-xs text-muted-foreground" >
@@ -1008,8 +1078,7 @@ function TopPostsCard({ posts }: { posts: BlogAnalyticsSchema["top_posts"] }) {
{ formatNumberPersian ( post . comments ) } ک ا م ن ت
{ formatNumberPersian ( post . comments ) } ک ا م ن ت
< / p >
< / p >
< / div >
< / div >
< Badge variant = "secondary" > { toPersianDigits ( index + 1 ) } < / Badge >
< / a >
< / div >
) )
) )
) : (
) : (
< p className = "text-sm text-muted-foreground" > د ا د ه ا ی و ج و د ن د ا ر د . < / p >
< p className = "text-sm text-muted-foreground" > د ا د ه ا ی و ج و د ن د ا ر د . < / p >
@@ -1034,7 +1103,12 @@ function UsersSection({
return (
return (
< div className = "space-y-6" dir = "rtl" >
< div className = "space-y-6" dir = "rtl" >
< FilterCard title = "فیلتر کاربران" description = "این فیلتر فقط روی کاربران و تاریخ عضویت آنها اعمال میشود." >
< FilterCard title = "فیلتر کاربران" description = "این فیلتر فقط روی کاربران و تاریخ عضویت آنها اعمال میشود." >
< DateRangeFilter value = { filters } onChange = { onFiltersChange } onReset = { ( ) = > onFiltersChange ( { from : "" , to : "" } ) } / >
< DateRangeFilter
value = { filters }
onChange = { onFiltersChange }
onReset = { ( ) = > onFiltersChange ( { from : "" , to : "" } ) }
resetDisabled = { ! filters . from && ! filters . to }
/ >
< / FilterCard >
< / FilterCard >
{ query . isLoading ? < SectionLoading / > : null }
{ query . isLoading ? < SectionLoading / > : null }
{ query . isError ? < SectionError error = { query . error } / > : null }
{ query . isError ? < SectionError error = { query . error } / > : null }
@@ -1092,12 +1166,13 @@ function EventsSection({
return (
return (
< div className = "space-y-6" >
< div className = "space-y-6" >
< FilterCard title = "فیلتر رویدادها" description = "این فیلتر فقط روی آمار رویداد، ثبتنام، درآمد و تنوع شرکتکنندگان اعمال میشود." >
< FilterCard title = "فیلتر رویدادها" description = "این فیلتر فقط روی آمار رویداد، ثبتنام، درآمد و تنوع شرکتکنندگان اعمال میشود." >
< div className = "grid gap-3 xl:grid-cols-[2fr_1.2fr]" >
< div className = "grid gap-3 xl:grid-cols-[2fr_1.2fr_auto ]" >
< div >
< div >
< DateRangeFilter
< DateRangeFilter
value = { filters }
value = { filters }
onChange = { ( next ) = > onFiltersChange ( { . . . filters , . . . next } ) }
onChange = { ( next ) = > onFiltersChange ( { . . . filters , . . . next } ) }
onReset = { reset }
onReset = { reset }
showReset = { false }
/ >
/ >
< / div >
< / div >
< div className = "space-y-2" >
< div className = "space-y-2" >
@@ -1120,6 +1195,9 @@ function EventsSection({
emptyText = "رویدادی پیدا نشد."
emptyText = "رویدادی پیدا نشد."
/ >
/ >
< / div >
< / div >
< div className = "flex items-end" >
< FilterResetButton disabled = { ! filters . from && ! filters . to && ! filters . eventId } onClick = { reset } / >
< / div >
< / div >
< / div >
< / FilterCard >
< / FilterCard >
{ query . isLoading ? < SectionLoading / > : null }
{ query . isLoading ? < SectionLoading / > : null }
@@ -1183,7 +1261,12 @@ function BlogSection({
return (
return (
< div className = "space-y-6" >
< div className = "space-y-6" >
< FilterCard title = "فیلتر بلاگ" description = "این فیلتر فقط روی نوشتهها و تعاملات بلاگ اعمال میشود و به رویدادها وابسته نیست." >
< FilterCard title = "فیلتر بلاگ" description = "این فیلتر فقط روی نوشتهها و تعاملات بلاگ اعمال میشود و به رویدادها وابسته نیست." >
< DateRangeFilter value = { filters } onChange = { onFiltersChange } onReset = { ( ) = > onFiltersChange ( { from : "" , to : "" } ) } / >
< DateRangeFilter
value = { filters }
onChange = { onFiltersChange }
onReset = { ( ) = > onFiltersChange ( { from : "" , to : "" } ) }
resetDisabled = { ! filters . from && ! filters . to }
/ >
< / FilterCard >
< / FilterCard >
{ query . isLoading ? < SectionLoading / > : null }
{ query . isLoading ? < SectionLoading / > : null }
{ query . isError ? < SectionError error = { query . error } / > : null }
{ query . isError ? < SectionError error = { query . error } / > : null }