feat: complete UI tasks, backend optimizations, coupon admin, user kundli
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user