RYP / src /App.tsx
Soumya79's picture
Upload 1361 files
f91a684 verified
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';
// SubTopicDetailsPage removed
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);
}, []);
// Hide scrollbars after 3s of mouse inactivity, show on mouse move
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);
};
// Start the timer immediately on mount
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 () => {
// Handle Google OAuth redirect callback (/auth/fallback/?token=...)
if (typeof window !== 'undefined') {
const params = new URLSearchParams(window.location.search);
const oauthToken = params.get('token');
const oauthError = params.get('error');
if (oauthError) {
// Clean up URL so the error param doesn't persist
window.history.replaceState({}, '', window.location.pathname);
// Fall through to show AuthPage with an error (handled below)
} else if (oauthToken) {
localStorage.setItem(AUTH_TOKEN_KEY, oauthToken);
// Clean up the URL so the token isn't visible
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) {
// Silently fail polling
}
}, 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 {
// Leave the rest of the UI as-is if the leaderboard refresh fails.
}
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';
}