feat: complete UI tasks, backend optimizations, coupon admin, user kundli

This commit is contained in:
2026-07-10 12:36:56 +05:30
parent 149d365218
commit d5199682e3
16 changed files with 782 additions and 78 deletions

View File

@@ -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>

View File

@@ -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>
)}