+
{w.solved} / {w.target} problems · {w.reason}
@@ -218,7 +217,7 @@ export default function RecommendationsPage() {
{[['Easy', diff.easyCount, 30, '#22C55E'], ['Medium', diff.mediumCount, 60, '#F59E0B'], ['Hard', diff.hardCount, 20, '#EF4444']].map(([l, v, t, c]) => (
{l}{v}/{t}
-
diff --git a/frontend/website/src/pages/SignupPage.jsx b/frontend/website/src/pages/SignupPage.jsx
index 088203c..74f9026 100644
--- a/frontend/website/src/pages/SignupPage.jsx
+++ b/frontend/website/src/pages/SignupPage.jsx
@@ -1,4 +1,4 @@
-import { useState, useEffect, useRef } from 'react'
+import { useEffect, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import * as api from '../services/api'
import GoogleAuthButton from '../components/GoogleAuthButton'
@@ -189,15 +189,15 @@ export default function SignupPage() {
marginBottom: '4px',
fontSize: '13px',
lineHeight: 1.55,
- color: '#c8943a',
+ color: 'var(--text-accent)',
display: 'flex',
gap: 9,
alignItems: 'flex-start',
}}>
⏳
- Heads up! Our backend runs on a free tier and may be
- sleeping. The first request can take 10–30 seconds to
+ Heads up! Our backend runs on a free tier and may be
+ sleeping. The first request can take 10–30 seconds to
wake up — please be patient. Once it's up, everything runs smoothly. ☕
diff --git a/frontend/website/src/services/api.js b/frontend/website/src/services/api.js
index b65fb58..18a5631 100755
--- a/frontend/website/src/services/api.js
+++ b/frontend/website/src/services/api.js
@@ -916,4 +916,4 @@ export async function broadcastNotification({ title, message, link }) {
// ── Recommendations ────────────────────────────────────────────────────────────
export async function completeDailyMission() {
return authFetchJson('/recommendations/daily-mission/complete', { method: 'POST' })
-}
+}
\ No newline at end of file
diff --git a/frontend/website/src/services/mockApi.js b/frontend/website/src/services/mockApi.js
new file mode 100644
index 0000000..899a463
--- /dev/null
+++ b/frontend/website/src/services/mockApi.js
@@ -0,0 +1,286 @@
+/**
+ * Mock API Wrapper
+ * Enable/disable with: localStorage.setItem('useMockApi', 'true'/'false')
+ * Check status with: localStorage.getItem('useMockApi')
+ */
+// localStorage.setItem('useMockApi', 'true');
+// localStorage.setItem('jwt_email', 'testuser@example.com');
+// localStorage.setItem('jwt_name', 'Test User');
+// localStorage.setItem('jwt_username', 'testuser');
+// window.location.href = '/dashboard';
+import {
+ MOCK_CHALLENGES_DATA,
+ MOCK_COMMUNITY_DATA,
+ MOCK_CONTESTS_DATA,
+ MOCK_DASHBOARD_DATA,
+ MOCK_PRACTICE_DATA,
+ MOCK_PROBLEMS_DATA,
+ MOCK_PROFILE_DATA,
+ MOCK_RECOMMENDATIONS_DATA,
+ MOCK_USER,
+} from './mockData'
+
+export function isMockApiEnabled() {
+ return localStorage.getItem('useMockApi') === 'true'
+}
+
+export function enableMockApi() {
+ localStorage.setItem('useMockApi', 'true')
+ console.log('✅ Mock API enabled. Page will reload.')
+ window.location.reload()
+}
+
+export function disableMockApi() {
+ localStorage.removeItem('useMockApi')
+ console.log('❌ Mock API disabled. Page will reload.')
+ window.location.reload()
+}
+
+export function getMockApiStatus() {
+ return {
+ enabled: isMockApiEnabled(),
+ message: isMockApiEnabled()
+ ? '✅ Using Mock API (no backend needed)'
+ : '❌ Using Real API (backend required)',
+ }
+}
+
+/**
+ * Intercept authFetchJson calls and return mock data
+ * Usage: wrap the real authFetchJson with this function
+ */
+export async function mockAuthFetchJson(path, options = {}) {
+ if (!isMockApiEnabled()) {
+ // Fall through to real API
+ return null
+ }
+
+ // Add small delay to simulate network latency
+ await new Promise(resolve => setTimeout(resolve, 300 + Math.random() * 200))
+
+ console.log(`🎭 [MOCK API] ${options.method || 'GET'} ${path}`)
+
+ // LOGIN - Check credentials
+ if (path === '/auth/login') {
+ const { email, password } = JSON.parse(options.body || '{}')
+ if (email && password) {
+ return {
+ ok: true,
+ data: {
+ email,
+ name: MOCK_USER.name,
+ username: MOCK_USER.username,
+ },
+ }
+ }
+ return { ok: false, error: 'Invalid email or password' }
+ }
+
+ // SIGNUP - Accept any new user
+ if (path === '/auth/signup/request') {
+ const { name, email, password, username } = JSON.parse(options.body || '{}')
+ if (email && password) {
+ return {
+ ok: true,
+ data: {
+ email,
+ name,
+ username,
+ requiresOTP: false,
+ },
+ }
+ }
+ return { ok: false, error: 'Invalid signup data' }
+ }
+
+ // DASHBOARD DATA
+ if (path === '/api/platforms/dashboard') {
+ return {
+ ok: true,
+ data: MOCK_DASHBOARD_DATA,
+ }
+ }
+
+ // PRACTICE MISSIONS
+ if (path === '/api/missions') {
+ return {
+ ok: true,
+ data: MOCK_PRACTICE_DATA,
+ }
+ }
+
+ // COMMUNITY POSTS
+ if (path === '/api/community/posts') {
+ return {
+ ok: true,
+ data: MOCK_COMMUNITY_DATA,
+ }
+ }
+
+ if (path.startsWith('/api/community/posts/')) {
+ const postId = path.split('/').pop()
+ return {
+ ok: true,
+ data: MOCK_COMMUNITY_DATA.posts.find(p => p.id === parseInt(postId)),
+ }
+ }
+
+ // CONTESTS
+ if (path === '/api/contests') {
+ return {
+ ok: true,
+ data: MOCK_CONTESTS_DATA,
+ }
+ }
+
+ // PROFILE
+ if (path === '/api/user/profile') {
+ return {
+ ok: true,
+ data: MOCK_PROFILE_DATA,
+ }
+ }
+
+ // PROFILE UPDATE
+ if (path === '/api/user/profile' && options.method === 'PUT') {
+ return {
+ ok: true,
+ data: { ...MOCK_PROFILE_DATA.user, ...JSON.parse(options.body || '{}') },
+ }
+ }
+
+ // CHALLENGES
+ if (path === '/api/challenges') {
+ return {
+ ok: true,
+ data: MOCK_CHALLENGES_DATA,
+ }
+ }
+
+ // PROBLEMS
+ if (path === '/api/problems') {
+ return {
+ ok: true,
+ data: MOCK_PROBLEMS_DATA,
+ }
+ }
+
+ // RECOMMENDATIONS
+ if (path === '/api/recommendations') {
+ return {
+ ok: true,
+ data: MOCK_RECOMMENDATIONS_DATA,
+ }
+ }
+
+ // VERIFICATION (always succeed for testing)
+ if (path === '/api/verify/start') {
+ const { platform, handle } = JSON.parse(options.body || '{}')
+ return {
+ ok: true,
+ data: {
+ problemSlug: 'two-sum',
+ problemName: 'Two Sum',
+ problemUrl: `https://${platform}.com/problems/two-sum/`,
+ startTime: new Date().toISOString(),
+ },
+ }
+ }
+
+ if (path === '/api/verify/check') {
+ return {
+ ok: true,
+ data: { verified: true },
+ }
+ }
+
+ // GET CURRENT USER
+ if (path === '/auth/me') {
+ return {
+ ok: true,
+ data: MOCK_USER,
+ }
+ }
+
+ // NOTIFICATIONS
+ if (path === '/api/notifications/unread-count') {
+ return {
+ ok: true,
+ data: { count: 3 },
+ }
+ }
+
+ if (path === '/api/notifications') {
+ return {
+ ok: true,
+ data: {
+ notifications: [
+ { id: 1, message: 'You solved Two Sum!', read: false, timestamp: new Date() },
+ { id: 2, message: 'New contest available!', read: false, timestamp: new Date() },
+ { id: 3, message: 'Keep your streak going!', read: true, timestamp: new Date() },
+ ],
+ },
+ }
+ }
+
+ // CHALLENGES
+ if (path === '/challenges/mine' || path === '/api/challenges/mine') {
+ return {
+ ok: true,
+ data: MOCK_CHALLENGES_DATA.challenges || [],
+ }
+ }
+
+ if (path === '/challenges/invitations' || path === '/api/challenges/invitations') {
+ return {
+ ok: true,
+ data: [],
+ }
+ }
+
+ // COMMUNITY POSTS
+ if (path.includes('/api/posts') || path.includes('/posts')) {
+ return {
+ ok: true,
+ data: {
+ posts: MOCK_COMMUNITY_DATA.posts || [],
+ hasNext: false,
+ },
+ }
+ }
+
+ // CATCH-ALL: For any missing endpoint, provide reasonable defaults
+ console.log(`🎭 [MOCK API] Returning default data for ${path}`)
+
+ // Return empty data based on endpoint pattern
+ if (path.includes('/dashboard')) {
+ return { ok: true, data: MOCK_DASHBOARD_DATA }
+ }
+ if (path.includes('/profile')) {
+ return { ok: true, data: MOCK_PROFILE_DATA }
+ }
+ if (path.includes('/community')) {
+ return { ok: true, data: MOCK_COMMUNITY_DATA }
+ }
+ if (path.includes('/problems')) {
+ return { ok: true, data: MOCK_PROBLEMS_DATA }
+ }
+ if (path.includes('/challenges')) {
+ return { ok: true, data: MOCK_CHALLENGES_DATA.challenges || [] }
+ }
+ if (path.includes('/recommendations')) {
+ return { ok: true, data: MOCK_RECOMMENDATIONS_DATA }
+ }
+ if (path.includes('/contests')) {
+ return { ok: true, data: MOCK_CONTESTS_DATA }
+ }
+ if (path.includes('/missions')) {
+ return { ok: true, data: MOCK_PRACTICE_DATA }
+ }
+
+ // FALLBACK - Return empty success for unknown endpoints
+ return {
+ ok: true,
+ data: [],
+ }
+}
diff --git a/frontend/website/src/services/mockData.js b/frontend/website/src/services/mockData.js
new file mode 100644
index 0000000..c008534
--- /dev/null
+++ b/frontend/website/src/services/mockData.js
@@ -0,0 +1,330 @@
+/**
+ * Mock API Data for Frontend Testing
+ * Replace this with real API calls when backend is ready
+ */
+
+export const MOCK_USER = {
+ email: 'testuser@example.com',
+ name: 'Test User',
+ username: 'testuser',
+ streak: 15,
+ totalProblems: 245,
+ totalContests: 12,
+}
+
+export const MOCK_DASHBOARD_DATA = {
+ streak: 15,
+ totalProblems: 245,
+ totalContests: 12,
+ totalTime: 1230,
+ easyCount: 89,
+ mediumCount: 120,
+ hardCount: 36,
+ platformData: {
+ leetcode: { count: 150, streak: 12 },
+ codeforces: { count: 95, streak: 8 },
+ geeksforgeeks: { count: 0, streak: 0 },
+ },
+ recentProblems: [
+ {
+ id: 1,
+ title: 'Two Sum',
+ platform: 'leetcode',
+ difficulty: 'Easy',
+ status: 'solved',
+ timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000),
+ },
+ {
+ id: 2,
+ title: 'Median of Two Sorted Arrays',
+ platform: 'leetcode',
+ difficulty: 'Hard',
+ status: 'solved',
+ timestamp: new Date(Date.now() - 5 * 60 * 60 * 1000),
+ },
+ {
+ id: 3,
+ title: 'A. Watermelon',
+ platform: 'codeforces',
+ difficulty: 'Easy',
+ status: 'attempted',
+ timestamp: new Date(Date.now() - 24 * 60 * 60 * 1000),
+ },
+ ],
+ achievements: [
+ { id: 1, name: 'First Steps', description: 'Solve your first problem', unlockedAt: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) },
+ { id: 2, name: '7-Day Streak', description: 'Maintain a 7-day streak', unlockedAt: new Date(Date.now() - 20 * 24 * 60 * 60 * 1000) },
+ { id: 3, name: 'Century Club', description: 'Solve 100 problems', unlockedAt: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000) },
+ ],
+}
+
+export const MOCK_PRACTICE_DATA = {
+ missions: [
+ {
+ id: 1,
+ category: 'mission',
+ difficulty: 'Easy',
+ platform: 'leetcode',
+ title: 'Two Sum',
+ description: 'Find two numbers that add up to target',
+ url: 'https://leetcode.com/problems/two-sum/',
+ estimatedTime: 15,
+ },
+ {
+ id: 2,
+ category: 'weakness',
+ difficulty: 'Medium',
+ platform: 'leetcode',
+ title: 'LRU Cache',
+ description: 'Design and implement LRU Cache',
+ url: 'https://leetcode.com/problems/lru-cache/',
+ estimatedTime: 45,
+ },
+ {
+ id: 3,
+ category: 'levelup',
+ difficulty: 'Hard',
+ platform: 'leetcode',
+ title: 'Merge K Sorted Lists',
+ description: 'Merge multiple sorted linked lists',
+ url: 'https://leetcode.com/problems/merge-k-sorted-lists/',
+ estimatedTime: 60,
+ },
+ {
+ id: 4,
+ category: 'explore',
+ difficulty: 'Medium',
+ platform: 'codeforces',
+ title: 'B. Restaurant',
+ description: 'Solve Codeforces problem',
+ url: 'https://codeforces.com/problemset/problem/1/B',
+ estimatedTime: 30,
+ },
+ {
+ id: 5,
+ category: 'stretch',
+ difficulty: 'Hard',
+ platform: 'leetcode',
+ title: 'Edit Distance',
+ description: 'Dynamic Programming challenge',
+ url: 'https://leetcode.com/problems/edit-distance/',
+ estimatedTime: 50,
+ },
+ ],
+}
+
+export const MOCK_COMMUNITY_DATA = {
+ posts: [
+ {
+ id: 1,
+ author: 'alice',
+ content: 'Just completed my 100th LeetCode problem! 🎉',
+ topic: 'general',
+ likes: 45,
+ comments: 12,
+ timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000),
+ liked: false,
+ },
+ {
+ id: 2,
+ author: 'bob',
+ content: 'Best way to master dynamic programming - Tips and tricks',
+ topic: 'dynamic-programming',
+ likes: 89,
+ comments: 23,
+ timestamp: new Date(Date.now() - 5 * 60 * 60 * 1000),
+ liked: true,
+ },
+ {
+ id: 3,
+ author: 'charlie',
+ content: 'Struggling with graph problems? Start with BFS/DFS',
+ topic: 'graphs',
+ likes: 67,
+ comments: 18,
+ timestamp: new Date(Date.now() - 12 * 60 * 60 * 1000),
+ liked: false,
+ },
+ {
+ id: 4,
+ author: 'diana',
+ content: 'System Design interview prep - resources and timeline',
+ topic: 'system-design',
+ likes: 123,
+ comments: 34,
+ timestamp: new Date(Date.now() - 24 * 60 * 60 * 1000),
+ liked: false,
+ },
+ ],
+}
+
+export const MOCK_CONTESTS_DATA = {
+ upcoming: [
+ {
+ id: 1,
+ platform: 'leetcode',
+ name: 'LeetCode Weekly Contest 385',
+ startTime: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
+ duration: 90,
+ participantCount: 5432,
+ difficulty: 'Medium',
+ registered: true,
+ },
+ {
+ id: 2,
+ platform: 'codeforces',
+ name: 'Codeforces Round #923',
+ startTime: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000),
+ duration: 120,
+ participantCount: 8234,
+ difficulty: 'Mixed',
+ registered: false,
+ },
+ ],
+ recent: [
+ {
+ id: 3,
+ platform: 'leetcode',
+ name: 'LeetCode Weekly Contest 384',
+ startTime: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
+ duration: 90,
+ rank: 245,
+ totalParticipants: 5123,
+ score: 18,
+ },
+ ],
+}
+
+export const MOCK_PROFILE_DATA = {
+ user: MOCK_USER,
+ stats: {
+ totalProblems: 245,
+ totalContests: 12,
+ currentStreak: 15,
+ longestStreak: 32,
+ easyCount: 89,
+ mediumCount: 120,
+ hardCount: 36,
+ acceptance: 0.87,
+ },
+ platforms: [
+ {
+ platform: 'leetcode',
+ username: 'testuser',
+ solved: 150,
+ verified: true,
+ verifiedAt: new Date(Date.now() - 60 * 24 * 60 * 60 * 1000),
+ },
+ {
+ platform: 'codeforces',
+ username: 'testuser',
+ solved: 95,
+ verified: true,
+ verifiedAt: new Date(Date.now() - 45 * 24 * 60 * 60 * 1000),
+ },
+ ],
+ badges: [
+ { name: 'Century', description: '100 problems solved', icon: '🏆' },
+ { name: 'Legend', description: '15-day streak', icon: '⭐' },
+ { name: 'Speedster', description: 'Complete 5 problems in 1 hour', icon: '⚡' },
+ ],
+}
+
+export const MOCK_CHALLENGES_DATA = {
+ challenges: [
+ {
+ id: 1,
+ title: 'Week 1: Arrays & Hashing',
+ description: 'Master the fundamentals',
+ difficulty: 'Easy',
+ duration: '7 days',
+ problems: 10,
+ completed: 6,
+ progress: 60,
+ },
+ {
+ id: 2,
+ title: 'Week 2: Two Pointers',
+ description: 'Solve problems efficiently',
+ difficulty: 'Medium',
+ duration: '7 days',
+ problems: 12,
+ completed: 4,
+ progress: 33,
+ },
+ {
+ id: 3,
+ title: 'Binary Trees Bootcamp',
+ description: 'Deep dive into tree problems',
+ difficulty: 'Medium',
+ duration: '14 days',
+ problems: 20,
+ completed: 0,
+ progress: 0,
+ },
+ ],
+}
+
+export const MOCK_PROBLEMS_DATA = {
+ problems: [
+ {
+ id: 1,
+ title: 'Two Sum',
+ difficulty: 'Easy',
+ platform: 'leetcode',
+ acceptance: 47.3,
+ likes: 15243,
+ dislikes: 456,
+ },
+ {
+ id: 2,
+ title: 'Add Two Numbers',
+ difficulty: 'Medium',
+ platform: 'leetcode',
+ acceptance: 32.1,
+ likes: 8234,
+ dislikes: 1234,
+ },
+ {
+ id: 3,
+ title: 'Median of Two Sorted Arrays',
+ difficulty: 'Hard',
+ platform: 'leetcode',
+ acceptance: 28.5,
+ likes: 9234,
+ dislikes: 2345,
+ },
+ {
+ id: 4,
+ title: 'A. Watermelon',
+ difficulty: 'Easy',
+ platform: 'codeforces',
+ acceptance: 98.2,
+ likes: 234,
+ dislikes: 12,
+ },
+ ],
+}
+
+export const MOCK_RECOMMENDATIONS_DATA = {
+ recommendations: [
+ {
+ id: 1,
+ title: 'Strengthen Your Weak Areas',
+ problems: [
+ { title: 'LRU Cache', difficulty: 'Hard', topic: 'Design' },
+ { title: 'Word Ladder', difficulty: 'Hard', topic: 'BFS' },
+ ],
+ reason: 'Based on your recent attempts',
+ },
+ {
+ id: 2,
+ title: 'Trending Problems This Week',
+ problems: [
+ { title: 'Maximum Sum Subarray', difficulty: 'Medium', topic: 'DP' },
+ { title: 'Merge K Sorted Lists', difficulty: 'Hard', topic: 'Heap' },
+ ],
+ reason: 'Popular among users with your skill level',
+ },
+ ],
+}
diff --git a/frontend/website/src/tour/TourContext.jsx b/frontend/website/src/tour/TourContext.jsx
new file mode 100644
index 0000000..743dd39
--- /dev/null
+++ b/frontend/website/src/tour/TourContext.jsx
@@ -0,0 +1,87 @@
+import { createContext, useContext, useState } from 'react'
+
+const TourContext = createContext()
+
+export const TourProvider = ({ children }) => {
+ const [isTourActive, setIsTourActive] = useState(false)
+ const [currentStep, setCurrentStep] = useState(0)
+ const [tourCompleted, setTourCompleted] = useState(() => {
+ // Check if user has already completed the tour
+ const stored = localStorage.getItem('algoSprint_tourCompleted')
+ return stored === 'true'
+ })
+
+ // Start tour - typically called after onboarding completes
+ const startTour = () => {
+ localStorage.setItem('algoSprint_tourStarted', 'true')
+ setIsTourActive(true)
+ setCurrentStep(0)
+ }
+
+ // Go to next step
+ const nextStep = () => {
+ setCurrentStep(prev => prev + 1)
+ }
+
+ // Go to previous step
+ const prevStep = () => {
+ setCurrentStep(prev => Math.max(0, prev - 1))
+ }
+
+ // Jump to specific step
+ const goToStep = (stepIndex) => {
+ setCurrentStep(stepIndex)
+ }
+
+ // Complete tour
+ const completeTour = () => {
+ setIsTourActive(false)
+ setTourCompleted(true)
+ localStorage.setItem('algoSprint_tourCompleted', 'true')
+ }
+
+ // Skip tour
+ const skipTour = () => {
+ setIsTourActive(false)
+ setTourCompleted(true)
+ localStorage.setItem('algoSprint_tourCompleted', 'true')
+ }
+
+ // Reset tour (for demo purposes)
+ const resetTour = () => {
+ setTourCompleted(false)
+ setCurrentStep(0)
+ setIsTourActive(false)
+ localStorage.removeItem('algoSprint_tourCompleted')
+ localStorage.removeItem('algoSprint_tourStarted')
+ }
+
+ return (
+
+ {children}
+
+ )
+}
+
+export const useTour = () => {
+ const context = useContext(TourContext)
+ if (!context) {
+ throw new Error('useTour must be used within TourProvider')
+ }
+ return context
+}
\ No newline at end of file
diff --git a/frontend/website/src/tour/TourOverlay.jsx b/frontend/website/src/tour/TourOverlay.jsx
new file mode 100644
index 0000000..4ed22a8
--- /dev/null
+++ b/frontend/website/src/tour/TourOverlay.jsx
@@ -0,0 +1,245 @@
+import { useEffect, useRef, useState } from 'react'
+import { useNavigate } from 'react-router-dom'
+import { useTour } from './TourContext'
+import { getTourStepByIndex, TOTAL_TOUR_STEPS } from './tourSteps'
+
+export default function TourOverlay() {
+ const navigate = useNavigate()
+ const { isTourActive, currentStep, nextStep, prevStep, skipTour, completeTour } = useTour()
+
+ const [highlightBox, setHighlightBox] = useState(null)
+ const [tooltipPos, setTooltipPos] = useState({ top: 0, left: 0 })
+ const [isVisible, setIsVisible] = useState(false)
+ const [isDarkMode, setIsDarkMode] = useState(false)
+ const overlayRef = useRef(null)
+
+ const step = getTourStepByIndex(currentStep)
+
+ // Sync theme configuration changes from document root attribute
+ useEffect(() => {
+ if (!isTourActive) return
+ const checkTheme = () => {
+ const theme = document.documentElement.getAttribute('data-theme')
+ setIsDarkMode(theme === 'dark')
+ }
+
+ checkTheme()
+ const observer = new MutationObserver(checkTheme)
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] })
+ return () => observer.disconnect()
+ }, [isTourActive])
+
+ // Layout step dynamic paths
+ useEffect(() => {
+ if (!isTourActive || !step) {
+ setIsVisible(false)
+ return
+ }
+
+ if (step.navigate) navigate(step.navigate)
+
+ const timer = setTimeout(() => setIsVisible(true), 400)
+ return () => clearTimeout(timer)
+ }, [currentStep, step, isTourActive, navigate])
+
+ // Handle recalculation tracking bounds
+ useEffect(() => {
+ if (!isTourActive || !step) return
+
+ const updatePositioning = () => {
+ if (!step.highlight || step.target === 'body') {
+ setHighlightBox(null)
+ return
+ }
+
+ const element = document.querySelector(step.target)
+ if (!element) return setHighlightBox(null)
+
+ const rect = element.getBoundingClientRect()
+ const padding = step.padding ?? 12
+
+ setHighlightBox({
+ top: rect.top - padding + window.scrollY,
+ left: rect.left - padding + window.scrollX,
+ width: rect.width + padding * 2,
+ height: rect.height + padding * 2,
+ })
+
+ const tooltipWidth = 360, tooltipHeight = 280, gap = 16, paddingViewport = 20
+ let top = 0, left = 0
+ const position = step.position || 'right'
+
+ switch (position) {
+ case 'right':
+ left = rect.right + window.scrollX + gap
+ top = rect.top + window.scrollY - (tooltipHeight - rect.height) / 2
+ break
+ case 'left':
+ left = rect.left + window.scrollX - tooltipWidth - gap
+ top = rect.top + window.scrollY - (tooltipHeight - rect.height) / 2
+ break
+ case 'bottom':
+ left = rect.left + window.scrollX + (rect.width - tooltipWidth) / 2
+ top = rect.bottom + window.scrollY + gap
+ break
+ case 'top':
+ left = rect.left + window.scrollX + (rect.width - tooltipWidth) / 2
+ top = rect.top + window.scrollY - tooltipHeight - gap
+ break
+ default:
+ left = (window.innerWidth - tooltipWidth) / 2
+ top = Math.max(120, (window.innerHeight - tooltipHeight) / 2)
+ }
+
+ if (left < paddingViewport) left = paddingViewport
+ if (left + tooltipWidth > window.innerWidth - paddingViewport) left = window.innerWidth - tooltipWidth - paddingViewport
+ top = Math.max(paddingViewport, Math.min(top, window.innerHeight - tooltipHeight - paddingViewport))
+
+ setTooltipPos({ top: Math.round(top), left: Math.round(left) })
+ }
+
+ const timer = setTimeout(updatePositioning, 500)
+ window.addEventListener('resize', updatePositioning)
+ window.addEventListener('scroll', updatePositioning)
+
+ return () => {
+ clearTimeout(timer)
+ window.removeEventListener('resize', updatePositioning)
+ window.removeEventListener('scroll', updatePositioning)
+ }
+ }, [isTourActive, step])
+
+ // Hotkeys Listener
+ useEffect(() => {
+ if (!isTourActive) return
+ const handleKeyDown = (e) => {
+ if (e.key === 'ArrowRight' || e.key === ' ') {
+ e.preventDefault()
+ currentStep < TOTAL_TOUR_STEPS - 1 ? nextStep() : completeTour()
+ } else if (e.key === 'ArrowLeft' && currentStep > 0) {
+ e.preventDefault()
+ prevStep()
+ } else if (e.key === 'Escape') {
+ e.preventDefault()
+ skipTour()
+ }
+ }
+ window.addEventListener('keydown', handleKeyDown)
+ return () => window.removeEventListener('keydown', handleKeyDown)
+ }, [isTourActive, currentStep, nextStep, prevStep, skipTour, completeTour])
+
+ if (!isTourActive || !step) return null
+
+ const isLastStep = currentStep === TOTAL_TOUR_STEPS - 1
+ const progress = ((currentStep + 1) / TOTAL_TOUR_STEPS) * 100
+
+ const maskColor = isDarkMode ? 'rgba(0, 0, 0, 0.25)' : 'rgba(0, 0, 0, 0.45)'
+
+ return (
+ <>
+ {/* Context Backdrop */}
+
+
+ {/* Spotlight Vector Wrap */}
+ {highlightBox && (
+
+ )}
+
+ {/* Main Presentation Card */}
+
+ {/* Header Row */}
+
+
+ STEP {currentStep + 1} OF {TOTAL_TOUR_STEPS}
+
+
+
+
+ {/* Content Elements */}
+
+
{step.title}
+ {/* FIXED: whiteSpace handles parsing '\n' tags correctly */}
+
+ {step.description}
+
+
+
+ {/* Micro Linear Tracker */}
+
+
+ {/* Balanced Actions Layout */}
+
+
+
+
+
+
+ >
+ )
+}
\ No newline at end of file
diff --git a/frontend/website/src/tour/TourStarter.jsx b/frontend/website/src/tour/TourStarter.jsx
new file mode 100644
index 0000000..d4f657b
--- /dev/null
+++ b/frontend/website/src/tour/TourStarter.jsx
@@ -0,0 +1,16 @@
+import { useTour } from './TourContext'
+
+/**
+ * TourStarter
+ * Button component to manually start the tour
+ * Can be placed in settings, profile, or anywhere user might want to re-run tour
+ */
+export default function TourStarter({ label = '🎯 Take the Tour', className = 'btn btn-secondary' }) {
+ const { startTour } = useTour()
+
+ return (
+
+ )
+}
\ No newline at end of file
diff --git a/frontend/website/src/tour/tourSteps.jsx b/frontend/website/src/tour/tourSteps.jsx
new file mode 100644
index 0000000..79b3b53
--- /dev/null
+++ b/frontend/website/src/tour/tourSteps.jsx
@@ -0,0 +1,133 @@
+/**
+ * tourSteps.js - Updated with Navigation
+ * Each step now navigates to the correct page/feature
+ */
+
+export const TOUR_STEPS = [
+ {
+ id: 'welcome',
+ title: '🚀 Welcome to AlgoSprint!',
+ description: 'You\'re all set! Let\'s take a quick tour to show you around.\n\nYou\'ll learn how to track your progress, find problems, compete with others, and master your coding skills.',
+ target: 'body',
+ position: 'center',
+ highlight: false,
+ navigate: null, // Stays on dashboard
+ },
+ {
+ id: 'sidebar',
+ title: '🧭 Your Navigation Hub',
+ description: 'This is your sidebar - your central command center!\n\nFrom here you can:\n• View your dashboard\n• Practice problems\n• Join challenges\n• Connect with community\n• Check your profile',
+ target: '.sidebar',
+ position: 'right',
+ highlight: true,
+ padding: 8,
+ navigate: null, // Stay on dashboard to see sidebar
+ },
+ {
+ id: 'streak',
+ title: '🔥 Build Your Streak',
+ description: 'This is your streak counter. Solve problems every day to maintain your streak!\n\nConsistency is the key to improvement. Your streak is visible to the community.',
+ target: '.sidebar-xp-bar',
+ position: 'right',
+ highlight: true,
+ padding: 8,
+ navigate: null,
+ },
+ {
+ id: 'dashboard-overview',
+ title: '📊 Your Personal Dashboard',
+ description: 'This is your command center where you see all your stats at a glance:\n\n• Problems solved\n• Current streak & ranking\n• Performance metrics\n• Weak topics analysis\n• Progress charts',
+ target: '.page-content',
+ position: 'center',
+ highlight: true,
+ padding: 0,
+ navigate: '/dashboard',
+ },
+ {
+ id: 'training',
+ title: '📚 Training Section',
+ description: 'Here\'s where you practice problems from LeetCode, Codeforces, and GeeksforGeeks.\n\nYou can:\n• Filter by difficulty\n• Search by topic\n• Track what you\'ve solved\n• Get curated recommendations\n• Solve problems from multiple platforms',
+ target: '.page-content',
+ position: 'center',
+ highlight: true,
+ padding: 0,
+ navigate: '/problems', // Navigate to problems page
+ },
+ {
+ id: 'challenges',
+ title: '🏆 Take on Challenges',
+ description: 'Compete with other users in real-time challenges!\n\nSolve problems faster, aim for the leaderboard, and earn recognition.\n\nGreat for:\n• Testing your skills\n• Networking with coders',
+ target: '.page-content',
+ position: 'center',
+ highlight: true,
+ padding: 0,
+ navigate: '/challenges',
+ },
+ {
+ id: 'community',
+ title: '👥 Connect with Community',
+ description: 'Share your progress, learn from others, and stay motivated!\n\nThe community section lets you:\n• See what others are working on\n• Share your solutions',
+ target: '.page-content',
+ position: 'center',
+ highlight: true,
+ padding: 0,
+ navigate: '/community',
+ },
+ {
+ id: 'profile',
+ title: '⚙️ Your Profile & Settings',
+ description: 'Customize your experience here!\n\nYou can:\n• Update your bio and picture\n• Connect more platforms\n• View detailed statistics',
+ target: '.page-content',
+ position: 'center',
+ highlight: true,
+ padding: 0,
+ navigate: '/profile',
+ },
+ {
+ id: 'topbar',
+ title: '🔔 Stay Updated',
+ description: 'The top bar keeps you in the loop with:\n\n• Notifications on new challenges\n• Updates on platform syncs\n• Quick access to search',
+ target: '.topbar',
+ position: 'bottom',
+ highlight: false,
+ padding: 8,
+ navigate: '/dashboard',
+ },
+ {
+ id: 'tips',
+ title: '💡 Pro Tips to Get Started',
+ description: 'Here are some tips to maximize your learning:\n\n1. Solve one problem daily to maintain your streak\n2. Start with Easy problems, work your way up\n3. Join challenges to practice under pressure\n\nRemember: consistency beats intensity! 💪',
+ target: 'body',
+ position: 'center',
+ highlight: false,
+ navigate: '/dashboard',
+ },
+ {
+ id: 'finish',
+ title: '✨ You\'re Ready to Code!',
+ description: 'You now have a complete overview of AlgoSprint.\n\nHere\'s what you should do next:\n\nHappy coding! 🚀',
+ target: 'body',
+ position: 'center',
+ highlight: false,
+ navigate: null, // User can decide where to go
+ },
+]
+
+/**
+ * Get tour step by ID
+ */
+export const getTourStep = (stepId) => {
+ return TOUR_STEPS.find(step => step.id === stepId)
+}
+
+/**
+ * Get tour step by index
+ */
+export const getTourStepByIndex = (index) => {
+ return TOUR_STEPS[index]
+}
+
+/**
+ * Total number of steps
+ */
+export const TOTAL_TOUR_STEPS = TOUR_STEPS.length
\ No newline at end of file
diff --git a/frontend/website/src/utils/themeStyles.js b/frontend/website/src/utils/themeStyles.js
new file mode 100644
index 0000000..f3ee3e6
--- /dev/null
+++ b/frontend/website/src/utils/themeStyles.js
@@ -0,0 +1,42 @@
+/** Inline style tokens that follow light/dark via CSS variables in index.css */
+
+export const CARD = {
+ background: 'var(--card-bg)',
+ backdropFilter: 'blur(24px)',
+ WebkitBackdropFilter: 'blur(24px)',
+ border: '1px solid var(--card-border)',
+ boxShadow: 'var(--surface-shadow)',
+ borderRadius: 18,
+ padding: 22,
+}
+
+export const CARD_COMPACT = {
+ ...CARD,
+ borderRadius: 16,
+ padding: 22,
+ backdropFilter: 'blur(20px)',
+ WebkitBackdropFilter: 'blur(20px)',
+}
+
+export const CHART_TOOLTIP = {
+ background: 'var(--chart-tooltip-bg)',
+ border: '1px solid var(--chart-tooltip-border)',
+ borderRadius: 10,
+ padding: '9px 14px',
+ backdropFilter: 'blur(16px)',
+ boxShadow: 'var(--shadow-md)',
+}
+
+export const PROGRESS_TRACK = {
+ background: 'var(--xp-track-bg)',
+ borderRadius: 10,
+ overflow: 'hidden',
+}
+
+export const HEATMAP_COLORS = [
+ 'var(--heatmap-0)',
+ 'var(--heatmap-1)',
+ 'var(--heatmap-2)',
+ 'var(--heatmap-3)',
+ 'var(--heatmap-4)',
+]