Add skeleton loading state for Search Performance page (#355)

This commit is contained in:
Ben Senescu 2026-07-05 18:53:56 -04:00 committed by GitHub
parent 7bcd8497a0
commit 053ac4c4cf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 62 additions and 12 deletions

View File

@ -0,0 +1,47 @@
// Skeleton loading state for the Search Performance (GSC) page. Mirrors the
// loaded layout — four totals cards over a tabbed table panel — so the shell
// stays put and only the data fills in, matching the other pages' loaders
// (e.g. DomainOverviewLoadingState, KeywordResearchLoadingState).
export function SearchPerformanceLoadingState() {
return (
<div className="space-y-4" aria-busy>
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
{Array.from({ length: 4 }).map((_, index) => (
<div
key={index}
className="rounded-lg border border-base-300 bg-base-100 p-4 space-y-2"
>
<div className="skeleton h-3 w-20" />
<div className="skeleton h-7 w-24" />
</div>
))}
</div>
<div className="overflow-hidden rounded-xl border border-base-300 bg-base-100">
<div className="flex flex-col gap-3 border-b border-base-300 px-4 py-3 lg:flex-row lg:items-center lg:justify-between">
<div className="flex items-center gap-4">
<div className="skeleton h-8 w-40" />
<div className="skeleton h-8 w-20" />
<div className="skeleton h-8 w-16" />
</div>
<div className="flex flex-wrap items-center gap-2">
<div className="skeleton h-8 w-36" />
<div className="skeleton h-8 w-36" />
<div className="skeleton h-8 w-36" />
</div>
</div>
<div className="space-y-3 p-4">
{Array.from({ length: 8 }).map((_, index) => (
<div key={index} className="grid grid-cols-5 gap-3">
<div className="skeleton col-span-2 h-4" />
<div className="skeleton h-4" />
<div className="skeleton h-4" />
<div className="skeleton h-4" />
</div>
))}
</div>
</div>
</div>
);
}

View File

@ -10,6 +10,7 @@ import { toast } from "sonner";
import { TableExportMenu } from "@/client/components/table/TableBulkActionBar"; import { TableExportMenu } from "@/client/components/table/TableBulkActionBar";
import { TablePagination } from "@/client/components/table/TablePagination"; import { TablePagination } from "@/client/components/table/TablePagination";
import { SearchConsoleConnectionCard } from "@/client/features/gsc/SearchConsoleConnectionCard"; import { SearchConsoleConnectionCard } from "@/client/features/gsc/SearchConsoleConnectionCard";
import { SearchPerformanceLoadingState } from "@/client/features/search-performance/SearchPerformanceLoadingState";
import { import {
DimensionTable, DimensionTable,
exportDimensionRows, exportDimensionRows,
@ -198,10 +199,7 @@ export function SearchPerformancePage({ projectId }: { projectId: string }) {
</div> </div>
{reportQuery.isPending ? ( {reportQuery.isPending ? (
<div className="flex items-center gap-2 p-8 text-sm text-base-content/60"> <SearchPerformanceLoadingState />
<Loader2 className="size-4 animate-spin" /> Loading Search Console
data
</div>
) : reportQuery.isError ? ( ) : reportQuery.isError ? (
<div className="alert alert-error"> <div className="alert alert-error">
<span className="text-sm"> <span className="text-sm">

View File

@ -32,25 +32,30 @@ export function AuthenticatedAppLayout({
React.useState(false); React.useState(false);
// On non-project pages (e.g. /settings) there's no projectId in the URL, so // On non-project pages (e.g. /settings) there's no projectId in the URL, so
// derive one for the nav/switcher: prefer the last-visited project, else the // derive one for the nav/switcher: prefer the last-visited project, else the
// most recent. Reading localStorage in an effect keeps SSR/first render stable. // most recent. The whole app tree is client-only (see root ClientOnly), so we
// can read localStorage synchronously during the first render — this lets the
// sidebar show the full project nav on the very first paint instead of briefly
// flashing only the always-visible Connect group while projects load.
const projectsQuery = useQuery({ const projectsQuery = useQuery({
queryKey: ["projects"], queryKey: ["projects"],
queryFn: () => getProjects(), queryFn: () => getProjects(),
enabled: !projectId, enabled: !projectId,
}); });
const [rememberedProjectId, setRememberedProjectId] = React.useState< const [rememberedProjectId] = React.useState<string | null>(() =>
string | null getLastProjectId(),
>(null); );
React.useEffect(() => {
setRememberedProjectId(getLastProjectId());
}, []);
const fallbackProjects = projectsQuery.data ?? []; const fallbackProjects = projectsQuery.data ?? [];
const fallbackProjectId = const fallbackProjectId =
fallbackProjects.find((project) => project.id === rememberedProjectId) fallbackProjects.find((project) => project.id === rememberedProjectId)
?.id ?? ?.id ??
fallbackProjects[0]?.id ?? fallbackProjects[0]?.id ??
null; null;
const sidebarProjectId = projectId ?? fallbackProjectId; // Once the projects list loads, fallbackProjectId is the validated choice
// (remembered-if-valid, else most recent). Before it loads, fall back to the
// remembered id so the project nav renders immediately; a stale id here only
// builds links that self-correct via the route guard once data arrives.
const sidebarProjectId =
projectId ?? fallbackProjectId ?? rememberedProjectId;
const shouldCheckSeoApiKeyStatus = location.pathname !== BILLING_ROUTE; const shouldCheckSeoApiKeyStatus = location.pathname !== BILLING_ROUTE;
const seoApiKeyStatusQuery = useQuery({ const seoApiKeyStatusQuery = useQuery({
queryKey: ["seoApiKeyStatus"], queryKey: ["seoApiKeyStatus"],