diff --git a/backend/main.py b/backend/main.py index 4f96c04..aa23c3f 100644 --- a/backend/main.py +++ b/backend/main.py @@ -147,6 +147,14 @@ def submit_test_attempt(payload: Dict[str, Any], session: Session = Depends(get_ attempt_id_str = payload.get("id") import uuid as _uuid attempt_id = _uuid.UUID(attempt_id_str) if attempt_id_str else _uuid.uuid4() + + # Check if this is a retake + existing_attempts_query = select(models.Attempt).where( + models.Attempt.user_id == user_id, + models.Attempt.source_id == source_id + ) + existing_attempts = session.exec(existing_attempts_query).all() + is_retake = len(existing_attempts) > 0 attempt_record = models.Attempt( id=attempt_id, @@ -160,7 +168,8 @@ def submit_test_attempt(payload: Dict[str, Any], session: Session = Depends(get_ score=0, total_questions=total_questions, accuracy=0.0, - status="Submitted" + status="Submitted", + is_retake=is_retake ) session.add(attempt_record) session.flush() @@ -522,7 +531,8 @@ async def upload_user_avatar(user_id: UUID, file: UploadFile = File(...), sessio contents = await file.read() file_extension = file.filename.split(".")[-1] if "." in file.filename else "jpg" - object_name = f"avatar_{user_id}.{file_extension}" + current_year = datetime.utcnow().year + object_name = f"avatars/{current_year}/avatar_{user_id}.{file_extension}" public_url = upload_file(contents, object_name, content_type=file.content_type) if not public_url: @@ -579,7 +589,8 @@ async def upload_admin_test( from worker import process_pdf_test pdf_bytes = await file.read() - object_name = f"tests/{uuid.uuid4()}_{file.filename}" + current_year = datetime.utcnow().year + object_name = f"tests/{current_year}/{uuid.uuid4()}_{file.filename}" url = storage.upload_file(pdf_bytes, object_name, content_type="application/pdf") if not url: raise HTTPException(status_code=500, detail="Failed to upload PDF to MinIO") @@ -739,4 +750,58 @@ def delete_test(test_id: UUID, session: Session = Depends(get_session)): # 7. Delete the Test itself session.delete(test_obj) session.commit() - return {"status": "success", "message": "Test and all associated questions, attempts, and mistakes deleted successfully"} \ No newline at end of file + return {"status": "success", "message": "Test and all associated questions, attempts, and mistakes deleted successfully"} + +# --- COUPON CRUD ROUTES --- + +class CouponCreate(BaseModel): + code: str + discount_percent: float + max_discount_amount: float + is_active: bool = True + expires_at: Optional[datetime] = None + +class CouponUpdate(BaseModel): + code: Optional[str] = None + discount_percent: Optional[float] = None + max_discount_amount: Optional[float] = None + is_active: Optional[bool] = None + expires_at: Optional[datetime] = None + +@app.get("/api/admin/coupons", tags=["Admin"]) +def get_admin_coupons(session: Session = Depends(get_session)): + coupons = session.exec(select(models.Coupon)).all() + return coupons + +@app.post("/api/admin/coupons", tags=["Admin"]) +def create_coupon(payload: CouponCreate, session: Session = Depends(get_session)): + coupon = models.Coupon(**payload.dict()) + session.add(coupon) + session.commit() + session.refresh(coupon) + return coupon + +@app.put("/api/admin/coupons/{coupon_id}", tags=["Admin"]) +def update_coupon(coupon_id: UUID, payload: CouponUpdate, session: Session = Depends(get_session)): + coupon = session.get(models.Coupon, coupon_id) + if not coupon: + raise HTTPException(status_code=404, detail="Coupon not found") + + update_data = payload.dict(exclude_unset=True) + for key, value in update_data.items(): + setattr(coupon, key, value) + + session.add(coupon) + session.commit() + session.refresh(coupon) + return coupon + +@app.delete("/api/admin/coupons/{coupon_id}", tags=["Admin"]) +def delete_coupon(coupon_id: UUID, session: Session = Depends(get_session)): + coupon = session.get(models.Coupon, coupon_id) + if not coupon: + raise HTTPException(status_code=404, detail="Coupon not found") + + session.delete(coupon) + session.commit() + return {"status": "success", "message": "Coupon deleted successfully"} \ No newline at end of file diff --git a/backend/models.py b/backend/models.py index 7ad5384..365e656 100644 --- a/backend/models.py +++ b/backend/models.py @@ -119,6 +119,23 @@ class Test(SQLModel, table=True): is_active: bool = Field(default=True) created_by: UUID +class Transaction(SQLModel, table=True): + id: UUID = Field(default_factory=uuid4, primary_key=True) + user_id: UUID = Field(foreign_key="user.id") + amount: float + tier: str + status: str + created_at: datetime = Field(default_factory=datetime.utcnow) + +class Coupon(SQLModel, table=True): + id: UUID = Field(default_factory=uuid4, primary_key=True) + code: str = Field(unique=True, index=True) + discount_percent: float + max_discount_amount: float + is_active: bool = Field(default=True) + expires_at: Optional[datetime] = None + created_at: datetime = Field(default_factory=datetime.utcnow) + class Attempt(SQLModel, table=True): id: UUID = Field(default_factory=uuid4, primary_key=True) user_id: UUID = Field(foreign_key="user.id") @@ -132,6 +149,7 @@ class Attempt(SQLModel, table=True): total_questions: int accuracy: float = Field(default=0.0) status: str = Field(default="In_Progress") # In_Progress, Submitted, Timed_Out + is_retake: bool = Field(default=False) class Mistake(SQLModel, table=True): id: UUID = Field(default_factory=uuid4, primary_key=True) diff --git a/backend_logs.txt b/backend_logs.txt new file mode 100644 index 0000000..c04e89a Binary files /dev/null and b/backend_logs.txt differ diff --git a/full_logs.txt b/full_logs.txt new file mode 100644 index 0000000..361f97b Binary files /dev/null and b/full_logs.txt differ diff --git a/src/app/admin/coupons/page.tsx b/src/app/admin/coupons/page.tsx new file mode 100644 index 0000000..cc9626c --- /dev/null +++ b/src/app/admin/coupons/page.tsx @@ -0,0 +1,155 @@ +"use client" + +import { useState, useEffect } from "react" +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { Badge } from "@/components/ui/badge" +import { Loader2, Plus, Trash2, Edit } from "lucide-react" + +export default function CouponsAdmin() { + const [coupons, setCoupons] = useState([]) + const [loading, setLoading] = useState(true) + const [submitting, setSubmitting] = useState(false) + const [newCoupon, setNewCoupon] = useState({ + code: "", + discount_percent: 10, + max_discount_amount: 100, + }) + + useEffect(() => { + fetchCoupons() + }, []) + + const fetchCoupons = async () => { + try { + const res = await fetch('/api/admin/coupons') + const data = await res.json() + setCoupons(data) + } catch (e) { + console.error(e) + } finally { + setLoading(false) + } + } + + const handleCreate = async () => { + if (!newCoupon.code) return + setSubmitting(true) + try { + await fetch('/api/admin/coupons', { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(newCoupon) + }) + setNewCoupon({ code: "", discount_percent: 10, max_discount_amount: 100 }) + await fetchCoupons() + } catch (e) { + console.error(e) + } finally { + setSubmitting(false) + } + } + + const handleDelete = async (id: string) => { + if (!confirm("Delete this coupon?")) return + try { + await fetch(`/api/admin/coupons/${id}`, { method: "DELETE" }) + await fetchCoupons() + } catch (e) { + console.error(e) + } + } + + return ( +
+
+

Coupon Management

+

Create and manage discount codes for subscriptions.

+
+ +
+ + + Create Coupon + Add a new promo code + + +
+ + setNewCoupon({...newCoupon, code: e.target.value.toUpperCase()})} + /> +
+
+ + setNewCoupon({...newCoupon, discount_percent: Number(e.target.value)})} + /> +
+
+ + setNewCoupon({...newCoupon, max_discount_amount: Number(e.target.value)})} + /> +
+ +
+
+ + + + Active Coupons + + + {loading ? ( +
+ ) : ( + + + + Code + Discount + Max Off + Status + Actions + + + + {coupons.length === 0 && ( + No coupons found. + )} + {coupons.map(c => ( + + {c.code} + {c.discount_percent}% + ₹{c.max_discount_amount} + + {c.is_active ? Active : Inactive} + + + + + + ))} + +
+ )} +
+
+
+
+ ) +} diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx index 2e0616a..7961399 100644 --- a/src/app/admin/layout.tsx +++ b/src/app/admin/layout.tsx @@ -1,7 +1,7 @@ "use client" import { ReactNode, useEffect } from "react" -import { LayoutDashboard, Users, Database, Terminal, Settings, FileText } from "lucide-react" +import { LayoutDashboard, Users, Database, Terminal, Settings, FileText, Tag } from "lucide-react" import Link from "next/link" import { usePathname } from "next/navigation" import { Logo } from "@/components/Logo" @@ -27,6 +27,7 @@ export default function AdminLayout({ children }: { children: ReactNode }) { } label="Question Bank" active={pathname.startsWith("/admin/questions")} /> } label="AI Agents" active={pathname.startsWith("/admin/agents")} /> } label="Students" active={pathname.startsWith("/admin/students")} /> + } label="Coupons" active={pathname.startsWith("/admin/coupons")} />
} label="System Settings" active={pathname.startsWith("/admin/settings")} /> diff --git a/src/app/admin/students/[studentId]/page.tsx b/src/app/admin/students/[studentId]/page.tsx new file mode 100644 index 0000000..a40a23c --- /dev/null +++ b/src/app/admin/students/[studentId]/page.tsx @@ -0,0 +1,202 @@ +"use client" + +import { useEffect, useState } from "react" +import { useParams, useRouter } from "next/navigation" +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { ArrowLeft, Loader2, User, Mail, Target, Award, Calendar, Activity, BookOpen } from "lucide-react" + +export default function StudentKundliPage() { + const params = useParams() + const router = useRouter() + const studentId = params.studentId as string + + const [loading, setLoading] = useState(true) + const [userData, setUserData] = useState(null) + + useEffect(() => { + // Simulated fetch of student's complete "Kundli" + setTimeout(() => { + setUserData({ + id: studentId, + name: "Rahul Sharma", + email: "rahul@example.com", + target: "NEET", + tier: "AI Pro", + joined: "Oct 12, 2025", + stats: { + total_tests: 42, + average_score: "78%", + streak: 45, + total_questions_solved: 3450, + ai_coach_interactions: 124 + }, + recent_tests: [ + { id: 1, name: "Full Syllabus NEET Mock #4", score: "620/720", date: "2 days ago", percentile: "98.5%" }, + { id: 2, name: "Physics: Kinematics DPP", score: "45/50", date: "4 days ago", percentile: "95.2%" }, + { id: 3, name: "Biology: Human Reproduction", score: "88/100", date: "1 week ago", percentile: "99.1%" } + ], + payments: [ + { id: "PAY_1", amount: "₹1599", date: "Oct 12, 2025", plan: "AI Pro (Yearly)", status: "Success" } + ], + weak_topics: ["Rotational Dynamics", "Organic Chemistry", "Genetics"] + }) + setLoading(false) + }, 800) + }, [studentId]) + + if (loading) { + return
+ } + + if (!userData) return
User not found
+ + return ( +
+ + +
+ {/* User Identity Card */} + + +
+
+ {userData.name.charAt(0)} +
+
+ {userData.name} + + {userData.email} + +
+
+
+ +
+
Target Exam
+
{userData.target}
+
+
+
Plan Tier
+ {userData.tier} +
+
+
Joined
+
{userData.joined}
+
+
+
User ID
+
{userData.id}
+
+
+
+ + {/* Stats Summary */} +
+ + +
{userData.stats.total_tests}
+
Tests Attempted
+
+
+ + +
{userData.stats.streak}
+
Day Streak
+
+
+ + +
{userData.stats.average_score}
+
Avg. Accuracy
+
+
+ + +
{userData.stats.total_questions_solved}
+
Questions Solved
+
+
+
+
+ + {/* Recent Activity & Weak Topics */} +
+ + + Recent Tests + + +
+ {userData.recent_tests.map((test: any) => ( +
+
+
{test.name}
+
{test.date}
+
+
+
{test.score}
+
{test.percentile} %ile
+
+
+ ))} +
+
+
+ + + + AI Identified Weaknesses + + +
    + {userData.weak_topics.map((topic: string, i: number) => ( +
  • +
    + {topic} +
  • + ))} +
+
+
+
+ + {/* Payment History */} + + + Payment History + + +
+ + + + + + + + + + + + {userData.payments.map((p: any) => ( + + + + + + + + ))} + +
Transaction IDPlanDateAmountStatus
{p.id}{p.plan}{p.date}{p.amount}{p.status}
+
+
+
+ +
+ ) +} diff --git a/src/app/admin/students/page.tsx b/src/app/admin/students/page.tsx index 2d84141..c9918ef 100644 --- a/src/app/admin/students/page.tsx +++ b/src/app/admin/students/page.tsx @@ -3,8 +3,10 @@ import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" import { Search, Download, MoreVertical } from "lucide-react" +import { useRouter } from "next/navigation" export default function StudentsPage() { + const router = useRouter() const students = [ { id: "STU-001", name: "Rahul Sharma", email: "rahul@example.com", tier: "AI Pro", target: "NEET", joined: "Oct 12, 2025", tests: 42, streak: 45, lastLogin: "Today, 08:30 AM" }, { id: "STU-002", name: "Priya Patel", email: "priya@example.com", tier: "Basic", target: "JEE Main", joined: "Nov 05, 2025", tests: 14, streak: 2, lastLogin: "Yesterday, 14:15 PM" }, @@ -50,7 +52,11 @@ export default function StudentsPage() { {students.map((s) => ( - + router.push(`/admin/students/${s.id}`)} + >
{s.name}
{s.email}
diff --git a/src/app/cbt/[examId]/page.tsx b/src/app/cbt/[examId]/page.tsx index 5c9fd26..ceba32d 100644 --- a/src/app/cbt/[examId]/page.tsx +++ b/src/app/cbt/[examId]/page.tsx @@ -37,6 +37,7 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri const [showPeriodicTable, setShowPeriodicTable] = useState(false); const [isPaused, setIsPaused] = useState(false); const [showSubmitConfirm, setShowSubmitConfirm] = useState(false); + const [accessError, setAccessError] = useState(null); const [localTheme, setLocalTheme] = useState('light'); useEffect(() => { @@ -97,6 +98,20 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri const testRes = await fetch(`${apiUrl}/api/tests/${examId}?user_id=b0e17378-864d-439e-8806-708dc48a891b`); if (!testRes.ok) throw new Error("Failed to fetch test structure"); const testData = await testRes.json(); + + // Strict boundary checking + const now = new Date(); + if (testData.scheduled_date && new Date(testData.scheduled_date) > now) { + setAccessError(`This exam is scheduled for ${new Date(testData.scheduled_date).toLocaleString()}. You cannot start it yet.`); + setMounted(true); + return; + } + if (testData.end_date && new Date(testData.end_date) < now) { + setAccessError(`This exam expired on ${new Date(testData.end_date).toLocaleString()} and is no longer available.`); + setMounted(true); + return; + } + setExamType(testData.type); // Extract all question IDs from sections @@ -232,7 +247,26 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri await submitAttemptToServer('b0e17378-864d-439e-8806-708dc48a891b'); } - if (!mounted || questions.length === 0) return
Loading Exam Engine...
; + if (!mounted || questions.length === 0) { + return
Initializing CBT Engine
+ } + + if (accessError) { + return ( +
+
+
+ +
+

Access Denied

+

{accessError}

+ +
+
+ ); + } if (status === 'NOT_STARTED') { return ( @@ -293,8 +327,18 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri const currentQuestion = questions[currentQuestionIndex]; const currentState = currentQuestion ? answers[currentQuestion.id] : null + // Helper to ensure math options missing $ delimiters get rendered properly + const renderLatex = (text: string) => { + if (typeof text !== 'string') return text; + // If it contains backslash but no dollar signs, we might want to wrap the whole thing in $ $ + if (text.includes('\\') && !text.includes('$')) { + return `$${text}$`; + } + return text; + }; + const handleOptionSelect = (qId: string, letter: string, type: string) => { - if (type === 'Multiple_Choice') { + if (type === 'MCQ_Multiple' || type === 'Multiple_Choice') { const currentSelected = currentState?.selected_answer || ""; const selectedArray = currentSelected.split(',').filter(Boolean); if (selectedArray.includes(letter)) { @@ -421,6 +465,13 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri {currentQuestion.text_en}
+ + {(currentQuestion.type === 'MCQ_Multiple' || currentQuestion.type === 'Multiple_Choice') && ( +

+ + This question has MULTIPLE CORRECT OPTIONS. Please select all that apply. +

+ )}
{currentQuestion.type === 'Numerical' ? ( @@ -441,7 +492,8 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri ) : ( currentQuestion.options?.map((option: string, idx: number) => { const letter = String.fromCharCode(65 + idx); - const isSelected = currentQuestion.type === 'Multiple_Choice' + const isMultiple = currentQuestion.type === 'MCQ_Multiple' || currentQuestion.type === 'Multiple_Choice'; + const isSelected = isMultiple ? (currentState?.selected_answer || "").split(',').includes(letter) : currentState?.selected_answer === letter; @@ -453,14 +505,14 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri ${isSelected ? 'border-primary bg-primary/5 shadow-[0_0_15px_-5px_var(--color-primary)]' : 'border-border bg-background hover:border-primary/50 hover:bg-secondary/20'} `} > -
{letter}
- {option} + {renderLatex(option)} @@ -481,6 +533,14 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
+4 / -1 +
@@ -488,6 +548,13 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri {currentQuestion.text_hi}
+ + {(currentQuestion.type === 'MCQ_Multiple' || currentQuestion.type === 'Multiple_Choice') && ( +

+ + This question has MULTIPLE CORRECT OPTIONS. Please select all that apply. +

+ )}
{currentQuestion.type === 'Numerical' ? ( @@ -508,7 +575,8 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri ) : ( currentQuestion.options_hi?.map((option: string, idx: number) => { const letter = String.fromCharCode(65 + idx); - const isSelected = currentQuestion.type === 'Multiple_Choice' + const isMultiple = currentQuestion.type === 'MCQ_Multiple' || currentQuestion.type === 'Multiple_Choice'; + const isSelected = isMultiple ? (currentState?.selected_answer || "").split(',').includes(letter) : currentState?.selected_answer === letter; @@ -520,14 +588,14 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri ${isSelected ? 'border-primary bg-primary/5 shadow-[0_0_15px_-5px_var(--color-primary)]' : 'border-border bg-background hover:border-primary/50 hover:bg-secondary/20'} `} > -
{letter}
- {option} + {renderLatex(option)} diff --git a/src/app/dashboard/cbt/history/[attemptId]/page.tsx b/src/app/dashboard/cbt/history/[attemptId]/page.tsx index f1b4ee2..0979f55 100644 --- a/src/app/dashboard/cbt/history/[attemptId]/page.tsx +++ b/src/app/dashboard/cbt/history/[attemptId]/page.tsx @@ -93,15 +93,15 @@ export default function AttemptHistoryPage({ params }: { params: Promise<{ attem

Review your performance, detailed solutions, and identify your weak spots.

-
- +
+
Score
{scoreCalc} / {maxScore}
- +
Time Taken
diff --git a/src/app/dashboard/cbt/page.tsx b/src/app/dashboard/cbt/page.tsx index 42829b4..be4c65f 100644 --- a/src/app/dashboard/cbt/page.tsx +++ b/src/app/dashboard/cbt/page.tsx @@ -16,6 +16,7 @@ interface TestData { total_time: number; difficulty: string; scheduled_date?: string; + end_date?: string; } export default function CBTPage() { @@ -26,29 +27,45 @@ export default function CBTPage() { const [attempts, setAttempts] = useState([]); const [showUpgradeModal, setShowUpgradeModal] = useState(false); const [showAllExams, setShowAllExams] = useState(false); + const [selectedPattern, setSelectedPattern] = useState(""); + const [dateFilter, setDateFilter] = useState(""); const { userId, user, hasAccessTo, loading: userLoading } = useActiveUser(); useEffect(() => { if (userLoading || !userId) return; - async function loadCBTExams() { + async function loadUser() { try { - // 1. Fetch user target exam const userRes = await fetch(`/api/users/${userId}`); if (!userRes.ok) throw new Error("Failed to load user target exam"); const userData = await userRes.json(); const pattern = userData.target_exam; setTargetExam(pattern === "NEET" ? "NEET" : pattern === "JEE_Advanced" ? "JEE Advanced" : "JEE Main"); + + // Only set the initial pattern if it hasn't been changed by the user + if (!selectedPattern) { + setSelectedPattern(pattern); + } + } catch (err) { + console.error("Error loading user:", err); + } + } + loadUser(); + }, [userId, userLoading]); - // 2. Fetch tests filtered by exam - const testRes = await fetch(`/api/tests?exam_pattern=${pattern}&user_id=${userId}`); + useEffect(() => { + if (!userId || !selectedPattern) return; + + async function loadTests() { + setLoading(true); + try { + const testRes = await fetch(`/api/tests?exam_pattern=${selectedPattern}&user_id=${userId}`); if (testRes.ok) { const testsData = await testRes.json(); setExams(testsData); } - // 3. Fetch user attempts const attemptsRes = await fetch(`/api/users/${userId}/attempts`); if (attemptsRes.ok) { const attemptsData = await attemptsRes.json(); @@ -60,12 +77,21 @@ export default function CBTPage() { setLoading(false); } } - loadCBTExams(); - }, [userId, userLoading]); + loadTests(); + }, [userId, selectedPattern]); - const filteredExams = exams.filter(exam => - exam.title.toLowerCase().includes(search.toLowerCase()) - ); + const filteredExams = exams.filter(exam => { + const matchesSearch = exam.title.toLowerCase().includes(search.toLowerCase()); + if (!matchesSearch) return false; + + if (dateFilter) { + if (!exam.scheduled_date) return false; + const scheduledDate = new Date(exam.scheduled_date).toISOString().split('T')[0]; + if (scheduledDate !== dateFilter) return false; + } + + return true; + }); const formatDate = (dateStr: string) => { return new Date(dateStr).toLocaleDateString("en-US", { year: 'numeric', month: 'short', day: 'numeric' }); @@ -73,6 +99,9 @@ export default function CBTPage() { // Get the highest score for each test if attempted multiple times const attemptsByTest = attempts.reduce((acc, curr) => { + // Isolate analytics to first attempt only - ignore retakes + if (curr.is_retake) return acc; + if (!acc[curr.source_id] || acc[curr.source_id].score < curr.score) { acc[curr.source_id] = curr; } @@ -88,18 +117,45 @@ export default function CBTPage() { Real exam environment simulations to conquer {targetExam || "NEET/JEE"}.

-
- - setSearch(e.target.value)} - className="w-full pl-10 pr-4 py-2 bg-background border border-border rounded-xl text-sm focus:ring-2 focus:ring-[#00F0FF] outline-none" - /> +
+
+ setDateFilter(e.target.value)} + className="w-full px-4 py-2 bg-background border border-border rounded-xl text-sm focus:ring-2 focus:ring-[#00F0FF] outline-none text-muted-foreground" + /> +
+
+ + setSearch(e.target.value)} + className="w-full pl-10 pr-4 py-2 bg-background border border-border rounded-xl text-sm focus:ring-2 focus:ring-[#00F0FF] outline-none" + /> +
+
+ {["JEE_MAIN", "JEE_ADVANCED", "NEET"].map(p => { + // Normalize to match db/backend pattern + const dbPattern = p === "JEE_MAIN" ? "JEE_Main" : p === "JEE_ADVANCED" ? "JEE_Advanced" : "NEET"; + return ( + + ) + })} +
+

Upcoming & Active Exams @@ -122,10 +178,12 @@ export default function CBTPage() { // Date logic const now = new Date(); const scheduledTime = exam.scheduled_date ? new Date(exam.scheduled_date) : now; + const endTime = exam.end_date ? new Date(exam.end_date) : null; const isUpcoming = scheduledTime > now; + const isExpired = endTime ? now > endTime : false; return ( - +
@@ -146,6 +204,10 @@ export default function CBTPage() { Premium + ) : isExpired ? ( + + Expired + ) : isAttempted ? ( Attempted @@ -157,7 +219,7 @@ export default function CBTPage() {

- {isUpcoming ? `Available on ${formatDate(exam.scheduled_date!)}` : 'Available Now'} + {isUpcoming ? `Available on ${formatDate(exam.scheduled_date!)}` : isExpired ? `Expired on ${formatDate(exam.end_date!)}` : 'Available Now'}
{exam.total_time} Min • {exam.difficulty} Difficulty
@@ -173,10 +235,29 @@ export default function CBTPage() { > Unlock Test + ) : isExpired && !isAttempted ? ( + + ) : isAttempted ? ( +
+ + + + {!isExpired && ( + + + + )} +
) : ( - - )} @@ -207,10 +288,12 @@ export default function CBTPage() { // Date logic const now = new Date(); const scheduledTime = exam.scheduled_date ? new Date(exam.scheduled_date) : now; + const endTime = exam.end_date ? new Date(exam.end_date) : null; const isUpcoming = scheduledTime > now; + const isExpired = endTime ? now > endTime : false; return ( - +
@@ -231,6 +314,10 @@ export default function CBTPage() { Premium + ) : isExpired ? ( + + Expired + ) : isAttempted ? ( Attempted @@ -242,7 +329,7 @@ export default function CBTPage() {
- {isUpcoming ? `Available on ${formatDate(exam.scheduled_date!)}` : 'Available Now'} + {isUpcoming ? `Available on ${formatDate(exam.scheduled_date!)}` : isExpired ? `Expired on ${formatDate(exam.end_date!)}` : 'Available Now'}
{exam.total_time} Min • {exam.difficulty} Difficulty
@@ -258,10 +345,29 @@ export default function CBTPage() { > Unlock Test + ) : isExpired && !isAttempted ? ( + + ) : isAttempted ? ( +
+ + + + {!isExpired && ( + + + + )} +
) : ( - - )} diff --git a/src/app/dashboard/dpp/page.tsx b/src/app/dashboard/dpp/page.tsx index 226064e..cb81c7a 100644 --- a/src/app/dashboard/dpp/page.tsx +++ b/src/app/dashboard/dpp/page.tsx @@ -9,9 +9,11 @@ import Link from "next/link" import { useActiveUser } from "@/hooks/useActiveUser" export default function DPPPage() { - const { user } = useActiveUser() + const { user, userId } = useActiveUser() const [dpps, setDpps] = useState([]) + const [attempts, setAttempts] = useState([]) const [loading, setLoading] = useState(true) + const [dateFilter, setDateFilter] = useState("") useEffect(() => { async function fetchDpps() { @@ -25,6 +27,14 @@ export default function DPPPage() { const data = await res.json() setDpps(data) } + + if (userId) { + const attemptsRes = await fetch(`/api/users/${userId}/attempts`); + if (attemptsRes.ok) { + const attemptsData = await attemptsRes.json(); + setAttempts(attemptsData); + } + } } catch (err) { console.error("Failed to fetch DPPs", err) } finally { @@ -43,6 +53,29 @@ export default function DPPPage() { } } + const attemptsByTest = attempts.reduce((acc, curr) => { + // Isolate analytics to first attempt only - ignore retakes + if (curr.is_retake) return acc; + + if (!acc[curr.source_id] || acc[curr.source_id].score < curr.score) { + acc[curr.source_id] = curr; + } + return acc; + }, {} as Record); + + const filteredDpps = dpps.filter(dpp => { + if (dateFilter) { + if (!dpp.scheduled_date) return false; + const scheduledDate = new Date(dpp.scheduled_date).toISOString().split('T')[0]; + if (scheduledDate !== dateFilter) return false; + } + return true; + }); + + const formatDate = (dateStr: string) => { + return new Date(dateStr).toLocaleDateString("en-US", { year: 'numeric', month: 'short', day: 'numeric' }); + } + return (
@@ -56,6 +89,12 @@ export default function DPPPage() {
+ setDateFilter(e.target.value)} + className="px-3 py-1.5 bg-background border border-border rounded-lg text-xs font-bold focus:ring-2 focus:ring-[#00F0FF] outline-none text-muted-foreground mr-2" + /> Difficulty: {['Mixed', 'Easy', 'Medium', 'Hard'].map((diff) => ( - +
+ {isUpcoming ? ( + + ) : isExpired && !isAttempted ? ( + + ) : isAttempted ? ( + <> + + + + {!isExpired && ( + + + + )} + ) : (
} label="Overview" active={pathname === "/dashboard"} /> - } label="Daily Practice" active={pathname.startsWith("/dashboard/dpp")} /> - } label="Study Materials" active={pathname.startsWith("/dashboard/materials")} /> + } label="Target DPPs" active={pathname.startsWith("/dashboard/dpp")} /> } label="Syllabus Tracker" active={pathname.startsWith("/dashboard/progress")} /> } label="Leaderboard" active={pathname.startsWith("/dashboard/leaderboard")} /> @@ -73,10 +72,17 @@ export default function DashboardLayout({ children }: { children: ReactNode }) { } - label="Exams (CBT)" + label="Mock Tests (CBT)" active={pathname.startsWith("/dashboard/cbt")} isLocked={!hasAccessTo("weekly-test-series")} /> + } + label="Quick Notes" + active={pathname.startsWith("/dashboard/materials")} + isLocked={!hasAccessTo("detailed-performance-analytics")} + /> } @@ -84,13 +90,6 @@ export default function DashboardLayout({ children }: { children: ReactNode }) { active={pathname.startsWith("/dashboard/analytics")} isLocked={!hasAccessTo("detailed-performance-analytics")} /> - } - label="Rank Predictor" - active={pathname.startsWith("/dashboard/rank-predictor")} - isLocked={!hasAccessTo("college-predictor")} - /> } @@ -111,6 +110,13 @@ export default function DashboardLayout({ children }: { children: ReactNode }) { active={pathname.startsWith("/dashboard/ai-coach")} isLocked={!hasAccessTo("ai-tutor-chat")} /> + } + label="Rank Predictor" + active={pathname.startsWith("/dashboard/rank-predictor")} + isLocked={!hasAccessTo("college-predictor")} + />
diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 8ef6f76..ad273a2 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -91,7 +91,10 @@ export default function DashboardPage() {
-

City Rank

+
+

City Rank

+ Pro +

#42 / 1,200

Top 5% in Jaipur

diff --git a/src/app/upgrade/page.tsx b/src/app/upgrade/page.tsx index f265922..e3effa7 100644 --- a/src/app/upgrade/page.tsx +++ b/src/app/upgrade/page.tsx @@ -109,7 +109,7 @@ export default function UpgradePage() { Free Basic access for everyone. -
₹10/month
+
₹10/year
Subject-wise diagnostics
@@ -124,11 +124,11 @@ export default function UpgradePage() { {/* Pro Tier */} - + Pro For serious aspirants. -
₹1299/month
+
₹1299/year
Everything in Free
@@ -150,11 +150,11 @@ export default function UpgradePage() {
{/* AI Coach Tier */} - + AI Coach Personalized mastery loop. -
₹1599/month
+
₹1599/year
Everything in Pro
diff --git a/src/components/AI/MistakeHeatmap.tsx b/src/components/AI/MistakeHeatmap.tsx index bb868d7..b02be62 100644 --- a/src/components/AI/MistakeHeatmap.tsx +++ b/src/components/AI/MistakeHeatmap.tsx @@ -56,10 +56,10 @@ export function MistakeHeatmap() { {row.attempts.map((errRate, i) => (
- {errRate}% + {errRate}%
))}