| import { useEffect, useState } from 'react'; |
| import { motion, AnimatePresence } from 'motion/react'; |
| import { Loader2, Coins, ShoppingCart, ShieldCheck } from 'lucide-react'; |
| import Sidebar from './components/Sidebar'; |
| import CodingSection from './components/CodingSection'; |
| import Compiler from './components/Compiler'; |
| import AptitudeSection from './components/AptitudeSection'; |
| import AptitudeCategoryPage from './components/AptitudeCategoryPage'; |
| import AptitudeContentPage from './components/AptitudeContentPage'; |
| import SolutionView from './components/SolutionView'; |
| import CodingSheet from './components/CodingSheet'; |
| import AuthPage from './components/AuthPage'; |
| import LandingPage, { type LandingAuthMode } from './components/LandingPage'; |
| import SystemDesignSection from './components/SystemDesignSection'; |
| import CSFundamentalsSection from './components/CSFundamentalsSection'; |
| import LearningPathsPage from './components/LearningPathsPage'; |
| |
| import AITeacherPage from './components/AITeacherPage'; |
| import ArenaHubSection from './components/ArenaHubSection'; |
| import ArenaLobby from './components/ArenaLobby'; |
| import ArenaBattle from './components/ArenaBattle'; |
| import ProfileSection from './components/ProfileSectionNew'; |
| import SettingsPage from './components/SettingsPage'; |
| import Dashboard from './components/Dashboard'; |
| import Playground from './components/Playground'; |
| import ShoppingPage from './components/ShoppingPage'; |
| import WeeklyContestPage from './components/WeeklyContestPage'; |
| import DailyContestPage from './components/DailyContestPage'; |
| import CompilerPage from './pages/CompilerPage'; |
| import FullLeaderboardPage from './components/FullLeaderboardPage'; |
| import DBMSPage from './components/DBMSPage'; |
| import DBMSContentPage from './components/DBMSContentPage'; |
| import CNPage from './components/CNPage'; |
| import CNContentPage from './components/CNContentPage'; |
| import OSPage from './components/OSPage'; |
| import OSContentPage from './components/OSContentPage'; |
| import CoinHistoryPage from './components/CoinHistoryPage'; |
| import QuestsPage from './components/QuestsPage'; |
| import QuestDetailPage from './components/QuestDetailPage'; |
| import { ALL_QUESTS } from './data/questData'; |
| import { |
| AUTH_TOKEN_KEY, |
| addCoins, |
| fetchSessionUser, |
| fetchUserProgress, |
| importUserProgress, |
| recordSolvedQuestion, |
| spendCoins, |
| type AuthUser, |
| type ProgressLeaderboardEntry, |
| type UserProgressStats, |
| } from './lib/authClient'; |
| import { |
| clearAnalyticsEvents, |
| createDefaultPreferences, |
| fontSizeToRootRem, |
| loadUserPreferences, |
| playUiSound, |
| saveUserPreferences, |
| sendBrowserNotification, |
| trackAnalyticsEvent, |
| type UserPreferences, |
| userScopedStorageKey, |
| } from './lib/preferences'; |
| import { type CodingQuestion, codingQuestions } from './data/codingQuestions'; |
|
|
| const ROUTE_TO_SECTION: Record<string, string> = { |
| '/compiler': 'compiler', |
| }; |
|
|
| const SECTION_TO_ROUTE: Record<string, string> = { |
| compiler: '/compiler', |
| }; |
|
|
| function getInitialSectionFromPath() { |
| if (typeof window === 'undefined') { |
| return 'dashboard'; |
| } |
|
|
| return ROUTE_TO_SECTION[window.location.pathname] ?? 'dashboard'; |
| } |
|
|
| export default function App() { |
| return <AppContent />; |
| } |
|
|
| function AppContent() { |
| const [user, setUser] = useState<AuthUser | null>(null); |
| const [isAuthReady, setIsAuthReady] = useState(false); |
| const [isProgressReady, setIsProgressReady] = useState(false); |
| const [showLanding, setShowLanding] = useState(true); |
| const [authMode, setAuthMode] = useState<LandingAuthMode>('login'); |
| const [activeSection, setActiveSection] = useState(getInitialSectionFromPath); |
| const [activeAptitudeCategory, setActiveAptitudeCategory] = useState<string | null>(null); |
| const [activeAptitudeTopicNo, setActiveAptitudeTopicNo] = useState<number | null>(null); |
| const [activeAptitudeMode, setActiveAptitudeMode] = useState<'study' | 'concept' | 'revision'>('study'); |
| const [selectedQuestion, setSelectedQuestion] = useState<CodingQuestion | null>(null); |
| const [viewMode, setViewMode] = useState<'code' | 'solution'>('code'); |
| const [solvedQuestionIds, setSolvedQuestionIds] = useState<string[]>([]); |
| const [dailyActivity, setDailyActivity] = useState<Record<string, number>>({}); |
| const [dailyActivityBreakdown, setDailyActivityBreakdown] = useState<Record<string, Record<string, number>>>({}); |
| const [leaderboard, setLeaderboard] = useState<ProgressLeaderboardEntry[]>([]); |
| const [codingLeaderboard, setCodingLeaderboard] = useState<ProgressLeaderboardEntry[]>([]); |
| const [codingSolvedCount, setCodingSolvedCount] = useState<number>(0); |
| const [arenaQuestion, setArenaQuestion] = useState<CodingQuestion | null>(null); |
| const [arenaDifficulty, setArenaDifficulty] = useState<'Easy' | 'Medium' | 'Hard'>('Medium'); |
| const [preferences, setPreferences] = useState<UserPreferences>(() => { |
| if (typeof window !== 'undefined' && localStorage.getItem('theme_mode') === 'light') { |
| return createDefaultPreferences('light'); |
| } |
| return createDefaultPreferences('dark'); |
| }); |
| const [isTwoFactorVerified, setIsTwoFactorVerified] = useState(true); |
| const [twoFactorCode, setTwoFactorCode] = useState(''); |
| const [twoFactorError, setTwoFactorError] = useState(''); |
| const [selectedDBMSTopicNo, setSelectedDBMSTopicNo] = useState<number | null>(null); |
| const [selectedCNTopicNo, setSelectedCNTopicNo] = useState<number | null>(null); |
| const [selectedOSSectionNo, setSelectedOSSectionNo] = useState<number | null>(null); |
| const [activeQuestId, setActiveQuestId] = useState<string | null>(null); |
|
|
| useEffect(() => { |
| if (typeof window === 'undefined') { |
| return; |
| } |
|
|
| if (preferences.themeMode === 'light') { |
| document.documentElement.classList.add('day-mode'); |
| localStorage.setItem('theme_mode', 'light'); |
| } else { |
| document.documentElement.classList.remove('day-mode'); |
| localStorage.setItem('theme_mode', 'dark'); |
| } |
|
|
| document.documentElement.style.fontSize = fontSizeToRootRem(preferences.fontSize); |
| document.documentElement.lang = interfaceLanguageToHtmlLang(preferences.language); |
| }, [preferences.fontSize, preferences.language, preferences.themeMode]); |
|
|
| useEffect(() => { |
| if (typeof window === 'undefined') { |
| return; |
| } |
|
|
| const route = SECTION_TO_ROUTE[activeSection]; |
|
|
| if (route && window.location.pathname !== route) { |
| window.history.pushState({}, '', route); |
| return; |
| } |
|
|
| if (!route && ROUTE_TO_SECTION[window.location.pathname]) { |
| window.history.pushState({}, '', '/'); |
| } |
| }, [activeSection]); |
|
|
| useEffect(() => { |
| if (typeof window === 'undefined') { |
| return; |
| } |
|
|
| const handlePopState = () => { |
| setActiveSection(ROUTE_TO_SECTION[window.location.pathname] ?? 'dashboard'); |
| }; |
|
|
| window.addEventListener('popstate', handlePopState); |
| return () => window.removeEventListener('popstate', handlePopState); |
| }, []); |
|
|
| |
| useEffect(() => { |
| let timer: ReturnType<typeof setTimeout>; |
|
|
| const showScrollbars = () => { |
| document.body.classList.remove('scrollbar-inactive'); |
| clearTimeout(timer); |
| timer = setTimeout(() => { |
| document.body.classList.add('scrollbar-inactive'); |
| }, 1500); |
| }; |
|
|
| |
| timer = setTimeout(() => { |
| document.body.classList.add('scrollbar-inactive'); |
| }, 1500); |
|
|
| window.addEventListener('mousemove', showScrollbars); |
| window.addEventListener('mousedown', showScrollbars); |
| window.addEventListener('touchstart', showScrollbars); |
| window.addEventListener('keydown', showScrollbars); |
| window.addEventListener('scroll', showScrollbars, true); |
|
|
| return () => { |
| clearTimeout(timer); |
| window.removeEventListener('mousemove', showScrollbars); |
| window.removeEventListener('mousedown', showScrollbars); |
| window.removeEventListener('touchstart', showScrollbars); |
| window.removeEventListener('keydown', showScrollbars); |
| window.removeEventListener('scroll', showScrollbars, true); |
| }; |
| }, []); |
|
|
| useEffect(() => { |
| if (!user) { |
| setPreferences((current) => |
| current.themeMode === 'light' ? createDefaultPreferences('light') : createDefaultPreferences('dark'), |
| ); |
| setIsTwoFactorVerified(true); |
| setTwoFactorCode(''); |
| setTwoFactorError(''); |
| return; |
| } |
|
|
| const loaded = loadUserPreferences(user.id, preferences.themeMode); |
| setPreferences(loaded); |
|
|
| if (!loaded.dataAnalytics) { |
| clearAnalyticsEvents(user.id); |
| } |
| }, [user?.id]); |
|
|
| useEffect(() => { |
| if (!user) { |
| return; |
| } |
|
|
| const sessionKey = userScopedStorageKey('two_factor_verified', user.id); |
| const alreadyVerified = window.sessionStorage.getItem(sessionKey) === 'true'; |
| const requiresTwoFactor = preferences.twoFactorEnabled && preferences.twoFactorCode.length >= 6; |
|
|
| setIsTwoFactorVerified(!requiresTwoFactor || alreadyVerified); |
| setTwoFactorCode(''); |
| setTwoFactorError(''); |
| }, [preferences.twoFactorCode, preferences.twoFactorEnabled, user?.id]); |
|
|
| useEffect(() => { |
| let cancelled = false; |
|
|
| (async () => { |
| |
| if (typeof window !== 'undefined') { |
| const params = new URLSearchParams(window.location.search); |
| const oauthToken = params.get('token'); |
| const oauthError = params.get('error'); |
|
|
| if (oauthError) { |
| |
| window.history.replaceState({}, '', window.location.pathname); |
| |
| } else if (oauthToken) { |
| localStorage.setItem(AUTH_TOKEN_KEY, oauthToken); |
| |
| window.history.replaceState({}, '', '/'); |
| try { |
| const sessionUser = await fetchSessionUser(oauthToken); |
| if (!cancelled) { |
| if (sessionUser) { |
| setUser(sessionUser); |
| setShowLanding(false); |
| } else { |
| localStorage.removeItem(AUTH_TOKEN_KEY); |
| } |
| setIsAuthReady(true); |
| } |
| } catch { |
| if (!cancelled) { |
| setIsAuthReady(true); |
| } |
| } |
| return; |
| } |
| } |
|
|
| const token = localStorage.getItem(AUTH_TOKEN_KEY); |
| if (!token) { |
| if (!cancelled) { |
| setUser(null); |
| setIsAuthReady(true); |
| } |
| return; |
| } |
|
|
| try { |
| const sessionUser = await fetchSessionUser(token); |
| if (cancelled) { |
| return; |
| } |
| if (!sessionUser) { |
| localStorage.removeItem(AUTH_TOKEN_KEY); |
| setUser(null); |
| } else { |
| setUser(sessionUser); |
| } |
| } catch { |
| if (!cancelled) { |
| setUser(null); |
| } |
| } |
|
|
| if (!cancelled) { |
| setIsAuthReady(true); |
| } |
| })(); |
|
|
| return () => { |
| cancelled = true; |
| }; |
| }, []); |
|
|
| useEffect(() => { |
| if (!user) { |
| return; |
| } |
|
|
| const token = localStorage.getItem(AUTH_TOKEN_KEY); |
| if (!token) { |
| return; |
| } |
|
|
| let cancelled = false; |
| let midnightTimer: number | undefined; |
|
|
| const syncSession = async () => { |
| try { |
| const sessionUser = await fetchSessionUser(token); |
| if (cancelled) { |
| return; |
| } |
|
|
| if (!sessionUser) { |
| localStorage.removeItem(AUTH_TOKEN_KEY); |
| setSolvedQuestionIds([]); |
| setDailyActivity({}); |
| setDailyActivityBreakdown({}); |
| setLeaderboard([]); |
| setIsProgressReady(true); |
| setUser(null); |
| return; |
| } |
|
|
| setUser((currentUser) => { |
| if (!currentUser || currentUser.id !== sessionUser.id) { |
| return sessionUser; |
| } |
|
|
| return { |
| ...currentUser, |
| ...sessionUser, |
| }; |
| }); |
| scheduleMidnightSync(); |
| } catch { |
| if (!cancelled) { |
| scheduleMidnightSync(); |
| } |
| } |
| }; |
|
|
| const scheduleMidnightSync = () => { |
| if (midnightTimer) { |
| window.clearTimeout(midnightTimer); |
| } |
| midnightTimer = window.setTimeout(() => { |
| void syncSession(); |
| }, msUntilNextLocalMidnight()); |
| }; |
|
|
| const handleWindowFocus = () => { |
| void syncSession(); |
| }; |
|
|
| const handleVisibilityChange = () => { |
| if (document.visibilityState === 'visible') { |
| void syncSession(); |
| } |
| }; |
|
|
| scheduleMidnightSync(); |
| window.addEventListener('focus', handleWindowFocus); |
| document.addEventListener('visibilitychange', handleVisibilityChange); |
|
|
| return () => { |
| cancelled = true; |
| if (midnightTimer) { |
| window.clearTimeout(midnightTimer); |
| } |
| window.removeEventListener('focus', handleWindowFocus); |
| document.removeEventListener('visibilitychange', handleVisibilityChange); |
| }; |
| }, [user?.id]); |
|
|
| useEffect(() => { |
| const handleUserUpdate = (event: Event) => { |
| const customEvent = event as CustomEvent<AuthUser>; |
| setUser(customEvent.detail); |
| }; |
|
|
| window.addEventListener('user-updated', handleUserUpdate); |
| return () => { |
| window.removeEventListener('user-updated', handleUserUpdate); |
| }; |
| }, []); |
|
|
| useEffect(() => { |
| if (!user || activeSection !== 'coding') return; |
|
|
| let mounted = true; |
| const pollInterval = setInterval(async () => { |
| try { |
| const token = localStorage.getItem(AUTH_TOKEN_KEY); |
| if (!token) return; |
| const stats = await fetchUserProgress(token); |
| if (mounted) { |
| setLeaderboard(stats.leaderboard); |
| setCodingLeaderboard(stats.codingLeaderboard || []); |
| setCodingSolvedCount(stats.codingSolvedCount || 0); |
| } |
| } catch (err) { |
| |
| } |
| }, 5000); |
|
|
| return () => { |
| mounted = false; |
| clearInterval(pollInterval); |
| }; |
| }, [user, activeSection]); |
|
|
| useEffect(() => { |
| if (!user) { |
| setSolvedQuestionIds([]); |
| setDailyActivity({}); |
| setDailyActivityBreakdown({}); |
| setLeaderboard([]); |
| setIsProgressReady(true); |
| return; |
| } |
|
|
| const token = localStorage.getItem(AUTH_TOKEN_KEY); |
| if (!token) { |
| setSolvedQuestionIds([]); |
| setDailyActivity({}); |
| setDailyActivityBreakdown({}); |
| setLeaderboard([]); |
| setIsProgressReady(true); |
| return; |
| } |
|
|
| let cancelled = false; |
| setIsProgressReady(false); |
|
|
| const loadProgress = async () => { |
| try { |
| let stats = await fetchUserProgress(token); |
| const legacySolvedQuestionIds = getLegacySolvedQuestionIds(user.id); |
| const legacyDailyActivity = getLegacyDailyActivity(user.id); |
|
|
| if (needsLegacyProgressImport(stats, legacySolvedQuestionIds, legacyDailyActivity)) { |
| const imported = await importUserProgress(token, { |
| solvedQuestionIds: legacySolvedQuestionIds, |
| dailyActivity: legacyDailyActivity, |
| }); |
| stats = imported.stats; |
| if (!cancelled) { |
| setUser(imported.user); |
| } |
| } |
|
|
| if (cancelled) { |
| return; |
| } |
|
|
| setSolvedQuestionIds(stats.solvedQuestionIds); |
| setDailyActivity(stats.dailyActivity); |
| setDailyActivityBreakdown(stats.dailyActivityBreakdown || {}); |
| setLeaderboard(stats.leaderboard); |
| setCodingLeaderboard(stats.codingLeaderboard || []); |
| setCodingSolvedCount(stats.codingSolvedCount || 0); |
| } catch { |
| if (!cancelled) { |
| setSolvedQuestionIds(getLegacySolvedQuestionIds(user.id)); |
| setDailyActivity(getLegacyDailyActivity(user.id)); |
| setDailyActivityBreakdown({}); |
| setLeaderboard([]); |
| setCodingLeaderboard([]); |
| setCodingSolvedCount(0); |
| } |
| } finally { |
| if (!cancelled) { |
| setIsProgressReady(true); |
| } |
| } |
| }; |
|
|
| void loadProgress(); |
|
|
| return () => { |
| cancelled = true; |
| }; |
| }, [user?.id]); |
|
|
| useEffect(() => { |
| if (!user) { |
| return; |
| } |
|
|
| if (preferences.autoSave) { |
| localStorage.setItem(`solved_questions_${user.id}`, JSON.stringify(solvedQuestionIds)); |
| localStorage.setItem(`daily_activity_${user.id}`, JSON.stringify(dailyActivity)); |
| return; |
| } |
|
|
| localStorage.removeItem(`solved_questions_${user.id}`); |
| localStorage.removeItem(`daily_activity_${user.id}`); |
| }, [dailyActivity, preferences.autoSave, solvedQuestionIds, user?.id]); |
|
|
| useEffect(() => { |
| if (!user) { |
| return; |
| } |
|
|
| trackAnalyticsEvent(user.id, preferences.dataAnalytics, 'section.opened', { |
| section: activeSection, |
| }); |
| }, [activeSection, preferences.dataAnalytics, user?.id]); |
|
|
| const updatePreferences = (nextPreferences: UserPreferences) => { |
| if (!user) { |
| setPreferences(nextPreferences); |
| return nextPreferences; |
| } |
|
|
| const saved = saveUserPreferences(user.id, nextPreferences); |
| setPreferences(saved); |
|
|
| const twoFactorSessionKey = userScopedStorageKey('two_factor_verified', user.id); |
| if (saved.twoFactorEnabled && !preferences.twoFactorEnabled) { |
| window.sessionStorage.setItem(twoFactorSessionKey, 'true'); |
| setIsTwoFactorVerified(true); |
| } |
| if (!saved.twoFactorEnabled) { |
| window.sessionStorage.removeItem(twoFactorSessionKey); |
| setIsTwoFactorVerified(true); |
| } |
|
|
| if (!saved.dataAnalytics) { |
| clearAnalyticsEvents(user.id); |
| } |
|
|
| return saved; |
| }; |
|
|
| const handleSelectQuestion = (question: CodingQuestion, mode: 'code' | 'solution') => { |
| setSelectedQuestion(question); |
| setViewMode(mode); |
| }; |
|
|
| const handleQuestionSolved = async (item: { id: string; title: string; difficulty: string }, language?: string) => { |
| if (!user) { |
| return; |
| } |
|
|
| const token = localStorage.getItem(AUTH_TOKEN_KEY); |
| if (!token) { |
| return; |
| } |
|
|
| try { |
| const result = await recordSolvedQuestion(token, { |
| questionId: item.id, |
| difficulty: item.difficulty, |
| language, |
| }); |
| setUser(result.user); |
| setSolvedQuestionIds(result.stats.solvedQuestionIds); |
| setDailyActivity(result.stats.dailyActivity); |
| setDailyActivityBreakdown(result.stats.dailyActivityBreakdown || {}); |
| setLeaderboard(result.stats.leaderboard); |
| setCodingLeaderboard(result.stats.codingLeaderboard || []); |
| setCodingSolvedCount(result.stats.codingSolvedCount || 0); |
| trackAnalyticsEvent(user.id, preferences.dataAnalytics, 'question.solved', { |
| questionId: item.id, |
| difficulty: item.difficulty, |
| }); |
| playUiSound(preferences.soundEnabled, 'success'); |
| sendBrowserNotification(preferences.pushNotifications, 'Problem solved', { |
| body: `${item.title} was added to your completed list.`, |
| icon: '/favicon.ico', |
| }); |
| } catch (error) { |
| console.error('[App] Failed to record solved question:', error); |
| playUiSound(preferences.soundEnabled, 'error'); |
| } |
| }; |
|
|
| const handlePurchase = async (price: number, itemName?: string) => { |
| const token = localStorage.getItem(AUTH_TOKEN_KEY); |
| if (!token) { |
| return false; |
| } |
|
|
| try { |
| const result = await spendCoins(token, price); |
| setUser(result.user); |
| setSolvedQuestionIds(result.stats.solvedQuestionIds); |
| setDailyActivity(result.stats.dailyActivity); |
| setDailyActivityBreakdown(result.stats.dailyActivityBreakdown || {}); |
| setLeaderboard(result.stats.leaderboard); |
| setCodingLeaderboard(result.stats.codingLeaderboard || []); |
| setCodingSolvedCount(result.stats.codingSolvedCount || 0); |
| if (user) { |
| trackAnalyticsEvent(user.id, preferences.dataAnalytics, 'reward.claimed', { |
| price, |
| itemName: itemName ?? null, |
| }); |
| } |
| playUiSound(preferences.soundEnabled, 'success'); |
| sendBrowserNotification(preferences.pushNotifications, 'Reward claimed', { |
| body: itemName ? `${itemName} has been reserved from the swag store.` : 'Your swag store purchase was completed.', |
| icon: '/favicon.ico', |
| }); |
| return true; |
| } catch (error) { |
| console.error('[App] Failed to spend coins:', error); |
| playUiSound(preferences.soundEnabled, 'error'); |
| return false; |
| } |
| }; |
|
|
| const handleContestPrizeClaim = async (amount: number, contestId: string, rank: number) => { |
| if (!user) { |
| return false; |
| } |
|
|
| const token = localStorage.getItem(AUTH_TOKEN_KEY); |
| if (!token) { |
| return false; |
| } |
|
|
| try { |
| const result = await addCoins(token, amount); |
| setUser(result.user); |
|
|
| try { |
| const stats = await fetchUserProgress(token); |
| setSolvedQuestionIds(stats.solvedQuestionIds); |
| setDailyActivity(stats.dailyActivity); |
| setLeaderboard(stats.leaderboard); |
| setCodingLeaderboard(stats.codingLeaderboard || []); |
| setCodingSolvedCount(stats.codingSolvedCount || 0); |
| } catch { |
| |
| } |
|
|
| trackAnalyticsEvent(user.id, preferences.dataAnalytics, 'contest.prize.claimed', { |
| contestId, |
| rank, |
| amount, |
| }); |
| playUiSound(preferences.soundEnabled, 'success'); |
| sendBrowserNotification(preferences.pushNotifications, 'Contest reward claimed', { |
| body: `${amount} coins were added to your account for finishing rank #${rank}.`, |
| icon: '/favicon.ico', |
| }); |
| return true; |
| } catch (error) { |
| console.error('[App] Failed to claim contest prize:', error); |
| playUiSound(preferences.soundEnabled, 'error'); |
| return false; |
| } |
| }; |
|
|
| const openAuthPage = (mode: LandingAuthMode = 'login') => { |
| setAuthMode(mode); |
| setShowLanding(false); |
| window.scrollTo(0, 0); |
| }; |
|
|
| if (!isAuthReady || (user && !isProgressReady)) { |
| return ( |
| <div className="h-screen bg-[#0f172a] flex items-center justify-center"> |
| <Loader2 className="animate-spin text-emerald-500" size={48} /> |
| </div> |
| ); |
| } |
|
|
| if (!user) { |
| if (showLanding) { |
| return <LandingPage onGetStarted={openAuthPage} />; |
| } |
|
|
| return ( |
| <AuthPage |
| onLoggedIn={setUser} |
| initialMode={authMode} |
| onBackToLanding={() => setShowLanding(true)} |
| /> |
| ); |
| } |
|
|
| const handleLogout = async () => { |
| if (user) { |
| window.sessionStorage.removeItem(userScopedStorageKey('two_factor_verified', user.id)); |
| } |
| localStorage.removeItem(AUTH_TOKEN_KEY); |
| setSolvedQuestionIds([]); |
| setDailyActivity({}); |
| setLeaderboard([]); |
| setAuthMode('login'); |
| setShowLanding(true); |
| setUser(null); |
| }; |
|
|
| const handleVerifyTwoFactor = () => { |
| if (!user) { |
| return; |
| } |
|
|
| if (twoFactorCode.trim() !== preferences.twoFactorCode) { |
| setTwoFactorError('That verification code does not match your saved security code.'); |
| playUiSound(preferences.soundEnabled, 'error'); |
| return; |
| } |
|
|
| window.sessionStorage.setItem(userScopedStorageKey('two_factor_verified', user.id), 'true'); |
| setIsTwoFactorVerified(true); |
| setTwoFactorCode(''); |
| setTwoFactorError(''); |
| playUiSound(preferences.soundEnabled, 'success'); |
| }; |
|
|
| if (user && !isTwoFactorVerified) { |
| return ( |
| <div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-[#05070b] p-6 text-white"> |
| <div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.05),transparent_26%),radial-gradient(circle_at_top_right,rgba(249,115,22,0.03),transparent_20%),radial-gradient(circle_at_bottom_left,rgba(99,102,241,0.04),transparent_22%),linear-gradient(135deg,#040507_0%,#07090d_50%,#050608_100%)]" /> |
| <div className="relative z-10 w-full max-w-md rounded-[24px] border border-emerald-500/15 bg-[#0c0c0c]/90 p-8 shadow-2xl backdrop-blur-xl"> |
| <div className="mb-5 flex h-14 w-14 items-center justify-center rounded-2xl bg-emerald-500/10 text-emerald-400"> |
| <ShieldCheck size={26} /> |
| </div> |
| <h2 className="text-3xl font-black tracking-tight text-white">Verify your sign-in</h2> |
| <p className="mt-3 text-sm leading-6 text-slate-400"> |
| Two-step verification is enabled for this account. Enter the 6-digit security code from your settings page to unlock RYP. |
| </p> |
| |
| <div className="mt-6 space-y-3"> |
| <label className="block text-xs font-black uppercase tracking-[0.22em] text-zinc-500"> |
| Security Code |
| </label> |
| <input |
| type="text" |
| value={twoFactorCode} |
| onChange={(event) => { |
| setTwoFactorCode(event.target.value.replace(/\D/g, '').slice(0, 6)); |
| if (twoFactorError) { |
| setTwoFactorError(''); |
| } |
| }} |
| onKeyDown={(event) => { |
| if (event.key === 'Enter') { |
| event.preventDefault(); |
| handleVerifyTwoFactor(); |
| } |
| }} |
| placeholder="Enter 6 digits" |
| className="w-full rounded-2xl border border-zinc-800 bg-zinc-900/80 px-4 py-3 text-lg font-bold tracking-[0.35em] text-white outline-none transition-all focus:border-emerald-500/50 focus:ring-1 focus:ring-emerald-500/30" |
| /> |
| {twoFactorError && <p className="text-sm text-rose-400">{twoFactorError}</p>} |
| </div> |
| |
| <div className="mt-6 flex flex-col gap-3 sm:flex-row"> |
| <button |
| type="button" |
| onClick={handleVerifyTwoFactor} |
| className="flex-1 rounded-2xl bg-emerald-600 px-5 py-3 text-sm font-black text-white transition-colors hover:bg-emerald-500" |
| > |
| Verify |
| </button> |
| <button |
| type="button" |
| onClick={handleLogout} |
| className="rounded-2xl border border-zinc-800 px-5 py-3 text-sm font-semibold text-zinc-300 transition-colors hover:border-zinc-700 hover:text-white" |
| > |
| Sign Out |
| </button> |
| </div> |
| </div> |
| </div> |
| ); |
| } |
|
|
| if (selectedQuestion) { |
| if (viewMode === 'solution') { |
| return ( |
| <SolutionView |
| question={selectedQuestion} |
| onBack={() => setSelectedQuestion(null)} |
| onGoToCode={() => setViewMode('code')} |
| fontSize={preferences.fontSize} |
| /> |
| ); |
| } |
|
|
| return ( |
| <Compiler |
| question={selectedQuestion} |
| onBack={() => setSelectedQuestion(null)} |
| onSolved={(lang) => handleQuestionSolved(selectedQuestion, lang)} |
| fontSize={preferences.fontSize} |
| /> |
| ); |
| } |
|
|
| const isViewportSection = |
| activeSection === 'dashboard' || activeSection === 'weekly-contest' || activeSection === 'daily-contest'; |
| const isFullscreenAppSection = |
| activeSection === 'ryp-quests' || activeSection === 'ryp-quest-detail'; |
| const usesFullscreenSectionLayout = |
| activeSection === 'ai-teacher' || activeSection === 'settings' || activeSection === 'compiler' || isFullscreenAppSection; |
|
|
| return ( |
| <div className="flex h-screen bg-[#05070b] text-white overflow-hidden relative"> |
| <div className="pointer-events-none fixed inset-0 z-0"> |
| <div className="absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.05),transparent_26%),radial-gradient(circle_at_top_right,rgba(249,115,22,0.03),transparent_20%),radial-gradient(circle_at_bottom_left,rgba(99,102,241,0.04),transparent_22%),linear-gradient(135deg,#040507_0%,#07090d_50%,#050608_100%)]" /> |
| <div className="absolute inset-0 bg-[linear-gradient(rgba(148,163,184,0.02)_1px,transparent_1px),linear-gradient(90deg,rgba(148,163,184,0.02)_1px,transparent_1px)] bg-[size:120px_120px] opacity-20" /> |
| <svg viewBox="0 0 360 260" fill="none" className="pointer-events-none absolute right-0 top-0 hidden h-[240px] w-[360px] text-slate-300/10 sm:block"><path d="M15 34H132C148 34 161 47 161 63V82C161 95 171 105 184 105H212" stroke="currentColor" strokeWidth="1.5" /><path d="M110 63H246C262 63 275 76 275 92V120C275 136 288 149 304 149H346" stroke="currentColor" strokeWidth="1.5" /><path d="M74 118H164C181 118 195 132 195 149V178C195 194 208 207 224 207H318" stroke="currentColor" strokeWidth="1.5" /><path d="M228 18V47C228 62 240 74 255 74H346" stroke="currentColor" strokeWidth="1.5" /><path d="M318 149V112C318 96 331 83 347 83H360" stroke="currentColor" strokeWidth="1.5" /><path d="M318 208V229C318 244 330 256 345 256H360" stroke="currentColor" strokeWidth="1.5" /><circle cx="110" cy="63" r="4" stroke="currentColor" strokeWidth="1.5" /><circle cx="74" cy="118" r="4" stroke="currentColor" strokeWidth="1.5" /><circle cx="228" cy="18" r="4" stroke="currentColor" strokeWidth="1.5" /><circle cx="318" cy="149" r="4" stroke="currentColor" strokeWidth="1.5" /><circle cx="212" cy="105" r="4" stroke="currentColor" strokeWidth="1.5" /><circle cx="346" cy="149" r="4" stroke="currentColor" strokeWidth="1.5" /></svg> |
| <svg viewBox="0 0 360 260" fill="none" className="pointer-events-none absolute bottom-0 left-0 hidden -scale-x-100 -scale-y-100 h-[240px] w-[360px] text-slate-300/10 sm:block"><path d="M15 34H132C148 34 161 47 161 63V82C161 95 171 105 184 105H212" stroke="currentColor" strokeWidth="1.5" /><path d="M110 63H246C262 63 275 76 275 92V120C275 136 288 149 304 149H346" stroke="currentColor" strokeWidth="1.5" /><path d="M74 118H164C181 118 195 132 195 149V178C195 194 208 207 224 207H318" stroke="currentColor" strokeWidth="1.5" /><path d="M228 18V47C228 62 240 74 255 74H346" stroke="currentColor" strokeWidth="1.5" /><path d="M318 149V112C318 96 331 83 347 83H360" stroke="currentColor" strokeWidth="1.5" /><path d="M318 208V229C318 244 330 256 345 256H360" stroke="currentColor" strokeWidth="1.5" /><circle cx="110" cy="63" r="4" stroke="currentColor" strokeWidth="1.5" /><circle cx="74" cy="118" r="4" stroke="currentColor" strokeWidth="1.5" /><circle cx="228" cy="18" r="4" stroke="currentColor" strokeWidth="1.5" /><circle cx="318" cy="149" r="4" stroke="currentColor" strokeWidth="1.5" /><circle cx="212" cy="105" r="4" stroke="currentColor" strokeWidth="1.5" /><circle cx="346" cy="149" r="4" stroke="currentColor" strokeWidth="1.5" /></svg> |
| </div> |
| |
| <Sidebar |
| activeSection={activeSection} |
| onSectionChange={setActiveSection} |
| user={user} |
| onLogout={handleLogout} |
| /> |
| |
| <main className="flex-1 overflow-hidden relative z-10"> |
| {activeSection === 'dashboard' && ( |
| <div className="absolute top-4 right-4 sm:top-6 sm:right-8 z-50 flex items-center gap-2 sm:gap-3"> |
| <button |
| onClick={() => setActiveSection('shopping')} |
| className="flex items-center gap-1.5 sm:gap-2 bg-[#0c0c0c]/80 backdrop-blur-xl border border-emerald-500/30 hover:bg-emerald-500/10 hover:border-emerald-500/50 transition-all px-2.5 sm:px-4 py-1.5 sm:py-2 rounded-xl sm:rounded-2xl shadow-2xl group" |
| > |
| <ShoppingCart className="text-emerald-400 group-hover:scale-110 transition-transform" size={16} /> |
| <span className="font-bold text-emerald-100 text-xs sm:text-sm tracking-wide hidden sm:inline-block">Shop</span> |
| </button> |
| <motion.button |
| onClick={() => setActiveSection('coin-history')} |
| whileHover={{ scale: 1.05 }} |
| className="flex items-center gap-1.5 sm:gap-2 bg-[#0c0c0c]/80 backdrop-blur-xl border border-yellow-500/30 hover:border-yellow-500/50 hover:bg-yellow-500/10 transition-colors px-2.5 sm:px-4 py-1.5 sm:py-2 rounded-xl sm:rounded-2xl shadow-2xl cursor-pointer" |
| > |
| <Coins className="text-yellow-400" size={16} /> |
| <span className="font-bold text-yellow-100 text-xs sm:text-sm tracking-wide">{user.coins || 0}</span> |
| </motion.button> |
| </div> |
| )} |
| |
| <div |
| className={`h-full pt-20 sm:pt-24 lg:pt-0 ${usesFullscreenSectionLayout ? 'overflow-hidden' : 'overflow-y-auto scrollbar-auto-hide' |
| }`} |
| > |
| <AnimatePresence mode="wait"> |
| <motion.div |
| key={activeSection} |
| initial={{ opacity: 0, y: 20 }} |
| animate={{ opacity: 1, y: 0 }} |
| exit={{ opacity: 0, y: -20 }} |
| transition={{ duration: 0.3, ease: 'easeInOut' }} |
| className={ |
| activeSection === 'ai-teacher' || activeSection === 'compiler' |
| ? 'h-full' |
| : activeSection === 'settings' |
| ? 'h-full p-4 sm:p-6 md:p-8 lg:p-10' |
| : isFullscreenAppSection |
| ? 'h-full p-3 sm:p-4 md:p-5 lg:p-6' |
| : isViewportSection |
| ? 'min-h-full h-auto lg:h-full p-3 sm:p-4 md:p-5 lg:p-6' |
| : 'p-4 sm:p-6 md:p-8 lg:p-10 min-h-full' |
| } |
| > |
| {activeSection === 'dashboard' && ( |
| <Dashboard |
| userId={user.id} |
| userName={user.displayName} |
| currentStreak={user.currentStreak} |
| longestStreak={user.longestStreak} |
| solvedCount={solvedQuestionIds.length} |
| solvedQuestionIds={solvedQuestionIds} |
| dailyActivity={dailyActivity} |
| dailyActivityBreakdown={dailyActivityBreakdown} |
| leaderboard={leaderboard} |
| onOpenSection={setActiveSection} |
| onViewFullLeaderboard={() => setActiveSection('leaderboard')} |
| /> |
| )} |
| {activeSection === 'coin-history' && ( |
| <CoinHistoryPage |
| userId={user.id} |
| onBack={() => setActiveSection('dashboard')} |
| currentCoins={user.coins || 0} |
| /> |
| )} |
| {activeSection === 'weekly-contest' && ( |
| <WeeklyContestPage |
| user={user} |
| solvedQuestionIds={solvedQuestionIds} |
| onSelectQuestion={handleSelectQuestion} |
| onClaimPrize={handleContestPrizeClaim} |
| /> |
| )} |
| {activeSection === 'daily-contest' && ( |
| <DailyContestPage |
| user={user} |
| solvedQuestionIds={solvedQuestionIds} |
| onSelectQuestion={handleSelectQuestion} |
| /> |
| )} |
| {activeSection === 'leaderboard' && ( |
| <FullLeaderboardPage |
| leaderboard={leaderboard} |
| onBack={() => setActiveSection('dashboard')} |
| /> |
| )} |
| {activeSection === 'ryp-quests' && ( |
| <QuestsPage |
| user={user} |
| onEnterQuest={(questId) => { |
| setActiveQuestId(questId); |
| setActiveSection('ryp-quest-detail'); |
| }} |
| /> |
| )} |
| {activeSection === 'ryp-quest-detail' && activeQuestId && (() => { |
| const quest = ALL_QUESTS.find((q) => q.id === activeQuestId); |
| const token = localStorage.getItem(AUTH_TOKEN_KEY) ?? ''; |
| return quest ? ( |
| <QuestDetailPage |
| quest={quest} |
| user={user} |
| token={token} |
| solvedQuestionIds={solvedQuestionIds} |
| onBack={() => setActiveSection('ryp-quests')} |
| onNavigate={(section, itemId) => { |
| if (section === 'coding' && itemId) { |
| const q = codingQuestions.find((cq) => cq.id === itemId); |
| if (q) { handleSelectQuestion(q, 'code'); return; } |
| } |
| setActiveSection(section); |
| }} |
| onAddCoins={async (amount, _source) => { |
| const result = await addCoins(token, amount); |
| setUser(result.user); |
| }} |
| /> |
| ) : null; |
| })()} |
| {activeSection === 'ai-teacher' && <AITeacherPage user={user} />} |
| {activeSection === 'coding-leaderboard' && ( |
| <FullLeaderboardPage |
| leaderboard={codingLeaderboard} |
| onBack={() => setActiveSection('coding')} |
| isCodingLeaderboard={true} |
| /> |
| )} |
| {activeSection === 'coding' && ( |
| <CodingSection |
| onSelectQuestion={handleSelectQuestion} |
| solvedQuestionIds={solvedQuestionIds} |
| solvedCount={codingSolvedCount} |
| userId={user.id} |
| leaderboard={codingLeaderboard} |
| onViewFullLeaderboard={() => setActiveSection('coding-leaderboard')} |
| /> |
| )} |
| {activeSection === 'compiler' && <CompilerPage />} |
| {activeSection === 'learning' && <LearningPathsPage />} |
| {activeSection === 'system-design' && <SystemDesignSection onSolve={(id, title) => handleQuestionSolved({ id: `sd-${id}`, title, difficulty: 'medium' })} />} |
| {activeSection === 'cs-fundamentals' && <CSFundamentalsSection onNavigateToDBMS={() => setActiveSection('dbms')} onNavigateToCN={() => setActiveSection('cn')} onNavigateToOS={() => setActiveSection('os')} onSolve={(id, title) => handleQuestionSolved({ id: `cs-${id}`, title, difficulty: 'easy' })} />} |
| {activeSection === 'aptitude' && <AptitudeSection onSelectCategory={(cat) => { |
| setActiveAptitudeCategory(cat.replace('-', '_')); |
| setActiveSection('aptitude-category'); |
| }} />} |
| {activeSection === 'aptitude-category' && activeAptitudeCategory && ( |
| <AptitudeCategoryPage |
| category={activeAptitudeCategory} |
| title={activeAptitudeCategory.split('_').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ')} |
| description={`Master the concepts of ${activeAptitudeCategory.replace('_', ' ')}.`} |
| onBack={() => setActiveSection('aptitude')} |
| onSelectTopic={(topicNo, mode) => { |
| setActiveAptitudeTopicNo(topicNo); |
| setActiveAptitudeMode(mode); |
| setActiveSection('aptitude-content'); |
| }} |
| /> |
| )} |
| {activeSection === 'aptitude-content' && activeAptitudeCategory && activeAptitudeTopicNo && ( |
| <AptitudeContentPage |
| category={activeAptitudeCategory} |
| topicNo={activeAptitudeTopicNo} |
| mode={activeAptitudeMode} |
| onBack={() => setActiveSection('aptitude-category')} |
| /> |
| )} |
| {activeSection === 'arena' && <ArenaHubSection onEnterArena={() => setActiveSection('arena-lobby')} />} |
| {activeSection === 'arena-lobby' && ( |
| <ArenaLobby |
| onBack={() => setActiveSection('arena')} |
| onMatchFound={(question, difficulty) => { |
| setArenaQuestion(question); |
| setArenaDifficulty(difficulty); |
| setActiveSection('arena-battle'); |
| }} |
| /> |
| )} |
| {activeSection === 'arena-battle' && arenaQuestion && ( |
| <ArenaBattle |
| question={arenaQuestion} |
| difficulty={arenaDifficulty} |
| onLeave={() => setActiveSection('arena')} |
| onSolved={(lang) => handleQuestionSolved(arenaQuestion, lang)} |
| fontSize={preferences.fontSize} |
| /> |
| )} |
| {activeSection === 'playground' && ( |
| <Playground |
| userId={user.id} |
| autoSaveEnabled={preferences.autoSave} |
| fontSize={preferences.fontSize} |
| /> |
| )} |
| {activeSection === 'shopping' && ( |
| <ShoppingPage |
| onBack={() => setActiveSection('dashboard')} |
| onOpenSettings={() => setActiveSection('settings')} |
| userCoins={user.coins || 0} |
| userId={user.id} |
| address={preferences.address} |
| onPurchase={handlePurchase} |
| /> |
| )} |
| {activeSection === 'sheets' && ( |
| <CodingSheet |
| onSelectQuestion={handleSelectQuestion} |
| solvedQuestionIds={solvedQuestionIds} |
| /> |
| )} |
| {activeSection === 'profile' && ( |
| <ProfileSection |
| user={user} |
| solvedCount={solvedQuestionIds.length} |
| solvedQuestionIds={solvedQuestionIds} |
| dailyActivity={dailyActivity} |
| /> |
| )} |
| {activeSection === 'settings' && ( |
| <SettingsPage |
| user={user} |
| onUserUpdate={setUser} |
| preferences={preferences} |
| onPreferencesChange={updatePreferences} |
| solvedCount={solvedQuestionIds.length} |
| /> |
| )} |
| {activeSection === 'dbms' && ( |
| <DBMSPage |
| onSelectTopic={(topicNo) => { |
| setSelectedDBMSTopicNo(topicNo); |
| setActiveSection('dbms-content'); |
| }} |
| onBack={() => setActiveSection('cs-fundamentals')} |
| onSolve={(id, title) => handleQuestionSolved({ id: `cs-dbms-${id}`, title, difficulty: 'easy' })} |
| /> |
| )} |
| {activeSection === 'dbms-content' && selectedDBMSTopicNo && ( |
| <DBMSContentPage |
| topicNo={selectedDBMSTopicNo} |
| onBack={() => setActiveSection('dbms')} |
| /> |
| )} |
| {activeSection === 'cn' && ( |
| <CNPage |
| onSelectTopic={(topicNo) => { |
| setSelectedCNTopicNo(topicNo); |
| setActiveSection('cn-content'); |
| }} |
| onBack={() => setActiveSection('cs-fundamentals')} |
| onSolve={(id, title) => handleQuestionSolved({ id: `cs-cn-${id}`, title, difficulty: 'easy' })} |
| /> |
| )} |
| {activeSection === 'cn-content' && selectedCNTopicNo && ( |
| <CNContentPage |
| chapterNo={selectedCNTopicNo} |
| onBack={() => setActiveSection('cn')} |
| /> |
| )} |
| {activeSection === 'os' && ( |
| <OSPage |
| onSelectSection={(sectionNo) => { |
| setSelectedOSSectionNo(sectionNo); |
| setActiveSection('os-content'); |
| }} |
| onBack={() => setActiveSection('cs-fundamentals')} |
| onSolve={(id, title) => handleQuestionSolved({ id: `cs-os-${id}`, title, difficulty: 'easy' })} |
| /> |
| )} |
| {activeSection === 'os-content' && selectedOSSectionNo && ( |
| <OSContentPage |
| sectionNo={selectedOSSectionNo} |
| onBack={() => setActiveSection('os')} |
| /> |
| )} |
| </motion.div> |
| </AnimatePresence> |
| </div> |
| </main> |
| </div> |
| ); |
| } |
|
|
| function getLegacySolvedQuestionIds(userId: string) { |
| const solvedFromApp = readLegacySolvedArray(`solved_questions_${userId}`); |
| const solvedFromCodingSection = readLegacyCompletedQuestions(); |
| return Array.from(new Set([...solvedFromApp, ...solvedFromCodingSection])); |
| } |
|
|
| function getLegacyDailyActivity(userId: string) { |
| const raw = localStorage.getItem(`daily_activity_${userId}`); |
| if (!raw) { |
| return {}; |
| } |
|
|
| try { |
| const parsed = JSON.parse(raw); |
| return typeof parsed === 'object' && parsed !== null ? parsed : {}; |
| } catch { |
| return {}; |
| } |
| } |
|
|
| function readLegacySolvedArray(key: string) { |
| const raw = localStorage.getItem(key); |
| if (!raw) { |
| return [] as string[]; |
| } |
|
|
| try { |
| const parsed = JSON.parse(raw); |
| return Array.isArray(parsed) ? parsed.filter((value): value is string => typeof value === 'string') : []; |
| } catch { |
| return []; |
| } |
| } |
|
|
| function readLegacyCompletedQuestions() { |
| const raw = localStorage.getItem('completed_coding_questions'); |
| if (!raw) { |
| return [] as string[]; |
| } |
|
|
| try { |
| const parsed = JSON.parse(raw); |
| if (!parsed || typeof parsed !== 'object') { |
| return []; |
| } |
|
|
| return Object.entries(parsed) |
| .filter(([, solved]) => Boolean(solved)) |
| .map(([questionId]) => questionId); |
| } catch { |
| return []; |
| } |
| } |
|
|
| function needsLegacyProgressImport( |
| stats: UserProgressStats, |
| legacySolvedQuestionIds: string[], |
| legacyDailyActivity: Record<string, number>, |
| ) { |
| if (legacySolvedQuestionIds.length > stats.solvedQuestionIds.length) { |
| return true; |
| } |
|
|
| for (const [key, count] of Object.entries(legacyDailyActivity)) { |
| if ((stats.dailyActivity[key] || 0) < count) { |
| return true; |
| } |
| } |
|
|
| return false; |
| } |
|
|
| function msUntilNextLocalMidnight() { |
| const now = new Date(); |
| const nextMidnight = new Date(now); |
| nextMidnight.setHours(24, 0, 5, 0); |
| return Math.max(60_000, nextMidnight.getTime() - now.getTime()); |
| } |
|
|
| function interfaceLanguageToHtmlLang(language: UserPreferences['language']) { |
| if (language === 'spanish') { |
| return 'es'; |
| } |
| if (language === 'french') { |
| return 'fr'; |
| } |
| if (language === 'german') { |
| return 'de'; |
| } |
| return 'en'; |
| } |
|
|