1
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:
2025-11-27 11:50:05 -05:00
parent 766cb4b493
commit 95f885930d
7 changed files with 113 additions and 709 deletions

View File

@@ -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" });
});
});

View File

@@ -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;
}

View File

@@ -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();
});
});
});

View File

@@ -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>
);

View File

@@ -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 = {

View File

@@ -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 }));
}

View File

@@ -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],
);
}