feat: complete UI tasks, backend optimizations, coupon admin, user kundli
This commit is contained in:
@@ -93,15 +93,15 @@ export default function AttemptHistoryPage({ params }: { params: Promise<{ attem
|
||||
<p className="text-muted-foreground">Review your performance, detailed solutions, and identify your weak spots.</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4">
|
||||
<Card className="bg-secondary/30 border-border min-w-32">
|
||||
<div className="flex gap-4 w-full md:w-auto">
|
||||
<Card className="bg-secondary/30 border-border flex-1 md:min-w-48">
|
||||
<CardContent className="p-4 flex flex-col items-center justify-center">
|
||||
<Award className="text-[#00F0FF] mb-2" size={24} />
|
||||
<div className="text-sm text-muted-foreground font-bold uppercase">Score</div>
|
||||
<div className="text-2xl font-bold">{scoreCalc} <span className="text-sm text-muted-foreground">/ {maxScore}</span></div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-secondary/30 border-border min-w-32">
|
||||
<Card className="bg-secondary/30 border-border flex-1 md:min-w-48">
|
||||
<CardContent className="p-4 flex flex-col items-center justify-center">
|
||||
<Clock className="text-primary mb-2" size={24} />
|
||||
<div className="text-sm text-muted-foreground font-bold uppercase">Time Taken</div>
|
||||
|
||||
@@ -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<any[]>([]);
|
||||
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
|
||||
const [showAllExams, setShowAllExams] = useState(false);
|
||||
const [selectedPattern, setSelectedPattern] = useState<string>("");
|
||||
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"}.
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative w-64">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" size={16} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search exams..."
|
||||
value={search}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<div className="flex flex-col sm:flex-row gap-3">
|
||||
<div className="relative w-full sm:w-48">
|
||||
<input
|
||||
type="date"
|
||||
value={dateFilter}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative w-full sm:w-64">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" size={16} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search exams..."
|
||||
value={search}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 overflow-x-auto pb-2 scrollbar-hide">
|
||||
{["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 (
|
||||
<Button
|
||||
key={p}
|
||||
variant={selectedPattern === dbPattern ? "default" : "outline"}
|
||||
onClick={() => setSelectedPattern(dbPattern)}
|
||||
className="rounded-full shadow-sm"
|
||||
>
|
||||
{p === "JEE_MAIN" ? "JEE Main" : p === "JEE_ADVANCED" ? "JEE Advanced" : "NEET"}
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-xl font-bold border-b border-border pb-2 flex items-center gap-2">
|
||||
<Clock size={20} className="text-[#00F0FF]" /> 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 (
|
||||
<Card key={exam.id} className={`hover:border-[#00F0FF]/50 hover:shadow-[0_0_15px_-5px_#00F0FF] transition-all group ${isAttempted ? 'border-primary/20' : ''} ${isLocked ? 'opacity-80' : ''}`}>
|
||||
<Card key={exam.id} className={`hover:border-[#00F0FF]/50 hover:shadow-[0_0_15px_-5px_#00F0FF] transition-all group ${isAttempted ? 'border-primary/20' : ''} ${isLocked || isExpired ? 'opacity-80' : ''}`}>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
@@ -146,6 +204,10 @@ export default function CBTPage() {
|
||||
<Badge variant="outline" className="bg-red-500/10 text-red-500 border-red-500/20 flex items-center gap-1">
|
||||
<Lock size={12} /> Premium
|
||||
</Badge>
|
||||
) : isExpired ? (
|
||||
<Badge variant="outline" className="bg-muted text-muted-foreground border-muted-foreground/30">
|
||||
Expired
|
||||
</Badge>
|
||||
) : isAttempted ? (
|
||||
<Badge variant="secondary" className="bg-green-500/10 text-green-500 border-green-500/20">
|
||||
Attempted
|
||||
@@ -157,7 +219,7 @@ export default function CBTPage() {
|
||||
</h3>
|
||||
<div className="text-sm text-muted-foreground space-y-1 mb-6">
|
||||
<div className="font-medium text-foreground">
|
||||
{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'}
|
||||
</div>
|
||||
<div>{exam.total_time} Min • {exam.difficulty} Difficulty</div>
|
||||
</div>
|
||||
@@ -173,10 +235,29 @@ export default function CBTPage() {
|
||||
>
|
||||
<Lock size={16} /> Unlock Test
|
||||
</Button>
|
||||
) : isExpired && !isAttempted ? (
|
||||
<Button disabled className="w-full gap-2 rounded-xl bg-secondary/50 text-muted-foreground border border-border font-bold">
|
||||
Expired
|
||||
</Button>
|
||||
) : isAttempted ? (
|
||||
<div className="flex gap-2">
|
||||
<Link href={`/cbt/${exam.id}/result`} className="flex-1">
|
||||
<Button className="w-full gap-2 rounded-xl bg-secondary border border-border text-foreground hover:bg-secondary/80 font-bold">
|
||||
<PlayCircle size={16} /> Result
|
||||
</Button>
|
||||
</Link>
|
||||
{!isExpired && (
|
||||
<Link href={`/cbt/${exam.id}`} className="flex-1">
|
||||
<Button className="w-full gap-2 rounded-xl bg-[#00F0FF]/10 border border-[#00F0FF]/30 text-[#00F0FF] hover:bg-[#00F0FF]/20 font-bold">
|
||||
Retake
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Link href={isAttempted ? `/cbt/${exam.id}/result` : `/cbt/${exam.id}`}>
|
||||
<Button className={`w-full gap-2 rounded-xl transition-colors font-bold ${isAttempted ? 'bg-secondary border border-border text-foreground hover:bg-secondary/80' : 'bg-secondary text-foreground hover:bg-[#00F0FF] hover:text-background'}`}>
|
||||
<PlayCircle size={16} /> {isAttempted ? 'View Result' : 'Start Exam'}
|
||||
<Link href={`/cbt/${exam.id}`}>
|
||||
<Button className="w-full gap-2 rounded-xl bg-secondary text-foreground hover:bg-[#00F0FF] hover:text-background font-bold transition-colors">
|
||||
<PlayCircle size={16} /> Start Exam
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
@@ -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 (
|
||||
<Card key={exam.id} className={`hover:border-[#00F0FF]/50 hover:shadow-[0_0_15px_-5px_#00F0FF] transition-all group ${isAttempted ? 'border-primary/20' : ''} ${isLocked ? 'opacity-80' : ''}`}>
|
||||
<Card key={exam.id} className={`hover:border-[#00F0FF]/50 hover:shadow-[0_0_15px_-5px_#00F0FF] transition-all group ${isAttempted ? 'border-primary/20' : ''} ${isLocked || isExpired ? 'opacity-80' : ''}`}>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
@@ -231,6 +314,10 @@ export default function CBTPage() {
|
||||
<Badge variant="outline" className="bg-red-500/10 text-red-500 border-red-500/20 flex items-center gap-1">
|
||||
<Lock size={12} /> Premium
|
||||
</Badge>
|
||||
) : isExpired ? (
|
||||
<Badge variant="outline" className="bg-muted text-muted-foreground border-muted-foreground/30">
|
||||
Expired
|
||||
</Badge>
|
||||
) : isAttempted ? (
|
||||
<Badge variant="secondary" className="bg-green-500/10 text-green-500 border-green-500/20">
|
||||
Attempted
|
||||
@@ -242,7 +329,7 @@ export default function CBTPage() {
|
||||
</h3>
|
||||
<div className="text-sm text-muted-foreground space-y-1 mb-6">
|
||||
<div className="font-medium text-foreground">
|
||||
{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'}
|
||||
</div>
|
||||
<div>{exam.total_time} Min • {exam.difficulty} Difficulty</div>
|
||||
</div>
|
||||
@@ -258,10 +345,29 @@ export default function CBTPage() {
|
||||
>
|
||||
<Lock size={16} /> Unlock Test
|
||||
</Button>
|
||||
) : isExpired && !isAttempted ? (
|
||||
<Button disabled className="w-full gap-2 rounded-xl bg-secondary/50 text-muted-foreground border border-border font-bold">
|
||||
Expired
|
||||
</Button>
|
||||
) : isAttempted ? (
|
||||
<div className="flex gap-2">
|
||||
<Link href={`/cbt/${exam.id}/result`} className="flex-1">
|
||||
<Button className="w-full gap-2 rounded-xl bg-secondary border border-border text-foreground hover:bg-secondary/80 font-bold">
|
||||
<PlayCircle size={16} /> Result
|
||||
</Button>
|
||||
</Link>
|
||||
{!isExpired && (
|
||||
<Link href={`/cbt/${exam.id}`} className="flex-1">
|
||||
<Button className="w-full gap-2 rounded-xl bg-[#00F0FF]/10 border border-[#00F0FF]/30 text-[#00F0FF] hover:bg-[#00F0FF]/20 font-bold">
|
||||
Retake
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Link href={isAttempted ? `/cbt/${exam.id}/result` : `/cbt/${exam.id}`}>
|
||||
<Button className={`w-full gap-2 rounded-xl transition-colors font-bold ${isAttempted ? 'bg-secondary border border-border text-foreground hover:bg-secondary/80' : 'bg-secondary text-foreground hover:bg-[#00F0FF] hover:text-background'}`}>
|
||||
<PlayCircle size={16} /> {isAttempted ? 'View Result' : 'Start Exam'}
|
||||
<Link href={`/cbt/${exam.id}`}>
|
||||
<Button className="w-full gap-2 rounded-xl bg-secondary text-foreground hover:bg-[#00F0FF] hover:text-background font-bold transition-colors">
|
||||
<PlayCircle size={16} /> Start Exam
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
@@ -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<any[]>([])
|
||||
const [attempts, setAttempts] = useState<any[]>([])
|
||||
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<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' });
|
||||
}
|
||||
|
||||
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">
|
||||
@@ -56,6 +89,12 @@ export default function DPPPage() {
|
||||
|
||||
<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'}`}>
|
||||
@@ -74,23 +113,31 @@ export default function DPPPage() {
|
||||
<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>
|
||||
) : dpps.length === 0 ? (
|
||||
<div className="col-span-2 text-center py-12 text-muted-foreground border border-dashed rounded-xl">No DPPs available yet. Upload some PDFs in the Admin Panel!</div>
|
||||
) : dpps.map((dpp, i) => {
|
||||
) : 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) });
|
||||
}
|
||||
// Temporary mock for status
|
||||
let status: string = "Pending"
|
||||
|
||||
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 ${status === 'Completed' ? 'opacity-70 hover:opacity-100 bg-secondary/10' : 'hover:border-[#00F0FF]/50 bg-background'}`}>
|
||||
<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 ${status === 'Completed' ? 'text-green-500' : 'text-foreground'}`}>
|
||||
{status === 'Completed' ? <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 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">
|
||||
@@ -99,6 +146,17 @@ export default function DPPPage() {
|
||||
</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}
|
||||
@@ -108,14 +166,30 @@ export default function DPPPage() {
|
||||
</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="text-muted-foreground group-hover:text-foreground transition-colors pl-4 shrink-0">
|
||||
{status === 'Completed' ? (
|
||||
<Link href={`/dashboard/cbt/history/${dpp.id}`}>
|
||||
<Button variant="ghost" size="sm" className="font-bold">Review</Button>
|
||||
</Link>
|
||||
<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={`/cbt/${dpp.id}/result`}>
|
||||
<Button variant="outline" size="sm" className="font-bold border-border">Result</Button>
|
||||
</Link>
|
||||
{!isExpired && (
|
||||
<Link href={`/cbt/${dpp.id}`}>
|
||||
<Button variant="secondary" size="sm" className="font-bold bg-[#00F0FF]/10 text-[#00F0FF] hover:bg-[#00F0FF]/20">Retake</Button>
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Link href={`/cbt/${dpp.id}`}>
|
||||
<Button variant="secondary" size="icon" className="rounded-full group-hover:bg-primary group-hover:text-primary-foreground hover:bg-primary hover:text-primary-foreground shadow-sm transition-all duration-300">
|
||||
|
||||
@@ -60,8 +60,7 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
|
||||
</p>
|
||||
</div>
|
||||
<NavItem href="/dashboard" icon={<LayoutDashboard size={18} />} label="Overview" active={pathname === "/dashboard"} />
|
||||
<NavItem href="/dashboard/dpp" icon={<FileText size={18} />} label="Daily Practice" active={pathname.startsWith("/dashboard/dpp")} />
|
||||
<NavItem href="/dashboard/materials" icon={<BookOpen size={18} />} label="Study Materials" active={pathname.startsWith("/dashboard/materials")} />
|
||||
<NavItem href="/dashboard/dpp" icon={<FileText size={18} />} label="Target DPPs" active={pathname.startsWith("/dashboard/dpp")} />
|
||||
<NavItem href="/dashboard/progress" icon={<Activity size={18} />} label="Syllabus Tracker" active={pathname.startsWith("/dashboard/progress")} />
|
||||
<NavItem href="/dashboard/leaderboard" icon={<Trophy size={18} />} label="Leaderboard" active={pathname.startsWith("/dashboard/leaderboard")} />
|
||||
|
||||
@@ -73,10 +72,17 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
|
||||
<NavItem
|
||||
href="/dashboard/cbt"
|
||||
icon={<Activity size={18} />}
|
||||
label="Exams (CBT)"
|
||||
label="Mock Tests (CBT)"
|
||||
active={pathname.startsWith("/dashboard/cbt")}
|
||||
isLocked={!hasAccessTo("weekly-test-series")}
|
||||
/>
|
||||
<NavItem
|
||||
href="/dashboard/materials"
|
||||
icon={<BookOpen size={18} />}
|
||||
label="Quick Notes"
|
||||
active={pathname.startsWith("/dashboard/materials")}
|
||||
isLocked={!hasAccessTo("detailed-performance-analytics")}
|
||||
/>
|
||||
<NavItem
|
||||
href="/dashboard/analytics"
|
||||
icon={<LineChart size={18} />}
|
||||
@@ -84,13 +90,6 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
|
||||
active={pathname.startsWith("/dashboard/analytics")}
|
||||
isLocked={!hasAccessTo("detailed-performance-analytics")}
|
||||
/>
|
||||
<NavItem
|
||||
href="/dashboard/rank-predictor"
|
||||
icon={<Trophy size={18} />}
|
||||
label="Rank Predictor"
|
||||
active={pathname.startsWith("/dashboard/rank-predictor")}
|
||||
isLocked={!hasAccessTo("college-predictor")}
|
||||
/>
|
||||
<NavItem
|
||||
href="/dashboard/community"
|
||||
icon={<BookOpen size={18} />}
|
||||
@@ -111,6 +110,13 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
|
||||
active={pathname.startsWith("/dashboard/ai-coach")}
|
||||
isLocked={!hasAccessTo("ai-tutor-chat")}
|
||||
/>
|
||||
<NavItem
|
||||
href="/dashboard/rank-predictor"
|
||||
icon={<Trophy size={18} />}
|
||||
label="Rank Predictor"
|
||||
active={pathname.startsWith("/dashboard/rank-predictor")}
|
||||
isLocked={!hasAccessTo("college-predictor")}
|
||||
/>
|
||||
</nav>
|
||||
|
||||
<div className="p-4 border-t border-border space-y-2 bg-secondary/30">
|
||||
|
||||
@@ -91,7 +91,10 @@ export default function DashboardPage() {
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-background to-secondary/50 z-0"></div>
|
||||
<CardContent className="p-5 flex flex-col items-center text-center relative z-10">
|
||||
<div className="p-3 bg-background border border-border shadow-sm rounded-full mb-3 text-foreground group-hover:scale-110 transition-transform"><Trophy size={24} /></div>
|
||||
<p className="text-sm font-bold text-muted-foreground uppercase tracking-wider mb-1">City Rank</p>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<p className="text-sm font-bold text-muted-foreground uppercase tracking-wider">City Rank</p>
|
||||
<Badge variant="outline" className="border-yellow-500/30 text-yellow-500 bg-yellow-500/5 rounded-md text-[10px] uppercase font-bold px-1.5 py-0">Pro</Badge>
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold">#42 <span className="text-base text-muted-foreground font-medium">/ 1,200</span></h3>
|
||||
<p className="text-xs text-muted-foreground font-medium mt-2 text-[#00F0FF]">Top 5% in Jaipur</p>
|
||||
</CardContent>
|
||||
|
||||
Reference in New Issue
Block a user