From 6d0c0d39ebea6549fa65392b743f136e617eb5c7 Mon Sep 17 00:00:00 2001 From: devjayy43 Date: Thu, 22 Oct 2026 10:54:17 +0100 Subject: [PATCH] fix: stabilize IntersectionObserver threshold, add /settings to auth guard, show skeletons before auth ready - use-intersection-observer: derive a memoized threshold from its serialized key so the observer only rebuilds when the value changes (#512, #513) - middleware: protect /settings and /settings/:path* (#515) - dashboard, credentials, rewards: render skeletons instead of null while auth is pending (#514) Co-Authored-By: Claude Haiku 5.5 --- src/app/credentials/page.tsx | 8 +++++++- src/app/dashboard/page.tsx | 8 +++++++- src/app/rewards/page.tsx | 8 +++++++- src/lib/hooks/use-intersection-observer.ts | 10 +++++++--- src/middleware.ts | 2 ++ 5 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/app/credentials/page.tsx b/src/app/credentials/page.tsx index 35103d3..c69908a 100644 --- a/src/app/credentials/page.tsx +++ b/src/app/credentials/page.tsx @@ -79,7 +79,13 @@ export default function CredentialsPage() { return sorted; }, [credentials, courseId, debouncedSearch, sort]); - if (!ready) return null; + if (!ready) { + return ( +
+ +
+ ); + } if (loading) { return ( diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 1654d59..47a7364 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -75,7 +75,13 @@ export default function DashboardPage() { const { credentials, loading: credentialsLoading, error: credentialsError } = useCredentials(); const progress = useCourseStore((s) => s.progress); - if (!ready) return null; + if (!ready) { + return ( +
+ +
+ ); + } const isLoading = coursesLoading || rewardsLoading || credentialsLoading || recommendedLoading; diff --git a/src/app/rewards/page.tsx b/src/app/rewards/page.tsx index 4f1d780..96bf3a4 100644 --- a/src/app/rewards/page.tsx +++ b/src/app/rewards/page.tsx @@ -35,7 +35,13 @@ export default function RewardsPage() { } }; - if (!ready) return null; + if (!ready) { + return ( +
+ +
+ ); + } if (loading) { return ( diff --git a/src/lib/hooks/use-intersection-observer.ts b/src/lib/hooks/use-intersection-observer.ts index 5b4995e..0c3fd5d 100644 --- a/src/lib/hooks/use-intersection-observer.ts +++ b/src/lib/hooks/use-intersection-observer.ts @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState, RefObject } from "react"; +import { useEffect, useMemo, useState, RefObject } from "react"; export interface UseIntersectionObserverOptions { threshold?: number | number[]; @@ -35,6 +35,10 @@ export function useIntersectionObserver( const isIntersecting = !!entry?.isIntersecting; const frozen = isIntersecting && freezeOnceVisible; + // Keyed on the serialized value so callers passing a new array literal each render don't rebuild the observer. + const thresholdKey = JSON.stringify(threshold); + const stableThreshold = useMemo(() => JSON.parse(thresholdKey), [thresholdKey]); + useEffect(() => { const node = elementRef?.current; if (!node || frozen || typeof window === "undefined" || !("IntersectionObserver" in window)) { @@ -45,7 +49,7 @@ export function useIntersectionObserver( ([newEntry]) => { setEntry(newEntry); }, - { threshold, root, rootMargin } + { threshold: stableThreshold, root, rootMargin } ); observer.observe(node); @@ -53,7 +57,7 @@ export function useIntersectionObserver( return () => { observer.disconnect(); }; - }, [elementRef, JSON.stringify(threshold), root, rootMargin, frozen]); + }, [elementRef, stableThreshold, root, rootMargin, frozen]); return { isIntersecting, diff --git a/src/middleware.ts b/src/middleware.ts index 355483c..d8794f8 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -10,6 +10,7 @@ const PROTECTED_PREFIXES = [ "/dashboard", "/rewards", "/credentials", + "/settings", "/courses/", // individual course pages, modules, quiz — NOT /courses list ]; @@ -89,6 +90,7 @@ export const config = { "/dashboard/:path*", "/rewards/:path*", "/credentials/:path*", + "/settings/:path*", "/courses/:courseId/:path*", // individual course + sub-pages only "/connect", "/onboarding",