Fix UI bugs, admin bulk upload limits, test ingestion bugs, and reset demo users

This commit is contained in:
2026-09-25 14:21:48 +05:30
parent 08e54ac055
commit eefcb12ffc
9 changed files with 5911 additions and 146 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -236,9 +236,13 @@ async def bulk_register_students(file: UploadFile = File(...), session: Session
decoded = content.decode('utf-8-sig') decoded = content.decode('utf-8-sig')
csv_reader = csv.DictReader(io.StringIO(decoded)) 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 = [] results = []
for row in csv_reader: for row in rows:
email = row.get("email", "").strip() email = row.get("email", "").strip()
name = row.get("name", "").strip() name = row.get("name", "").strip()
phone = row.get("phone", "").strip() phone = row.get("phone", "").strip()
@@ -436,7 +440,7 @@ def login(payload: LoginPayload, session: Session = Depends(get_session)):
# --- CBT ENGINE --- # --- CBT ENGINE ---
@app.get("/api/tests", response_model=List[models.Test], tags=["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) query = select(models.Test).where(models.Test.is_active == True)
if exam_pattern: if exam_pattern:
if exam_pattern == 'JEE_NEET': 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() return session.exec(query).all()
@app.get("/api/tests/{test_id}", response_model=models.Test, tags=["CBT Engine"]) @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) test = session.get(models.Test, test_id)
if not test: if not test:
raise HTTPException(status_code=404, detail="Requested examination structure not found.") raise HTTPException(status_code=404, detail="Requested examination structure not found.")
return test return test
@app.get("/api/tests/{test_id}/questions", response_model=List[models.Question], tags=["CBT Engine"]) @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) test = session.get(models.Test, test_id)
if not test: if not test:
raise HTTPException(status_code=404, detail="Requested examination structure not found.") 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 return question
@app.post("/api/attempts/submit", response_model=models.Attempt, tags=["CBT Engine"]) @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") user_id = payload.get("user_id")
source_type_raw = payload.get("source_type", "Test") source_type_raw = payload.get("source_type", "Test")
source_id = payload.get("source_id") source_id = payload.get("source_id")

File diff suppressed because it is too large Load Diff

View 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
View 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()

View File

@@ -138,14 +138,6 @@ export default function SettingsPage() {
</CardContent> </CardContent>
</Card> </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>
</div> </div>
) )

View File

@@ -46,14 +46,31 @@ export default function StudentsPage() {
const [selectedIds, setSelectedIds] = useState<string[]>([]) const [selectedIds, setSelectedIds] = useState<string[]>([])
const [deleting, setDeleting] = useState(false) const [deleting, setDeleting] = useState(false)
const [isUploading, setIsUploading] = useState(false) const [isUploading, setIsUploading] = useState(false)
const [uploadProgress, setUploadProgress] = useState(0)
const [uploadTimeLeft, setUploadTimeLeft] = useState(0)
const fileInputRef = useRef<HTMLInputElement>(null) const fileInputRef = useRef<HTMLInputElement>(null)
const handleBulkUpload = async (e: React.ChangeEvent<HTMLInputElement>) => { const handleBulkUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0] const file = e.target.files?.[0]
if (!file) return if (!file) return
setIsUploading(true) setIsUploading(true)
setLoading(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 { try {
const formData = new FormData() const formData = new FormData()
formData.append("file", file) formData.append("file", file)
@@ -62,9 +79,16 @@ export default function StudentsPage() {
body: formData, 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() const results = await rawRes.json()
// Delay alert slightly to let progress bar reach 100% visually
setTimeout(async () => {
alert(`Successfully registered ${results.length} students!`) alert(`Successfully registered ${results.length} students!`)
// Download CSV with passwords using Blob to fix formatting on Windows // Download CSV with passwords using Blob to fix formatting on Windows
@@ -85,12 +109,18 @@ export default function StudentsPage() {
// Refresh students // Refresh students
const fresh = await fetchWithAuth("/api/admin/students").then(r => r.json()) const fresh = await fetchWithAuth("/api/admin/students").then(r => r.json())
setStudents(fresh) setStudents(fresh)
} catch (err: any) {
alert("Error: " + err.message)
} finally {
setIsUploading(false) setIsUploading(false)
setLoading(false) setLoading(false)
if (fileInputRef.current) fileInputRef.current.value = "" if (fileInputRef.current) fileInputRef.current.value = ""
}, 500);
} catch (err: any) {
alert("Error: " + err.message)
setIsUploading(false)
setLoading(false)
if (fileInputRef.current) fileInputRef.current.value = ""
} finally {
clearInterval(progressInterval);
} }
} }
@@ -136,6 +166,46 @@ export default function StudentsPage() {
.finally(() => setLoading(false)) .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 filtered = students.filter(s => {
const searchMatch = (s.name || "").toLowerCase().includes(search.toLowerCase()) || const searchMatch = (s.name || "").toLowerCase().includes(search.toLowerCase()) ||
@@ -162,6 +232,7 @@ export default function StudentsPage() {
{loading ? "Loading..." : `${students.length} students enrolled`} {loading ? "Loading..." : `${students.length} students enrolled`}
</p> </p>
</div> </div>
<div className="flex flex-col items-end gap-1">
<div className="flex gap-2"> <div className="flex gap-2">
{selectedIds.length > 0 && ( {selectedIds.length > 0 && (
<Button variant="outline" className="text-red-500 border-red-500 hover:bg-red-500 hover:text-white" disabled={deleting} onClick={handleDeleteSelected}> <Button variant="outline" className="text-red-500 border-red-500 hover:bg-red-500 hover:text-white" disabled={deleting} onClick={handleDeleteSelected}>
@@ -176,11 +247,28 @@ export default function StudentsPage() {
<Button variant="outline" className="border-border hover:bg-secondary hover:text-white" onClick={downloadTemplate}> <Button variant="outline" className="border-border hover:bg-secondary hover:text-white" onClick={downloadTemplate}>
<Download size={16} className="mr-2" /> Download Template <Download size={16} className="mr-2" /> Download Template
</Button> </Button>
<Button variant="outline" className="border-border hover:bg-secondary hover:text-white"> <Button variant="outline" className="border-border hover:bg-secondary hover:text-white" onClick={exportCSV}>
<Download size={16} className="mr-2" /> Export CSV <Download size={16} className="mr-2" /> Export CSV
</Button> </Button>
</div> </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"> <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="All">All Courses</option>
<option value="JEE_Main">JEE Main</option> <option value="JEE_Main">JEE Main</option>
<option value="JEE_Advanced">JEE Advanced</option>
<option value="NEET">NEET</option> <option value="NEET">NEET</option>
<option value="JEE_NEET">JEE + NEET</option> <option value="JEE_NEET">JEE + NEET</option>
</select> </select>
@@ -336,3 +423,4 @@ export default function StudentsPage() {
</div> </div>
) )
} }

View File

@@ -129,7 +129,7 @@ export default function CBTPage() {
} }
return ( 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="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 className="flex flex-col md:flex-row md:items-end justify-between gap-4">
<div> <div>
@@ -360,6 +360,6 @@ export default function CBTPage() {
</div> </div>
)} )}
</div> </div>
</PremiumGate> </>
); );
} }

View File

@@ -1,6 +1,7 @@
"use client" "use client"
import { fetchWithAuth } from "@/lib/apiClient"; import { fetchWithAuth } from "@/lib/apiClient";
import Link from "next/link";
import { useRef, useState, useEffect } from "react" import { useRef, useState, useEffect } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
@@ -14,6 +15,8 @@ export default function DashboardPage() {
const { user, userId, loading: userLoading, hasAccessTo, trialInfo } = useActiveUser() const { user, userId, loading: userLoading, hasAccessTo, trialInfo } = useActiveUser()
const [userStats, setUserStats] = useState<any>(null) const [userStats, setUserStats] = useState<any>(null)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [dpps, setDpps] = useState<any[]>([])
const [cbts, setCbts] = useState<any[]>([])
const [downloading, setDownloading] = useState(false) const [downloading, setDownloading] = useState(false)
const [shareMenuOpen, setShareMenuOpen] = useState(false) const [shareMenuOpen, setShareMenuOpen] = useState(false)
const progressRef = useRef<HTMLDivElement>(null) const progressRef = useRef<HTMLDivElement>(null)
@@ -25,20 +28,26 @@ export default function DashboardPage() {
setLoading(false) setLoading(false)
return return
} }
async function fetchStats() { async function fetchStatsAndTests() {
try { try {
const res = await fetchWithAuth(`/api/users/${currentUserId}/stats`) const pattern = user?.target_exam || "JEE_Main";
if (res.ok) { const [statsRes, dppRes, cbtRes] = await Promise.all([
const data = await res.json() fetchWithAuth(`/api/users/${currentUserId}/stats`),
setUserStats(data) 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) { } catch (e) {
console.error("Failed to load user stats", e) console.error("Failed to load user data", e)
} finally { } finally {
setLoading(false) setLoading(false)
} }
} }
fetchStats() fetchStatsAndTests()
}, [userLoading, user?.id, userId]) }, [userLoading, user?.id, userId])
const getShareUrl = () => { const getShareUrl = () => {
@@ -227,61 +236,29 @@ export default function DashboardPage() {
<Button variant="ghost" size="sm" className="text-muted-foreground font-medium">View All</Button> <Button variant="ghost" size="sm" className="text-muted-foreground font-medium">View All</Button>
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
<Card className="hover:border-primary/30 hover:shadow-md cursor-pointer transition-all duration-300 group"> {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"> <CardContent className="p-5 flex items-center justify-between">
<div className="flex gap-4 items-center"> <div className="flex gap-4 items-center">
<div className="p-3 bg-secondary rounded-xl text-foreground"> <div className="p-3 bg-secondary rounded-xl text-foreground">
<BrainCircuit size={20} /> <BrainCircuit size={20} />
</div> </div>
<div> <div>
<h3 className="font-medium text-base group-hover:text-primary transition-colors">Physics: Rotational Dynamics</h3> <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">20 Questions • High Priority</p> <p className="text-sm text-muted-foreground mt-0.5">{dpp.total_time} Minutes • {dpp.difficulty || "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>
{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} />
</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>
</div> </div>
<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"> <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} /> Start <ArrowRight size={14} />
</Button> </Button>
</Link>
</CardContent> </CardContent>
</Card> </Card>
))
) : ( ) : (
<Card className="relative opacity-60 cursor-not-allowed border-dashed"> <p className="text-muted-foreground text-sm py-4">No DPPs scheduled for today.</p>
<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>
)} )}
</div> </div>
</section> </section>
@@ -293,38 +270,33 @@ export default function DashboardPage() {
Upcoming CBTs Upcoming CBTs
</h2> </h2>
</div> </div>
<Card className="overflow-hidden"> <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 className="p-6 border-b border-border flex items-center justify-between bg-background">
<div> <div>
<div className="flex items-center gap-3 mb-2"> <div className="flex items-center gap-3 mb-2">
<Badge variant="outline" className="border-primary/20 bg-primary/5 text-primary">Mock Test</Badge> <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">Tomorrow, 10:00 AM</span> <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> </div>
<h3 className="font-semibold text-lg tracking-tight">{isNeet ? 'NEET Mock Test 4' : 'JEE Mock Test 4'}</h3> <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"> <p className="text-sm text-muted-foreground flex items-center gap-1.5 mt-1.5">
<Clock size={14} /> 180 Minutes • 720 Marks <Clock size={14} /> {cbt.total_time} Minutes • {cbt.difficulty || 'Medium'}
</p> </p>
</div> </div>
<Button variant="secondary" className="rounded-xl font-medium">Remind Me</Button> <Link href={`/dashboard/cbt`}>
<Button variant={idx === 0 ? "secondary" : "outline"} className="rounded-xl font-medium">Start</Button>
</Link>
</div> </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="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> </Card>
))
) : (
<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>
)}
</div>
</section> </section>
</div> </div>
@@ -340,3 +312,4 @@ export default function DashboardPage() {
</div> </div>
) )
} }