diff --git a/app/courses/create/page.tsx b/app/courses/create/page.tsx index 16ba554..28e4fc1 100644 --- a/app/courses/create/page.tsx +++ b/app/courses/create/page.tsx @@ -10,23 +10,26 @@ import Link from "next/link" export default function CreateCourse() { const { data: session, status } = useSession() const router = useRouter() - const [isLoading, setIsLoading] = useState(true) + const [mounted, setMounted] = useState(false) const [courseName, setCourseName] = useState("") const [subject, setSubject] = useState("") const [description, setDescription] = useState("") + useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect + setMounted(true) + }, []) + + const email = session?.user?.email; + const isRedirecting = status === "unauthenticated" || (status === "authenticated" && !!email && !isOnboardingComplete(email)) + useEffect(() => { if (status === "unauthenticated") { router.push("/login") - } else if (status === "authenticated" && session?.user?.email) { - const email = session?.user?.email; - if (email && !isOnboardingComplete(email)) { - router.push("/onboarding") - } else { - setIsLoading(false) - } + } else if (status === "authenticated" && email && !isOnboardingComplete(email)) { + router.push("/onboarding") } - }, [status, session, router]) + }, [status, email, router]) const handleSubmit = (e: React.FormEvent) => { e.preventDefault() @@ -35,7 +38,7 @@ export default function CreateCourse() { router.push("/courses") } - if (status === "loading" || isLoading) { + if (status === "loading" || isRedirecting || !mounted) { return (
diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index e3de948..ff07210 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -60,48 +60,43 @@ 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 { theme, toggleTheme, mounted: themeMounted } = useTheme() + const [mounted, setMounted] = useState(false) + + useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect + setMounted(true) + }, []) + + const email = session?.user?.email; + const isRedirecting = status === "unauthenticated" || (status === "authenticated" && !!email && !isOnboardingComplete(email)) // Check authentication and onboarding useEffect(() => { if (status === "unauthenticated") { router.push("/login") - return - } - - if (status === "authenticated" && session?.user?.email) { - // Check if user has completed onboarding - const email = session?.user?.email; - if (email && !isOnboardingComplete(email)) { - router.push("/onboarding") - return - } - + } else if (status === "authenticated" && email && !isOnboardingComplete(email)) { + router.push("/onboarding") + } else if (status === "authenticated" && email) { // 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)) { + if (!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]) + }, [status, email, router]) + + const profile = mounted ? getUserProfile() : null + const userStats = profile?.stats || defaultStats // Get display name from session or profile const displayName = session?.user?.name?.split(" ")[0] || "Learner" @@ -111,7 +106,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" || isRedirecting || !mounted) { return (
@@ -170,7 +165,7 @@ export default function Dashboard() { title={theme === 'dark' ? "Switch to Light Mode" : "Switch to Dark Mode"} aria-label={theme === 'dark' ? "Switch to Light Mode" : "Switch to Dark Mode"} > - + {theme === 'dark' ? 'light_mode' : 'dark_mode'} diff --git a/app/quizzes/page.tsx b/app/quizzes/page.tsx index b2bfe73..c992bba 100644 --- a/app/quizzes/page.tsx +++ b/app/quizzes/page.tsx @@ -14,31 +14,31 @@ export default function MyQuizzes() { const { data: session, status } = useSession() const router = useRouter() const { t } = useLanguage() - const [quizzes, setQuizzes] = useState([]) - const [isLoading, setIsLoading] = useState(true) + const [quizzesState, setQuizzesState] = useState(null) + const [mounted, setMounted] = useState(false) + + useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect + setMounted(true) + }, []) + + const email = session?.user?.email; + const isRedirecting = status === "unauthenticated" || (status === "authenticated" && !!email && !isOnboardingComplete(email)) useEffect(() => { if (status === "unauthenticated") { router.push("/login") - return + } else if (status === "authenticated" && email && !isOnboardingComplete(email)) { + router.push("/onboarding") } + }, [status, email, router]) - if (status === "authenticated" && session?.user?.email) { - const email = session?.user?.email; - if (email && !isOnboardingComplete(email)) { - router.push("/onboarding") - return - } - - setQuizzes(getUserQuizzes()) - setIsLoading(false) - } - }, [status, session, router]) + const quizzes = mounted ? (quizzesState !== null ? quizzesState : getUserQuizzes()) : [] const handleDelete = (id: string) => { if (confirm(t("quizzes.delete_confirm"))) { deleteQuiz(id) - setQuizzes(getUserQuizzes()) + setQuizzesState(getUserQuizzes()) } } @@ -52,7 +52,7 @@ export default function MyQuizzes() { }) } - if (status === "loading" || isLoading) { + if (status === "loading" || isRedirecting || !mounted) { return (
diff --git a/app/schedule/page.tsx b/app/schedule/page.tsx index e563699..de28b3d 100644 --- a/app/schedule/page.tsx +++ b/app/schedule/page.tsx @@ -10,22 +10,25 @@ import { isOnboardingComplete } from "@/lib/userStore" export default function Schedule() { const { data: session, status } = useSession() const router = useRouter() - const [isLoading, setIsLoading] = useState(true) + const [mounted, setMounted] = useState(false) + + useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect + setMounted(true) + }, []) + + const email = session?.user?.email; + const isRedirecting = status === "unauthenticated" || (status === "authenticated" && !!email && !isOnboardingComplete(email)) useEffect(() => { if (status === "unauthenticated") { router.push("/login") - } else if (status === "authenticated" && session?.user?.email) { - const email = session?.user?.email; - if (email && !isOnboardingComplete(email)) { - router.push("/onboarding") - } else { - setIsLoading(false) - } + } else if (status === "authenticated" && email && !isOnboardingComplete(email)) { + router.push("/onboarding") } - }, [status, session, router]) + }, [status, email, router]) - if (status === "loading" || isLoading) { + if (status === "loading" || isRedirecting || !mounted) { return (
diff --git a/components/ThemeProvider.tsx b/components/ThemeProvider.tsx index 6f75f31..e83e171 100644 --- a/components/ThemeProvider.tsx +++ b/components/ThemeProvider.tsx @@ -14,23 +14,32 @@ interface ThemeContextType { const ThemeContext = createContext(undefined) export function ThemeProvider({ children }: { children: React.ReactNode }) { - const [theme, setThemeState] = useState("dark") + const [themeState, setThemeState] = useState(null) const [mounted, setMounted] = useState(false) // Initialize theme from localStorage or system preference useEffect(() => { - const savedTheme = localStorage.getItem("theme") as Theme | null - if (savedTheme) { - setThemeState(savedTheme) - } else if (window.matchMedia("(prefers-color-scheme: dark)").matches) { - setThemeState("dark") - } else { - setThemeState("light") - } + // eslint-disable-next-line react-hooks/set-state-in-effect setMounted(true) - // eslint-disable-next-line react-hooks/exhaustive-deps }, []) + let derivedTheme: Theme = "dark" + if (mounted) { + if (themeState) { + derivedTheme = themeState + } else { + const savedTheme = localStorage.getItem("theme") as Theme | null + if (savedTheme) { + derivedTheme = savedTheme + } else if (window.matchMedia("(prefers-color-scheme: dark)").matches) { + derivedTheme = "dark" + } else { + derivedTheme = "light" + } + } + } + const theme = derivedTheme; + // Apply theme class to document useEffect(() => { if (!mounted) return diff --git a/lib/i18n.tsx b/lib/i18n.tsx index 3ebccd5..b6a8095 100644 --- a/lib/i18n.tsx +++ b/lib/i18n.tsx @@ -823,17 +823,18 @@ const translations: Record> = { // ... (translations object remains unchanged) ... export function LanguageProvider({ children }: { children: React.ReactNode }) { - const [language, setLanguageState] = useState("English") + const [languageState, setLanguageState] = useState(null) + const [mounted, setMounted] = useState(false) // Load from storage on mount useEffect(() => { - const profile = getUserProfile() - if (profile?.language) { - setLanguageState(profile.language) - } - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/set-state-in-effect + setMounted(true) }, []) + const profile = mounted ? getUserProfile() : null + const language = languageState || profile?.language || "English" + const setLanguage = (lang: Language) => { setLanguageState(lang) // Persistence is handled by the calling component (Profile page) or we could move it here