Skip to content

Commit bec5ee0

Browse files
committed
fix: upstash
1 parent c177ece commit bec5ee0

File tree

5 files changed

+391
-76
lines changed

5 files changed

+391
-76
lines changed

apps/dashboard/app/(main)/websites/[id]/_components/tabs/performance-tab.tsx

Lines changed: 239 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client';
22

33
import { getCountryCode, getCountryName } from '@databuddy/shared';
4-
import { Lightning } from '@phosphor-icons/react';
4+
import { LightningIcon } from '@phosphor-icons/react';
55
import { useCallback, useEffect, useMemo, useState } from 'react';
66
import { DataTable } from '@/components/analytics/data-table';
77
import { CountryFlag } from '@/components/analytics/icons/CountryFlag';
@@ -13,6 +13,7 @@ import type { FullTabProps } from '../utils/types';
1313
import { PerformanceMetricCell } from './performance/_components/performance-metric-cell';
1414
import { PerformanceSummaryCard } from './performance/_components/performance-summary-card';
1515
import { WebVitalsChart } from './performance/_components/web-vitals-chart';
16+
import { WebVitalsMetricCell } from './performance/_components/web-vitals-metric-cell';
1617
import { formatNumber } from './performance/_utils/performance-utils';
1718

1819
interface 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+
66120
const 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

Comments
 (0)