* Fix tab active state wiring across result views * Add spam filters to referring domains
241 lines
6.9 KiB
TypeScript
241 lines
6.9 KiB
TypeScript
import { useMemo } from "react";
|
|
import { StatCard } from "@/client/features/audit/shared";
|
|
import {
|
|
exportPages,
|
|
exportPerformance,
|
|
} from "@/client/features/audit/results/export";
|
|
import type { AuditResultsData } from "@/client/features/audit/results/types";
|
|
import {
|
|
ExportDropdown,
|
|
isLighthouseFailure,
|
|
PagesTable,
|
|
PerformanceTable,
|
|
} from "@/client/features/audit/results/ResultsTables";
|
|
|
|
type ResultsTab = "pages" | "performance";
|
|
|
|
export function ResultsView({
|
|
projectId,
|
|
data,
|
|
onTabChange,
|
|
tab,
|
|
}: {
|
|
projectId: string;
|
|
data: AuditResultsData;
|
|
tab: string;
|
|
onTabChange: (tab: ResultsTab) => void;
|
|
}) {
|
|
const { audit, pages, lighthouse } = data;
|
|
const hasPerformanceTab = lighthouse.length > 0;
|
|
const activeTab = hasPerformanceTab ? tab : "pages";
|
|
const stats = useResultStats(pages, lighthouse);
|
|
|
|
return (
|
|
<>
|
|
<StatsGrid
|
|
pagesCrawled={audit.pagesCrawled}
|
|
totalPages={pages.length}
|
|
totalLighthouse={lighthouse.length}
|
|
averageResponseMs={stats.averageResponseMs}
|
|
lighthouseSummary={stats.lighthouseSummary}
|
|
/>
|
|
|
|
<div className="card bg-base-100 border border-base-300">
|
|
<div className="card-body gap-3">
|
|
<ResultsHeader
|
|
pageCount={pages.length}
|
|
lighthouseCount={lighthouse.length}
|
|
hasPerformanceTab={hasPerformanceTab}
|
|
activeTab={activeTab}
|
|
onTabChange={onTabChange}
|
|
onExport={(format) => {
|
|
if (activeTab === "performance") {
|
|
exportPerformance(lighthouse, pages, format);
|
|
return;
|
|
}
|
|
exportPages(pages, format);
|
|
}}
|
|
/>
|
|
|
|
{activeTab === "pages" && <PagesTable pages={pages} />}
|
|
{activeTab === "performance" && lighthouse.length > 0 && (
|
|
<PerformanceTable
|
|
auditId={audit.id}
|
|
projectId={projectId}
|
|
lighthouse={lighthouse}
|
|
pages={pages}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function useResultStats(
|
|
pages: AuditResultsData["pages"],
|
|
lighthouse: AuditResultsData["lighthouse"],
|
|
) {
|
|
const averageResponseMs = useMemo(() => {
|
|
if (pages.length === 0) return 0;
|
|
const total = pages.reduce(
|
|
(sum: number, page: AuditResultsData["pages"][number]) =>
|
|
sum + (page.responseTimeMs ?? 0),
|
|
0,
|
|
);
|
|
return Math.round(total / pages.length);
|
|
}, [pages]);
|
|
|
|
const lighthouseSummary = useMemo(() => {
|
|
const failed = lighthouse.filter(
|
|
(row: AuditResultsData["lighthouse"][number]) => isLighthouseFailure(row),
|
|
).length;
|
|
const successful = lighthouse.filter(
|
|
(row: AuditResultsData["lighthouse"][number]) =>
|
|
!isLighthouseFailure(row),
|
|
);
|
|
const averageScore = (
|
|
key: "performanceScore" | "seoScore" | "accessibilityScore",
|
|
) => {
|
|
const values = successful
|
|
.map((row: AuditResultsData["lighthouse"][number]) => row[key])
|
|
.filter((value: number | null): value is number => value != null);
|
|
if (values.length === 0) return null;
|
|
const total = values.reduce((sum: number, value) => sum + value, 0);
|
|
return Math.round(total / values.length);
|
|
};
|
|
|
|
return {
|
|
failed,
|
|
avgPerformance: averageScore("performanceScore"),
|
|
avgSeo: averageScore("seoScore"),
|
|
avgAccessibility: averageScore("accessibilityScore"),
|
|
};
|
|
}, [lighthouse]);
|
|
|
|
return { averageResponseMs, lighthouseSummary };
|
|
}
|
|
|
|
function ResultsHeader({
|
|
pageCount,
|
|
lighthouseCount,
|
|
hasPerformanceTab,
|
|
activeTab,
|
|
onTabChange,
|
|
onExport,
|
|
}: {
|
|
pageCount: number;
|
|
lighthouseCount: number;
|
|
hasPerformanceTab: boolean;
|
|
activeTab: string;
|
|
onTabChange: (tab: ResultsTab) => void;
|
|
onExport: (format: "csv" | "json" | "sheets") => void;
|
|
}) {
|
|
const tabs: Array<{ tab: ResultsTab; label: string }> = [
|
|
{ tab: "pages", label: `Pages (${pageCount})` },
|
|
{ tab: "performance", label: `Performance (${lighthouseCount})` },
|
|
];
|
|
|
|
return (
|
|
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-3">
|
|
{hasPerformanceTab ? (
|
|
<div role="tablist" className="tabs tabs-box w-fit">
|
|
{tabs.map(({ label, tab }) => {
|
|
const isActive = activeTab === tab;
|
|
|
|
return (
|
|
<button
|
|
key={tab}
|
|
type="button"
|
|
role="tab"
|
|
aria-selected={isActive}
|
|
className={`tab ${isActive ? "tab-active" : ""}`}
|
|
onClick={() => onTabChange(tab)}
|
|
>
|
|
{label}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
) : (
|
|
<h3 className="text-base font-medium">Pages ({pageCount})</h3>
|
|
)}
|
|
|
|
<ExportDropdown onExport={onExport} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function StatsGrid({
|
|
pagesCrawled,
|
|
totalPages,
|
|
totalLighthouse,
|
|
averageResponseMs,
|
|
lighthouseSummary,
|
|
}: {
|
|
pagesCrawled: number;
|
|
totalPages: number;
|
|
totalLighthouse: number;
|
|
averageResponseMs: number;
|
|
lighthouseSummary: {
|
|
failed: number;
|
|
avgPerformance: number | null;
|
|
avgSeo: number | null;
|
|
avgAccessibility: number | null;
|
|
};
|
|
}) {
|
|
return (
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
<StatCard label="Pages Crawled" value={String(pagesCrawled)} />
|
|
<StatCard label="Total URLs" value={String(totalPages)} />
|
|
<StatCard label="Lighthouse Tests" value={String(totalLighthouse)} />
|
|
<StatCard label="Avg Response" value={`${averageResponseMs}ms`} />
|
|
{totalLighthouse > 0 && (
|
|
<>
|
|
<StatCard
|
|
label="Avg Lighthouse Perf"
|
|
value={
|
|
lighthouseSummary.avgPerformance == null
|
|
? "-"
|
|
: String(lighthouseSummary.avgPerformance)
|
|
}
|
|
className={scoreClass(lighthouseSummary.avgPerformance)}
|
|
/>
|
|
<StatCard
|
|
label="Avg Lighthouse SEO"
|
|
value={
|
|
lighthouseSummary.avgSeo == null
|
|
? "-"
|
|
: String(lighthouseSummary.avgSeo)
|
|
}
|
|
className={scoreClass(lighthouseSummary.avgSeo)}
|
|
/>
|
|
<StatCard
|
|
label="Avg Lighthouse A11y"
|
|
value={
|
|
lighthouseSummary.avgAccessibility == null
|
|
? "-"
|
|
: String(lighthouseSummary.avgAccessibility)
|
|
}
|
|
className={scoreClass(lighthouseSummary.avgAccessibility)}
|
|
/>
|
|
<StatCard
|
|
label="Lighthouse Failures"
|
|
value={String(lighthouseSummary.failed)}
|
|
className={
|
|
lighthouseSummary.failed > 0 ? "text-error" : "text-success"
|
|
}
|
|
/>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function scoreClass(score: number | null) {
|
|
if (score == null) return "";
|
|
if (score >= 90) return "text-success";
|
|
if (score >= 50) return "text-warning";
|
|
return "text-error";
|
|
}
|