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"],