From 053ac4c4cfd32b538b6ecd7931a48a48c0cc9873 Mon Sep 17 00:00:00 2001
From: Ben Senescu <44480372+bensenescu@users.noreply.github.com>
Date: Sun, 5 Jul 2026 18:53:56 -0400
Subject: [PATCH] Add skeleton loading state for Search Performance page (#355)
---
.../SearchPerformanceLoadingState.tsx | 47 +++++++++++++++++++
.../SearchPerformancePage.tsx | 6 +--
src/client/layout/AppShell.tsx | 21 +++++----
3 files changed, 62 insertions(+), 12 deletions(-)
create mode 100644 src/client/features/search-performance/SearchPerformanceLoadingState.tsx
diff --git a/src/client/features/search-performance/SearchPerformanceLoadingState.tsx b/src/client/features/search-performance/SearchPerformanceLoadingState.tsx
new file mode 100644
index 0000000..df42e75
--- /dev/null
+++ b/src/client/features/search-performance/SearchPerformanceLoadingState.tsx
@@ -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 (
+
+
+ {Array.from({ length: 4 }).map((_, index) => (
+
+ ))}
+
+
+
+
+
+
+ {Array.from({ length: 8 }).map((_, index) => (
+
+ ))}
+
+
+
+ );
+}
diff --git a/src/client/features/search-performance/SearchPerformancePage.tsx b/src/client/features/search-performance/SearchPerformancePage.tsx
index 0535fac..d082789 100644
--- a/src/client/features/search-performance/SearchPerformancePage.tsx
+++ b/src/client/features/search-performance/SearchPerformancePage.tsx
@@ -10,6 +10,7 @@ import { toast } from "sonner";
import { TableExportMenu } from "@/client/components/table/TableBulkActionBar";
import { TablePagination } from "@/client/components/table/TablePagination";
import { SearchConsoleConnectionCard } from "@/client/features/gsc/SearchConsoleConnectionCard";
+import { SearchPerformanceLoadingState } from "@/client/features/search-performance/SearchPerformanceLoadingState";
import {
DimensionTable,
exportDimensionRows,
@@ -198,10 +199,7 @@ export function SearchPerformancePage({ projectId }: { projectId: string }) {
{reportQuery.isPending ? (
-
- Loading Search Console
- data…
-
+
) : reportQuery.isError ? (
diff --git a/src/client/layout/AppShell.tsx b/src/client/layout/AppShell.tsx
index a6e1736..f4231f9 100644
--- a/src/client/layout/AppShell.tsx
+++ b/src/client/layout/AppShell.tsx
@@ -32,25 +32,30 @@ export function AuthenticatedAppLayout({
React.useState(false);
// 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
- // 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({
queryKey: ["projects"],
queryFn: () => getProjects(),
enabled: !projectId,
});
- const [rememberedProjectId, setRememberedProjectId] = React.useState<
- string | null
- >(null);
- React.useEffect(() => {
- setRememberedProjectId(getLastProjectId());
- }, []);
+ const [rememberedProjectId] = React.useState(() =>
+ getLastProjectId(),
+ );
const fallbackProjects = projectsQuery.data ?? [];
const fallbackProjectId =
fallbackProjects.find((project) => project.id === rememberedProjectId)
?.id ??
fallbackProjects[0]?.id ??
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 seoApiKeyStatusQuery = useQuery({
queryKey: ["seoApiKeyStatus"],