update error

This commit is contained in:
5t4l1n
2025-07-28 23:19:59 +05:30
parent 7f6531b097
commit 8816091e63
19 changed files with 4407 additions and 691 deletions
+280
View File
@@ -0,0 +1,280 @@
"use client"
import { useState, useEffect, useRef } from "react"
import { useRouter } from "next/navigation"
import { useAuth } from "@/context/auth-context"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Separator } from "@/components/ui/separator"
import { Alert, AlertDescription } from "@/components/ui/alert"
import { Wallet, Mail, Lock, Loader2, Shield, CheckCircle2, AlertCircle } from "lucide-react"
import { toast } from "react-hot-toast"
export default function LoginPage() {
const {
connectWallet,
loginWithEmail,
isLoadingAuth,
walletConnected,
walletAddress,
firebaseUser,
authMethod
} = useAuth()
const router = useRouter()
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
const [isEmailLogin, setIsEmailLogin] = useState(false)
const [isConnectingWallet, setIsConnectingWallet] = useState(false)
const [isSubmittingEmail, setIsSubmittingEmail] = useState(false)
const hasRedirected = useRef(false)
// ✅ Check for existing authentication
useEffect(() => {
if (hasRedirected.current || isLoadingAuth) return
const checkAuth = setTimeout(() => {
if (isLoadingAuth) return
const isAuthenticated = (walletConnected && walletAddress) || firebaseUser
if (isAuthenticated && !hasRedirected.current) {
console.log('✅ User already authenticated, redirecting to dashboard...')
hasRedirected.current = true
router.replace("/dashboard")
}
}, 500)
return () => clearTimeout(checkAuth)
}, [isLoadingAuth, walletConnected, walletAddress, firebaseUser, router])
// ✅ Handle MetaMask connection
const handleWalletConnect = async () => {
if (isConnectingWallet || isLoadingAuth) return
setIsConnectingWallet(true)
try {
console.log('🦊 Starting MetaMask connection...')
// Check if MetaMask is installed
if (typeof window !== 'undefined' && !window.ethereum) {
toast.error("MetaMask not detected. Please install MetaMask extension.")
window.open('https://metamask.io/download/', '_blank')
return
}
const success = await connectWallet()
if (success) {
console.log('✅ MetaMask connection successful')
// Redirect will be handled by useEffect
}
} catch (error: any) {
console.error('❌ Wallet connection error:', error)
} finally {
setIsConnectingWallet(false)
}
}
// ✅ Handle email login
const handleEmailLogin = async (e: React.FormEvent) => {
e.preventDefault()
if (isSubmittingEmail || isLoadingAuth) return
if (!email.trim() || !password.trim()) {
toast.error("Please enter both email and password")
return
}
setIsSubmittingEmail(true)
try {
await loginWithEmail(email, password)
// Redirect will be handled by useEffect
} catch (error: any) {
console.error('❌ Email login failed:', error)
toast.error(error.message || "Login failed. Please check your credentials.")
} finally {
setIsSubmittingEmail(false)
}
}
// Show connected state
if ((walletConnected && walletAddress) || firebaseUser) {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 via-indigo-50 to-purple-50 dark:from-gray-900 dark:via-blue-900/20 dark:to-purple-900/20 p-4">
<Card className="w-full max-w-md shadow-2xl">
<CardHeader className="text-center">
<CheckCircle2 className="w-12 h-12 text-green-600 mx-auto mb-4" />
<CardTitle className="text-xl font-bold text-green-600">
{walletConnected ? "MetaMask Connected! 🦊" : "Email Login Successful! 📧"}
</CardTitle>
</CardHeader>
<CardContent className="text-center space-y-4">
<Alert className="border-green-200 bg-green-50">
<AlertDescription className="text-green-700">
{walletConnected
? `🦊 ${walletAddress?.slice(0, 6)}...${walletAddress?.slice(-4)}`
: `📧 ${firebaseUser?.email}`
}
</AlertDescription>
</Alert>
<Button
onClick={() => {
if (!hasRedirected.current) {
hasRedirected.current = true
router.replace("/dashboard")
}
}}
className="w-full"
>
Go to Dashboard
</Button>
</CardContent>
</Card>
</div>
)
}
// Show loading while initializing
if (isLoadingAuth) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<Loader2 className="w-8 h-8 animate-spin mx-auto mb-4" />
<p>Initializing...</p>
</div>
</div>
)
}
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 via-indigo-50 to-purple-50 dark:from-gray-900 dark:via-blue-900/20 dark:to-purple-900/20 p-4">
<Card className="w-full max-w-md shadow-2xl">
<CardHeader className="text-center space-y-4">
<div className="mx-auto w-16 h-16 bg-gradient-to-br from-purple-500 to-blue-600 rounded-full flex items-center justify-center">
<Shield className="w-8 h-8 text-white" />
</div>
<CardTitle className="text-2xl font-bold bg-gradient-to-r from-purple-600 to-blue-600 bg-clip-text text-transparent">
Welcome to OpenLearnX! 🎓
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* MetaMask Login */}
<div className="space-y-4">
<Button
onClick={handleWalletConnect}
disabled={isConnectingWallet || isLoadingAuth || isSubmittingEmail}
className="w-full bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 text-white py-3"
>
{isConnectingWallet ? (
<>
<Loader2 className="w-5 h-5 mr-2 animate-spin" />
Connecting MetaMask...
</>
) : (
<>
<Wallet className="w-5 h-5 mr-2" />
Connect MetaMask Wallet 🦊
</>
)}
</Button>
<div className="bg-purple-50 dark:bg-purple-900/20 p-3 rounded-lg">
<p className="text-xs text-purple-700 dark:text-purple-300 text-center">
Recommended: Get Web3 features and blockchain verification!
</p>
</div>
</div>
<Separator />
{/* Email Login */}
<div className="space-y-4">
<Button
variant="outline"
onClick={() => setIsEmailLogin(!isEmailLogin)}
disabled={isConnectingWallet || isSubmittingEmail}
className="w-full"
>
<Mail className="w-4 h-4 mr-2" />
{isEmailLogin ? 'Hide Email Login' : 'Login with Email'}
</Button>
{isEmailLogin && (
<form onSubmit={handleEmailLogin} className="space-y-4">
<div>
<Label htmlFor="email">Email Address</Label>
<Input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter your email"
disabled={isSubmittingEmail || isConnectingWallet}
required
/>
</div>
<div>
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Enter your password"
disabled={isSubmittingEmail || isConnectingWallet}
required
/>
</div>
<Button
type="submit"
disabled={isSubmittingEmail || isConnectingWallet || !email.trim() || !password.trim()}
className="w-full"
>
{isSubmittingEmail ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Logging in...
</>
) : (
<>
<Lock className="w-4 h-4 mr-2" />
Login with Email
</>
)}
</Button>
</form>
)}
</div>
{/* MetaMask Installation Help */}
{typeof window !== 'undefined' && !window.ethereum && (
<Alert className="border-orange-200 bg-orange-50">
<AlertCircle className="w-4 h-4 text-orange-600" />
<AlertDescription className="text-orange-700">
MetaMask not detected.
<Button
variant="link"
className="p-0 h-auto font-semibold text-orange-600 ml-1"
onClick={() => window.open('https://metamask.io/download/', '_blank')}
>
Install MetaMask
</Button>
</AlertDescription>
</Alert>
)}
</CardContent>
</Card>
</div>
)
}
+116
View File
@@ -1,5 +1,121 @@
"use client"
import { useEffect, useState } from "react"
import { useRouter } from "next/navigation"
import { useAuth } from "@/context/auth-context"
import { DashboardStatsOverview } from "@/components/dashboard-stats"
import { Loader2, AlertCircle } from "lucide-react"
import { Button } from "@/components/ui/button"
export default function DashboardPage() {
const { isLoadingAuth, walletConnected, walletAddress, firebaseUser, authMethod } = useAuth()
const router = useRouter()
const [showDashboard, setShowDashboard] = useState(false)
const [debugInfo, setDebugInfo] = useState<any>(null)
useEffect(() => {
// Debug authentication state
const authState = {
isLoadingAuth,
walletConnected,
walletAddress: !!walletAddress,
firebaseUser: !!firebaseUser,
authMethod,
localStorage: {
token: !!localStorage.getItem('openlearnx_jwt_token'),
wallet: !!localStorage.getItem('openlearnx_wallet'),
user: !!localStorage.getItem('openlearnx_user')
}
}
setDebugInfo(authState)
console.log('📊 Dashboard auth state:', authState)
// Give auth some time to initialize
const timer = setTimeout(() => {
const isAuthenticated = (walletConnected && walletAddress) || firebaseUser
if (isAuthenticated) {
console.log('✅ User authenticated, showing dashboard')
setShowDashboard(true)
} else if (!isLoadingAuth) {
console.log('❌ User not authenticated, redirecting to login')
router.replace("/auth/login")
}
}, 2000) // Wait 2 seconds for auth to stabilize
return () => clearTimeout(timer)
}, [isLoadingAuth, walletConnected, walletAddress, firebaseUser, authMethod, router])
// Show loading state
if (isLoadingAuth || !showDashboard) {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 dark:from-gray-900 dark:via-blue-900 dark:to-purple-900">
<div className="text-center space-y-4 max-w-md mx-auto p-6">
<Loader2 className="w-12 h-12 animate-spin mx-auto text-purple-600" />
<div className="space-y-2">
<h3 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
Loading Dashboard...
</h3>
<p className="text-gray-600 dark:text-gray-400">
{walletConnected ? `Connected to ${walletAddress?.slice(0, 6)}...${walletAddress?.slice(-4)}` :
firebaseUser ? `Logged in as ${firebaseUser.email}` :
'Verifying authentication...'}
</p>
</div>
{/* Debug info in development */}
{process.env.NODE_ENV === 'development' && debugInfo && (
<details className="text-left text-xs text-gray-500 bg-gray-100 dark:bg-gray-800 p-2 rounded mt-4">
<summary>Debug Info</summary>
<pre>{JSON.stringify(debugInfo, null, 2)}</pre>
</details>
)}
</div>
</div>
)
}
// Show error state if no auth after loading
if (!walletConnected && !firebaseUser && !isLoadingAuth) {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 dark:from-gray-900 dark:via-blue-900 dark:to-purple-900">
<div className="text-center space-y-4 max-w-md mx-auto p-6">
<AlertCircle className="w-16 h-16 text-red-500 mx-auto" />
<h3 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
Authentication Required
</h3>
<p className="text-gray-600 dark:text-gray-400">
Please log in to access your dashboard.
</p>
<div className="space-y-2">
<Button onClick={() => router.push("/auth/login")} className="w-full">
Go to Login
</Button>
<Button
variant="outline"
onClick={() => {
localStorage.clear()
window.location.href = "/auth/login"
}}
className="w-full"
>
Clear Data & Login
</Button>
</div>
{/* Debug info */}
{process.env.NODE_ENV === 'development' && (
<details className="text-left text-xs text-gray-500 bg-gray-100 dark:bg-gray-800 p-2 rounded mt-4">
<summary>Debug Info</summary>
<pre>{JSON.stringify(debugInfo, null, 2)}</pre>
</details>
)}
</div>
</div>
)
}
// Show dashboard if authenticated
return <DashboardStatsOverview />
}
+20 -6
View File
@@ -10,9 +10,10 @@ import { ThemeProvider } from "@/components/theme-provider"
const inter = Inter({ subsets: ["latin"] })
export const metadata: Metadata = {
title: "OpenLearnX - Decentralized Adaptive Learning",
description: "AI-powered adaptive testing with blockchain-secured credentials.",
generator: 'v0.dev'
title: "OpenLearnX - Comprehensive Learning Dashboard",
description: "AI-powered adaptive learning with blockchain integration, real-time analytics, and professional progress tracking.",
keywords: "learning, coding, blockchain, AI, analytics, professional development",
generator: 'OpenLearnX v2.0'
}
export default function RootLayout({
@@ -25,9 +26,22 @@ export default function RootLayout({
<body className={inter.className}>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
<AuthProvider>
<Navbar />
<main>{children}</main>
<Toaster position="top-right" />
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 dark:from-gray-900 dark:via-blue-900 dark:to-purple-900">
<Navbar />
<main className="transition-all duration-300">{children}</main>
<Toaster
position="top-right"
toastOptions={{
duration: 4000,
style: {
background: 'rgba(17, 24, 39, 0.95)',
color: '#fff',
backdropFilter: 'blur(16px)',
border: '1px solid rgba(75, 85, 99, 0.3)',
},
}}
/>
</div>
</AuthProvider>
</ThemeProvider>
</body>
+314
View File
@@ -0,0 +1,314 @@
"use client"
import { useState, useEffect } from "react"
import { useRouter } from "next/navigation"
import { useAuth } from "@/context/auth-context"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Separator } from "@/components/ui/separator"
import { Alert, AlertDescription } from "@/components/ui/alert"
import { Wallet, Mail, Lock, Loader2, Shield, AlertCircle, CheckCircle2 } from "lucide-react"
import { toast } from "react-hot-toast"
export function LoginComponent() {
const {
connectWallet,
loginWithEmail,
isLoadingAuth,
walletConnected,
walletAddress,
user,
firebaseUser,
authMethod
} = useAuth()
const router = useRouter()
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
const [isEmailLogin, setIsEmailLogin] = useState(false)
const [isConnectingWallet, setIsConnectingWallet] = useState(false)
const [connectionStatus, setConnectionStatus] = useState<'idle' | 'connecting' | 'connected' | 'error'>('idle')
// ✅ Check if user is already authenticated
useEffect(() => {
if (!isLoadingAuth) {
if (walletConnected && walletAddress) {
console.log('✅ MetaMask already connected:', walletAddress)
setConnectionStatus('connected')
toast.success("Already connected to MetaMask!")
router.push("/dashboard")
} else if (firebaseUser) {
console.log('✅ Firebase user already logged in:', firebaseUser.email)
router.push("/dashboard")
}
}
}, [isLoadingAuth, walletConnected, walletAddress, firebaseUser, router])
const handleWalletConnect = async () => {
setIsConnectingWallet(true)
setConnectionStatus('connecting')
try {
console.log('🦊 Starting MetaMask connection...')
// Check if MetaMask is installed
if (typeof window !== 'undefined' && !window.ethereum) {
toast.error("MetaMask not detected. Please install MetaMask extension.")
setConnectionStatus('error')
return
}
const success = await connectWallet()
if (success) {
setConnectionStatus('connected')
console.log('✅ MetaMask connection successful')
toast.success("MetaMask connected successfully! 🦊")
// Small delay to ensure state is updated
setTimeout(() => {
router.push("/dashboard")
}, 1000)
} else {
setConnectionStatus('error')
toast.error("Failed to connect MetaMask. Please try again.")
}
} catch (error: any) {
console.error('❌ Wallet connection error:', error)
setConnectionStatus('error')
if (error.message?.includes('User rejected')) {
toast.error("Connection cancelled by user.")
} else if (error.message?.includes('MetaMask not detected')) {
toast.error("Please install MetaMask extension.")
} else {
toast.error("MetaMask connection failed. Please try again.")
}
} finally {
setIsConnectingWallet(false)
}
}
const handleEmailLogin = async (e: React.FormEvent) => {
e.preventDefault()
if (!email.trim() || !password.trim()) {
toast.error("Please enter both email and password")
return
}
if (!email.includes('@')) {
toast.error("Please enter a valid email address")
return
}
try {
console.log('📧 Attempting email login for:', email)
await loginWithEmail(email, password)
toast.success("Logged in successfully!")
router.push("/dashboard")
} catch (error: any) {
console.error('❌ Email login failed:', error)
toast.error(error.message || "Login failed. Please check your credentials.")
}
}
// ✅ Show connected state if already authenticated
if (connectionStatus === 'connected' || (walletConnected && walletAddress)) {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 via-indigo-50 to-purple-50 dark:from-gray-900 dark:via-blue-900/20 dark:to-purple-900/20 p-4">
<Card className="w-full max-w-md shadow-2xl border-0 bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm">
<CardHeader className="text-center">
<div className="mx-auto w-12 h-12 bg-green-100 dark:bg-green-900/20 rounded-full flex items-center justify-center mb-4">
<CheckCircle2 className="w-6 h-6 text-green-600" />
</div>
<CardTitle className="text-xl font-bold text-green-600">
MetaMask Connected! 🦊
</CardTitle>
</CardHeader>
<CardContent className="text-center space-y-4">
<Alert className="border-green-200 bg-green-50 dark:bg-green-900/20">
<Wallet className="w-4 h-4 text-green-600" />
<AlertDescription className="text-green-700 dark:text-green-300">
🦊 {walletAddress?.slice(0, 6)}...{walletAddress?.slice(-4)}
</AlertDescription>
</Alert>
<div className="space-y-2">
<Button onClick={() => router.push("/dashboard")} className="w-full">
Go to Dashboard
</Button>
<Button
variant="outline"
onClick={() => window.location.reload()}
className="w-full"
>
Connect Different Wallet
</Button>
</div>
</CardContent>
</Card>
</div>
)
}
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 via-indigo-50 to-purple-50 dark:from-gray-900 dark:via-blue-900/20 dark:to-purple-900/20 p-4">
<Card className="w-full max-w-md shadow-2xl border-0 bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm">
<CardHeader className="text-center space-y-4">
<div className="mx-auto w-16 h-16 bg-gradient-to-br from-purple-500 to-blue-600 rounded-full flex items-center justify-center">
<Shield className="w-8 h-8 text-white" />
</div>
<div className="space-y-2">
<CardTitle className="text-2xl font-bold bg-gradient-to-r from-purple-600 to-blue-600 bg-clip-text text-transparent">
Welcome to OpenLearnX! 🎓
</CardTitle>
<p className="text-gray-600 dark:text-gray-300">
Connect your MetaMask wallet or login with email
</p>
</div>
</CardHeader>
<CardContent className="space-y-6">
{/* MetaMask Login - Primary Option */}
<div className="space-y-4">
<Button
onClick={handleWalletConnect}
disabled={isConnectingWallet || isLoadingAuth || connectionStatus === 'connecting'}
className="w-full bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 text-white py-3 transition-all duration-200"
>
{isConnectingWallet || connectionStatus === 'connecting' ? (
<>
<Loader2 className="w-5 h-5 mr-2 animate-spin" />
Connecting MetaMask...
</>
) : connectionStatus === 'error' ? (
<>
<AlertCircle className="w-5 h-5 mr-2" />
Retry MetaMask Connection
</>
) : (
<>
<Wallet className="w-5 h-5 mr-2" />
Connect MetaMask Wallet 🦊
</>
)}
</Button>
<div className="bg-purple-50 dark:bg-purple-900/20 p-3 rounded-lg">
<p className="text-xs text-purple-700 dark:text-purple-300 text-center">
Recommended: Get Web3 features, blockchain verification, and token rewards!
</p>
</div>
</div>
<div className="relative">
<Separator />
<div className="absolute inset-0 flex items-center justify-center">
<span className="bg-white dark:bg-gray-800 px-3 text-sm text-gray-500">
or
</span>
</div>
</div>
{/* Email Login - Alternative Option */}
<div className="space-y-4">
<Button
variant="outline"
onClick={() => setIsEmailLogin(!isEmailLogin)}
className="w-full"
>
<Mail className="w-4 h-4 mr-2" />
{isEmailLogin ? 'Hide Email Login' : 'Login with Email'}
</Button>
{isEmailLogin && (
<form onSubmit={handleEmailLogin} className="space-y-4 mt-4">
<div>
<Label htmlFor="email">Email Address</Label>
<Input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter your email"
disabled={isLoadingAuth}
required
/>
</div>
<div>
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Enter your password"
disabled={isLoadingAuth}
required
/>
</div>
<Button
type="submit"
disabled={isLoadingAuth || !email.trim() || !password.trim()}
className="w-full"
>
{isLoadingAuth ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Logging in...
</>
) : (
<>
<Lock className="w-4 h-4 mr-2" />
Login with Email
</>
)}
</Button>
</form>
)}
</div>
{/* MetaMask Installation Help */}
{typeof window !== 'undefined' && !window.ethereum && (
<Alert className="border-orange-200 bg-orange-50 dark:bg-orange-900/20">
<AlertCircle className="w-4 h-4 text-orange-600" />
<AlertDescription className="text-orange-700 dark:text-orange-300">
MetaMask not detected.
<Button
variant="link"
className="p-0 h-auto font-semibold text-orange-600 ml-1"
onClick={() => window.open('https://metamask.io/download/', '_blank')}
>
Install MetaMask
</Button>
</AlertDescription>
</Alert>
)}
{/* Connection Status */}
{connectionStatus === 'error' && (
<Alert className="border-red-200 bg-red-50 dark:bg-red-900/20">
<AlertCircle className="w-4 h-4 text-red-600" />
<AlertDescription className="text-red-700 dark:text-red-300">
Connection failed. Please make sure MetaMask is unlocked and try again.
</AlertDescription>
</Alert>
)}
<div className="text-center">
<p className="text-xs text-gray-500">
New to OpenLearnX? Your account will be created automatically upon first login.
</p>
</div>
</CardContent>
</Card>
</div>
)
}
+155
View File
@@ -0,0 +1,155 @@
"use client"
import { useState } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Badge } from "@/components/ui/badge"
import { Alert, AlertDescription } from "@/components/ui/alert"
import {
User, Wallet, CheckCircle2, AlertCircle,
Loader2, Sparkles, Shield
} from "lucide-react"
import { toast } from "react-hot-toast"
import api from "@/lib/api"
interface UsernameSetupProps {
userProfile: {
user_id: string
wallet_address?: string
display_name?: string
username_set?: boolean
avatar_url?: string
}
onUsernameSet: (profile: any) => void
}
export function UsernameSetup({ userProfile, onUsernameSet }: UsernameSetupProps) {
const [username, setUsername] = useState("")
const [isSubmitting, setIsSubmitting] = useState(false)
const handleSubmitUsername = async () => {
if (!username.trim()) {
toast.error("Please enter a username")
return
}
if (username.length < 3) {
toast.error("Username must be at least 3 characters long")
return
}
setIsSubmitting(true)
try {
let response
try {
response = await api.post("/api/dashboard/set-username", {
username: username.trim()
})
} catch (error) {
// Fallback to update-profile
response = await api.post("/api/dashboard/update-profile", {
display_name: username.trim()
})
}
if (response.success) {
toast.success(`Username "${username}" set successfully! 🎉`)
onUsernameSet(response.profile || {
...userProfile,
display_name: username.trim(),
username_set: true
})
} else {
toast.error(response.error || "Failed to set username")
}
} catch (error: any) {
console.error('Username setting error:', error)
toast.error("Failed to set username. Please try again.")
} finally {
setIsSubmitting(false)
}
}
const walletAddress = userProfile?.wallet_address || userProfile?.user_id
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 via-indigo-50 to-purple-50 dark:from-gray-900 dark:via-blue-900/20 dark:to-purple-900/20 p-4">
<Card className="w-full max-w-md shadow-2xl">
<CardHeader className="text-center space-y-4">
<div className="mx-auto w-16 h-16 bg-gradient-to-br from-purple-500 to-blue-600 rounded-full flex items-center justify-center">
<User className="w-8 h-8 text-white" />
</div>
<CardTitle className="text-2xl font-bold bg-gradient-to-r from-purple-600 to-blue-600 bg-clip-text text-transparent">
Welcome to OpenLearnX! 🎓
</CardTitle>
<div className="bg-gradient-to-r from-purple-100 to-blue-100 dark:from-purple-900/30 dark:to-blue-900/30 p-4 rounded-lg">
<div className="flex items-center justify-center gap-2 mb-2">
<Wallet className="w-5 h-5 text-purple-600" />
<Badge variant="secondary" className="bg-purple-600 text-white">
MetaMask Connected
</Badge>
</div>
<p className="text-xs text-gray-600 dark:text-gray-400 font-mono break-all">
{walletAddress?.slice(0, 6)}...{walletAddress?.slice(-4)}
</p>
</div>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<Label htmlFor="username">Choose Your Username</Label>
<Input
id="username"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Enter your username"
maxLength={25}
disabled={isSubmitting}
/>
<div className="text-xs text-gray-500 space-y-1">
<p> 3-25 characters</p>
<p> Letters, numbers, and underscores recommended</p>
</div>
</div>
<div className="bg-blue-50 dark:bg-blue-900/20 p-4 rounded-lg">
<h4 className="font-medium text-blue-900 dark:text-blue-100 mb-2 flex items-center gap-2">
<Sparkles className="w-4 h-4" />
What you'll get:
</h4>
<ul className="text-sm text-blue-700 dark:text-blue-300 space-y-1">
<li> Personalized learning dashboard</li>
<li> Global leaderboard ranking</li>
<li> Blockchain-verified achievements</li>
<li> Community interaction</li>
</ul>
</div>
<Button
onClick={handleSubmitUsername}
disabled={!username.trim() || username.length < 3 || isSubmitting}
className="w-full bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700"
>
{isSubmitting ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Setting Username...
</>
) : (
<>
<Shield className="w-4 h-4 mr-2" />
Set Username & Continue
</>
)}
</Button>
</CardContent>
</Card>
</div>
)
}
+572 -96
View File
@@ -1,168 +1,644 @@
// frontend/components/dashboard-stats.tsx - ONLY REAL DATA
"use client"
import { useState, useEffect } from "react"
import { useAuth } from "@/context/auth-context"
import { useRouter } from "next/router"
import { useRouter } from "next/navigation"
import { toast } from "react-hot-toast"
import type { DashboardStats, ActivityData } from "@/lib/types"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Loader2, Award, BookOpen, Code, CheckCircle2, TrendingUp } from "lucide-react"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Progress } from "@/components/ui/progress"
import {
Trophy, BookOpen, Code, CheckCircle2, Wallet, Shield,
Activity, Target, Timer, Award, Zap, Globe, User,
BarChart3, Flame, Brain, Loader2, AlertCircle
} from "lucide-react"
import { UsernameSetup } from "./UsernameSetup"
import api from "@/lib/api"
interface DashboardStats {
total_xp: number
courses_completed: number
coding_problems_solved: number
quiz_accuracy: number
coding_streak: number
longest_streak: number
total_courses: number
total_quizzes: number
global_rank: number
weekly_activity: number[]
monthly_goals: { target: number; completed: number }
blockchain: {
wallet_connected: boolean
wallet_address: string
total_earned: number
transactions: number
certificates: number
verified_achievements: number
}
learning_analytics: {
time_spent_hours: number
average_session_minutes: number
completion_rate: number
favorite_topics: string[]
skill_levels: { [key: string]: number }
}
recent_achievements: Array<{
id: string
title: string
description: string
earned_at: string
points: number
rarity: string
}>
}
interface UserProfile {
user_id: string
wallet_address?: string
display_name?: string
username_set?: boolean
avatar_url?: string
created_at: string
}
interface ActivityData {
id: string
type: string
title: string
description: string
completed_at: string
points_earned: number
blockchain_verified?: boolean
}
interface LeaderboardEntry {
rank: number
user_id: string
username: string
display_name?: string
total_xp: number
streak: number
avatar?: string
wallet_address?: string
}
export function DashboardStatsOverview() {
const { user, firebaseUser, isLoadingAuth, authMethod, token } = useAuth() // Check token for access
const { walletAddress, walletConnected, isLoadingAuth } = useAuth()
const router = useRouter()
const [stats, setStats] = useState<DashboardStats | null>(null)
const [userProfile, setUserProfile] = useState<UserProfile | null>(null)
const [activity, setActivity] = useState<ActivityData[]>([])
const [leaderboard, setLeaderboard] = useState<LeaderboardEntry[]>([])
const [isLoadingData, setIsLoadingData] = useState(true)
const [usernameRequired, setUsernameRequired] = useState(false)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
if (!isLoadingAuth && !user && !firebaseUser) {
// Allow either MetaMask or Firebase user
toast.error("Please login to view your dashboard.")
router.push("/")
if (!isLoadingAuth && !walletConnected) {
toast.error("Please connect your MetaMask wallet to view dashboard.")
router.push("/auth/login")
return
}
const fetchDashboardData = async () => {
setIsLoadingData(true)
setError(null)
try {
// --- ORIGINAL API CALLS (UNCOMMENT WHEN BACKEND IS READY) ---
const statsResponse = await api.get<DashboardStats>("/api/dashboard/stats")
setStats(statsResponse.data)
if (walletConnected && walletAddress) {
fetchPureMongoDBData()
}
}, [walletConnected, walletAddress, isLoadingAuth, router])
const activityResponse = await api.get<ActivityData[]>("/api/dashboard/activity")
setActivity(activityResponse.data)
} catch (err: any) {
console.error("Failed to fetch dashboard data:", err)
setError(err.response?.data?.message || "Failed to load dashboard data.")
toast.error(err.response?.data?.message || "Failed to load dashboard data.")
} finally {
setIsLoadingData(false) // Handled by setTimeout
const fetchPureMongoDBData = async () => {
setIsLoadingData(true)
setError(null)
try {
console.log('📊 Fetching PURE MongoDB data for wallet:', walletAddress)
const [statsRes, activityRes, leaderboardRes] = await Promise.all([
api.get<{
success: boolean
data?: DashboardStats
user_profile: UserProfile
username_required?: boolean
data_source: string
message?: string
}>("/api/dashboard/comprehensive-stats"),
api.get<{success: boolean, data: ActivityData[], data_source: string}>("/api/dashboard/recent-activity"),
api.get<{success: boolean, data: LeaderboardEntry[], data_source: string}>("/api/dashboard/global-leaderboard")
])
// ✅ VERIFY DATA SOURCE IS PURE MONGODB
if (statsRes.data.data_source !== "pure_mongodb_data" && statsRes.data.data_source !== "empty_real_data") {
console.error("❌ Data source is not pure MongoDB:", statsRes.data.data_source)
toast.error("Invalid data source detected. Refreshing...")
return
}
}
if (user || firebaseUser) {
// Only fetch if either user type is logged in
fetchDashboardData()
}
}, [user, firebaseUser, isLoadingAuth, router, token])
if (statsRes.data.success) {
if (statsRes.data.username_required) {
setUsernameRequired(true)
setUserProfile(statsRes.data.user_profile)
setIsLoadingData(false)
return
}
setStats(statsRes.data.data || null)
setUserProfile(statsRes.data.user_profile)
setUsernameRequired(false)
console.log('✅ Pure MongoDB data loaded for user:', statsRes.data.user_profile?.display_name)
console.log('📊 Data source verified:', statsRes.data.data_source)
}
if (activityRes.data.success && activityRes.data.data_source === "pure_mongodb_data") {
setActivity(activityRes.data.data)
console.log('✅ Real activity loaded:', activityRes.data.data.length, 'items')
}
if (leaderboardRes.data.success && leaderboardRes.data.data_source === "pure_mongodb_data") {
setLeaderboard(leaderboardRes.data.data)
console.log('✅ Real leaderboard loaded:', leaderboardRes.data.data.length, 'users')
}
} catch (err: any) {
console.error("Failed to fetch pure MongoDB data:", err)
setError(err.response?.data?.message || "Failed to load dashboard data.")
if (err.response?.status === 401) {
toast.error("MetaMask authentication required.")
router.push("/auth/login")
} else {
toast.error("Failed to load real dashboard data.")
setStats(null)
setActivity([])
setLeaderboard([])
}
} finally {
setIsLoadingData(false)
}
}
const handleUsernameSet = (profile: UserProfile) => {
setUserProfile(profile)
setUsernameRequired(false)
fetchPureMongoDBData()
}
const formatTimeAgo = (dateString: string) => {
const diff = Date.now() - new Date(dateString).getTime()
const hours = Math.floor(diff / (1000 * 60 * 60))
const days = Math.floor(hours / 24)
if (days > 0) return `${days}d ago`
if (hours > 0) return `${hours}h ago`
return 'Just now'
}
const getRarityColor = (rarity: string) => {
switch (rarity) {
case 'legendary': return 'bg-gradient-to-r from-yellow-400 to-orange-500 text-white'
case 'epic': return 'bg-gradient-to-r from-purple-500 to-pink-500 text-white'
case 'rare': return 'bg-gradient-to-r from-blue-500 to-cyan-500 text-white'
default: return 'bg-gradient-to-r from-gray-500 to-gray-600 text-white'
}
}
// Loading state
if (isLoadingAuth || isLoadingData) {
return (
<div className="flex justify-center items-center min-h-[calc(100vh-64px)]">
<Loader2 className="h-8 w-8 animate-spin text-primary-purple" />
<span className="ml-2 text-lg">Loading dashboard...</span>
<div className="flex justify-center items-center min-h-screen">
<div className="text-center space-y-4">
<div className="relative">
<div className="w-16 h-16 border-4 border-purple-200 border-t-purple-600 rounded-full animate-spin mx-auto"></div>
<div className="absolute inset-0 flex items-center justify-center">
<BarChart3 className="w-6 h-6 text-purple-600" />
</div>
</div>
<div className="space-y-2">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
Loading Pure MongoDB Data
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">
Fetching your real learning progress from database...
</p>
{walletAddress && (
<p className="text-xs text-purple-600 dark:text-purple-400 font-mono">
🦊 {walletAddress.slice(0, 6)}...{walletAddress.slice(-4)}
</p>
)}
</div>
</div>
</div>
)
}
if (error) {
// Username setup required
if (usernameRequired && userProfile) {
return (
<div className="flex justify-center items-center min-h-[calc(100vh-64px)] text-red-500">
<p>{error}</p>
<UsernameSetup
userProfile={userProfile}
onUsernameSet={handleUsernameSet}
/>
)
}
// Empty state - no real data
if (!stats && userProfile) {
return (
<div className="container mx-auto py-8 px-4 max-w-7xl">
<div className="text-center py-16">
<div className="mb-6">
<BookOpen className="w-16 h-16 text-gray-400 mx-auto mb-4" />
<h2 className="text-2xl font-semibold text-gray-900 dark:text-gray-100 mb-2">
No Learning Data Found
</h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">
Start your learning journey to see real analytics here!
</p>
{/* Show user profile info */}
<div className="bg-purple-50 dark:bg-purple-900/20 p-4 rounded-lg mb-6 max-w-md mx-auto">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-purple-600 flex items-center justify-center">
<User className="w-5 h-5 text-white" />
</div>
<div className="text-left">
<p className="font-medium text-gray-900 dark:text-gray-100">
{userProfile.display_name || 'New Learner'}
</p>
<p className="text-xs text-gray-600 dark:text-gray-400">
🦊 {userProfile.wallet_address?.slice(0, 6)}...{userProfile.wallet_address?.slice(-4)}
</p>
<p className="text-xs text-green-600">
Ready for learning - Pure MongoDB tracking
</p>
</div>
</div>
</div>
</div>
<div className="space-x-4">
<Button onClick={() => router.push('/courses')} className="mr-4">
<BookOpen className="w-4 h-4 mr-2" />
Browse Courses
</Button>
<Button variant="outline" onClick={() => router.push('/quizzes')}>
<Brain className="w-4 h-4 mr-2" />
Take a Quiz
</Button>
</div>
</div>
</div>
)
}
// Error state
if (!stats) {
return (
<div className="flex flex-col items-center justify-center min-h-[calc(100vh-64px)] text-gray-600 dark:text-gray-300">
<p className="text-xl mb-4">No dashboard data available.</p>
<p>Start learning to see your progress!</p>
<div className="container mx-auto py-8 px-4 max-w-7xl">
<div className="text-center py-16">
<AlertCircle className="w-16 h-16 text-red-400 mx-auto mb-4" />
<h2 className="text-2xl font-semibold text-gray-900 dark:text-gray-100 mb-2">
Unable to Load Dashboard
</h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">
Please ensure your MetaMask wallet is connected and try again.
</p>
<Button onClick={fetchPureMongoDBData}>
<Globe className="w-4 h-4 mr-2" />
Retry Loading
</Button>
</div>
</div>
)
}
return (
<div className="container mx-auto py-8 px-4">
{authMethod === "firebase" && !token && (
<div className="bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-4 mb-6 rounded-md dark:bg-yellow-900 dark:border-yellow-600 dark:text-yellow-200">
<p className="font-bold">Limited Access</p>
<p>
You are logged in with email. Full functionality, including personalized stats and activity tracking,
requires connecting your MetaMask wallet.
</p>
<div className="container mx-auto py-8 px-4 max-w-7xl">
{/* Header with Real User Info */}
<div className="mb-8">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-6">
<div className="space-y-2">
<div className="flex items-center gap-3">
<div className="relative">
<img
src={userProfile?.avatar_url || `https://api.dicebear.com/7.x/avataaars/svg?seed=${userProfile?.user_id || 'default'}`}
alt="User Avatar"
className="w-12 h-12 rounded-full border-2 border-purple-600"
/>
{stats.coding_streak > 0 && (
<div className="absolute -top-1 -right-1 w-6 h-6 bg-orange-500 rounded-full flex items-center justify-center">
<Flame className="w-3 h-3 text-white" />
</div>
)}
</div>
<div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">
Welcome, {userProfile?.display_name || 'Learner'}! 🦊
</h1>
<p className="text-gray-600 dark:text-gray-400">
Your real learning progress from MongoDB
</p>
<div className="flex items-center gap-2 mt-1">
<Badge variant="secondary" className="text-xs font-mono">
🦊 {walletAddress?.slice(0, 6)}...{walletAddress?.slice(-4)}
</Badge>
<Badge variant="default" className="text-xs bg-green-600">
Pure MongoDB Data
</Badge>
</div>
</div>
</div>
</div>
<div className="flex items-center gap-4">
<Badge variant="outline" className="px-3 py-1">
<Globe className="w-4 h-4 mr-1" />
Rank #{stats.global_rank.toLocaleString()}
</Badge>
<Badge variant="secondary" className="px-3 py-1">
<Zap className="w-4 h-4 mr-1" />
{stats.total_xp.toLocaleString()} XP
</Badge>
<Badge variant="default" className="px-3 py-1 bg-purple-600">
<Wallet className="w-4 h-4 mr-1" />
MetaMask Verified
</Badge>
</div>
</div>
)}
<h1 className="text-3xl font-bold text-primary-purple mb-8 text-center">Your Dashboard</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
<Card className="bg-white shadow-md rounded-lg p-6 dark:bg-gray-800 dark:text-gray-100">
</div>
{/* Real Metrics Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
{/* Coding Streak */}
<Card className="relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-orange-500/10 to-red-500/10" />
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total XP</CardTitle>
<Award className="h-4 w-4 text-primary-blue" />
<CardTitle className="text-sm font-medium">Real Coding Streak</CardTitle>
<Flame className="h-5 w-5 text-orange-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.total_xp}</div>
<p className="text-xs text-gray-500 dark:text-gray-400">Accumulated experience points</p>
<div className="text-3xl font-bold text-orange-600 dark:text-orange-400">
{stats.coding_streak}
</div>
<p className="text-xs text-gray-600 dark:text-gray-400 mt-1">
Best: {stats.longest_streak} days
</p>
<div className="mt-2">
<Progress
value={stats.longest_streak > 0 ? (stats.coding_streak / stats.longest_streak) * 100 : 0}
className="h-2"
/>
</div>
</CardContent>
</Card>
<Card className="bg-white shadow-md rounded-lg p-6 dark:bg-gray-800 dark:text-gray-100">
{/* Course Progress */}
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Courses Completed</CardTitle>
<BookOpen className="h-4 w-4 text-primary-purple" />
<CardTitle className="text-sm font-medium">Real Course Progress</CardTitle>
<BookOpen className="h-5 w-5 text-blue-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.courses_completed}</div>
<p className="text-xs text-gray-500 dark:text-gray-400">Courses you've finished</p>
<div className="text-3xl font-bold">
{stats.courses_completed}/{stats.total_courses}
</div>
<p className="text-xs text-gray-600 dark:text-gray-400 mt-1">
{stats.total_courses > 0 ? Math.round((stats.courses_completed / stats.total_courses) * 100) : 0}% completed
</p>
<div className="mt-2">
<Progress
value={stats.total_courses > 0 ? (stats.courses_completed / stats.total_courses) * 100 : 0}
className="h-2"
/>
</div>
</CardContent>
</Card>
<Card className="bg-white shadow-md rounded-lg p-6 dark:bg-gray-800 dark:text-gray-100">
{/* Problem Solving */}
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Problems Solved</CardTitle>
<Code className="h-4 w-4 text-primary-blue" />
<CardTitle className="text-sm font-medium">Real Problems Solved</CardTitle>
<Code className="h-5 w-5 text-purple-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.coding_problems_solved}</div>
<p className="text-xs text-gray-500 dark:text-gray-400">Coding challenges mastered</p>
<div className="text-3xl font-bold text-purple-600 dark:text-purple-400">
{stats.coding_problems_solved}
</div>
<p className="text-xs text-gray-600 dark:text-gray-400 mt-1">
{stats.learning_analytics.completion_rate.toFixed(1)}% success rate
</p>
<Badge variant="outline" className="text-xs mt-1">
<Shield className="w-3 h-3 mr-1" />
MongoDB Verified
</Badge>
</CardContent>
</Card>
<Card className="bg-white shadow-md rounded-lg p-6 dark:bg-gray-800 dark:text-gray-100">
{/* Quiz Performance */}
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Quiz Accuracy</CardTitle>
<CheckCircle2 className="h-4 w-4 text-primary-purple" />
<CardTitle className="text-sm font-medium">Real Quiz Accuracy</CardTitle>
<Brain className="h-5 w-5 text-green-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.quiz_accuracy.toFixed(1)}%</div>
<p className="text-xs text-gray-500 dark:text-gray-400">Overall quiz performance</p>
</CardContent>
</Card>
<Card className="bg-white shadow-md rounded-lg p-6 dark:bg-gray-800 dark:text-gray-100">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Coding Streak</CardTitle>
<TrendingUp className="h-4 w-4 text-primary-blue" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.coding_streak} days</div>
<p className="text-xs text-gray-500 dark:text-gray-400">Consecutive days coding</p>
<div className="text-3xl font-bold text-green-600 dark:text-green-400">
{stats.quiz_accuracy.toFixed(1)}%
</div>
<p className="text-xs text-gray-600 dark:text-gray-400 mt-1">
{stats.total_quizzes} real quizzes completed
</p>
</CardContent>
</Card>
</div>
<h2 className="text-2xl font-bold text-primary-purple mb-6 text-center">Activity Heatmap (Coming Soon)</h2>
<Card className="bg-white shadow-md rounded-lg p-6 dark:bg-gray-800 dark:text-gray-100 mb-8">
<CardContent className="h-48 flex items-center justify-center text-gray-500 dark:text-gray-400">
<p>Interactive activity heatmap visualization will appear here.</p>
{/* Real Learning Analytics */}
<Card className="mb-8">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<BarChart3 className="w-5 h-5" />
Real Learning Analytics from MongoDB
<Badge variant="outline" className="text-xs ml-2">
100% Authentic Data
</Badge>
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* Time Stats */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="text-center p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<Timer className="w-8 h-8 text-blue-600 mx-auto mb-2" />
<div className="text-2xl font-bold text-blue-600">
{stats.learning_analytics.time_spent_hours}h
</div>
<p className="text-sm text-gray-600 dark:text-gray-400">Real Time Spent</p>
</div>
<div className="text-center p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<Target className="w-8 h-8 text-green-600 mx-auto mb-2" />
<div className="text-2xl font-bold text-green-600">
{stats.learning_analytics.average_session_minutes}m
</div>
<p className="text-sm text-gray-600 dark:text-gray-400">Avg Session</p>
</div>
<div className="text-center p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg">
<CheckCircle2 className="w-8 h-8 text-purple-600 mx-auto mb-2" />
<div className="text-2xl font-bold text-purple-600">
{stats.learning_analytics.completion_rate.toFixed(1)}%
</div>
<p className="text-sm text-gray-600 dark:text-gray-400">Real Completion Rate</p>
</div>
</div>
{/* Real Skill Levels */}
<div className="space-y-3">
<h4 className="font-semibold text-gray-900 dark:text-gray-100">Real Skill Progression from MongoDB</h4>
{Object.entries(stats.learning_analytics.skill_levels).map(([skill, level]) => (
<div key={skill} className="space-y-1">
<div className="flex justify-between text-sm">
<span className="font-medium">{skill}</span>
<span className="text-gray-600 dark:text-gray-400">{level}%</span>
</div>
<Progress value={level} className="h-2" />
</div>
))}
</div>
{/* Real Weekly Activity */}
<div className="space-y-3">
<h4 className="font-semibold text-gray-900 dark:text-gray-100">Real Weekly Activity Pattern</h4>
<div className="flex items-end space-x-2 h-24">
{stats.weekly_activity.map((activity, index) => {
const maxActivity = Math.max(...stats.weekly_activity) || 1
return (
<div key={index} className="flex-1 flex flex-col items-center">
<div
className="w-full bg-gradient-to-t from-purple-600 to-cyan-500 rounded-t-sm transition-all duration-300 hover:from-purple-700 hover:to-cyan-600"
style={{ height: `${(activity / maxActivity) * 100}%` }}
title={`${activity} real activities`}
/>
<span className="text-xs text-gray-500 mt-1">
{['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'][index]}
</span>
</div>
)
})}
</div>
</div>
</CardContent>
</Card>
<h2 className="text-2xl font-bold text-primary-purple mb-6 text-center">
Strengths/Weaknesses & Leaderboard (Coming Soon)
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card className="bg-white shadow-md rounded-lg p-6 dark:bg-gray-800 dark:text-gray-100">
<CardContent className="h-48 flex items-center justify-center text-gray-500 dark:text-gray-400">
<p>Radar chart for strengths/weaknesses will appear here.</p>
{/* Real Recent Activity */}
<Card className="mb-8">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Activity className="w-5 h-5" />
Real Activity History from MongoDB
</CardTitle>
</CardHeader>
<CardContent>
{activity.length > 0 ? (
<div className="space-y-4">
{activity.map((item) => (
<div key={item.id} className="flex items-center gap-4 p-4 rounded-lg border bg-gradient-to-r from-white to-gray-50 dark:from-gray-800 dark:to-gray-700 hover:shadow-md transition-shadow">
<div className={`p-2 rounded-lg ${
item.type === 'course' ? 'bg-blue-100 dark:bg-blue-900/30' :
item.type === 'quiz' ? 'bg-green-100 dark:bg-green-900/30' :
item.type === 'coding' ? 'bg-purple-100 dark:bg-purple-900/30' :
'bg-yellow-100 dark:bg-yellow-900/30'
}`}>
{item.type === 'course' && <BookOpen className="w-4 h-4 text-blue-600" />}
{item.type === 'quiz' && <Brain className="w-4 h-4 text-green-600" />}
{item.type === 'coding' && <Code className="w-4 h-4 text-purple-600" />}
{item.type === 'achievement' && <Award className="w-4 h-4 text-yellow-600" />}
</div>
<div className="flex-1 space-y-1">
<div className="flex items-center justify-between">
<h4 className="font-semibold text-sm">{item.title}</h4>
{item.blockchain_verified && (
<Badge variant="secondary" className="text-xs">
<Shield className="w-3 h-3 mr-1" />
Verified
</Badge>
)}
</div>
<p className="text-xs text-gray-600 dark:text-gray-400">{item.description}</p>
<div className="flex items-center justify-between">
<span className="text-xs text-gray-500">
{formatTimeAgo(item.completed_at)}
</span>
<span className="text-xs font-medium text-green-600">
+{item.points_earned} XP
</span>
</div>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-8 text-gray-500">
<Activity className="w-8 h-8 mx-auto mb-2 opacity-50" />
<p className="text-sm">No real activity found in MongoDB</p>
<p className="text-xs">Start learning to see your authentic activity here!</p>
</div>
)}
</CardContent>
</Card>
{/* Real Global Leaderboard */}
{leaderboard.length > 0 && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Trophy className="w-5 h-5" />
Real Global Leaderboard from MongoDB
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
{leaderboard.slice(0, 10).map((entry) => (
<div key={entry.user_id} className="flex items-center gap-4 p-3 rounded-lg bg-gray-50 dark:bg-gray-800">
<div className="flex items-center gap-3">
<div className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold ${
entry.rank === 1 ? 'bg-yellow-500 text-white' :
entry.rank === 2 ? 'bg-gray-400 text-white' :
entry.rank === 3 ? 'bg-amber-600 text-white' :
'bg-gray-200 text-gray-700'
}`}>
{entry.rank}
</div>
<img
src={entry.avatar || `https://api.dicebear.com/7.x/avataaars/svg?seed=${entry.user_id}`}
alt={entry.username}
className="w-8 h-8 rounded-full"
/>
</div>
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-semibold">{entry.display_name || entry.username}</span>
{entry.wallet_address && (
<Badge variant="outline" className="text-xs">
🦊 Real User
</Badge>
)}
</div>
<p className="text-xs text-gray-600 dark:text-gray-400">
{entry.user_id.slice(0, 8)}...{entry.user_id.slice(-4)}
</p>
</div>
<div className="text-right">
<div className="font-bold text-purple-600">{entry.total_xp.toLocaleString()} Real XP</div>
<div className="text-xs text-gray-500">{entry.streak} day streak</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
<Card className="bg-white shadow-md rounded-lg p-6 dark:bg-gray-800 dark:text-gray-100">
<CardContent className="h-48 flex items-center justify-center text-gray-500 dark:text-gray-400">
<p>Global leaderboard will appear here.</p>
</CardContent>
</Card>
</div>
)}
</div>
)
}
+1 -1
View File
@@ -200,4 +200,4 @@ export function useAuth() {
throw new Error("useAuth must be used within an AuthProvider")
}
return context
}
}
+1 -1
View File
@@ -22,4 +22,4 @@ api.interceptors.request.use(
},
)
export default api
export default api
+79 -119
View File
@@ -1,10 +1,21 @@
// User types
export interface User {
id: string
wallet_address: string
created_at: string
last_login: string
}
// Authentication request/response types
export interface AuthNonceRequest {
wallet_address: string
}
export interface AuthNonceResponse {
success: boolean
nonce: string
message: string
timestamp: string
}
export interface AuthVerifyRequest {
@@ -16,135 +27,84 @@ export interface AuthVerifyRequest {
export interface AuthVerifyResponse {
success: boolean
token: string
user: {
wallet_address: string
// Add other user details if available from your backend
}
user: User
message: string
}
export interface QuestionOption {
id: string
text: string
// Dashboard types
export interface UserProfile {
user_id: string
wallet_address?: string
display_name?: string
username_set?: boolean
avatar_url?: string
created_at?: string
}
export interface Question {
id: string
text: string
options: QuestionOption[]
type: string // e.g., "multiple_choice"
}
export interface TestStartRequest {
subject: string
}
export interface TestStartResponse {
session_id: string
question: Question
question_number: number
total_questions: number
}
export interface Feedback {
correct: boolean
confidence_score: number
explanation: string
correct_answer?: string // Optional, if backend provides it
}
export interface TestAnswerRequest {
session_id: string
question_id: string
answer: number // Index of the selected option
}
export interface TestAnswerResponse {
feedback: Feedback
next_question: Question | null
test_completed: boolean
}
export interface User {
wallet_address: string
// Add other user details
}
// New types for Course Platform
export interface Lesson {
id: string
title: string
type: "video" | "text" | "code" | "quiz"
content: string // URL for video, markdown for text, code snippet for code
completed: boolean
}
export interface Module {
id: string
title: string
lessons: Lesson[]
}
export interface Course {
id: string
title: string
subject: string
description: string
progress: number // 0-100
modules: Module[]
}
// New types for Coding Platform
export interface CodingProblem {
id: string
title: string
category: string
difficulty: "Easy" | "Medium" | "Hard"
description: string
initial_code: { [key: string]: string } // e.g., { "python": "def solve():\n pass" }
test_cases: { input: string; expected_output: string }[]
solved: boolean
}
export interface CodeExecutionResult {
output: string
error: string | null
runtime: number
correct: boolean
}
// New types for Quiz Platform (reusing existing Test types)
export interface Quiz {
id: string
title: string
topic: string
difficulty: "Easy" | "Medium" | "Hard"
recent_performance?: number // 0-100
}
export interface QuizResult {
score: number
total_questions: number
correct_answers: number
per_question_breakdown: {
question_id: string
correct: boolean
explanation: string
user_answer: string
correct_answer: string
}[]
}
// New types for Dashboard
export interface DashboardStats {
total_xp: number
courses_in_progress: number
courses_completed: number
coding_problems_solved: number
quiz_accuracy: number // overall average
quiz_accuracy: number
coding_streak: number
longest_streak: number
total_courses: number
total_quizzes: number
global_rank: number
weekly_activity: number[]
monthly_goals: {
target: number
completed: number
}
blockchain: {
wallet_connected: boolean
wallet_address: string | null
total_earned: number
transactions: number
certificates: number
verified_achievements: number
}
learning_analytics: {
time_spent_hours: number
average_session_minutes: number
completion_rate: number
favorite_topics: string[]
skill_levels: {
[key: string]: number
}
}
recent_achievements: Achievement[]
}
export interface Achievement {
id: string
title: string
description: string
earned_at: string
points: number
rarity: string
}
export interface ActivityData {
date: string // YYYY-MM-DD
count: number // Number of activities
id: string
type: string
title: string
description: string
completed_at: string
points_earned: number
success_rate: number
difficulty: string
blockchain_verified: boolean
}
export interface LeaderboardEntry {
rank: number
user_id: string
username: string
display_name?: string
total_xp: number
streak: number
avatar: string
badges: string[]
wallet_address?: string
}
+9 -3
View File
@@ -39,6 +39,9 @@
"@radix-ui/react-toggle-group": "1.1.1",
"@radix-ui/react-tooltip": "1.1.6",
"axios": "latest",
"badge": "link:@/components/ui/badge",
"button": "link:@/components/ui/button",
"card": "link:@/components/ui/card",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "1.0.4",
@@ -49,20 +52,23 @@
"geist": "^1.3.1",
"input-otp": "1.4.1",
"lucide-react": "^0.454.0",
"next": "15.2.4",
"next": "15.4.4",
"next-themes": "latest",
"react": "^19",
"progress": "link:@/components/ui/progress",
"react": "^19.1.0",
"react-day-picker": "9.8.0",
"react-dom": "^19",
"react-dom": "^19.1.0",
"react-hook-form": "^7.54.1",
"react-hot-toast": "latest",
"react-markdown": "latest",
"react-resizable-panels": "^2.1.7",
"recharts": "2.15.0",
"separator": "link:@/components/ui/separator",
"sonner": "^1.7.1",
"tailwind-merge": "^2.5.5",
"tailwindcss-animate": "^1.0.7",
"vaul": "^0.9.6",
"web3": "^4.16.0",
"zod": "^3.24.1"
},
"devDependencies": {
+787 -154
View File
File diff suppressed because it is too large Load Diff
+11 -3
View File
@@ -1,10 +1,11 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"target": "es5",
"lib": ["dom", "dom.iterable", "es6"],
"allowJs": true,
"target": "ES6",
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
@@ -18,8 +19,15 @@
"name": "next"
}
],
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
"@/*": ["./*"],
"@/components/*": ["./components/*"],
"@/hooks/*": ["./hooks/*"],
"@/lib/*": ["./lib/*"],
"@/utils/*": ["./utils/*"],
"@/types/*": ["./types/*"],
"@/app/*": ["./app/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],