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:
2026-07-07 13:43:30 +05:30
parent 426b98ddab
commit 04db17ea0e
24 changed files with 913 additions and 236 deletions

View File

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