Fix UI bugs, admin bulk upload limits, test ingestion bugs, and reset demo users
This commit is contained in:
1832
backend/jee_advanced_full_90q.json
Normal file
1832
backend/jee_advanced_full_90q.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -236,9 +236,13 @@ async def bulk_register_students(file: UploadFile = File(...), session: Session
|
||||
decoded = content.decode('utf-8-sig')
|
||||
csv_reader = csv.DictReader(io.StringIO(decoded))
|
||||
|
||||
rows = list(csv_reader)
|
||||
if len(rows) > 500:
|
||||
raise HTTPException(status_code=400, detail="Maximum limit of 500 users per bulk registration exceeded.")
|
||||
|
||||
results = []
|
||||
|
||||
for row in csv_reader:
|
||||
for row in rows:
|
||||
email = row.get("email", "").strip()
|
||||
name = row.get("name", "").strip()
|
||||
phone = row.get("phone", "").strip()
|
||||
@@ -436,7 +440,7 @@ def login(payload: LoginPayload, session: Session = Depends(get_session)):
|
||||
# --- CBT ENGINE ---
|
||||
|
||||
@app.get("/api/tests", response_model=List[models.Test], tags=["CBT Engine"])
|
||||
def list_active_tests(exam_pattern: Optional[str] = None, type: Optional[str] = None, session: Session = Depends(get_session), current_user: models.User = Depends(require_tier([models.TierEnum.PRO, models.TierEnum.AI_COACH]))):
|
||||
def list_active_tests(exam_pattern: Optional[str] = None, type: Optional[str] = None, session: Session = Depends(get_session), current_user: models.User = Depends(get_current_user)):
|
||||
query = select(models.Test).where(models.Test.is_active == True)
|
||||
if exam_pattern:
|
||||
if exam_pattern == 'JEE_NEET':
|
||||
@@ -450,14 +454,14 @@ def list_active_tests(exam_pattern: Optional[str] = None, type: Optional[str] =
|
||||
return session.exec(query).all()
|
||||
|
||||
@app.get("/api/tests/{test_id}", response_model=models.Test, tags=["CBT Engine"])
|
||||
def get_test_by_id(test_id: UUID, user_id: Optional[UUID] = None, session: Session = Depends(get_session), current_user: models.User = Depends(require_tier([models.TierEnum.PRO, models.TierEnum.AI_COACH]))):
|
||||
def get_test_by_id(test_id: UUID, user_id: Optional[UUID] = None, session: Session = Depends(get_session), current_user: models.User = Depends(get_current_user)):
|
||||
test = session.get(models.Test, test_id)
|
||||
if not test:
|
||||
raise HTTPException(status_code=404, detail="Requested examination structure not found.")
|
||||
return test
|
||||
|
||||
@app.get("/api/tests/{test_id}/questions", response_model=List[models.Question], tags=["CBT Engine"])
|
||||
def get_questions_by_test_id(test_id: UUID, session: Session = Depends(get_session), current_user: models.User = Depends(require_tier([models.TierEnum.PRO, models.TierEnum.AI_COACH]))):
|
||||
def get_questions_by_test_id(test_id: UUID, session: Session = Depends(get_session), current_user: models.User = Depends(get_current_user)):
|
||||
test = session.get(models.Test, test_id)
|
||||
if not test:
|
||||
raise HTTPException(status_code=404, detail="Requested examination structure not found.")
|
||||
@@ -498,7 +502,7 @@ def get_question_by_id(question_id: UUID, session: Session = Depends(get_session
|
||||
return question
|
||||
|
||||
@app.post("/api/attempts/submit", response_model=models.Attempt, tags=["CBT Engine"])
|
||||
def submit_test_attempt(payload: Dict[str, Any], session: Session = Depends(get_session), current_user: models.User = Depends(require_tier([models.TierEnum.PRO, models.TierEnum.AI_COACH]))):
|
||||
def submit_test_attempt(payload: Dict[str, Any], session: Session = Depends(get_session), current_user: models.User = Depends(get_current_user)):
|
||||
user_id = payload.get("user_id")
|
||||
source_type_raw = payload.get("source_type", "Test")
|
||||
source_id = payload.get("source_id")
|
||||
|
||||
3782
backend/neet_180_questions.json
Normal file
3782
backend/neet_180_questions.json
Normal file
File diff suppressed because it is too large
Load Diff
30
backend/reset_demo_users.py
Normal file
30
backend/reset_demo_users.py
Normal file
@@ -0,0 +1,30 @@
|
||||
|
||||
from uuid import UUID
|
||||
from sqlmodel import Session
|
||||
from database import engine
|
||||
import models
|
||||
from main import ensure_demo_user
|
||||
|
||||
demo_ids = [
|
||||
UUID("11111111-1111-1111-1111-111111111111"),
|
||||
UUID("22222222-2222-2222-2222-222222222222"),
|
||||
UUID("33333333-3333-3333-3333-333333333333"),
|
||||
UUID("44444444-4444-4444-4444-444444444444")
|
||||
]
|
||||
|
||||
with Session(engine) as session:
|
||||
for uid in demo_ids:
|
||||
existing = session.get(models.User, uid)
|
||||
if existing:
|
||||
print(f"Deleting {existing.name}...")
|
||||
session.exec(models.Subscription.__table__.delete().where(models.Subscription.user_id == uid))
|
||||
session.exec(models.Mistake.__table__.delete().where(models.Mistake.user_id == uid))
|
||||
session.exec(models.Attempt.__table__.delete().where(models.Attempt.user_id == uid))
|
||||
session.exec(models.StudyPlan.__table__.delete().where(models.StudyPlan.user_id == uid))
|
||||
session.exec(models.Transaction.__table__.delete().where(models.Transaction.user_id == uid))
|
||||
session.delete(existing)
|
||||
session.commit()
|
||||
|
||||
user = ensure_demo_user(uid, session)
|
||||
print(f"Recreated: {user.name}")
|
||||
|
||||
64
backend/upload_test.py
Normal file
64
backend/upload_test.py
Normal file
@@ -0,0 +1,64 @@
|
||||
import sys
|
||||
sys.path.append('.')
|
||||
from sqlmodel import Session, select
|
||||
from database import engine
|
||||
from models import User, Test, Question, QuestionTypeEnum, ExamEnum
|
||||
|
||||
def upload_json():
|
||||
import json
|
||||
|
||||
with open('neet_180_questions.json', 'r', encoding='utf-8') as f:
|
||||
questions = json.load(f)
|
||||
|
||||
with Session(engine) as session:
|
||||
admin = session.exec(select(User).where(User.tier == 'AI Coach')).first()
|
||||
if not admin:
|
||||
print('No admin found')
|
||||
return
|
||||
|
||||
payload = {
|
||||
'title': 'Test NEET Mock Upload',
|
||||
'type': 'Mock',
|
||||
'exam_pattern': 'NEET',
|
||||
'time_limit': 180,
|
||||
'admin_id': str(admin.id),
|
||||
'questions_json': questions
|
||||
}
|
||||
|
||||
test_obj = Test(
|
||||
title=payload['title'],
|
||||
type=payload['type'],
|
||||
exam_pattern=ExamEnum.NEET,
|
||||
sections={},
|
||||
total_time=payload['time_limit'],
|
||||
difficulty='Hard',
|
||||
is_active=True,
|
||||
created_by=admin.id
|
||||
)
|
||||
session.add(test_obj)
|
||||
session.flush()
|
||||
|
||||
for q in questions:
|
||||
q_obj = Question(
|
||||
subject=q.get('subject'),
|
||||
topic=q.get('topic', 'General'),
|
||||
subtopic=q.get('subtopic', ''),
|
||||
difficulty=q.get('difficulty', 'Medium'),
|
||||
type=QuestionTypeEnum(q.get('type', 'SCQ')),
|
||||
question_text=q.get('question_text', ''),
|
||||
options_json=q.get('options', []),
|
||||
correct_answer=q.get('correct_answer', ''),
|
||||
solution_text=q.get('explanation', ''), language='English',
|
||||
test_id=test_obj.id, created_by=admin.id,
|
||||
section_name=q.get('subject')
|
||||
)
|
||||
session.add(q_obj)
|
||||
|
||||
session.commit()
|
||||
print('Successfully added test with ID:', test_obj.id)
|
||||
|
||||
upload_json()
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -138,14 +138,6 @@ export default function SettingsPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-red-500/20 bg-red-500/5">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-red-500">Danger Zone</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button onClick={() => alert("Cache Purged.")} className="bg-red-500 text-white hover:bg-red-600 font-bold">Purge Generated Cache</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -46,14 +46,31 @@ export default function StudentsPage() {
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>([])
|
||||
const [deleting, setDeleting] = useState(false)
|
||||
const [isUploading, setIsUploading] = useState(false)
|
||||
const [uploadProgress, setUploadProgress] = useState(0)
|
||||
const [uploadTimeLeft, setUploadTimeLeft] = useState(0)
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const handleBulkUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
setIsUploading(true)
|
||||
setLoading(true)
|
||||
setUploadProgress(0)
|
||||
setUploadTimeLeft(15) // Simulate 15s max parsing time
|
||||
|
||||
const progressInterval = setInterval(() => {
|
||||
setUploadProgress(prev => {
|
||||
if (prev >= 95) return prev;
|
||||
return prev + 5;
|
||||
});
|
||||
setUploadTimeLeft(prev => {
|
||||
if (prev <= 1) return 1;
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append("file", file)
|
||||
@@ -62,35 +79,48 @@ export default function StudentsPage() {
|
||||
body: formData,
|
||||
})
|
||||
|
||||
if (!rawRes.ok) throw new Error("Upload failed")
|
||||
if (!rawRes.ok) {
|
||||
const errorData = await rawRes.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || "Upload failed");
|
||||
}
|
||||
|
||||
setUploadProgress(100);
|
||||
const results = await rawRes.json()
|
||||
alert(`Successfully registered ${results.length} students!`)
|
||||
|
||||
// Download CSV with passwords using Blob to fix formatting on Windows
|
||||
const csvRows = ["name,email,password"];
|
||||
results.forEach((row: any) => {
|
||||
csvRows.push(`${row.name},${row.email},${row.password}`);
|
||||
});
|
||||
const csvContent = csvRows.join("\r\n");
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.setAttribute("href", url);
|
||||
link.setAttribute("download", "bulk_registered_passwords.csv");
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
// Delay alert slightly to let progress bar reach 100% visually
|
||||
setTimeout(async () => {
|
||||
alert(`Successfully registered ${results.length} students!`)
|
||||
|
||||
// Download CSV with passwords using Blob to fix formatting on Windows
|
||||
const csvRows = ["name,email,password"];
|
||||
results.forEach((row: any) => {
|
||||
csvRows.push(`${row.name},${row.email},${row.password}`);
|
||||
});
|
||||
const csvContent = csvRows.join("\r\n");
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.setAttribute("href", url);
|
||||
link.setAttribute("download", "bulk_registered_passwords.csv");
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
// Refresh students
|
||||
const fresh = await fetchWithAuth("/api/admin/students").then(r => r.json())
|
||||
setStudents(fresh)
|
||||
setIsUploading(false)
|
||||
setLoading(false)
|
||||
if (fileInputRef.current) fileInputRef.current.value = ""
|
||||
}, 500);
|
||||
|
||||
// Refresh students
|
||||
const fresh = await fetchWithAuth("/api/admin/students").then(r => r.json())
|
||||
setStudents(fresh)
|
||||
} catch (err: any) {
|
||||
alert("Error: " + err.message)
|
||||
} finally {
|
||||
setIsUploading(false)
|
||||
setLoading(false)
|
||||
if (fileInputRef.current) fileInputRef.current.value = ""
|
||||
} finally {
|
||||
clearInterval(progressInterval);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,6 +166,46 @@ export default function StudentsPage() {
|
||||
.finally(() => setLoading(false))
|
||||
}, [])
|
||||
|
||||
const exportCSV = () => {
|
||||
if (filtered.length === 0) {
|
||||
alert("No data to export");
|
||||
return;
|
||||
}
|
||||
const headers = ["Name", "Email", "Phone", "Plan", "Target", "Tests Taken", "Accuracy", "Streak", "Joined Date", "Status"];
|
||||
const csvRows = [headers.join(",")];
|
||||
|
||||
filtered.forEach(s => {
|
||||
const status = s.is_active !== false ? "Enabled" : "Disabled";
|
||||
const date = formatDate(s.created_at);
|
||||
const plan = s.tier;
|
||||
const target = s.target_exam;
|
||||
const row = [
|
||||
`"${s.name}"`,
|
||||
`"${s.email || ''}"`,
|
||||
`"${s.phone || ''}"`,
|
||||
`"${plan}"`,
|
||||
`"${target}"`,
|
||||
s.total_tests || 0,
|
||||
`${s.avg_accuracy || 0}%`,
|
||||
s.streak_count || 0,
|
||||
`"${date}"`,
|
||||
`"${status}"`
|
||||
];
|
||||
csvRows.push(row.join(","));
|
||||
});
|
||||
|
||||
const csvContent = csvRows.join("\r\n");
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.setAttribute("href", url);
|
||||
link.setAttribute("download", "students_export.csv");
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
|
||||
const filtered = students.filter(s => {
|
||||
const searchMatch = (s.name || "").toLowerCase().includes(search.toLowerCase()) ||
|
||||
@@ -162,25 +232,43 @@ export default function StudentsPage() {
|
||||
{loading ? "Loading..." : `${students.length} students enrolled`}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{selectedIds.length > 0 && (
|
||||
<Button variant="outline" className="text-red-500 border-red-500 hover:bg-red-500 hover:text-white" disabled={deleting} onClick={handleDeleteSelected}>
|
||||
<Trash2 size={16} className="mr-2" /> Delete ({selectedIds.length})
|
||||
</Button>
|
||||
)}
|
||||
<input type="file" accept=".csv" className="hidden" ref={fileInputRef} onChange={handleBulkUpload} disabled={isUploading} />
|
||||
<Button variant="outline" className="border-border hover:bg-secondary hover:text-white" disabled={isUploading} onClick={() => fileInputRef.current?.click()}>
|
||||
{isUploading ? <RefreshCw size={16} className="mr-2 animate-spin" /> : <Upload size={16} className="mr-2" />}
|
||||
{isUploading ? "Uploading..." : "Bulk Register CSV"}
|
||||
</Button>
|
||||
<Button variant="outline" className="border-border hover:bg-secondary hover:text-white" onClick={downloadTemplate}>
|
||||
<Download size={16} className="mr-2" /> Download Template
|
||||
</Button>
|
||||
<Button variant="outline" className="border-border hover:bg-secondary hover:text-white">
|
||||
<Download size={16} className="mr-2" /> Export CSV
|
||||
</Button>
|
||||
<div className="flex flex-col items-end gap-1">
|
||||
<div className="flex gap-2">
|
||||
{selectedIds.length > 0 && (
|
||||
<Button variant="outline" className="text-red-500 border-red-500 hover:bg-red-500 hover:text-white" disabled={deleting} onClick={handleDeleteSelected}>
|
||||
<Trash2 size={16} className="mr-2" /> Delete ({selectedIds.length})
|
||||
</Button>
|
||||
)}
|
||||
<input type="file" accept=".csv" className="hidden" ref={fileInputRef} onChange={handleBulkUpload} disabled={isUploading} />
|
||||
<Button variant="outline" className="border-border hover:bg-secondary hover:text-white" disabled={isUploading} onClick={() => fileInputRef.current?.click()}>
|
||||
{isUploading ? <RefreshCw size={16} className="mr-2 animate-spin" /> : <Upload size={16} className="mr-2" />}
|
||||
{isUploading ? "Uploading..." : "Bulk Register CSV"}
|
||||
</Button>
|
||||
<Button variant="outline" className="border-border hover:bg-secondary hover:text-white" onClick={downloadTemplate}>
|
||||
<Download size={16} className="mr-2" /> Download Template
|
||||
</Button>
|
||||
<Button variant="outline" className="border-border hover:bg-secondary hover:text-white" onClick={exportCSV}>
|
||||
<Download size={16} className="mr-2" /> Export CSV
|
||||
</Button>
|
||||
</div>
|
||||
<span className="text-[10px] text-muted-foreground mr-1">Max 500 students per upload</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isUploading && (
|
||||
<div className="bg-secondary/20 border border-primary/20 rounded-xl p-4 flex flex-col gap-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="font-semibold text-primary">Parsing and registering students...</span>
|
||||
<span className="text-muted-foreground">Estimated time: ~{uploadTimeLeft}s</span>
|
||||
</div>
|
||||
<div className="w-full bg-secondary rounded-full h-2 overflow-hidden">
|
||||
<div
|
||||
className="bg-primary h-2 rounded-full transition-all duration-1000 ease-in-out"
|
||||
style={{ width: `${uploadProgress}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-secondary/10 border border-border rounded-xl overflow-hidden backdrop-blur-sm">
|
||||
|
||||
@@ -212,7 +300,6 @@ export default function StudentsPage() {
|
||||
>
|
||||
<option value="All">All Courses</option>
|
||||
<option value="JEE_Main">JEE Main</option>
|
||||
<option value="JEE_Advanced">JEE Advanced</option>
|
||||
<option value="NEET">NEET</option>
|
||||
<option value="JEE_NEET">JEE + NEET</option>
|
||||
</select>
|
||||
@@ -336,3 +423,4 @@ export default function StudentsPage() {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ export default function CBTPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<PremiumGate featureKey="weekly-test-series">
|
||||
<>
|
||||
<div className="p-8 space-y-8 animate-in fade-in zoom-in-95 origin-top duration-500 max-w-7xl mx-auto">
|
||||
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4">
|
||||
<div>
|
||||
@@ -360,6 +360,6 @@ export default function CBTPage() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PremiumGate>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { fetchWithAuth } from "@/lib/apiClient";
|
||||
import Link from "next/link";
|
||||
import { useRef, useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
@@ -14,6 +15,8 @@ export default function DashboardPage() {
|
||||
const { user, userId, loading: userLoading, hasAccessTo, trialInfo } = useActiveUser()
|
||||
const [userStats, setUserStats] = useState<any>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [dpps, setDpps] = useState<any[]>([])
|
||||
const [cbts, setCbts] = useState<any[]>([])
|
||||
const [downloading, setDownloading] = useState(false)
|
||||
const [shareMenuOpen, setShareMenuOpen] = useState(false)
|
||||
const progressRef = useRef<HTMLDivElement>(null)
|
||||
@@ -25,20 +28,26 @@ export default function DashboardPage() {
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
async function fetchStats() {
|
||||
async function fetchStatsAndTests() {
|
||||
try {
|
||||
const res = await fetchWithAuth(`/api/users/${currentUserId}/stats`)
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
setUserStats(data)
|
||||
}
|
||||
const pattern = user?.target_exam || "JEE_Main";
|
||||
const [statsRes, dppRes, cbtRes] = await Promise.all([
|
||||
fetchWithAuth(`/api/users/${currentUserId}/stats`),
|
||||
fetchWithAuth(`/api/tests?type=DPS&exam_pattern=${pattern}`),
|
||||
fetchWithAuth(`/api/tests?type=Mock&exam_pattern=${pattern}`)
|
||||
]);
|
||||
|
||||
if (statsRes.ok) setUserStats(await statsRes.json());
|
||||
if (dppRes.ok) setDpps((await dppRes.json()).slice(0, 2));
|
||||
if (cbtRes.ok) setCbts((await cbtRes.json()).slice(0, 2));
|
||||
|
||||
} catch (e) {
|
||||
console.error("Failed to load user stats", e)
|
||||
console.error("Failed to load user data", e)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
fetchStats()
|
||||
fetchStatsAndTests()
|
||||
}, [userLoading, user?.id, userId])
|
||||
|
||||
const getShareUrl = () => {
|
||||
@@ -227,61 +236,29 @@ export default function DashboardPage() {
|
||||
<Button variant="ghost" size="sm" className="text-muted-foreground font-medium">View All</Button>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<Card className="hover:border-primary/30 hover:shadow-md cursor-pointer transition-all duration-300 group">
|
||||
<CardContent className="p-5 flex items-center justify-between">
|
||||
<div className="flex gap-4 items-center">
|
||||
<div className="p-3 bg-secondary rounded-xl text-foreground">
|
||||
<BrainCircuit size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium text-base group-hover:text-primary transition-colors">Physics: Rotational Dynamics</h3>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">20 Questions • High Priority</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" className="gap-2 rounded-xl group-hover:bg-primary group-hover:text-primary-foreground group-hover:border-primary transition-all">
|
||||
Start <ArrowRight size={14} />
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{hasAccessTo("topic-wise-microtests-unlimited") ? (
|
||||
<Card className="hover:border-primary/30 hover:shadow-md cursor-pointer transition-all duration-300 group">
|
||||
<CardContent className="p-5 flex items-center justify-between">
|
||||
<div className="flex gap-4 items-center">
|
||||
<div className="p-3 bg-secondary rounded-xl text-foreground">
|
||||
<BrainCircuit size={20} />
|
||||
{dpps.length > 0 ? (
|
||||
dpps.map((dpp) => (
|
||||
<Card key={dpp.id} className="hover:border-primary/30 hover:shadow-md cursor-pointer transition-all duration-300 group">
|
||||
<CardContent className="p-5 flex items-center justify-between">
|
||||
<div className="flex gap-4 items-center">
|
||||
<div className="p-3 bg-secondary rounded-xl text-foreground">
|
||||
<BrainCircuit size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium text-base group-hover:text-primary transition-colors">{dpp.title}</h3>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">{dpp.total_time} Minutes • {dpp.difficulty || "Medium"} Priority</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium text-base group-hover:text-primary transition-colors">
|
||||
{isNeet ? "Biology: Human Physiology" : "Maths: Integral Calculus"}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">30 Questions • Medium Priority</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" className="gap-2 rounded-xl group-hover:bg-primary group-hover:text-primary-foreground group-hover:border-primary transition-all">
|
||||
Start <ArrowRight size={14} />
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Link href={`/dashboard/dpp`}>
|
||||
<Button variant="outline" size="sm" className="gap-2 rounded-xl group-hover:bg-primary group-hover:text-primary-foreground group-hover:border-primary transition-all">
|
||||
Start <ArrowRight size={14} />
|
||||
</Button>
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
<Card className="relative opacity-60 cursor-not-allowed border-dashed">
|
||||
<CardContent className="p-5 flex items-center justify-between">
|
||||
<div className="flex gap-4 items-center">
|
||||
<div className="p-3 bg-secondary rounded-xl text-muted-foreground">
|
||||
<Lock size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium text-base text-muted-foreground">
|
||||
{isNeet ? "Biology: Human Physiology" : "Maths: Integral Calculus"}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mt-0.5 flex items-center gap-1"><Lock size={12}/> Pro Feature</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" disabled className="gap-2 rounded-xl">
|
||||
Locked
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<p className="text-muted-foreground text-sm py-4">No DPPs scheduled for today.</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
@@ -293,38 +270,33 @@ export default function DashboardPage() {
|
||||
Upcoming CBTs
|
||||
</h2>
|
||||
</div>
|
||||
<Card className="overflow-hidden">
|
||||
<div className="p-6 border-b border-border flex items-center justify-between bg-background">
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<Badge variant="outline" className="border-primary/20 bg-primary/5 text-primary">Mock Test</Badge>
|
||||
<span className="text-xs text-muted-foreground font-semibold uppercase tracking-wider">Tomorrow, 10:00 AM</span>
|
||||
</div>
|
||||
<h3 className="font-semibold text-lg tracking-tight">{isNeet ? 'NEET Mock Test 4' : 'JEE Mock Test 4'}</h3>
|
||||
<p className="text-sm text-muted-foreground flex items-center gap-1.5 mt-1.5">
|
||||
<Clock size={14} /> 180 Minutes • 720 Marks
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="secondary" className="rounded-xl font-medium">Remind Me</Button>
|
||||
</div>
|
||||
{hasAccessTo("weekly-test-series") ? (
|
||||
<div className="p-5 bg-secondary/50 flex items-center justify-between transition-colors hover:bg-secondary/70 cursor-pointer group">
|
||||
<div>
|
||||
<h3 className="font-medium text-sm text-foreground flex items-center gap-2"><Activity size={14} className="text-primary group-hover:animate-pulse"/> Part Test 3: Mechanics</h3>
|
||||
<p className="text-xs text-muted-foreground mt-0.5 font-medium ml-6">Oct 24, 2026</p>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs rounded-lg group-hover:bg-primary group-hover:text-primary-foreground group-hover:border-primary">Start <ArrowRight size={12} className="ml-1" /></Button>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{cbts.length > 0 ? (
|
||||
cbts.map((cbt, idx) => (
|
||||
<Card key={cbt.id} className="overflow-hidden">
|
||||
<div className="p-6 border-b border-border flex items-center justify-between bg-background">
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<Badge variant="outline" className="border-primary/20 bg-primary/5 text-primary">{cbt.exam_pattern}</Badge>
|
||||
<span className="text-xs text-muted-foreground font-semibold uppercase tracking-wider">{cbt.scheduled_date ? new Date(cbt.scheduled_date).toLocaleDateString() : 'Available Now'}</span>
|
||||
</div>
|
||||
<h3 className="font-semibold text-lg tracking-tight">{cbt.title}</h3>
|
||||
<p className="text-sm text-muted-foreground flex items-center gap-1.5 mt-1.5">
|
||||
<Clock size={14} /> {cbt.total_time} Minutes • {cbt.difficulty || 'Medium'}
|
||||
</p>
|
||||
</div>
|
||||
<Link href={`/dashboard/cbt`}>
|
||||
<Button variant={idx === 0 ? "secondary" : "outline"} className="rounded-xl font-medium">Start</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
<div className="p-5 bg-secondary/50 flex items-center justify-between transition-colors hover:bg-secondary/70 opacity-75">
|
||||
<div>
|
||||
<h3 className="font-medium text-sm text-foreground flex items-center gap-2"><Lock size={14} className="text-muted-foreground"/> Part Test 3: Mechanics</h3>
|
||||
<p className="text-xs text-muted-foreground mt-0.5 font-medium ml-6">Oct 24, 2026</p>
|
||||
</div>
|
||||
<Badge variant="secondary" className="text-xs">Pro Only</Badge>
|
||||
</div>
|
||||
<Card className="overflow-hidden p-6 border-dashed opacity-70">
|
||||
<p className="text-muted-foreground text-sm">No upcoming CBTs at the moment.</p>
|
||||
</Card>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -340,3 +312,4 @@ export default function DashboardPage() {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user