207 lines
6.6 KiB
TypeScript
207 lines
6.6 KiB
TypeScript
import type { ColumnDef } from '@tanstack/react-table';
|
|
import { ExternalLinkIcon } from 'lucide-react';
|
|
import { useMemo } from 'react';
|
|
import { PageSparkline } from '@/components/pages/page-sparkline';
|
|
import { createHeaderColumn } from '@/components/ui/data-table/data-table-helpers';
|
|
import { useAppContext } from '@/hooks/use-app-context';
|
|
import { fancyMinutes, useNumber } from '@/hooks/use-numer-formatter';
|
|
import type { RouterOutputs } from '@/trpc/client';
|
|
|
|
export type PageRow = RouterOutputs['event']['pages'][number] & {
|
|
gsc?: { clicks: number; impressions: number; ctr: number; position: number };
|
|
};
|
|
|
|
export function useColumns({
|
|
projectId,
|
|
isGscConnected,
|
|
previousMap,
|
|
}: {
|
|
projectId: string;
|
|
isGscConnected: boolean;
|
|
previousMap?: Map<string, number>;
|
|
}): ColumnDef<PageRow>[] {
|
|
const number = useNumber();
|
|
const { apiUrl } = useAppContext();
|
|
|
|
return useMemo<ColumnDef<PageRow>[]>(() => {
|
|
const cols: ColumnDef<PageRow>[] = [
|
|
{
|
|
id: 'page',
|
|
accessorFn: (row) => `${row.origin}${row.path} ${row.title ?? ''}`,
|
|
header: createHeaderColumn('Page'),
|
|
size: 400,
|
|
meta: { bold: true },
|
|
cell: ({ row }) => {
|
|
const page = row.original;
|
|
return (
|
|
<div className="flex min-w-0 items-center gap-3">
|
|
<img
|
|
alt=""
|
|
className="size-4 shrink-0 rounded-sm"
|
|
loading="lazy"
|
|
onError={(e) => {
|
|
(e.target as HTMLImageElement).style.display = 'none';
|
|
}}
|
|
src={`${apiUrl}/misc/favicon?url=${page.origin}`}
|
|
/>
|
|
<div className="min-w-0">
|
|
{page.title && (
|
|
<div className="truncate font-medium text-sm leading-tight">
|
|
{page.title}
|
|
</div>
|
|
)}
|
|
<div className="flex min-w-0 items-center gap-1">
|
|
<span className="truncate font-mono text-muted-foreground text-xs">
|
|
{page.path}
|
|
</span>
|
|
<a
|
|
className="shrink-0 opacity-0 transition-opacity group-hover/row:opacity-100"
|
|
href={page.origin + page.path}
|
|
onClick={(e) => e.stopPropagation()}
|
|
rel="noreferrer noopener"
|
|
target="_blank"
|
|
>
|
|
<ExternalLinkIcon className="size-3 text-muted-foreground" />
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
id: 'trend',
|
|
header: 'Trend',
|
|
enableSorting: false,
|
|
size: 96,
|
|
cell: ({ row }) => (
|
|
<PageSparkline
|
|
origin={row.original.origin}
|
|
path={row.original.path}
|
|
projectId={projectId}
|
|
/>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: 'pageviews',
|
|
header: createHeaderColumn('Views'),
|
|
size: 80,
|
|
cell: ({ row }) => (
|
|
<span className="font-mono text-sm tabular-nums">
|
|
{number.short(row.original.pageviews)}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: 'sessions',
|
|
header: createHeaderColumn('Sessions'),
|
|
size: 90,
|
|
cell: ({ row }) => {
|
|
const prev = previousMap?.get(
|
|
row.original.origin + row.original.path
|
|
);
|
|
if (prev == null) {
|
|
return <span className="text-muted-foreground">—</span>;
|
|
}
|
|
if (prev === 0) {
|
|
return (
|
|
<div className="flex items-center gap-2">
|
|
<span className="font-mono text-sm tabular-nums">
|
|
{number.short(row.original.sessions)}
|
|
</span>
|
|
<span className="text-muted-foreground">new</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const pct = ((row.original.sessions - prev) / prev) * 100;
|
|
const isPos = pct >= 0;
|
|
|
|
return (
|
|
<div className="flex items-center gap-2">
|
|
<span className="font-mono text-sm tabular-nums">
|
|
{number.short(row.original.sessions)}
|
|
</span>
|
|
<span
|
|
className={`font-mono text-sm tabular-nums ${isPos ? 'text-emerald-600 dark:text-emerald-400' : 'text-red-600 dark:text-red-400'}`}
|
|
>
|
|
{isPos ? '+' : ''}
|
|
{pct.toFixed(1)}%
|
|
</span>
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
accessorKey: 'bounce_rate',
|
|
header: createHeaderColumn('Bounce'),
|
|
size: 80,
|
|
cell: ({ row }) => (
|
|
<span className="font-mono text-sm tabular-nums">
|
|
{row.original.bounce_rate.toFixed(0)}%
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: 'avg_duration',
|
|
header: createHeaderColumn('Duration'),
|
|
size: 90,
|
|
cell: ({ row }) => (
|
|
<span className="whitespace-nowrap font-mono text-sm tabular-nums">
|
|
{fancyMinutes(row.original.avg_duration)}
|
|
</span>
|
|
),
|
|
},
|
|
];
|
|
|
|
if (isGscConnected) {
|
|
cols.push(
|
|
{
|
|
id: 'gsc_impressions',
|
|
accessorFn: (row) => row.gsc?.impressions ?? 0,
|
|
header: createHeaderColumn('Impr.'),
|
|
size: 80,
|
|
cell: ({ row }) =>
|
|
row.original.gsc ? (
|
|
<span className="font-mono text-sm tabular-nums">
|
|
{number.short(row.original.gsc.impressions)}
|
|
</span>
|
|
) : (
|
|
<span className="text-muted-foreground">—</span>
|
|
),
|
|
},
|
|
{
|
|
id: 'gsc_ctr',
|
|
accessorFn: (row) => row.gsc?.ctr ?? 0,
|
|
header: createHeaderColumn('CTR'),
|
|
size: 70,
|
|
cell: ({ row }) =>
|
|
row.original.gsc ? (
|
|
<span className="font-mono text-sm tabular-nums">
|
|
{(row.original.gsc.ctr * 100).toFixed(1)}%
|
|
</span>
|
|
) : (
|
|
<span className="text-muted-foreground">—</span>
|
|
),
|
|
},
|
|
{
|
|
id: 'gsc_clicks',
|
|
accessorFn: (row) => row.gsc?.clicks ?? 0,
|
|
header: createHeaderColumn('Clicks'),
|
|
size: 80,
|
|
cell: ({ row }) =>
|
|
row.original.gsc ? (
|
|
<span className="font-mono text-sm tabular-nums">
|
|
{number.short(row.original.gsc.clicks)}
|
|
</span>
|
|
) : (
|
|
<span className="text-muted-foreground">—</span>
|
|
),
|
|
}
|
|
);
|
|
}
|
|
|
|
return cols;
|
|
}, [isGscConnected, number, apiUrl, projectId, previousMap]);
|
|
}
|