You've already forked domainstack.io
mirror of
https://github.com/jakejarvis/domainstack.io.git
synced 2025-12-02 19:33:48 -05:00
refactor: remove createSectionWithData utility and replace with direct TRPC query usage in DomainReportView
This commit is contained in:
@@ -1,162 +0,0 @@
|
||||
/* @vitest-environment jsdom */
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createSectionWithData } from "@/components/domain/create-section-with-data";
|
||||
|
||||
// Mock the error boundary to avoid class component complexity in tests
|
||||
vi.mock("@/components/domain/section-error-boundary", () => ({
|
||||
SectionErrorBoundary: ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
sectionName: string;
|
||||
}) => <div>{children}</div>,
|
||||
}));
|
||||
|
||||
describe("createSectionWithData", () => {
|
||||
it("should create a component that queries and renders section", () => {
|
||||
const testData = { message: "test data" };
|
||||
const mockUseQuery = vi.fn(() => ({
|
||||
data: testData,
|
||||
}));
|
||||
|
||||
const MockSection = vi.fn(({ data }: { data: { message: string } }) => (
|
||||
<div data-testid="section">{data.message}</div>
|
||||
));
|
||||
|
||||
const MockSkeleton = vi.fn(() => (
|
||||
<div data-testid="skeleton">Loading...</div>
|
||||
));
|
||||
|
||||
const SectionWithData = createSectionWithData(
|
||||
mockUseQuery,
|
||||
MockSection,
|
||||
MockSkeleton,
|
||||
"Test Section",
|
||||
);
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<SectionWithData domain="example.com" />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
// Verify the section renders with the correct data
|
||||
expect(screen.getByTestId("section")).toBeInTheDocument();
|
||||
expect(screen.getByText("test data")).toBeInTheDocument();
|
||||
|
||||
// Verify useQuery was called with the domain
|
||||
expect(mockUseQuery).toHaveBeenCalledWith("example.com");
|
||||
});
|
||||
|
||||
it("should map data correctly to section props", () => {
|
||||
const testData = { records: ["A", "AAAA", "MX"] };
|
||||
|
||||
const mockUseQuery = vi.fn(() => ({
|
||||
data: testData,
|
||||
}));
|
||||
|
||||
const MockSection = vi.fn(
|
||||
({ domain, data }: { domain: string; data: { records: string[] } }) => (
|
||||
<div data-testid="section">
|
||||
{domain}: {data.records.join(", ")}
|
||||
</div>
|
||||
),
|
||||
);
|
||||
|
||||
const MockSkeleton = () => <div data-testid="skeleton">Loading...</div>;
|
||||
|
||||
const SectionWithData = createSectionWithData(
|
||||
mockUseQuery,
|
||||
MockSection,
|
||||
MockSkeleton,
|
||||
"Test Section",
|
||||
);
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<SectionWithData domain="example.com" />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
// Verify the section renders with correctly mapped props
|
||||
expect(screen.getByTestId("section")).toBeInTheDocument();
|
||||
expect(screen.getByText("example.com: A, AAAA, MX")).toBeInTheDocument();
|
||||
|
||||
// Verify MockSection was called with the correct props
|
||||
expect(MockSection).toHaveBeenCalled();
|
||||
const callArgs = MockSection.mock.calls[0][0];
|
||||
expect(callArgs.domain).toBe("example.com");
|
||||
expect(callArgs.data).toEqual(testData);
|
||||
});
|
||||
|
||||
it("should pass domain to useQuery hook", () => {
|
||||
const mockUseQuery = vi.fn(() => ({
|
||||
data: { test: "value" },
|
||||
}));
|
||||
|
||||
const MockSection = () => <div>Section</div>;
|
||||
const MockSkeleton = () => <div>Loading</div>;
|
||||
|
||||
const SectionWithData = createSectionWithData(
|
||||
mockUseQuery,
|
||||
MockSection,
|
||||
MockSkeleton,
|
||||
"Test Section",
|
||||
);
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<SectionWithData domain="test.com" />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
// Verify useQuery was called with the correct domain
|
||||
expect(mockUseQuery).toHaveBeenCalledWith("test.com");
|
||||
});
|
||||
|
||||
it("should support passing both domain and data to section", () => {
|
||||
const mockUseQuery = vi.fn(() => ({
|
||||
data: { seoData: "meta" },
|
||||
}));
|
||||
|
||||
const MockSection = vi.fn(
|
||||
({ domain, data }: { domain: string; data: { seoData: string } }) => (
|
||||
<div>
|
||||
{domain}: {data.seoData}
|
||||
</div>
|
||||
),
|
||||
);
|
||||
|
||||
const MockSkeleton = () => <div>Loading</div>;
|
||||
|
||||
const SectionWithData = createSectionWithData(
|
||||
mockUseQuery,
|
||||
MockSection,
|
||||
MockSkeleton,
|
||||
"Test Section",
|
||||
);
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<SectionWithData domain="example.com" />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
// Verify both domain and data are passed to section
|
||||
expect(mockUseQuery).toHaveBeenCalledWith("example.com");
|
||||
expect(MockSection).toHaveBeenCalled();
|
||||
const callArgs = MockSection.mock.calls[0][0];
|
||||
expect(callArgs.domain).toBe("example.com");
|
||||
expect(callArgs.data).toEqual({ seoData: "meta" });
|
||||
});
|
||||
});
|
||||
@@ -1,56 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import type { ComponentType } from "react";
|
||||
import { Suspense } from "react";
|
||||
import { SectionErrorBoundary } from "@/components/domain/section-error-boundary";
|
||||
|
||||
interface QueryResult<TData> {
|
||||
data: TData;
|
||||
}
|
||||
|
||||
type UseQueryHook<TData> = (domain: string) => QueryResult<TData>;
|
||||
|
||||
/**
|
||||
* Higher-order factory that creates a SectionWithData component
|
||||
* following the Suspense+query+render pattern with error boundary protection.
|
||||
*
|
||||
* @param useQuery - The query hook to fetch data (e.g., useHostingQuery)
|
||||
* @param Section - The presentational component to render with data
|
||||
* @param Skeleton - The skeleton component to show during loading
|
||||
* @param sectionName - Name of the section for error tracking (e.g., "Hosting", "DNS")
|
||||
* @param mapDataToProps - Optional function to map query data to Section props. Defaults to `(domain, data) => ({ domain, data })`
|
||||
* @returns A SectionWithData component that handles Suspense, data fetching, and error boundaries
|
||||
*/
|
||||
export function createSectionWithData<
|
||||
TData,
|
||||
TProps extends Record<string, unknown> = { domain: string; data: TData },
|
||||
>(
|
||||
useQuery: UseQueryHook<TData>,
|
||||
Section: ComponentType<TProps>,
|
||||
Skeleton: ComponentType,
|
||||
sectionName: string,
|
||||
mapDataToProps?: (domain: string, data: TData) => TProps,
|
||||
) {
|
||||
const defaultMapper = (domain: string, data: TData) =>
|
||||
({ domain, data }) as unknown as TProps;
|
||||
|
||||
const mapper = mapDataToProps ?? defaultMapper;
|
||||
|
||||
function SectionContent({ domain }: { domain: string }) {
|
||||
const { data } = useQuery(domain);
|
||||
const props = mapper(domain, data);
|
||||
return <Section {...props} />;
|
||||
}
|
||||
|
||||
function SectionWithData({ domain }: { domain: string }) {
|
||||
return (
|
||||
<SectionErrorBoundary sectionName={sectionName}>
|
||||
<Suspense fallback={<Skeleton />}>
|
||||
<SectionContent domain={domain} />
|
||||
</Suspense>
|
||||
</SectionErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
return SectionWithData;
|
||||
}
|
||||
@@ -1,343 +0,0 @@
|
||||
/* @vitest-environment jsdom */
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { userEvent } from "@testing-library/user-event";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { DomainReportView } from "./domain-report-view";
|
||||
|
||||
vi.mock("@/lib/json-export", () => ({
|
||||
exportDomainData: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/trpc/client", () => ({
|
||||
useTRPC: () => ({
|
||||
domain: {
|
||||
getRegistration: {
|
||||
queryOptions: (input: { domain: string }) => ({
|
||||
queryKey: ["getRegistration", input],
|
||||
}),
|
||||
},
|
||||
getDnsRecords: {
|
||||
queryOptions: (input: { domain: string }) => ({
|
||||
queryKey: ["getDnsRecords", input],
|
||||
}),
|
||||
},
|
||||
getHosting: {
|
||||
queryOptions: (input: { domain: string }) => ({
|
||||
queryKey: ["getHosting", input],
|
||||
}),
|
||||
},
|
||||
getCertificates: {
|
||||
queryOptions: (input: { domain: string }) => ({
|
||||
queryKey: ["getCertificates", input],
|
||||
}),
|
||||
},
|
||||
getHeaders: {
|
||||
queryOptions: (input: { domain: string }) => ({
|
||||
queryKey: ["getHeaders", input],
|
||||
}),
|
||||
},
|
||||
getSeo: {
|
||||
queryOptions: (input: { domain: string }) => ({
|
||||
queryKey: ["getSeo", input],
|
||||
}),
|
||||
},
|
||||
getFavicon: {
|
||||
queryOptions: (input: { domain: string }) => ({
|
||||
queryKey: ["getFavicon", input],
|
||||
}),
|
||||
},
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock implementation for useRegistrationQuery
|
||||
const mockUseRegistrationQuery = vi.fn((domain: string) => ({
|
||||
data: {
|
||||
isRegistered: true,
|
||||
domain,
|
||||
source: "rdap" as "rdap" | "whois" | null,
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/hooks/use-domain-queries", () => ({
|
||||
useRegistrationQuery: (domain: string) => mockUseRegistrationQuery(domain),
|
||||
useDnsQuery: vi.fn(() => ({ data: { records: [] } })),
|
||||
useHostingQuery: vi.fn(() => ({ data: null })),
|
||||
useCertificatesQuery: vi.fn(() => ({ data: [] })),
|
||||
useHeadersQuery: vi.fn(() => ({ data: [] })),
|
||||
useSeoQuery: vi.fn(() => ({ data: null })),
|
||||
}));
|
||||
|
||||
vi.mock("@/hooks/use-domain-history", () => ({
|
||||
useDomainHistory: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/domain/domain-unregistered-state", () => ({
|
||||
DomainUnregisteredState: () => <div>Unregistered</div>,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/domain/export-button", () => ({
|
||||
ExportButton: ({
|
||||
onExportAction,
|
||||
disabled,
|
||||
}: {
|
||||
onExportAction: () => void;
|
||||
disabled: boolean;
|
||||
}) => (
|
||||
<button type="button" onClick={onExportAction} disabled={disabled}>
|
||||
Export
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/domain/favicon", () => ({
|
||||
Favicon: () => <div>Favicon</div>,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/domain/screenshot-tooltip", () => ({
|
||||
ScreenshotTooltip: ({ children }: { children: React.ReactNode }) => (
|
||||
<div>{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/domain/tools-dropdown", () => ({
|
||||
ToolsDropdown: () => <div>Tools</div>,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/domain/create-section-with-data", () => ({
|
||||
createSectionWithData: () => () => <div>Section</div>,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/domain/registration/registration-section", () => ({
|
||||
RegistrationSection: () => <div>Registration</div>,
|
||||
}));
|
||||
|
||||
vi.mock(
|
||||
"@/components/domain/registration/registration-section-skeleton",
|
||||
() => ({
|
||||
RegistrationSectionSkeleton: () => <div>Registration Skeleton</div>,
|
||||
}),
|
||||
);
|
||||
|
||||
vi.mock("@/components/domain/hosting/hosting-section", () => ({
|
||||
HostingSection: () => <div>Hosting</div>,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/domain/hosting/hosting-section-skeleton", () => ({
|
||||
HostingSectionSkeleton: () => <div>Hosting Skeleton</div>,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/domain/dns/dns-section", () => ({
|
||||
DnsSection: () => <div>DNS</div>,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/domain/dns/dns-section-skeleton", () => ({
|
||||
DnsSectionSkeleton: () => <div>DNS Skeleton</div>,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/domain/certificates/certificates-section", () => ({
|
||||
CertificatesSection: () => <div>Certificates</div>,
|
||||
}));
|
||||
|
||||
vi.mock(
|
||||
"@/components/domain/certificates/certificates-section-skeleton",
|
||||
() => ({
|
||||
CertificatesSectionSkeleton: () => <div>Certificates Skeleton</div>,
|
||||
}),
|
||||
);
|
||||
|
||||
vi.mock("@/components/domain/headers/headers-section", () => ({
|
||||
HeadersSection: () => <div>Headers</div>,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/domain/headers/headers-section-skeleton", () => ({
|
||||
HeadersSectionSkeleton: () => <div>Headers Skeleton</div>,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/domain/seo/seo-section", () => ({
|
||||
SeoSection: () => <div>SEO</div>,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/domain/seo/seo-section-skeleton", () => ({
|
||||
SeoSectionSkeleton: () => <div>SEO Skeleton</div>,
|
||||
}));
|
||||
|
||||
describe("DomainReportView Export", () => {
|
||||
beforeEach(() => {
|
||||
// Reset to default mock implementation
|
||||
mockUseRegistrationQuery.mockImplementation((domain: string) => ({
|
||||
data: {
|
||||
isRegistered: true,
|
||||
domain,
|
||||
source: "rdap",
|
||||
},
|
||||
}));
|
||||
});
|
||||
it("calls exportDomainData with cached query data when Export button is clicked", async () => {
|
||||
const { exportDomainData } = await import("@/lib/json-export");
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
gcTime: Number.POSITIVE_INFINITY,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Pre-populate cache with test data
|
||||
const domain = "example.com";
|
||||
queryClient.setQueryData(["getRegistration", { domain }], {
|
||||
isRegistered: true,
|
||||
domain: "example.com",
|
||||
});
|
||||
queryClient.setQueryData(["getDnsRecords", { domain }], { records: [] });
|
||||
queryClient.setQueryData(["getHosting", { domain }], { provider: "test" });
|
||||
queryClient.setQueryData(["getCertificates", { domain }], []);
|
||||
queryClient.setQueryData(["getHeaders", { domain }], []);
|
||||
queryClient.setQueryData(["getSeo", { domain }], { title: "Test" });
|
||||
queryClient.setQueryData(["getFavicon", { domain }], {
|
||||
url: "https://test-store.public.blob.vercel-storage.com/abcdef0123456789abcdef0123456789/32x32.webp",
|
||||
});
|
||||
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<DomainReportView domain={domain} />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
// Wait for component to render
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("example.com")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Wait for export button to be enabled (all data loaded)
|
||||
const exportButton = screen.getByText("Export");
|
||||
await waitFor(() => {
|
||||
expect(exportButton).not.toBeDisabled();
|
||||
});
|
||||
|
||||
// Click export button
|
||||
await userEvent.click(exportButton);
|
||||
|
||||
// Verify exportDomainData was called with aggregated data
|
||||
expect(exportDomainData).toHaveBeenCalledWith(domain, {
|
||||
registration: { isRegistered: true, domain: "example.com" },
|
||||
dns: { records: [] },
|
||||
hosting: { provider: "test" },
|
||||
certificates: [],
|
||||
headers: [],
|
||||
seo: { title: "Test" },
|
||||
});
|
||||
});
|
||||
|
||||
it("disables export button until all data is loaded", async () => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
gcTime: Number.POSITIVE_INFINITY,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const domain = "example.com";
|
||||
|
||||
// Only set registration data initially
|
||||
queryClient.setQueryData(["registration", { domain }], {
|
||||
isRegistered: true,
|
||||
domain: "example.com",
|
||||
});
|
||||
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<DomainReportView domain={domain} />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
// Wait for component to render
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("example.com")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Export button should be disabled when not all data is loaded
|
||||
const exportButton = screen.getByText("Export");
|
||||
expect(exportButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it("supports testing different domains via domain-aware mock", async () => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
gcTime: Number.POSITIVE_INFINITY,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const domain = "test-domain.org";
|
||||
|
||||
// Pre-populate cache with test data for different domain
|
||||
queryClient.setQueryData(["registration", { domain }], {
|
||||
isRegistered: true,
|
||||
domain,
|
||||
source: "whois",
|
||||
});
|
||||
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<DomainReportView domain={domain} />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
// Wait for component to render with the correct domain
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("test-domain.org")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Verify mock was called with the correct domain
|
||||
expect(mockUseRegistrationQuery).toHaveBeenCalledWith(domain);
|
||||
});
|
||||
|
||||
it("can test unregistered domains using mockImplementation", async () => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
gcTime: Number.POSITIVE_INFINITY,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const domain = "unregistered-domain.test";
|
||||
|
||||
// Customize mock to return unregistered state
|
||||
// Note: source must be non-null to trigger unregistered state view
|
||||
mockUseRegistrationQuery.mockImplementation((d: string) => ({
|
||||
data: {
|
||||
isRegistered: false,
|
||||
domain: d,
|
||||
source: "rdap",
|
||||
},
|
||||
}));
|
||||
|
||||
// Pre-populate cache with unregistered data
|
||||
queryClient.setQueryData(["registration", { domain }], {
|
||||
isRegistered: false,
|
||||
domain,
|
||||
source: "rdap",
|
||||
});
|
||||
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<DomainReportView domain={domain} />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
// Wait for unregistered state to render
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Unregistered")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { Suspense } from "react";
|
||||
import { CertificatesSection } from "@/components/domain/certificates/certificates-section";
|
||||
import { CertificatesSectionSkeleton } from "@/components/domain/certificates/certificates-section-skeleton";
|
||||
import { createSectionWithData } from "@/components/domain/create-section-with-data";
|
||||
import { DnsSection } from "@/components/domain/dns/dns-section";
|
||||
import { DnsSectionSkeleton } from "@/components/domain/dns/dns-section-skeleton";
|
||||
import { DomainLoadingState } from "@/components/domain/domain-loading-state";
|
||||
@@ -15,69 +15,71 @@ import { HostingSection } from "@/components/domain/hosting/hosting-section";
|
||||
import { HostingSectionSkeleton } from "@/components/domain/hosting/hosting-section-skeleton";
|
||||
import { RegistrationSection } from "@/components/domain/registration/registration-section";
|
||||
import { RegistrationSectionSkeleton } from "@/components/domain/registration/registration-section-skeleton";
|
||||
import { SectionErrorBoundary } from "@/components/domain/section-error-boundary";
|
||||
import { SeoSection } from "@/components/domain/seo/seo-section";
|
||||
import { SeoSectionSkeleton } from "@/components/domain/seo/seo-section-skeleton";
|
||||
import { useDomainExport } from "@/hooks/use-domain-export";
|
||||
import { useDomainHistory } from "@/hooks/use-domain-history";
|
||||
import {
|
||||
useCertificatesQuery,
|
||||
useDnsQuery,
|
||||
useHeadersQuery,
|
||||
useHostingQuery,
|
||||
useRegistrationQuery,
|
||||
useSeoQuery,
|
||||
} from "@/hooks/use-domain-queries";
|
||||
import { useDomainQueryKeys } from "@/hooks/use-domain-query-keys";
|
||||
import { useTRPC } from "@/lib/trpc/client";
|
||||
|
||||
// Create section components using the factory
|
||||
const RegistrationSectionWithData = createSectionWithData(
|
||||
useRegistrationQuery,
|
||||
RegistrationSection,
|
||||
RegistrationSectionSkeleton,
|
||||
"Registration",
|
||||
// Section content components that fetch and render data
|
||||
function RegistrationSectionContent({ domain }: { domain: string }) {
|
||||
const trpc = useTRPC();
|
||||
const { data } = useSuspenseQuery(
|
||||
trpc.domain.getRegistration.queryOptions({ domain }),
|
||||
);
|
||||
return <RegistrationSection domain={domain} data={data} />;
|
||||
}
|
||||
|
||||
const HostingSectionWithData = createSectionWithData(
|
||||
useHostingQuery,
|
||||
HostingSection,
|
||||
HostingSectionSkeleton,
|
||||
"Hosting",
|
||||
function HostingSectionContent({ domain }: { domain: string }) {
|
||||
const trpc = useTRPC();
|
||||
const { data } = useSuspenseQuery(
|
||||
trpc.domain.getHosting.queryOptions({ domain }),
|
||||
);
|
||||
return <HostingSection domain={domain} data={data} />;
|
||||
}
|
||||
|
||||
const DnsSectionWithData = createSectionWithData(
|
||||
useDnsQuery,
|
||||
DnsSection,
|
||||
DnsSectionSkeleton,
|
||||
"DNS",
|
||||
function DnsSectionContent({ domain }: { domain: string }) {
|
||||
const trpc = useTRPC();
|
||||
const { data } = useSuspenseQuery(
|
||||
trpc.domain.getDnsRecords.queryOptions({ domain }),
|
||||
);
|
||||
return <DnsSection domain={domain} data={data} />;
|
||||
}
|
||||
|
||||
const CertificatesSectionWithData = createSectionWithData(
|
||||
useCertificatesQuery,
|
||||
CertificatesSection,
|
||||
CertificatesSectionSkeleton,
|
||||
"Certificates",
|
||||
function CertificatesSectionContent({ domain }: { domain: string }) {
|
||||
const trpc = useTRPC();
|
||||
const { data } = useSuspenseQuery(
|
||||
trpc.domain.getCertificates.queryOptions({ domain }),
|
||||
);
|
||||
return <CertificatesSection domain={domain} data={data} />;
|
||||
}
|
||||
|
||||
const HeadersSectionWithData = createSectionWithData(
|
||||
useHeadersQuery,
|
||||
HeadersSection,
|
||||
HeadersSectionSkeleton,
|
||||
"Headers",
|
||||
function HeadersSectionContent({ domain }: { domain: string }) {
|
||||
const trpc = useTRPC();
|
||||
const { data } = useSuspenseQuery(
|
||||
trpc.domain.getHeaders.queryOptions({ domain }),
|
||||
);
|
||||
return <HeadersSection domain={domain} data={data} />;
|
||||
}
|
||||
|
||||
const SeoSectionWithData = createSectionWithData(
|
||||
useSeoQuery,
|
||||
SeoSection,
|
||||
SeoSectionSkeleton,
|
||||
"SEO",
|
||||
function SeoSectionContent({ domain }: { domain: string }) {
|
||||
const trpc = useTRPC();
|
||||
const { data } = useSuspenseQuery(
|
||||
trpc.domain.getSeo.queryOptions({ domain }),
|
||||
);
|
||||
return <SeoSection domain={domain} data={data} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inner content component - queries registration and conditionally shows sections.
|
||||
* This component suspends until registration data is ready.
|
||||
*/
|
||||
function DomainReportContent({ domain }: { domain: string }) {
|
||||
const { data: registration } = useRegistrationQuery(domain);
|
||||
const trpc = useTRPC();
|
||||
const { data: registration } = useSuspenseQuery(
|
||||
trpc.domain.getRegistration.queryOptions({ domain }),
|
||||
);
|
||||
|
||||
// Show unregistered state if confirmed unregistered
|
||||
const isConfirmedUnregistered =
|
||||
@@ -86,11 +88,8 @@ function DomainReportContent({ domain }: { domain: string }) {
|
||||
// Add to search history (only for registered domains)
|
||||
useDomainHistory(isConfirmedUnregistered ? "" : domain);
|
||||
|
||||
// Get memoized query keys for all sections
|
||||
const queryKeys = useDomainQueryKeys(domain);
|
||||
|
||||
// Track export state and get export handler
|
||||
const { handleExport, allDataLoaded } = useDomainExport(domain, queryKeys);
|
||||
const { handleExport, allDataLoaded } = useDomainExport(domain);
|
||||
|
||||
if (isConfirmedUnregistered) {
|
||||
return <DomainUnregisteredState domain={domain} />;
|
||||
@@ -105,12 +104,41 @@ function DomainReportContent({ domain }: { domain: string }) {
|
||||
/>
|
||||
|
||||
<div className="space-y-4">
|
||||
<RegistrationSectionWithData domain={domain} />
|
||||
<HostingSectionWithData domain={domain} />
|
||||
<DnsSectionWithData domain={domain} />
|
||||
<CertificatesSectionWithData domain={domain} />
|
||||
<HeadersSectionWithData domain={domain} />
|
||||
<SeoSectionWithData domain={domain} />
|
||||
<SectionErrorBoundary sectionName="Registration">
|
||||
<Suspense fallback={<RegistrationSectionSkeleton />}>
|
||||
<RegistrationSectionContent domain={domain} />
|
||||
</Suspense>
|
||||
</SectionErrorBoundary>
|
||||
|
||||
<SectionErrorBoundary sectionName="Hosting">
|
||||
<Suspense fallback={<HostingSectionSkeleton />}>
|
||||
<HostingSectionContent domain={domain} />
|
||||
</Suspense>
|
||||
</SectionErrorBoundary>
|
||||
|
||||
<SectionErrorBoundary sectionName="DNS">
|
||||
<Suspense fallback={<DnsSectionSkeleton />}>
|
||||
<DnsSectionContent domain={domain} />
|
||||
</Suspense>
|
||||
</SectionErrorBoundary>
|
||||
|
||||
<SectionErrorBoundary sectionName="Certificates">
|
||||
<Suspense fallback={<CertificatesSectionSkeleton />}>
|
||||
<CertificatesSectionContent domain={domain} />
|
||||
</Suspense>
|
||||
</SectionErrorBoundary>
|
||||
|
||||
<SectionErrorBoundary sectionName="Headers">
|
||||
<Suspense fallback={<HeadersSectionSkeleton />}>
|
||||
<HeadersSectionContent domain={domain} />
|
||||
</Suspense>
|
||||
</SectionErrorBoundary>
|
||||
|
||||
<SectionErrorBoundary sectionName="SEO">
|
||||
<Suspense fallback={<SeoSectionSkeleton />}>
|
||||
<SeoSectionContent domain={domain} />
|
||||
</Suspense>
|
||||
</SectionErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,27 +1,36 @@
|
||||
import { notifyManager, useQueryClient } from "@tanstack/react-query";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { useLogger } from "@/hooks/use-logger";
|
||||
import { analytics } from "@/lib/analytics/client";
|
||||
import { exportDomainData } from "@/lib/json-export";
|
||||
|
||||
type QueryKeys = {
|
||||
getRegistration: readonly unknown[];
|
||||
getDnsRecords: readonly unknown[];
|
||||
getHosting: readonly unknown[];
|
||||
getCertificates: readonly unknown[];
|
||||
getHeaders: readonly unknown[];
|
||||
getSeo: readonly unknown[];
|
||||
};
|
||||
import { useTRPC } from "@/lib/trpc/client";
|
||||
|
||||
/**
|
||||
* Hook to handle domain data export and track when all section data is loaded.
|
||||
* Subscribes to query cache updates and provides a handler to export all domain data.
|
||||
*/
|
||||
export function useDomainExport(domain: string, queryKeys: QueryKeys) {
|
||||
export function useDomainExport(domain: string) {
|
||||
const trpc = useTRPC();
|
||||
const queryClient = useQueryClient();
|
||||
const logger = useLogger({ component: "DomainExport" });
|
||||
const [allDataLoaded, setAllDataLoaded] = useState(false);
|
||||
|
||||
// Build query keys directly using tRPC's queryOptions
|
||||
const queryKeys = useMemo(
|
||||
() => ({
|
||||
registration: trpc.domain.getRegistration.queryOptions({ domain })
|
||||
.queryKey,
|
||||
dns: trpc.domain.getDnsRecords.queryOptions({ domain }).queryKey,
|
||||
hosting: trpc.domain.getHosting.queryOptions({ domain }).queryKey,
|
||||
certificates: trpc.domain.getCertificates.queryOptions({ domain })
|
||||
.queryKey,
|
||||
headers: trpc.domain.getHeaders.queryOptions({ domain }).queryKey,
|
||||
seo: trpc.domain.getSeo.queryOptions({ domain }).queryKey,
|
||||
}),
|
||||
[trpc, domain],
|
||||
);
|
||||
|
||||
const queryKeysRef = useRef(queryKeys);
|
||||
|
||||
// Update ref when queryKeys change
|
||||
@@ -63,17 +72,13 @@ export function useDomainExport(domain: string, queryKeys: QueryKeys) {
|
||||
analytics.track("export_json_clicked", { domain });
|
||||
|
||||
try {
|
||||
// Read data from cache using provided query keys
|
||||
const registrationData = queryClient.getQueryData(
|
||||
queryKeys.getRegistration,
|
||||
);
|
||||
const dnsData = queryClient.getQueryData(queryKeys.getDnsRecords);
|
||||
const hostingData = queryClient.getQueryData(queryKeys.getHosting);
|
||||
const certificatesData = queryClient.getQueryData(
|
||||
queryKeys.getCertificates,
|
||||
);
|
||||
const headersData = queryClient.getQueryData(queryKeys.getHeaders);
|
||||
const seoData = queryClient.getQueryData(queryKeys.getSeo);
|
||||
// Read data from cache using query keys
|
||||
const registrationData = queryClient.getQueryData(queryKeys.registration);
|
||||
const dnsData = queryClient.getQueryData(queryKeys.dns);
|
||||
const hostingData = queryClient.getQueryData(queryKeys.hosting);
|
||||
const certificatesData = queryClient.getQueryData(queryKeys.certificates);
|
||||
const headersData = queryClient.getQueryData(queryKeys.headers);
|
||||
const seoData = queryClient.getQueryData(queryKeys.seo);
|
||||
|
||||
// Aggregate into export format
|
||||
const exportData = {
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { useTRPC } from "@/lib/trpc/client";
|
||||
|
||||
/**
|
||||
* Modern Suspense-based data fetching.
|
||||
*
|
||||
* All queries use useSuspenseQuery - they suspend rendering until data is ready.
|
||||
* No isLoading states, no error states in components - Suspense and Error Boundaries handle everything.
|
||||
*
|
||||
* For conditional queries (secondary sections gated by registration), we use a wrapper
|
||||
* component pattern in the UI layer to conditionally render based on registration data.
|
||||
*/
|
||||
|
||||
export function useRegistrationQuery(domain: string) {
|
||||
const trpc = useTRPC();
|
||||
return useSuspenseQuery(trpc.domain.getRegistration.queryOptions({ domain }));
|
||||
}
|
||||
|
||||
export function useDnsQuery(domain: string) {
|
||||
const trpc = useTRPC();
|
||||
return useSuspenseQuery(trpc.domain.getDnsRecords.queryOptions({ domain }));
|
||||
}
|
||||
|
||||
export function useHostingQuery(domain: string) {
|
||||
const trpc = useTRPC();
|
||||
return useSuspenseQuery(trpc.domain.getHosting.queryOptions({ domain }));
|
||||
}
|
||||
|
||||
export function useCertificatesQuery(domain: string) {
|
||||
const trpc = useTRPC();
|
||||
return useSuspenseQuery(trpc.domain.getCertificates.queryOptions({ domain }));
|
||||
}
|
||||
|
||||
export function useHeadersQuery(domain: string) {
|
||||
const trpc = useTRPC();
|
||||
return useSuspenseQuery(trpc.domain.getHeaders.queryOptions({ domain }));
|
||||
}
|
||||
|
||||
export function useSeoQuery(domain: string) {
|
||||
const trpc = useTRPC();
|
||||
return useSuspenseQuery(trpc.domain.getSeo.queryOptions({ domain }));
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { useMemo } from "react";
|
||||
import { useTRPC } from "@/lib/trpc/client";
|
||||
|
||||
/**
|
||||
* Hook to generate memoized query keys for all domain sections.
|
||||
* Prevents repeated queryOptions calls and provides consistent keys.
|
||||
*/
|
||||
export function useDomainQueryKeys(domain: string) {
|
||||
const trpc = useTRPC();
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
getRegistration: trpc.domain.getRegistration.queryOptions({ domain })
|
||||
.queryKey,
|
||||
getDnsRecords: trpc.domain.getDnsRecords.queryOptions({ domain })
|
||||
.queryKey,
|
||||
getHosting: trpc.domain.getHosting.queryOptions({ domain }).queryKey,
|
||||
getCertificates: trpc.domain.getCertificates.queryOptions({ domain })
|
||||
.queryKey,
|
||||
getHeaders: trpc.domain.getHeaders.queryOptions({ domain }).queryKey,
|
||||
getSeo: trpc.domain.getSeo.queryOptions({ domain }).queryKey,
|
||||
getFavicon: trpc.domain.getFavicon.queryOptions({ domain }).queryKey,
|
||||
}),
|
||||
[trpc, domain],
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user