Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 13 additions & 10 deletions app/courses/create/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -35,7 +38,7 @@ export default function CreateCourse() {
router.push("/courses")
}

if (status === "loading" || isLoading) {
if (status === "loading" || isRedirecting || !mounted) {
return (
<div className="flex h-screen w-full items-center justify-center bg-background-dark">
<div className="flex flex-col items-center gap-4">
Expand Down
47 changes: 21 additions & 26 deletions app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,48 +60,43 @@ export default function Dashboard() {
const { data: session, status } = useSession()
const router = useRouter()
const { t } = useLanguage()
const [userStats, setUserStats] = useState<UserStats>(defaultStats)
const [userData, setUserData] = useState<UserData>(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"
Expand All @@ -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 (
<div className="flex h-screen w-full items-center justify-center bg-background-dark">
<div className="flex flex-col items-center gap-4">
Expand Down Expand Up @@ -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"}
>
<span className={`material-symbols-outlined ${!mounted ? 'invisible' : ''}`}>
<span className={`material-symbols-outlined ${!themeMounted ? 'invisible' : ''}`}>
{theme === 'dark' ? 'light_mode' : 'dark_mode'}
</span>
</button>
Expand Down
32 changes: 16 additions & 16 deletions app/quizzes/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,31 +14,31 @@ export default function MyQuizzes() {
const { data: session, status } = useSession()
const router = useRouter()
const { t } = useLanguage()
const [quizzes, setQuizzes] = useState<SavedQuiz[]>([])
const [isLoading, setIsLoading] = useState(true)
const [quizzesState, setQuizzesState] = useState<SavedQuiz[] | null>(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())
}
}

Expand All @@ -52,7 +52,7 @@ export default function MyQuizzes() {
})
}

if (status === "loading" || isLoading) {
if (status === "loading" || isRedirecting || !mounted) {
return (
<div className="flex h-screen w-full items-center justify-center bg-background-dark">
<div className="flex flex-col items-center gap-4">
Expand Down
23 changes: 13 additions & 10 deletions app/schedule/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className="flex h-screen w-full items-center justify-center bg-background-dark">
<div className="flex flex-col items-center gap-4">
Expand Down
29 changes: 19 additions & 10 deletions components/ThemeProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,32 @@ interface ThemeContextType {
const ThemeContext = createContext<ThemeContextType | undefined>(undefined)

export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setThemeState] = useState<Theme>("dark")
const [themeState, setThemeState] = useState<Theme | null>(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;
Comment on lines +17 to +41
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Theme toggle needs double-click 🐞 Bug ≡ Correctness

ThemeProvider now derives theme from localStorage while themeState starts as null, but
toggleTheme() still toggles based on prev state. When the derived theme is "dark" and
themeState is null, the first toggle sets state to "dark" again so the UI doesn't switch until a
second click.
Agent Prompt
## Issue description
`toggleTheme()` currently toggles from `themeState` (which may be `null`), while the displayed `theme` is derived from localStorage/system preference. This causes the first toggle to be a no-op when the derived theme is `dark`.

## Issue Context
- `themeState` is initialized to `null` and `theme` is derived during render.
- `toggleTheme` uses `setThemeState(prev => ...)`, so `prev=null` produces `'dark'`.

## Fix Focus Areas
- components/ThemeProvider.tsx[16-59]

## Suggested fix
Update `toggleTheme` to toggle based on the currently-derived `theme` (from context/render), e.g. `setThemeState(theme === 'dark' ? 'light' : 'dark')`, or initialize `themeState` to the derived theme once mounted so `prev` is never `null`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


// Apply theme class to document
useEffect(() => {
if (!mounted) return
Expand Down
13 changes: 7 additions & 6 deletions lib/i18n.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -823,17 +823,18 @@ const translations: Record<string, Record<string, string>> = {
// ... (translations object remains unchanged) ...

export function LanguageProvider({ children }: { children: React.ReactNode }) {
const [language, setLanguageState] = useState<Language>("English")
const [languageState, setLanguageState] = useState<Language | null>(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
Expand Down