@@ -33,6 +33,74 @@ const MapComponent = dynamic(
3333 }
3434) ;
3535
36+ interface CountryData {
37+ country : string ;
38+ country_code ?: string ;
39+ visitors : number ;
40+ pageviews : number ;
41+ }
42+
43+ interface CountryRowProps {
44+ country : CountryData ;
45+ totalVisitors : number ;
46+ onCountrySelect : ( countryCode : string ) => void ;
47+ }
48+
49+ function CountryRow ( {
50+ country,
51+ totalVisitors,
52+ onCountrySelect,
53+ } : CountryRowProps ) {
54+ const percentage =
55+ totalVisitors > 0 ? ( country . visitors / totalVisitors ) * 100 : 0 ;
56+ const getColor = ( pct : number ) =>
57+ pct >= 50
58+ ? [ 'rgba(34, 197, 94, 0.08)' , 'rgba(34, 197, 94, 0.8)' ]
59+ : pct >= 25
60+ ? [ 'rgba(59, 130, 246, 0.08)' , 'rgba(59, 130, 246, 0.8)' ]
61+ : pct >= 10
62+ ? [ 'rgba(245, 158, 11, 0.08)' , 'rgba(245, 158, 11, 0.8)' ]
63+ : [ 'rgba(107, 114, 128, 0.06)' , 'rgba(107, 114, 128, 0.7)' ] ;
64+ const [ bgColor , accentColor ] = getColor ( percentage ) ;
65+
66+ return (
67+ < button
68+ className = "flex w-full cursor-pointer items-center gap-2.5 px-1.5 py-1.5 text-left transition-all hover:opacity-80"
69+ onClick = { ( ) =>
70+ onCountrySelect (
71+ country . country_code ?. toUpperCase ( ) || country . country . toUpperCase ( )
72+ )
73+ }
74+ style = { {
75+ background : percentage > 0 ? bgColor : undefined ,
76+ boxShadow :
77+ percentage > 0 ? `inset 2px 0 0 0 ${ accentColor } ` : undefined ,
78+ } }
79+ type = "button"
80+ >
81+ < div className = "relative h-4 w-5 flex-shrink-0 overflow-hidden border border-border/20 shadow-sm" >
82+ < Image
83+ alt = { `${ country . country } flag` }
84+ className = "object-cover"
85+ fill
86+ sizes = "32p"
87+ src = { `https://purecatamphetamine.github.io/country-flag-icons/3x2/${ country . country_code ?. toUpperCase ( ) || country . country . toUpperCase ( ) } .svg` }
88+ />
89+ </ div >
90+ < div className = "min-w-0 flex-1" >
91+ < div className = "flex items-center justify-between" >
92+ < div className = "truncate font-medium text-xs" > { country . country } </ div >
93+ < span className = "ml-1 font-semibold text-primary text-xs" >
94+ { country . visitors > 999
95+ ? `${ ( country . visitors / 1000 ) . toFixed ( 0 ) } k`
96+ : country . visitors . toString ( ) }
97+ </ span >
98+ </ div >
99+ </ div >
100+ </ button >
101+ ) ;
102+ }
103+
36104function WebsiteMapPage ( ) {
37105 const { id } = useParams < { id : string } > ( ) ;
38106 const [ mode ] = useState < 'total' | 'perCapita' > ( 'total' ) ;
@@ -101,14 +169,14 @@ function WebsiteMapPage() {
101169 }
102170
103171 return (
104- < div
105- className = "h-screen overflow-hidden"
106- style = { {
172+ < div
173+ className = "h-screen overflow-hidden"
174+ style = { {
107175 width : 'calc(100% + 3rem)' ,
108176 marginTop : '-1.5rem' ,
109177 marginLeft : '-1.5rem' ,
110178 marginRight : '-1.5rem' ,
111- marginBottom : '-1.5rem'
179+ marginBottom : '-1.5rem' ,
112180 } }
113181 >
114182 < div className = "relative h-full w-full" >
@@ -124,85 +192,62 @@ function WebsiteMapPage() {
124192
125193 { /* Top 5 Countries Overlay */ }
126194 < div className = "absolute top-2 right-2 z-20" >
127- < Card className = "border-sidebar-border bg-background/90 backdrop-blur-md shadow-xl w-60" >
128- < CardHeader className = "pb-2 pt-3 px-3" >
129- < CardTitle className = "flex items-center gap-1.5 text-xs font-medium" >
130- < GlobeIcon className = "h-3 w-3 text-primary" weight = "duotone" />
131- Top 5 Countries
195+ < Card className = "w-56 max-w-[90vw] gap-0 border-sidebar-border bg-background/95 py-0 shadow-xl backdrop-blur-md sm:w-64" >
196+ < CardHeader className = "px-3 pt-2.5 pb-2" >
197+ < CardTitle className = "flex items-center gap-1.5 font-semibold text-xs" >
198+ < GlobeIcon
199+ className = "h-3.5 w-3.5 text-primary"
200+ weight = "duotone"
201+ />
202+ Top Countries
132203 </ CardTitle >
133204 </ CardHeader >
134- < CardContent className = "p-0 pb-1 " >
205+ < CardContent className = "p-0" >
135206 { isLoading ? (
136207 < div className = "space-y-1 px-3 pb-2" >
137208 { new Array ( 5 ) . fill ( 0 ) . map ( ( _ , i ) => (
138209 < div
139- className = "flex items-center justify-between py-1"
210+ className = "flex items-center gap-2.5 py-1.5 "
140211 key = { `country-skeleton-${ i + 1 } ` }
141212 >
142- < div className = "flex items-center gap-1.5" >
143- < Skeleton className = "h-2.5 w-4 rounded" />
144- < Skeleton className = "h-2.5 w-12" />
145- </ div >
146- < Skeleton className = "h-2.5 w-6" />
213+ < Skeleton className = "h-2.5 w-4" />
214+ < Skeleton className = "h-2.5 flex-1" />
215+ < Skeleton className = "h-2.5 w-8" />
147216 </ div >
148217 ) ) }
149218 </ div >
150219 ) : topCountries . length > 0 ? (
151- < div className = "px-3 pb-2" >
152- { topCountries . map ( ( country ) => {
153- const percentage =
154- totalVisitors > 0
155- ? ( country . visitors / totalVisitors ) * 100
156- : 0 ;
157- return (
158- < button
159- className = "flex w-full cursor-pointer items-center justify-between py-1.5 text-left transition-colors hover:bg-primary/5 rounded-sm"
160- key = { country . country }
161- onClick = { ( ) =>
162- handleCountrySelect (
163- country . country_code ?. toUpperCase ( ) ||
164- country . country . toUpperCase ( )
165- )
166- }
167- type = "button"
168- >
169- < div className = "flex items-center gap-1.5 min-w-0 flex-1" >
170- < div className = "relative h-2.5 w-4 flex-shrink-0 overflow-hidden rounded shadow-sm" >
171- < Image
172- alt = { `${ country . country } flag` }
173- className = "object-cover"
174- fill
175- sizes = "16px"
176- src = { `https://purecatamphetamine.github.io/country-flag-icons/3x2/${ country . country_code ?. toUpperCase ( ) || country . country . toUpperCase ( ) } .svg` }
177- />
178- </ div >
179- < div className = "min-w-0 flex-1" >
180- < div className = "truncate font-medium text-xs" >
181- { country . country }
182- </ div >
183- </ div >
184- </ div >
185- < div className = "flex items-center gap-1.5 text-right" >
186- < div className = "text-muted-foreground text-xs" >
187- { percentage . toFixed ( 0 ) } %
188- </ div >
189- < div className = "font-semibold text-xs min-w-0 text-primary" >
190- { country . visitors > 999 ? `${ ( country . visitors / 1000 ) . toFixed ( 0 ) } k` : country . visitors . toString ( ) }
191- </ div >
192- </ div >
193- </ button >
194- ) ;
195- } ) }
220+ < div className = "space-y-0.5" >
221+ { topCountries . map ( ( country ) => (
222+ < CountryRow
223+ country = { country }
224+ key = { country . country }
225+ onCountrySelect = { handleCountrySelect }
226+ totalVisitors = { totalVisitors }
227+ />
228+ ) ) }
229+
230+ { /* Total visitors summary */ }
231+ < div className = "border-border/50 border-t px-2 pt-1.5 pb-1.5" >
232+ < div className = "flex items-center justify-between text-xs" >
233+ < span className = "text-muted-foreground" > Total</ span >
234+ < span className = "font-semibold text-primary" >
235+ { totalVisitors > 999
236+ ? `${ ( totalVisitors / 1000 ) . toFixed ( 0 ) } k`
237+ : totalVisitors . toLocaleString ( ) }
238+ </ span >
239+ </ div >
240+ </ div >
196241 </ div >
197242 ) : (
198- < div className = "flex flex-col items-center justify-center py-6 text-center px-3 " >
199- < div className = "flex h-6 w-6 items-center justify-center rounded bg-muted/20 mb-1 " >
243+ < div className = "flex flex-col items-center justify-center px-3 py-6 text-center" >
244+ < div className = "mb-1.5 flex h-6 w-6 items-center justify-center bg-muted/20" >
200245 < GlobeIcon
201246 className = "h-3 w-3 text-muted-foreground/50"
202247 weight = "duotone"
203248 />
204249 </ div >
205- < p className = "text-muted-foreground text-xs" >
250+ < p className = "font-medium text-muted-foreground text-xs" >
206251 No data
207252 </ p >
208253 </ div >
0 commit comments