Implement active user tiers, class-based Tailwind theme switcher, 3-tiled static dashboard layout, Minio object storage integration, school & state settings database sync, and streak wishes onboarding modal
This commit is contained in:
@@ -5,7 +5,8 @@ import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Activity, Clock, PlayCircle, Search } from "lucide-react";
|
||||
import { Activity, Clock, PlayCircle, Search, Lock } from "lucide-react";
|
||||
import { useActiveUser } from "@/hooks/useActiveUser";
|
||||
|
||||
interface TestData {
|
||||
id: string;
|
||||
@@ -22,27 +23,31 @@ export default function CBTPage() {
|
||||
const [search, setSearch] = useState("");
|
||||
const [targetExam, setTargetExam] = useState("");
|
||||
const [attempts, setAttempts] = useState<any[]>([]);
|
||||
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
|
||||
|
||||
const userId = "b0e17378-864d-439e-8806-708dc48a891b";
|
||||
const { userId, user, hasAccessTo, loading: userLoading } = useActiveUser();
|
||||
|
||||
useEffect(() => {
|
||||
if (userLoading || !userId) return;
|
||||
|
||||
async function loadCBTExams() {
|
||||
try {
|
||||
// 1. Fetch user target exam
|
||||
const userRes = await fetch(`http://localhost:8000/api/users/${userId}`);
|
||||
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" : "JEE Main");
|
||||
|
||||
// 2. Fetch tests filtered by exam
|
||||
const testRes = await fetch(`http://localhost:8000/api/tests?exam_pattern=${pattern}`);
|
||||
if (!testRes.ok) throw new Error("Failed to fetch tests");
|
||||
const testsData = await testRes.json();
|
||||
setExams(testsData);
|
||||
const testRes = await fetch(`/api/tests?exam_pattern=${pattern}&user_id=${userId}`);
|
||||
if (testRes.ok) {
|
||||
const testsData = await testRes.json();
|
||||
setExams(testsData);
|
||||
}
|
||||
|
||||
// 3. Fetch user attempts
|
||||
const attemptsRes = await fetch(`http://localhost:8000/api/users/${userId}/attempts`);
|
||||
const attemptsRes = await fetch(`/api/users/${userId}/attempts`);
|
||||
if (attemptsRes.ok) {
|
||||
const attemptsData = await attemptsRes.json();
|
||||
setAttempts(attemptsData);
|
||||
@@ -54,7 +59,7 @@ export default function CBTPage() {
|
||||
}
|
||||
}
|
||||
loadCBTExams();
|
||||
}, []);
|
||||
}, [userId, userLoading]);
|
||||
|
||||
const filteredExams = exams.filter(exam =>
|
||||
exam.title.toLowerCase().includes(search.toLowerCase())
|
||||
@@ -109,19 +114,24 @@ export default function CBTPage() {
|
||||
{filteredExams.map((exam) => {
|
||||
const attempt = attemptsByTest[exam.id];
|
||||
const isAttempted = !!attempt;
|
||||
const isLocked = !hasAccessTo("weekly-test-series");
|
||||
|
||||
return (
|
||||
<Card key={exam.id} className={`hover:border-[#00F0FF]/50 hover:shadow-[0_0_15px_-5px_#00F0FF] transition-all group ${isAttempted ? 'border-primary/20' : ''}`}>
|
||||
<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' : ''}`}>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<Badge variant="outline" className="bg-[#00F0FF]/10 text-[#00F0FF] border-[#00F0FF]/20">
|
||||
{exam.type} Test
|
||||
</Badge>
|
||||
{isAttempted && (
|
||||
{isLocked ? (
|
||||
<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>
|
||||
) : isAttempted ? (
|
||||
<Badge variant="secondary" className="bg-green-500/10 text-green-500 border-green-500/20">
|
||||
Attempted
|
||||
</Badge>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
<h3 className="font-bold text-lg leading-tight mb-2 group-hover:text-[#00F0FF] transition-colors">
|
||||
{exam.title}
|
||||
@@ -130,11 +140,20 @@ export default function CBTPage() {
|
||||
<div className="font-medium text-foreground">Available Now</div>
|
||||
<div>{exam.total_time} Min • {exam.difficulty} Difficulty</div>
|
||||
</div>
|
||||
<Link href={`/cbt/${exam.id}`}>
|
||||
<Button className={`w-full gap-2 rounded-xl transition-colors font-bold ${isAttempted ? 'bg-secondary border border-border text-foreground' : 'bg-secondary text-foreground hover:bg-[#00F0FF] hover:text-background'}`}>
|
||||
<PlayCircle size={16} /> {isAttempted ? 'Retake Exam' : 'Start Exam'}
|
||||
{isLocked ? (
|
||||
<Button
|
||||
onClick={() => setShowUpgradeModal(true)}
|
||||
className="w-full gap-2 rounded-xl bg-secondary text-muted-foreground border border-dashed border-border hover:border-[#00F0FF]/50 hover:text-foreground font-bold"
|
||||
>
|
||||
<Lock size={16} /> Unlock Test
|
||||
</Button>
|
||||
</Link>
|
||||
) : (
|
||||
<Link href={`/cbt/${exam.id}`}>
|
||||
<Button className={`w-full gap-2 rounded-xl transition-colors font-bold ${isAttempted ? 'bg-secondary border border-border text-foreground' : 'bg-secondary text-foreground hover:bg-[#00F0FF] hover:text-background'}`}>
|
||||
<PlayCircle size={16} /> {isAttempted ? 'Retake Exam' : 'Start Exam'}
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)})}
|
||||
@@ -185,6 +204,30 @@ export default function CBTPage() {
|
||||
)})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showUpgradeModal && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm animate-in fade-in duration-200">
|
||||
<div className="bg-background border border-border shadow-2xl rounded-2xl w-[450px] p-8 text-center space-y-6 animate-in zoom-in-95 duration-200">
|
||||
<div className="size-16 rounded-full bg-red-500/10 flex items-center justify-center mx-auto text-red-500">
|
||||
<Lock size={32} />
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold">Upgrade to Premium Plan</h3>
|
||||
<p className="text-muted-foreground text-sm leading-relaxed">
|
||||
This CBT Mock Test is a premium feature. Free users get access to 1 Daily Practice Sheet and 5 Topic-wise Micro-tests. Upgrade to Pro or AI Coach to unlock unlimited practice and full-length weekly test series!
|
||||
</p>
|
||||
<div className="flex gap-4 pt-2">
|
||||
<Button variant="outline" className="flex-1 rounded-xl" onClick={() => setShowUpgradeModal(false)}>
|
||||
Maybe Later
|
||||
</Button>
|
||||
<Link href="/upgrade" className="flex-1">
|
||||
<Button className="w-full rounded-xl bg-primary text-primary-foreground font-bold shadow-[0_0_15px_-3px_var(--color-primary)]">
|
||||
View Plans
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user