Add skeleton loading state for Search Performance page (#355)
This commit is contained in:
parent
7bcd8497a0
commit
053ac4c4cf
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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">
|
||||||
|
|||||||
@ -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"],
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user