11'use client' ;
22
33import { getCountryCode , getCountryName } from '@databuddy/shared' ;
4- import { Lightning } from '@phosphor-icons/react' ;
4+ import { LightningIcon } from '@phosphor-icons/react' ;
55import { useCallback , useEffect , useMemo , useState } from 'react' ;
66import { DataTable } from '@/components/analytics/data-table' ;
77import { CountryFlag } from '@/components/analytics/icons/CountryFlag' ;
@@ -13,6 +13,7 @@ import type { FullTabProps } from '../utils/types';
1313import { PerformanceMetricCell } from './performance/_components/performance-metric-cell' ;
1414import { PerformanceSummaryCard } from './performance/_components/performance-summary-card' ;
1515import { WebVitalsChart } from './performance/_components/web-vitals-chart' ;
16+ import { WebVitalsMetricCell } from './performance/_components/web-vitals-metric-cell' ;
1617import { formatNumber } from './performance/_utils/performance-utils' ;
1718
1819interface CellProps {
@@ -63,6 +64,59 @@ const performanceColumns = [
6364 } ,
6465] ;
6566
67+ const webVitalsColumns = [
68+ {
69+ id : 'visitors' ,
70+ accessorKey : 'visitors' ,
71+ header : 'Visitors' ,
72+ cell : ( { getValue } : CellProps ) => formatNumber ( getValue ( ) as number ) ,
73+ } ,
74+ {
75+ id : 'avg_lcp' ,
76+ accessorKey : 'avg_lcp' ,
77+ header : 'LCP' ,
78+ cell : ( { row } : CellProps ) => (
79+ < WebVitalsMetricCell
80+ metric = "lcp"
81+ value = { row . original . avg_lcp as number }
82+ />
83+ ) ,
84+ } ,
85+ {
86+ id : 'avg_fcp' ,
87+ accessorKey : 'avg_fcp' ,
88+ header : 'FCP' ,
89+ cell : ( { row } : CellProps ) => (
90+ < WebVitalsMetricCell
91+ metric = "fcp"
92+ value = { row . original . avg_fcp as number }
93+ />
94+ ) ,
95+ } ,
96+ {
97+ id : 'avg_fid' ,
98+ accessorKey : 'avg_fid' ,
99+ header : 'FID' ,
100+ cell : ( { row } : CellProps ) => (
101+ < WebVitalsMetricCell
102+ metric = "fid"
103+ value = { row . original . avg_fid as number }
104+ />
105+ ) ,
106+ } ,
107+ {
108+ id : 'avg_inp' ,
109+ accessorKey : 'avg_inp' ,
110+ header : 'INP' ,
111+ cell : ( { row } : CellProps ) => (
112+ < WebVitalsMetricCell
113+ metric = "inp"
114+ value = { row . original . avg_inp as number }
115+ />
116+ ) ,
117+ } ,
118+ ] ;
119+
66120const createNameColumn = (
67121 header : string ,
68122 iconRenderer ?: ( name : string ) => React . ReactNode ,
@@ -324,6 +378,135 @@ export function WebsitePerformanceTab({
324378 } ) ) ;
325379 } , [ processedData ] ) ;
326380
381+ const webVitalsTabs = useMemo ( ( ) => {
382+ const formatPageName = ( name : string ) => {
383+ try {
384+ return name . startsWith ( 'http' ) ? new URL ( name ) . pathname : name ;
385+ } catch {
386+ return name . startsWith ( '/' ) ? name : `/${ name } ` ;
387+ }
388+ } ;
389+
390+ const getCountryIcon = ( name : string ) => {
391+ const countryItem = processedData . webVitalsByCountry . find (
392+ ( item ) => ( item as { country_name ?: string } ) . country_name === name
393+ ) ;
394+ return (
395+ < CountryFlag
396+ country = {
397+ ( countryItem as { country_code ?: string } ) ?. country_code || name
398+ }
399+ size = { 16 }
400+ />
401+ ) ;
402+ } ;
403+
404+ const getRegionCountryIcon = ( name : string ) => {
405+ if ( typeof name !== 'string' || ! name . includes ( ',' ) ) {
406+ return < CountryFlag country = { '' } size = { 16 } /> ;
407+ }
408+ const countryPart = name . split ( ',' ) [ 1 ] ?. trim ( ) ;
409+ const code = getCountryCode ( countryPart || '' ) ;
410+ return < CountryFlag country = { code } size = { 16 } /> ;
411+ } ;
412+
413+ const formatRegionName = ( name : string ) => {
414+ if ( typeof name !== 'string' || ! name . includes ( ',' ) ) {
415+ return name || 'Unknown region' ;
416+ }
417+ const [ region , countryPart ] = name . split ( ',' ) . map ( ( s ) => s . trim ( ) ) ;
418+ if ( ! ( region && countryPart ) ) {
419+ return name || 'Unknown region' ;
420+ }
421+ const code = getCountryCode ( countryPart ) ;
422+ const countryName = getCountryName ( code ) ;
423+ if (
424+ countryName &&
425+ region &&
426+ countryName . toLowerCase ( ) === region . toLowerCase ( )
427+ ) {
428+ return countryName ;
429+ }
430+ return countryName ? `${ region } , ${ countryName } ` : name ;
431+ } ;
432+
433+ interface WebVitalsTabConfig {
434+ id : string ;
435+ label : string ;
436+ data : unknown [ ] ;
437+ iconRenderer ?: ( name : string ) => React . ReactNode ;
438+ nameFormatter ?: ( name : string ) => string ;
439+ getFilter : ( row : { name : string } ) => { field : string ; value : string } ;
440+ }
441+
442+ const webVitalsConfigs : WebVitalsTabConfig [ ] = [
443+ {
444+ id : 'web-vitals-pages' ,
445+ label : 'Pages' ,
446+ data : processedData . webVitalsByPage ,
447+ iconRenderer : undefined ,
448+ nameFormatter : formatPageName ,
449+ getFilter : ( row ) => ( { field : 'path' , value : row . name } ) ,
450+ } ,
451+ {
452+ id : 'web-vitals-countries' ,
453+ label : 'Country' ,
454+ data : processedData . webVitalsByCountry ,
455+ iconRenderer : getCountryIcon ,
456+ getFilter : ( row ) => ( { field : 'country' , value : row . name } ) ,
457+ } ,
458+ {
459+ id : 'web-vitals-regions' ,
460+ label : 'Regions' ,
461+ data : processedData . webVitalsByRegion ,
462+ iconRenderer : getRegionCountryIcon ,
463+ nameFormatter : formatRegionName ,
464+ getFilter : ( row ) => ( { field : 'region' , value : row . name } ) ,
465+ } ,
466+ {
467+ id : 'web-vitals-browsers' ,
468+ label : 'Browsers' ,
469+ data : processedData . webVitalsByBrowser ,
470+ iconRenderer : ( name : string ) => < BrowserIcon name = { name } size = "sm" /> ,
471+ getFilter : ( row ) => ( { field : 'browser_name' , value : row . name } ) ,
472+ } ,
473+ {
474+ id : 'web-vitals-os' ,
475+ label : 'Operating Systems' ,
476+ data : processedData . webVitalsByOS ,
477+ iconRenderer : ( name : string ) => < OSIcon name = { name } size = "sm" /> ,
478+ getFilter : ( row ) => ( { field : 'os_name' , value : row . name } ) ,
479+ } ,
480+ ] ;
481+
482+ return webVitalsConfigs . map ( ( config ) => ( {
483+ id : config . id ,
484+ label : config . label ,
485+ data : ( config . data as Record < string , unknown > [ ] ) . map ( ( item , i ) => ( {
486+ name :
487+ ( item as { country_name ?: string } ) . country_name ||
488+ ( item as { name ?: string } ) . name ||
489+ 'Unknown' ,
490+ visitors : ( item as { visitors ?: number } ) . visitors || 0 ,
491+ avg_lcp : ( item as { avg_lcp ?: number } ) . avg_lcp ,
492+ avg_fcp : ( item as { avg_fcp ?: number } ) . avg_fcp ,
493+ avg_fid : ( item as { avg_fid ?: number } ) . avg_fid ,
494+ avg_inp : ( item as { avg_inp ?: number } ) . avg_inp ,
495+ country_code : ( item as { country_code ?: string } ) . country_code ,
496+ _uniqueKey : `${ config . id } -${ i } ` ,
497+ } ) ) ,
498+ columns : [
499+ createNameColumn (
500+ config . label ,
501+ config . iconRenderer ,
502+ config . nameFormatter
503+ ) ,
504+ ...webVitalsColumns ,
505+ ] ,
506+ getFilter : config . getFilter ,
507+ } ) ) ;
508+ } , [ processedData ] ) ;
509+
327510 if ( error ) {
328511 return (
329512 < div className = "mt-4 rounded-md border border-red-200 bg-red-50 p-3 dark:border-red-800 dark:bg-red-950/20" >
@@ -353,47 +536,68 @@ export function WebsitePerformanceTab({
353536 }
354537 isLoading = { isLoading }
355538 isRefreshing = { isRefreshing }
539+ onAddFilter = { onAddFilter }
540+ webVitalsTabs = { webVitalsTabs }
356541 />
357542
358543 { /* Performance Overview */ }
359- { hasData && (
360- < div className = "rounded border bg-muted/20 p-4" >
361- < div className = "mb-4 flex items-start gap-2" >
362- < Lightning className = "mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
363- < div >
364- < p className = "mb-1 font-medium text-foreground" >
365- Performance Overview
366- </ p >
367- < p className = "text-muted-foreground text-xs" >
368- Core Web Vitals and performance metrics.{ ' ' }
369- < span className = "font-medium text-green-600" > Good</ span > ,
370- < span className = "ml-1 font-medium text-yellow-600" >
371- Needs Improvement
372- </ span >
373- ,< span className = "ml-1 font-medium text-red-600" > Poor</ span > { ' ' }
374- ratings.
375- </ p >
376- </ div >
544+ < div className = "rounded border bg-muted/20 p-4" >
545+ < div className = "mb-4 flex items-start gap-2" >
546+ < LightningIcon className = "mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
547+ < div >
548+ < p className = "mb-1 font-medium text-foreground" >
549+ Performance Overview
550+ </ p >
551+ < p className = "text-muted-foreground text-xs" >
552+ Core Web Vitals and performance metrics.{ ' ' }
553+ < span className = "font-medium text-green-600" > Good</ span > ,
554+ < span className = "ml-1 font-medium text-yellow-600" >
555+ Needs Improvement
556+ </ span >
557+ ,< span className = "ml-1 font-medium text-red-600" > Poor</ span > { ' ' }
558+ ratings.
559+ </ p >
377560 </ div >
561+ </ div >
378562
379- < PerformanceSummaryCard
380- activeFilter = { activeFilter }
381- onFilterChange = { setActiveFilter }
382- summary = { performanceSummary }
383- />
384-
385- < div className = "mt-6" >
386- < DataTable
387- description = { description }
388- isLoading = { isLoading || isRefreshing }
389- minHeight = { 500 }
390- onAddFilter = { onAddFilter }
391- tabs = { tabs }
392- title = "Performance Analysis"
563+ { hasData ? (
564+ < >
565+ < PerformanceSummaryCard
566+ activeFilter = { activeFilter }
567+ onFilterChange = { setActiveFilter }
568+ summary = { performanceSummary }
393569 />
570+
571+ < div className = "mt-6" >
572+ < DataTable
573+ description = { description }
574+ isLoading = { isLoading || isRefreshing }
575+ minHeight = { 500 }
576+ onAddFilter = { onAddFilter }
577+ tabs = { tabs }
578+ title = "Performance Analysis"
579+ />
580+ </ div >
581+ </ >
582+ ) : isLoading ? (
583+ < div className = "flex items-center justify-center py-12" >
584+ < div className = "text-center" >
585+ < div className = "mx-auto mb-4 h-8 w-8 animate-spin rounded-full border-primary border-b-2" />
586+ < p className = "text-muted-foreground text-sm" >
587+ Loading performance data...
588+ </ p >
589+ </ div >
394590 </ div >
395- </ div >
396- ) }
591+ ) : (
592+ < div className = "flex items-center justify-center py-12" >
593+ < div className = "text-center" >
594+ < p className = "text-muted-foreground text-sm" >
595+ No performance data available for the selected period.
596+ </ p >
597+ </ div >
598+ </ div >
599+ ) }
600+ </ div >
397601 </ div >
398602 ) ;
399603}
0 commit comments