import { useMutation, useQuery } from "@tanstack/react-query"; import { AlertCircle, TriangleAlert } from "lucide-react"; import { toast } from "sonner"; import { exportAuditLighthouseIssues, getAuditLighthouseIssues, } from "@/serverFunctions/lighthouse"; import { downloadFile } from "@/client/lib/download"; import { exportTableToSheets } from "@/client/lib/exportToSheets"; import type { CategoryTab, ExportPayload, LighthouseIssue } from "./types"; import { categoryLabel, issuesToCsv, issuesToTable } from "./utils"; import { LighthouseIssueList, LighthouseIssuesHeader, LighthouseIssuesToolbar, } from "./LighthouseIssuesParts"; import { categoryTabs } from "./types"; type LighthouseIssuesScreenProps = { projectId: string; resultId: string; category: CategoryTab; backLabel: string; onBack: () => void; onCategoryChange: (next: CategoryTab) => void; }; export function LighthouseIssuesScreen(props: LighthouseIssuesScreenProps) { const { projectId, resultId, category, backLabel, onBack, onCategoryChange } = props; const issuesQuery = useQuery({ queryKey: ["auditLighthouseIssues", projectId, resultId], queryFn: () => getAuditLighthouseIssues({ data: { projectId, resultId, }, }), }); const exportMutation = useMutation({ mutationFn: ( data: ExportPayload, ): Promise<{ filename: string; content: string }> => exportAuditLighthouseIssues({ data: { projectId, resultId, ...data, }, }), }); const { allIssues, categoryCounts, runCopy, runExport, runExportCsv, runExportSheets, selectedCategoryLabel, severityCounts, visibleIssues, } = useLighthouseIssuesActions({ category, exportMutation, allIssues: issuesQuery.data?.issues ?? [], }); const issuesErrorMessage = issuesQuery.error instanceof Error ? issuesQuery.error.message : "Failed to load Lighthouse issues."; const showsLegacyPayloadNotice = issuesQuery.data != null && !issuesQuery.data.hasIssueDetails; const emptyMessage = showsLegacyPayloadNotice ? "This audit was saved without issue-level Lighthouse details. Re-run the audit to populate this screen." : undefined; return (
{issuesQuery.isError ? (
{issuesErrorMessage}
) : null} {showsLegacyPayloadNotice ? (
This Lighthouse run was stored before issue details were preserved. Re-run the audit to see category counts and issue cards.
) : null} { void runCopy(data, message); }} onExport={(data) => { void runExport(data); }} onExportCsv={runExportCsv} onExportSheets={runExportSheets} />
); } function useLighthouseIssuesActions({ allIssues, category, exportMutation, }: { allIssues: LighthouseIssue[]; category: CategoryTab; exportMutation: { mutateAsync: ( data: ExportPayload, ) => Promise<{ filename: string; content: string }>; }; }) { const visibleIssues = category === "all" ? allIssues : allIssues.filter((issue) => issue.category === category); const selectedCategoryLabel = categoryLabel(category); const categoryCounts = getCategoryCounts(allIssues); const severityCounts = getSeverityCounts(visibleIssues); const runExport = async (data: ExportPayload) => { try { const exported = await exportMutation.mutateAsync(data); downloadFile(exported.content, exported.filename, "application/json"); toast.success("Download started"); } catch (error) { const message = error instanceof Error ? error.message : "Failed to export payload"; toast.error(message); } }; const runExportCsv = ( rows: LighthouseIssue[], variant: "all" | "current", ) => { const filename = `lighthouse-${variant}-${category}-issues.csv`; downloadFile(issuesToCsv(rows), filename, "text/csv"); toast.success("CSV download started"); }; const runExportSheets = ( rows: LighthouseIssue[], variant: "all" | "current", ) => { const table = issuesToTable(rows); void exportTableToSheets({ headers: table.headers, rows: table.rows, feature: `lighthouse_issues_${variant}`, }); }; const runCopy = async (data: ExportPayload, toastMessage: string) => { try { const exported = await exportMutation.mutateAsync(data); await navigator.clipboard.writeText(exported.content); toast.success(toastMessage); } catch (error) { const message = error instanceof Error ? error.message : "Failed to copy payload"; toast.error(message); } }; return { allIssues, categoryCounts, runCopy, runExport, runExportCsv, runExportSheets, selectedCategoryLabel, severityCounts, visibleIssues, }; } function getCategoryCounts( allIssues: LighthouseIssue[], ): Record { return categoryTabs.reduce>( (acc, tab) => { if (tab === "all") { acc[tab] = allIssues.length; return acc; } acc[tab] = allIssues.filter((issue) => issue.category === tab).length; return acc; }, { all: allIssues.length, performance: 0, accessibility: 0, "best-practices": 0, seo: 0, }, ); } function getSeverityCounts(issues: LighthouseIssue[]) { return { critical: issues.filter((issue) => issue.severity === "critical").length, warning: issues.filter((issue) => issue.severity === "warning").length, info: issues.filter((issue) => issue.severity === "info").length, }; }