feat: setup admin JSON generation and pdf viewer for free plan DPPs
This commit is contained in:
289
src/app/dashboard/ai-coach/cbt-dpp/[id]/page.tsx
Normal file
289
src/app/dashboard/ai-coach/cbt-dpp/[id]/page.tsx
Normal file
@@ -0,0 +1,289 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { BrainCircuit, Clock, CheckCircle2, AlertTriangle, ArrowLeft, ArrowRight, ShieldAlert, Sparkles, Send } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { useActiveUser } from "@/hooks/useActiveUser"
|
||||
import { useRouter } from "next/navigation"
|
||||
|
||||
export default function AIComCBTDPPPage() {
|
||||
const { user, hasAccessTo, loading } = useActiveUser()
|
||||
const router = useRouter()
|
||||
|
||||
const [activeQuestion, setActiveQuestion] = useState(0)
|
||||
const [selectedAnswers, setSelectedAnswers] = useState<Record<number, string>>({})
|
||||
const [timeLeft, setTimeLeft] = useState(1800) // 30 minutes
|
||||
const [submitted, setSubmitted] = useState(false)
|
||||
|
||||
// Mock AI CBT DPP questions
|
||||
const sampleQuestions = [
|
||||
{
|
||||
id: 1,
|
||||
subject: "Physics",
|
||||
topic: "Rotational Dynamics & Torque",
|
||||
question: "A uniform disc of mass M and radius R rotates about an axis passing through its center and perpendicular to its plane. Its moment of inertia about a tangent parallel to its plane is:",
|
||||
options: ["(1/2) M R²", "(5/4) M R²", "(3/2) M R²", "(7/4) M R²"],
|
||||
correct: 1,
|
||||
explanation: "Using parallel axis theorem: I_tangent = I_cm + M R^2 = (1/4 M R^2) + M R^2 = 5/4 M R^2."
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
subject: "Chemistry",
|
||||
topic: "Chemical Equilibrium & Kp/Kc",
|
||||
question: "For the reaction N2(g) + 3H2(g) ⇌ 2NH3(g), the value of Kp / Kc at temperature T Kelvin is given by:",
|
||||
options: ["(RT)⁻²", "(RT)⁻¹", "(RT)", "(RT)²"],
|
||||
correct: 0,
|
||||
explanation: "Δn_g = 2 - (1 + 3) = -2. Therefore, Kp = Kc (RT)^(-2), so Kp / Kc = (RT)^(-2)."
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
subject: "Biology / Maths",
|
||||
topic: "Calculus / Cell Biology",
|
||||
question: "Which organelle is responsible for the synthesis of lipids and steroidal hormones in eukaryotic cells?",
|
||||
options: ["Rough Endoplasmic Reticulum", "Smooth Endoplasmic Reticulum", "Golgi Apparatus", "Lysosome"],
|
||||
correct: 1,
|
||||
explanation: "Smooth Endoplasmic Reticulum (SER) is the primary site for synthesis of lipids and steroid hormones."
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
subject: "Physics",
|
||||
topic: "Electrostatics & Potential",
|
||||
question: "Two point charges +q and -q are placed at distance 2a. The electric potential at the midpoint of the line joining them is:",
|
||||
options: ["Zero", "q / (4πε₀a)", "2q / (4πε₀a)", "Infinity"],
|
||||
correct: 0,
|
||||
explanation: "V_total = V_+q + V_-q = (q / 4πε₀a) + (-q / 4πε₀a) = 0."
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
subject: "Chemistry",
|
||||
topic: "Organic Mechanism & Nucleophiles",
|
||||
question: "Which of the following undergoes SN1 reaction at the fastest rate?",
|
||||
options: ["Methyl chloride", "Ethyl chloride", "Isopropyl chloride", "Tert-butyl chloride"],
|
||||
correct: 3,
|
||||
explanation: "SN1 rate depends on carbocation stability. Tert-butyl carbocation (3°) is the most stable."
|
||||
}
|
||||
]
|
||||
|
||||
useEffect(() => {
|
||||
if (submitted || timeLeft <= 0) return
|
||||
const timer = setInterval(() => {
|
||||
setTimeLeft(prev => Math.max(0, prev - 1))
|
||||
}, 1000)
|
||||
return () => clearInterval(timer)
|
||||
}, [submitted, timeLeft])
|
||||
|
||||
const formatTime = (secs: number) => {
|
||||
const m = Math.floor(secs / 60)
|
||||
const s = secs % 60
|
||||
return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
const handleOptionSelect = (qIdx: number, optIdx: number) => {
|
||||
if (submitted) return
|
||||
setSelectedAnswers(prev => ({ ...prev, [qIdx]: String.fromCharCode(65 + optIdx) }))
|
||||
}
|
||||
|
||||
const calculateScore = () => {
|
||||
let score = 0
|
||||
sampleQuestions.forEach((q, idx) => {
|
||||
const selected = selectedAnswers[idx]
|
||||
if (selected) {
|
||||
const selectedOptIdx = selected.charCodeAt(0) - 65
|
||||
if (selectedOptIdx === q.correct) score += 4
|
||||
else score -= 1
|
||||
}
|
||||
})
|
||||
return score
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <div className="p-8 text-center text-muted-foreground">Loading AI Coach CBT Engine...</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 md:p-8 space-y-6 max-w-6xl mx-auto min-h-screen animate-in fade-in duration-500">
|
||||
{/* Top Header */}
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 bg-secondary/20 border border-border p-6 rounded-3xl backdrop-blur-xl">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/dashboard/dpp">
|
||||
<Button variant="outline" size="icon" className="rounded-xl border-border hover:bg-secondary">
|
||||
<ArrowLeft size={18} />
|
||||
</Button>
|
||||
</Link>
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-xs font-black uppercase tracking-widest text-[#00F0FF] bg-[#00F0FF]/10 px-2.5 py-0.5 rounded-full border border-[#00F0FF]/30">
|
||||
AI CBT DPP Simulator
|
||||
</span>
|
||||
<span className="text-xs font-bold text-muted-foreground">• {user?.target_exam?.replace("_", " ") || "JEE/NEET"}</span>
|
||||
</div>
|
||||
<h1 className="text-2xl font-black tracking-tight">Interactive Target DPP #14</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2 px-4 py-2 bg-background border border-border rounded-2xl font-mono text-lg font-bold text-[#00F0FF] shadow-inner">
|
||||
<Clock size={18} className="text-[#00F0FF] animate-pulse" />
|
||||
{formatTime(timeLeft)}
|
||||
</div>
|
||||
{!submitted ? (
|
||||
<Button
|
||||
onClick={() => setSubmitted(true)}
|
||||
className="bg-[#00F0FF] text-black hover:bg-[#00F0FF]/90 font-bold px-6 rounded-xl shadow-[0_0_15px_rgba(0,240,255,0.2)]"
|
||||
>
|
||||
Submit Test <Send size={16} className="ml-2" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={() => { setSubmitted(false); setSelectedAnswers({}); setTimeLeft(1800); setActiveQuestion(0) }}
|
||||
variant="outline"
|
||||
className="font-bold rounded-xl border-border"
|
||||
>
|
||||
Retake CBT DPP
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{submitted && (
|
||||
<Card className="border-emerald-500/30 bg-emerald-500/10 p-6 rounded-3xl space-y-4 animate-in zoom-in-95">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<CheckCircle2 size={32} className="text-emerald-400" />
|
||||
<div>
|
||||
<h3 className="text-xl font-black text-emerald-400">CBT DPP Completed!</h3>
|
||||
<p className="text-xs text-muted-foreground">Your performance analysis has been evaluated by the AI Evaluator.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className="text-3xl font-black text-emerald-400">{calculateScore()} / 20</span>
|
||||
<p className="text-xs text-muted-foreground font-bold">Total Marks</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Main CBT Grid */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
||||
{/* Question Area */}
|
||||
<div className="lg:col-span-3">
|
||||
<Card className="border-border bg-background/80 backdrop-blur-xl p-6 rounded-3xl shadow-xl space-y-6">
|
||||
<div className="flex justify-between items-center border-b border-border pb-4">
|
||||
<span className="text-sm font-bold text-[#00F0FF] uppercase tracking-wider">
|
||||
Question {activeQuestion + 1} of {sampleQuestions.length}
|
||||
</span>
|
||||
<span className="text-xs font-bold bg-secondary px-3 py-1 rounded-full text-muted-foreground">
|
||||
{sampleQuestions[activeQuestion].topic}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<p className="text-lg font-bold leading-relaxed text-foreground">
|
||||
{sampleQuestions[activeQuestion].question}
|
||||
</p>
|
||||
|
||||
<div className="space-y-3 pt-2">
|
||||
{sampleQuestions[activeQuestion].options.map((opt, optIdx) => {
|
||||
const letter = String.fromCharCode(65 + optIdx)
|
||||
const isSelected = selectedAnswers[activeQuestion] === letter
|
||||
const isCorrect = sampleQuestions[activeQuestion].correct === optIdx
|
||||
|
||||
let btnStyle = "border-border bg-secondary/20 hover:border-primary/50 text-foreground"
|
||||
if (isSelected) {
|
||||
btnStyle = "border-[#00F0FF] bg-[#00F0FF]/15 text-[#00F0FF] font-bold shadow-[0_0_12px_rgba(0,240,255,0.15)]"
|
||||
}
|
||||
if (submitted) {
|
||||
if (isCorrect) btnStyle = "border-emerald-500 bg-emerald-500/20 text-emerald-400 font-bold"
|
||||
else if (isSelected && !isCorrect) btnStyle = "border-red-500 bg-red-500/20 text-red-400 font-bold"
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={optIdx}
|
||||
onClick={() => handleOptionSelect(activeQuestion, optIdx)}
|
||||
className={`flex items-center gap-4 p-4 rounded-2xl border cursor-pointer transition-all ${btnStyle}`}
|
||||
>
|
||||
<span className="w-8 h-8 rounded-xl bg-background/80 border border-border flex items-center justify-center font-bold text-sm shrink-0">
|
||||
{letter}
|
||||
</span>
|
||||
<span className="text-base font-medium flex-1">{opt}</span>
|
||||
{submitted && isCorrect && <CheckCircle2 size={20} className="text-emerald-400" />}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{submitted && (
|
||||
<div className="mt-6 p-4 rounded-2xl bg-[#00F0FF]/10 border border-[#00F0FF]/30 text-sm text-[#00F0FF] space-y-1 animate-in fade-in">
|
||||
<span className="font-extrabold uppercase text-xs tracking-wider block">AI Solution & Explanation:</span>
|
||||
<p>{sampleQuestions[activeQuestion].explanation}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Navigation buttons */}
|
||||
<div className="flex justify-between items-center pt-4 border-t border-border">
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={activeQuestion === 0}
|
||||
onClick={() => setActiveQuestion(prev => Math.max(0, prev - 1))}
|
||||
className="rounded-xl font-bold border-border"
|
||||
>
|
||||
<ArrowLeft size={16} className="mr-2" /> Previous
|
||||
</Button>
|
||||
<Button
|
||||
disabled={activeQuestion === sampleQuestions.length - 1}
|
||||
onClick={() => setActiveQuestion(prev => Math.min(sampleQuestions.length - 1, prev + 1))}
|
||||
className="bg-[#00F0FF] text-black hover:bg-[#00F0FF]/90 font-bold rounded-xl"
|
||||
>
|
||||
Next Question <ArrowRight size={16} className="ml-2" />
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Question Palette Sidebar */}
|
||||
<div className="space-y-4">
|
||||
<Card className="border-border bg-background/80 backdrop-blur-xl p-5 rounded-3xl shadow-xl space-y-4">
|
||||
<h3 className="text-sm font-bold uppercase tracking-wider text-muted-foreground">Question Palette</h3>
|
||||
<div className="grid grid-cols-5 gap-2">
|
||||
{sampleQuestions.map((q, idx) => {
|
||||
const isAns = !!selectedAnswers[idx]
|
||||
const isCurr = activeQuestion === idx
|
||||
return (
|
||||
<button
|
||||
key={q.id}
|
||||
onClick={() => setActiveQuestion(idx)}
|
||||
className={`h-10 rounded-xl text-xs font-extrabold transition-all border ${
|
||||
isCurr
|
||||
? "border-[#00F0FF] bg-[#00F0FF] text-black ring-2 ring-[#00F0FF]/50"
|
||||
: isAns
|
||||
? "bg-[#00F0FF]/20 border-[#00F0FF]/40 text-[#00F0FF]"
|
||||
: "bg-secondary/40 border-border text-muted-foreground hover:border-primary"
|
||||
}`}
|
||||
>
|
||||
{idx + 1}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="pt-4 border-t border-border space-y-2 text-xs font-semibold text-muted-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-3 h-3 rounded-md bg-[#00F0FF]" /> Current Question
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-3 h-3 rounded-md bg-[#00F0FF]/20 border border-[#00F0FF]/40" /> Answered
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-3 h-3 rounded-md bg-secondary/40 border border-border" /> Unattempted
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
302
src/app/dashboard/ai-coach/cbt-dpp/page.tsx
Normal file
302
src/app/dashboard/ai-coach/cbt-dpp/page.tsx
Normal file
@@ -0,0 +1,302 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { BrainCircuit, Play, CheckCircle2, Download, Filter, Lock, FileText, Eye, ShieldAlert, X, Sparkles } from "lucide-react"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { useActiveUser } from "@/hooks/useActiveUser"
|
||||
|
||||
export default function PrepPilotAIPage() {
|
||||
const { user, userId } = useActiveUser()
|
||||
const [dpps, setDpps] = useState<any[]>([])
|
||||
const [attempts, setAttempts] = useState<any[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [dateFilter, setDateFilter] = useState("")
|
||||
const [selectedPdfDpp, setSelectedPdfDpp] = useState<any | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchDpps() {
|
||||
if (!user) return; // Wait for user to be available
|
||||
|
||||
try {
|
||||
const pattern = user.target_exam || "JEE_Main";
|
||||
// Also fetch from the correct Next.js proxied route instead of localhost:8000
|
||||
const res = await fetch(`/api/tests?type=DPS&exam_pattern=${pattern}`);
|
||||
if (res.ok) {
|
||||
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 {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
fetchDpps()
|
||||
}, [user])
|
||||
|
||||
const getDifficultyColor = (diff: string) => {
|
||||
switch(diff) {
|
||||
case 'Easy': return 'text-emerald-500 bg-emerald-500/10 border-emerald-500/20';
|
||||
case 'Medium': return 'text-amber-500 bg-amber-500/10 border-amber-500/20';
|
||||
case 'Hard': return 'text-destructive bg-destructive/10 border-destructive/20';
|
||||
default: return 'text-primary bg-primary/10 border-primary/20';
|
||||
}
|
||||
}
|
||||
|
||||
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<string, any>);
|
||||
|
||||
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' });
|
||||
}
|
||||
|
||||
const getPdfUrl = (title: string) => {
|
||||
const t = title.toLowerCase();
|
||||
if (t.includes('phys')) return '/dpps/dps-phy-ch4-20260707.pdf#toolbar=0&navpanes=0&scrollbar=0';
|
||||
if (t.includes('chem')) return '/dpps/dps-che-ch4-20260707.pdf#toolbar=0&navpanes=0&scrollbar=0';
|
||||
if (t.includes('math')) return '/dpps/dps-mth-ch4-20260707.pdf#toolbar=0&navpanes=0&scrollbar=0';
|
||||
if (t.includes('bio')) return '/dpps/dps-bio-ch4-20260707.pdf#toolbar=0&navpanes=0&scrollbar=0';
|
||||
// Default fallback
|
||||
return '/dpps/dps-phy-ch4-20260707.pdf#toolbar=0&navpanes=0&scrollbar=0';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-8 space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500 max-w-5xl mx-auto">
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between p-8 bg-gradient-to-r from-secondary/50 to-background border border-border rounded-2xl shadow-sm gap-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight mb-2">PrepPilot AI <span className="text-[#00F0FF] text-xl">&</span> CBT Simulator</h1>
|
||||
<p className="text-muted-foreground font-medium max-w-xl">
|
||||
Select an interactive Target DPP to launch the AI-powered CBT examination simulator.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row justify-between items-center gap-4 border-b border-border pb-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="date"
|
||||
value={dateFilter}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<span className="text-sm font-bold text-muted-foreground mr-2"><Filter size={16} className="inline mr-1"/> Difficulty:</span>
|
||||
{['Mixed', 'Easy', 'Medium', 'Hard'].map((diff) => (
|
||||
<button key={diff} className={`px-3 py-1.5 rounded-lg text-xs font-bold border transition-colors ${diff === 'Mixed' ? 'bg-primary text-primary-foreground border-primary' : 'bg-background text-muted-foreground border-border hover:border-primary/50'}`}>
|
||||
{diff}
|
||||
</button>
|
||||
))}
|
||||
<div className="flex items-center gap-1 ml-2 text-xs text-muted-foreground bg-secondary/50 px-2 py-1 rounded-md">
|
||||
<Lock size={12}/> Pro
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" className="gap-2 rounded-xl text-sm font-bold">
|
||||
<Download size={16} /> Export as PDF <span className="ml-1 text-[10px] uppercase tracking-wider bg-primary/10 text-primary px-1.5 py-0.5 rounded-md">Pro</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{loading ? (
|
||||
<div className="col-span-2 text-center py-12 text-muted-foreground">Loading AI-generated DPPs...</div>
|
||||
) : filteredDpps.length === 0 ? (
|
||||
<div className="col-span-2 text-center py-12 text-muted-foreground border border-dashed rounded-xl">No DPPs available for the selected criteria.</div>
|
||||
) : filteredDpps.map((dpp, i) => {
|
||||
// Calculate total questions dynamically from sections
|
||||
let count = 0;
|
||||
if (dpp.sections) {
|
||||
Object.values(dpp.sections).forEach((s: any) => { count += (s.questions?.length || 0) });
|
||||
}
|
||||
|
||||
const attempt = attemptsByTest[dpp.id];
|
||||
const isAttempted = !!attempt;
|
||||
|
||||
// Date logic
|
||||
const now = new Date();
|
||||
const scheduledTime = dpp.scheduled_date ? new Date(dpp.scheduled_date) : now;
|
||||
const endTime = dpp.end_date ? new Date(dpp.end_date) : null;
|
||||
const isUpcoming = scheduledTime > now;
|
||||
const isExpired = endTime ? now > endTime : false;
|
||||
|
||||
return (
|
||||
<Card key={dpp.id} className={`group cursor-pointer transition-all hover:shadow-md ${isAttempted ? 'opacity-70 hover:opacity-100 bg-secondary/10 border-primary/20' : isExpired ? 'opacity-80' : 'hover:border-[#00F0FF]/50 bg-background'}`}>
|
||||
<CardContent className="p-6 flex items-center justify-between">
|
||||
<div className="flex gap-4">
|
||||
<div className={`mt-1 flex-shrink-0 ${isAttempted ? 'text-green-500' : 'text-foreground'}`}>
|
||||
{isAttempted ? <CheckCircle2 size={24} /> : <div className="size-6 rounded-full border-2 border-muted-foreground group-hover:border-[#00F0FF] transition-colors flex items-center justify-center"></div>}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-[#00F0FF]">
|
||||
{user?.target_exam?.replace("_", " ") || "JEE/NEET"}
|
||||
</span>
|
||||
<span className="text-muted-foreground text-xs font-bold">•</span>
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-muted-foreground">{dpp.difficulty} Level</span>
|
||||
{isUpcoming ? (
|
||||
<>
|
||||
<span className="text-muted-foreground text-xs font-bold">•</span>
|
||||
<span className="text-[10px] font-bold uppercase text-yellow-500 bg-yellow-500/10 px-1.5 py-0.5 rounded">Upcoming</span>
|
||||
</>
|
||||
) : isExpired ? (
|
||||
<>
|
||||
<span className="text-muted-foreground text-xs font-bold">•</span>
|
||||
<span className="text-[10px] font-bold uppercase text-muted-foreground bg-secondary px-1.5 py-0.5 rounded">Expired</span>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
<h3 className="font-bold text-lg leading-tight group-hover:text-[#00F0FF] transition-colors flex items-center gap-2">
|
||||
{dpp.title}
|
||||
<span className={`text-[10px] uppercase px-2 py-0.5 rounded-md border font-bold tracking-wider ${getDifficultyColor(dpp.difficulty)}`}>
|
||||
{dpp.difficulty}
|
||||
</span>
|
||||
</h3>
|
||||
<div className="text-sm text-muted-foreground mt-2 font-medium">
|
||||
{count} Questions • {dpp.total_time} mins
|
||||
{dpp.scheduled_date && (
|
||||
<span className="ml-2 border-l border-border pl-2">
|
||||
{isUpcoming ? `Available on ${formatDate(dpp.scheduled_date)}` : isExpired ? `Expired on ${formatDate(dpp.end_date!)}` : 'Available Now'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row gap-2 shrink-0">
|
||||
{isUpcoming ? (
|
||||
<Button disabled variant="outline" size="sm" className="font-bold">Starts Soon</Button>
|
||||
) : isExpired && !isAttempted ? (
|
||||
<Button disabled variant="outline" size="sm" className="font-bold bg-secondary/50">Expired</Button>
|
||||
) : isAttempted ? (
|
||||
<>
|
||||
<Link href={user?.tier === "Free" ? "#" : `/cbt/${dpp.id}/result`}>
|
||||
<Button onClick={() => user?.tier === "Free" && setSelectedPdfDpp(dpp)} variant="outline" size="sm" className="font-bold border-border">Result</Button>
|
||||
</Link>
|
||||
{!isExpired && (
|
||||
<Link href={user?.tier === "Free" ? "#" : `/dashboard/ai-coach/cbt-dpp/${dpp.id}`}>
|
||||
<Button onClick={() => user?.tier === "Free" && setSelectedPdfDpp(dpp)} variant="secondary" size="sm" className="font-bold bg-[#00F0FF]/10 text-[#00F0FF] hover:bg-[#00F0FF]/20">
|
||||
{user?.tier === "Free" ? "Read PDF" : "Retake CBT"}
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
user?.tier === "Free" ? (
|
||||
<Button onClick={() => setSelectedPdfDpp(dpp)} variant="secondary" className="rounded-xl bg-primary/10 text-primary hover:bg-primary/20 font-bold gap-2 text-xs">
|
||||
<Eye size={14} /> Open PDF
|
||||
</Button>
|
||||
) : (
|
||||
<Link href={`/dashboard/ai-coach/cbt-dpp/${dpp.id}`}>
|
||||
<Button variant="secondary" size="sm" className="rounded-xl bg-[#00F0FF] text-black hover:bg-[#00F0FF]/90 font-extrabold gap-2 text-xs shadow-[0_0_12px_rgba(0,240,255,0.2)]">
|
||||
<Play size={14} className="fill-black" /> Launch CBT DPP
|
||||
</Button>
|
||||
</Link>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)})}
|
||||
</div>
|
||||
|
||||
{/* ── PROTECTED PDF READER MODAL (FREE TIER) ── */}
|
||||
{selectedPdfDpp && (
|
||||
<div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-md flex items-center justify-center p-4 animate-in fade-in duration-300">
|
||||
<div className="bg-background border border-border rounded-3xl w-full max-w-4xl max-h-[90vh] flex flex-col overflow-hidden shadow-2xl">
|
||||
{/* Modal Header */}
|
||||
<div className="p-5 border-b border-border bg-secondary/30 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2.5 bg-primary/10 rounded-xl border border-primary/20 text-primary">
|
||||
<FileText size={22} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-black text-lg flex items-center gap-2">
|
||||
{selectedPdfDpp.title}
|
||||
<span className="text-[10px] bg-yellow-500/10 text-yellow-500 border border-yellow-500/20 px-2 py-0.5 rounded-full uppercase tracking-wider font-extrabold">
|
||||
Protected In-App PDF View
|
||||
</span>
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground font-medium">
|
||||
{selectedPdfDpp.difficulty} Level • {user?.target_exam?.replace("_", " ") || "JEE/NEET"} Practice Sheet
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => setSelectedPdfDpp(null)}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="rounded-full border-border hover:bg-secondary"
|
||||
>
|
||||
<X size={18} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Security Notice Banner */}
|
||||
<div className="bg-amber-500/10 border-b border-amber-500/20 px-6 py-2.5 flex items-center justify-between text-xs text-amber-400 font-bold">
|
||||
<span className="flex items-center gap-2">
|
||||
<ShieldAlert size={16} /> Read-Only Mode (Free Tier) — Downloads, text copying, and sharing are restricted.
|
||||
</span>
|
||||
<Link href="/upgrade">
|
||||
<Button size="sm" className="bg-amber-500 text-black hover:bg-amber-400 font-extrabold h-7 text-[11px] rounded-lg">
|
||||
Upgrade for Interactive CBT Mode →
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Protected PDF Document Sheet Content (select-none) */}
|
||||
<div
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
className="flex-1 w-full h-full min-h-[60vh] select-none bg-[#525659]"
|
||||
>
|
||||
<embed
|
||||
src={getPdfUrl(selectedPdfDpp.title)}
|
||||
type="application/pdf"
|
||||
className="w-full h-full rounded-b-lg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Modal Footer */}
|
||||
<div className="p-4 border-t border-border bg-secondary/30 flex items-center justify-between">
|
||||
<span className="text-xs text-muted-foreground font-medium">
|
||||
📄 PragatiVeer Protected PDF Reader v2.0
|
||||
</span>
|
||||
<div className="flex gap-3">
|
||||
<Button onClick={() => setSelectedPdfDpp(null)} variant="outline" className="rounded-xl font-bold">
|
||||
Close PDF Reader
|
||||
</Button>
|
||||
<Link href="/upgrade">
|
||||
<Button className="bg-[#00F0FF] text-black hover:bg-[#00F0FF]/90 font-extrabold rounded-xl shadow-[0_0_15px_rgba(0,240,255,0.2)]">
|
||||
<Sparkles size={16} className="mr-2" /> Upgrade to AI Coach for Interactive CBT
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user