diff --git a/app/courses/create/page.tsx b/app/courses/create/page.tsx index 16ba554..5c5880f 100644 --- a/app/courses/create/page.tsx +++ b/app/courses/create/page.tsx @@ -10,10 +10,15 @@ import Link from "next/link" export default function CreateCourse() { const { data: session, status } = useSession() const router = useRouter() - const [isLoading, setIsLoading] = useState(true) const [courseName, setCourseName] = useState("") const [subject, setSubject] = useState("") const [description, setDescription] = useState("") + const [isMounted, setIsMounted] = useState(false) + + useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect + setIsMounted(true) + }, []) useEffect(() => { if (status === "unauthenticated") { @@ -22,12 +27,13 @@ export default function CreateCourse() { const email = session?.user?.email; if (email && !isOnboardingComplete(email)) { router.push("/onboarding") - } else { - setIsLoading(false) } } }, [status, session, router]) + const isRedirecting = status === "unauthenticated" || + (status === "authenticated" && session?.user?.email && !isOnboardingComplete(session.user.email)) + const handleSubmit = (e: React.FormEvent) => { e.preventDefault() // In a real app, this would save to a database @@ -35,7 +41,7 @@ export default function CreateCourse() { router.push("/courses") } - if (status === "loading" || isLoading) { + if (status === "loading" || !isMounted || isRedirecting) { return (
diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index e3de948..4d40d71 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -60,12 +60,17 @@ export default function Dashboard() { const { data: session, status } = useSession() const router = useRouter() const { t } = useLanguage() - const [userStats, setUserStats] = useState(defaultStats) const [userData, setUserData] = useState(emptyUserData) - const [isLoading, setIsLoading] = useState(true) const [showTutorial, setShowTutorial] = useState(false) const [activityPeriod, setActivityPeriod] = useState("week") const { theme, toggleTheme, mounted } = useTheme() + const [isClientMounted, setIsClientMounted] = useState(false) + + // Track client hydration + useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect + setIsClientMounted(true) + }, []) // Check authentication and onboarding useEffect(() => { @@ -75,8 +80,7 @@ export default function Dashboard() { } if (status === "authenticated" && session?.user?.email) { - // Check if user has completed onboarding - const email = session?.user?.email; + const email = session.user.email; if (email && !isOnboardingComplete(email)) { router.push("/onboarding") return @@ -85,24 +89,24 @@ export default function Dashboard() { // Record today's activity recordActivity() - // Load user stats - const profile = getUserProfile() - if (profile?.stats) { - setUserStats(profile.stats) - } - - // Check if tutorial should be shown if (email && !isTutorialComplete(email)) { + // eslint-disable-next-line react-hooks/set-state-in-effect setShowTutorial(true) } // Load user data (empty for new users) setUserData(emptyUserData) - setIsLoading(false) } }, [status, session, router]) + // Compute redirect state for render short-circuiting + const isRedirecting = status === "unauthenticated" || + (status === "authenticated" && session?.user?.email && !isOnboardingComplete(session.user.email)) + + // Derive stats directly instead of using state to prevent set-state-in-effect + const userStats = isClientMounted ? (getUserProfile()?.stats || defaultStats) : defaultStats + // Get display name from session or profile const displayName = session?.user?.name?.split(" ")[0] || "Learner" @@ -111,7 +115,7 @@ export default function Dashboard() { const hasActivity = userData.weeklyActivity.some(v => v > 0) const hasEvents = userData.upcomingEvents.length > 0 - if (status === "loading" || isLoading) { + if (status === "loading" || !isClientMounted || isRedirecting) { return (
diff --git a/app/layout.tsx b/app/layout.tsx index 6d40965..1e362ad 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -38,6 +38,7 @@ export default function RootLayout({ /> + {/* eslint-disable-next-line @next/next/no-page-custom-font */} diff --git a/app/quizzes/page.tsx b/app/quizzes/page.tsx index b2bfe73..ffef1d8 100644 --- a/app/quizzes/page.tsx +++ b/app/quizzes/page.tsx @@ -15,7 +15,12 @@ export default function MyQuizzes() { const router = useRouter() const { t } = useLanguage() const [quizzes, setQuizzes] = useState([]) - const [isLoading, setIsLoading] = useState(true) + const [isMounted, setIsMounted] = useState(false) + + useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect + setIsMounted(true) + }, []) useEffect(() => { if (status === "unauthenticated") { @@ -29,11 +34,20 @@ export default function MyQuizzes() { router.push("/onboarding") return } + } + }, [status, session, router]) + const isRedirecting = status === "unauthenticated" || + (status === "authenticated" && session?.user?.email && !isOnboardingComplete(session.user.email)) + + // Load quizzes + useEffect(() => { + if (status === "authenticated" && session?.user?.email && !isRedirecting) { + // eslint-disable-next-line react-hooks/set-state-in-effect setQuizzes(getUserQuizzes()) - setIsLoading(false) } - }, [status, session, router]) + }, [status, session, isRedirecting]) + const handleDelete = (id: string) => { if (confirm(t("quizzes.delete_confirm"))) { @@ -52,7 +66,7 @@ export default function MyQuizzes() { }) } - if (status === "loading" || isLoading) { + if (status === "loading" || !isMounted || isRedirecting) { return (
diff --git a/app/schedule/page.tsx b/app/schedule/page.tsx index e563699..72b5374 100644 --- a/app/schedule/page.tsx +++ b/app/schedule/page.tsx @@ -10,7 +10,12 @@ import { isOnboardingComplete } from "@/lib/userStore" export default function Schedule() { const { data: session, status } = useSession() const router = useRouter() - const [isLoading, setIsLoading] = useState(true) + const [isMounted, setIsMounted] = useState(false) + + useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect + setIsMounted(true) + }, []) useEffect(() => { if (status === "unauthenticated") { @@ -19,13 +24,14 @@ export default function Schedule() { const email = session?.user?.email; if (email && !isOnboardingComplete(email)) { router.push("/onboarding") - } else { - setIsLoading(false) } } }, [status, session, router]) - if (status === "loading" || isLoading) { + const isRedirecting = status === "unauthenticated" || + (status === "authenticated" && session?.user?.email && !isOnboardingComplete(session.user.email)) + + if (status === "loading" || !isMounted || isRedirecting) { return (
diff --git a/lib/auth.ts b/lib/auth.ts index a8df71c..dcb7de3 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -3,7 +3,7 @@ import NextAuth from "next-auth" import Google from "next-auth/providers/google" import GitHub from "next-auth/providers/github" -// @ts-ignore +// @ts-expect-error - Expected due to missing or mismatched types from NextAuth beta export const { handlers, signIn, signOut, auth } = NextAuth({ providers: [ Google({ diff --git a/lib/security.test.ts b/lib/security.test.ts index bcf78eb..d8d602c 100644 --- a/lib/security.test.ts +++ b/lib/security.test.ts @@ -63,7 +63,7 @@ describe('security lib', () => { // Passing something that isn't a string might trigger an error if not handled // although verifyData expects a string type. - // @ts-ignore + // @ts-expect-error - Testing invalid runtime type input for verifyData const isValid = verifyData(testData, { not: 'a string' }); assert.strictEqual(isValid, false); }); diff --git a/lib/setupTests.ts b/lib/setupTests.ts index 5c58739..00a7379 100644 --- a/lib/setupTests.ts +++ b/lib/setupTests.ts @@ -32,13 +32,13 @@ export class MockStorage { export function setupTests() { if (typeof global.window === "undefined") { - // @ts-ignore - global.window = {}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (global as any).window = {}; } if (typeof global.localStorage === "undefined") { - // @ts-ignore - global.localStorage = new MockStorage(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (global as any).localStorage = new MockStorage(); } }