fix(dashboard): DPP tags properly dynamic based on user target exam

This commit is contained in:
2026-07-08 12:46:44 +05:30
parent c87b305646
commit 1a6168f724
28 changed files with 853 additions and 304 deletions

View File

@@ -1,6 +1,6 @@
"use client"
import { ReactNode } from "react"
import { ReactNode, useEffect } from "react"
import { LayoutDashboard, Users, Database, Terminal, Settings, FileText } from "lucide-react"
import Link from "next/link"
import { usePathname } from "next/navigation"
@@ -9,6 +9,10 @@ import { Logo } from "@/components/Logo"
export default function AdminLayout({ children }: { children: ReactNode }) {
const pathname = usePathname()
useEffect(() => {
document.documentElement.classList.add('dark');
}, []);
return (
<div className="flex h-screen bg-background overflow-hidden text-foreground selection:bg-[#00F0FF]/20">
{/* Sidebar */}

View File

@@ -1,3 +1,5 @@
"use client"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Search, Download, MoreVertical } from "lucide-react"

View File

@@ -7,6 +7,8 @@ import { Badge } from "@/components/ui/badge"
import { Switch } from "@/components/ui/switch"
import { Input } from "@/components/ui/input"
import { Loader2, Upload, FileText, Clock, CalendarIcon } from "lucide-react"
import DatePicker from "react-datepicker"
import "react-datepicker/dist/react-datepicker.css"
import { useActiveUser } from "@/hooks/useActiveUser"
@@ -15,13 +17,13 @@ export default function AdminTestsPage() {
const [loading, setLoading] = useState(true)
const [uploading, setUploading] = useState(false)
const { userId } = useActiveUser()
const fileInputRef = useRef<HTMLInputElement>(null)
// Upload Form State
const [title, setTitle] = useState("")
const [examPattern, setExamPattern] = useState("JEE_Main")
const [timeLimit, setTimeLimit] = useState("180")
const [scheduledDate, setScheduledDate] = useState("")
const [scheduledDate, setScheduledDate] = useState<Date | null>(null)
const [endDate, setEndDate] = useState<Date | null>(null)
const [questionsJson, setQuestionsJson] = useState("")
const fetchTests = async () => {
try {
@@ -56,37 +58,48 @@ export default function AdminTestsPage() {
const handleUpload = async (e: React.FormEvent) => {
e.preventDefault()
if (!fileInputRef.current?.files?.[0]) return
if (!questionsJson) return
if (!userId) return
setUploading(true)
const formData = new FormData()
formData.append("file", fileInputRef.current.files[0])
formData.append("title", title)
formData.append("exam_pattern", examPattern)
formData.append("time_limit", timeLimit)
formData.append("admin_id", userId)
if (scheduledDate) {
formData.append("scheduled_date", new Date(scheduledDate).toISOString())
let parsedQuestions = [];
try {
parsedQuestions = JSON.parse(questionsJson);
} catch(err) {
alert("Invalid JSON format. Please paste a valid JSON array.");
setUploading(false);
return;
}
const payload = {
title,
exam_pattern: examPattern,
time_limit: parseInt(timeLimit),
admin_id: userId,
questions_json: parsedQuestions,
scheduled_date: scheduledDate ? scheduledDate.toISOString() : null,
end_date: endDate ? endDate.toISOString() : null,
};
try {
const res = await fetch("http://localhost:8000/api/admin/tests/upload", {
const res = await fetch("http://localhost:8000/api/admin/tests/create_manual", {
method: "POST",
body: formData
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
})
if (res.ok) {
alert("Test uploaded and sent to AI for processing!")
alert("Test created successfully!")
setTitle("")
if (fileInputRef.current) fileInputRef.current.value = ""
setQuestionsJson("")
fetchTests()
} else {
alert("Upload failed")
alert("Creation failed")
}
} catch (error) {
console.error(error)
alert("Upload failed")
alert("Creation failed")
} finally {
setUploading(false)
}
@@ -106,9 +119,9 @@ export default function AdminTestsPage() {
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Upload className="w-5 h-5 text-primary" />
Upload New Test
Create Manual Test
</CardTitle>
<CardDescription>Upload a PDF containing questions. AI will automatically extract and format them.</CardDescription>
<CardDescription>Paste the structured JSON block of questions in LaTeX format to create a deterministic test.</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleUpload} className="space-y-4">
@@ -135,20 +148,52 @@ export default function AdminTestsPage() {
<Input required type="number" value={timeLimit} onChange={e => setTimeLimit(e.target.value)} />
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Scheduled Date (Optional)</label>
<Input type="datetime-local" value={scheduledDate} onChange={e => setScheduledDate(e.target.value)} />
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2 flex flex-col">
<label className="text-sm font-medium">Scheduled Start (Optional)</label>
<DatePicker
selected={scheduledDate}
onChange={(date) => setScheduledDate(date)}
showTimeSelect
timeFormat="HH:mm"
timeIntervals={15}
timeCaption="time"
dateFormat="MMMM d, yyyy h:mm aa"
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
placeholderText="Select start date & time"
/>
</div>
<div className="space-y-2 flex flex-col">
<label className="text-sm font-medium">Scheduled End (Optional)</label>
<DatePicker
selected={endDate}
onChange={(date) => setEndDate(date)}
showTimeSelect
timeFormat="HH:mm"
timeIntervals={15}
timeCaption="time"
dateFormat="MMMM d, yyyy h:mm aa"
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
placeholderText="Select end date & time"
/>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Test PDF File</label>
<Input required type="file" accept=".pdf" ref={fileInputRef} className="cursor-pointer" />
<p className="text-xs text-muted-foreground mt-1">Make sure it contains clear questions and options.</p>
<label className="text-sm font-medium">Questions JSON (LaTeX Format)</label>
<textarea
required
value={questionsJson}
onChange={e => setQuestionsJson(e.target.value)}
className="w-full min-h-[250px] p-3 text-sm font-mono border rounded-md bg-background focus:ring-primary focus:border-primary"
placeholder='[{"section": "Physics", "text_en": "Question text...", "options_en": ["A", "B", "C", "D"], "correct_answer": "A"}]'
/>
<p className="text-xs text-muted-foreground mt-1">Paste your questions as a JSON array.</p>
</div>
<Button type="submit" className="w-full" disabled={uploading}>
{uploading ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Upload className="w-4 h-4 mr-2" />}
{uploading ? "Processing PDF via AI..." : "Upload & Parse"}
{uploading ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <FileText className="w-4 h-4 mr-2" />}
{uploading ? "Creating Test..." : "Create Test from JSON"}
</Button>
</form>
</CardContent>

View File

@@ -87,6 +87,7 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
id: qData.id,
number: idx + 1,
subject: qData.subject,
type: qData.type,
text_en: qData.question_text,
text_hi: qData.question_text_hi || qData.question_text,
options: Object.values(qData.options || {}),
@@ -351,7 +352,15 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
</div>
<div className="space-y-3 mt-8">
{currentQuestion.options.map((option: string, idx: number) => {
{(currentQuestion.type === 'Subjective' || currentQuestion.type === 'Numerical') ? (
<textarea
className="w-full min-h-[150px] p-4 rounded-xl border border-border bg-background focus:ring-2 focus:ring-primary focus:border-transparent resize-y text-base"
placeholder="Type your answer or explanation here..."
value={currentState?.selected_answer || ""}
onChange={(e) => selectAnswer(currentQuestion.id, e.target.value)}
/>
) : (
currentQuestion.options?.map((option: string, idx: number) => {
const letter = String.fromCharCode(65 + idx);
const isSelected = currentState?.selected_answer === letter;
return (
@@ -374,7 +383,8 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
</span>
</button>
)
})}
})
)}
</div>
</div>
</div>
@@ -387,6 +397,9 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
<Badge variant="outline" className="px-3 py-1 text-sm text-muted-foreground font-medium rounded-lg">प्रश्न {currentQuestion.number}</Badge>
<span className="text-[10px] uppercase font-bold text-muted-foreground bg-secondary/30 px-2 py-0.5 rounded-md border border-border">हिंदी</span>
</div>
<div className="flex items-center gap-3">
<span className="text-xs font-bold uppercase tracking-wider text-muted-foreground bg-secondary/30 px-2 py-1 rounded-md">+4 / -1</span>
</div>
</div>
<div className="prose prose-neutral dark:prose-invert max-w-none text-lg leading-relaxed text-foreground font-medium">
<ReactMarkdown remarkPlugins={[remarkMath]} rehypePlugins={[rehypeKatex]}>
@@ -395,7 +408,15 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
</div>
<div className="space-y-3 mt-8">
{currentQuestion.options_hi?.map((option: string, idx: number) => {
{(currentQuestion.type === 'Subjective' || currentQuestion.type === 'Numerical') ? (
<textarea
className="w-full min-h-[150px] p-4 rounded-xl border border-border bg-background focus:ring-2 focus:ring-primary focus:border-transparent resize-y text-base"
placeholder="अपना उत्तर या स्पष्टीकरण यहाँ टाइप करें..."
value={currentState?.selected_answer || ""}
onChange={(e) => selectAnswer(currentQuestion.id, e.target.value)}
/>
) : (
currentQuestion.options_hi?.map((option: string, idx: number) => {
const letter = String.fromCharCode(65 + idx);
const isSelected = currentState?.selected_answer === letter;
return (
@@ -418,7 +439,8 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
</span>
</button>
)
})}
})
)}
</div>
</div>
</div>
@@ -503,7 +525,13 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
</div>
<div className="p-4 border-t border-border bg-background flex flex-col gap-3">
<div className="flex gap-2">
<Button variant="outline" onClick={() => markForReview(currentQuestion.id)} className="flex-1 text-xs font-bold h-9 bg-secondary/50 hover:bg-amber-500/10 hover:text-amber-500 hover:border-amber-500/50"><Flag size={14} className="mr-1.5"/> Mark</Button>
<Button
variant="outline"
onClick={() => markForReview(currentQuestion.id)}
className={`flex-1 text-xs font-bold h-9 transition-colors ${currentState?.status.includes('MARKED') ? 'bg-amber-500/10 text-amber-500 border-amber-500/50' : 'bg-secondary/50 hover:bg-amber-500/10 hover:text-amber-500 hover:border-amber-500/50'}`}
>
<Flag size={14} className="mr-1.5"/> {currentState?.status.includes('MARKED') ? 'Unmark' : 'Mark'}
</Button>
<Button variant="outline" onClick={() => clearResponse(currentQuestion.id)} className="flex-1 text-xs font-bold h-9 bg-secondary/50 hover:bg-destructive/10 hover:text-destructive hover:border-destructive/50"><XCircle size={14} className="mr-1.5"/> Clear</Button>
</div>
<div className="flex gap-2">

View File

@@ -83,7 +83,7 @@ export default function AnalyticsPage() {
<Tooltip
cursor={{fill: 'transparent'}}
contentStyle={{ backgroundColor: '#1f2937', borderColor: '#374151', borderRadius: '8px', color: '#fff' }}
formatter={(value: number) => [`${value}% Accuracy`, 'Accuracy']}
formatter={(value: any) => [`${value}% Accuracy`, 'Accuracy']}
/>
<Bar dataKey="accuracy" radius={[0, 4, 4, 0]} barSize={12}>
{heatmapData.map((entry, index) => (

View File

@@ -123,7 +123,7 @@ export default function AttemptHistoryPage({ params }: { params: Promise<{ attem
{idx + 1}
</div>
<div>
<h3 className="font-medium text-lg leading-relaxed">{q.text}</h3>
<h3 className="font-medium text-lg leading-relaxed">{q.question_text || q.text_en || q.text}</h3>
<div className="flex gap-2 mt-2">
<Badge variant="outline" className="text-xs">{q.topic}</Badge>
<Badge variant="outline" className="text-xs">{q.difficulty}</Badge>
@@ -141,13 +141,19 @@ export default function AttemptHistoryPage({ params }: { params: Promise<{ attem
<div className="space-y-2">
<p className="text-sm font-bold text-muted-foreground uppercase">Your Answer</p>
<div className={`p-3 rounded-lg border ${ans.is_correct ? 'bg-green-500/10 border-green-500/30 text-green-500' : 'bg-red-500/10 border-red-500/30 text-red-500'}`}>
{q.options[ans.selected_answer] ? `${ans.selected_answer}) ${q.options[ans.selected_answer]}` : 'Not Attempted'}
{ans.is_subjective || q.type === 'Subjective' || q.type === 'Numerical'
? (ans.selected_answer || 'Not Attempted')
: (q.options && q.options[ans.selected_answer] ? `${ans.selected_answer}) ${q.options[ans.selected_answer]}` : ans.selected_answer || 'Not Attempted')}
</div>
</div>
<div className="space-y-2">
<p className="text-sm font-bold text-muted-foreground uppercase">Correct Answer</p>
<div className="p-3 rounded-lg border bg-green-500/10 border-green-500/30 text-green-500">
{q.correct_answer?.correct}) {q.options[q.correct_answer?.correct]}
{ans.is_subjective || q.type === 'Subjective' || q.type === 'Numerical'
? (q.correct_answer?.value || q.correct_answer?.correct || 'Subjective text')
: (q.options && q.options[q.correct_answer?.correct || q.correct_answer?.value]
? `${q.correct_answer?.correct || q.correct_answer?.value}) ${q.options[q.correct_answer?.correct || q.correct_answer?.value]}`
: (q.correct_answer?.correct || q.correct_answer?.value || 'N/A'))}
</div>
</div>
</div>

View File

@@ -38,7 +38,7 @@ export default function CBTPage() {
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");
setTargetExam(pattern === "NEET" ? "NEET" : pattern === "JEE_Advanced" ? "JEE Advanced" : "JEE Main");
// 2. Fetch tests filtered by exam
const testRes = await fetch(`/api/tests?exam_pattern=${pattern}&user_id=${userId}`);

View File

@@ -2,7 +2,8 @@
import { useState } from "react"
import { Card, CardContent, CardHeader, CardTitle, CardFooter } from "@/components/ui/card"
import { MessageSquare, ThumbsUp, ChevronUp, Search, Image as ImageIcon, CheckCircle } from "lucide-react"
import { MessageSquare, ThumbsUp, ChevronUp, Search, Image as ImageIcon, CheckCircle, Lock } from "lucide-react"
import { useActiveUser } from "@/hooks/useActiveUser"
interface Doubt {
id: string
@@ -20,6 +21,7 @@ interface Doubt {
}
export default function CommunityPage() {
const { hasAccessTo } = useActiveUser()
const [activeFilter, setActiveFilter] = useState<'all' | 'my-doubts' | 'solved' | 'unsolved'>('all')
const [searchQuery, setSearchQuery] = useState('')
@@ -87,9 +89,16 @@ export default function CommunityPage() {
<p className="text-muted-foreground mt-2 font-medium">Ask questions, share knowledge, and learn together with the community.</p>
</div>
<button className="px-5 py-2.5 bg-[#00F0FF] text-black font-bold rounded-xl shadow-[0_0_15px_-3px_rgba(0,240,255,0.4)] hover:bg-[#00d0dd] transition-colors">
Ask a Doubt
</button>
{hasAccessTo("ask-doubts-from-tutors") ? (
<button className="px-5 py-2.5 bg-[#00F0FF] text-black font-bold rounded-xl shadow-[0_0_15px_-3px_rgba(0,240,255,0.4)] hover:bg-[#00d0dd] transition-colors">
Ask a Doubt
</button>
) : (
<button disabled className="px-5 py-2.5 bg-secondary text-muted-foreground font-bold rounded-xl flex items-center gap-2 cursor-not-allowed">
<Lock size={16} />
Pro Feature
</button>
)}
</div>
<div className="flex flex-col md:flex-row gap-4">

View File

@@ -6,15 +6,21 @@ import { BrainCircuit, Play, CheckCircle2, Download, Filter, Lock } from "lucide
import { useEffect, useState } from "react"
import Link from "next/link"
import { useActiveUser } from "@/hooks/useActiveUser"
export default function DPPPage() {
const { user } = useActiveUser()
const [dpps, setDpps] = useState<any[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
async function fetchDpps() {
if (!user) return; // Wait for user to be available
try {
const res = await fetch("http://localhost:8000/api/tests?type=DPS")
const pattern = user.target_exam || "JEE_Main";
// Also fetch from the correct Next.js proxied route instead of localhost:8000
const res = await fetch(`/api/tests?type=DPS&exam_pattern=${pattern}`);
if (res.ok) {
const data = await res.json()
setDpps(data)
@@ -26,7 +32,7 @@ export default function DPPPage() {
}
}
fetchDpps()
}, [])
}, [user])
const getDifficultyColor = (diff: string) => {
switch(diff) {
@@ -80,7 +86,7 @@ export default function DPPPage() {
Object.values(dpp.sections).forEach((s: any) => { count += (s.questions?.length || 0) });
}
// Temporary mock for status
const status = "Pending"
let status: string = "Pending"
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'}`}>
@@ -91,7 +97,9 @@ export default function DPPPage() {
</div>
<div>
<div className="flex items-center gap-2 mb-1">
<span className="text-xs font-bold uppercase tracking-wider text-[#00F0FF]">{dpp.exam_pattern?.replace("_", " ") || "JEE/NEET"}</span>
<span className="text-xs font-bold uppercase tracking-wider text-[#00F0FF]">
{user?.target_exam?.replace("_", " ") || "JEE/NEET"}
</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>
</div>
@@ -113,7 +121,7 @@ export default function DPPPage() {
</Link>
) : (
<Link href={`/cbt/${dpp.id}`}>
<Button variant="secondary" size="icon" className="rounded-full group-hover:bg-[#00F0FF] group-hover:text-black dark:group-hover:text-black hover:bg-[#00F0FF] hover:text-black dark:hover:text-black shadow-sm transition-all duration-300">
<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">
<Play size={16} className="ml-1" />
</Button>
</Link>

View File

@@ -59,7 +59,13 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
</div>
<NavItem href="/dashboard/leaderboard" icon={<Trophy size={18} />} label="Leaderboard" active={pathname.startsWith("/dashboard/leaderboard")} />
<NavItem href="/dashboard/progress" icon={<Activity size={18} />} label="Syllabus Tracker" active={pathname.startsWith("/dashboard/progress")} />
<NavItem href="/dashboard/community" icon={<BookOpen size={18} />} label="Doubt Forum" active={pathname.startsWith("/dashboard/community")} />
<NavItem
href="/dashboard/community"
icon={<BookOpen size={18} />}
label="Doubt Forum"
active={pathname.startsWith("/dashboard/community")}
isLocked={!hasAccessTo("ask-doubts-from-tutors")}
/>
<div className="pt-4 pb-2">
<p className="px-3 text-xs font-bold uppercase tracking-widest text-muted-foreground">AI Intelligence</p>
@@ -159,7 +165,7 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
<div className="flex items-center gap-4">
{/* Streak count moved to top header */}
{user && (
<Link href="/dashboard/streaks" className="hover:opacity-85 transition-opacity flex items-center gap-1.5 bg-secondary/50 border border-border px-3 py-1 rounded-xl">
<Link href="/dashboard/streaks" className="hover:opacity-85 transition-opacity">
<StreakCounter streak={user.streak_count} />
</Link>
)}

View File

@@ -30,12 +30,7 @@ export default function DashboardPage() {
return (
<div className="p-8 max-w-6xl mx-auto space-y-10 animate-in fade-in duration-500 slide-in-from-bottom-4">
<header className="flex justify-end mb-2">
<Button size="lg" className="gap-2 shadow-sm bg-[#00F0FF] text-black hover:bg-[#00F0FF]/90 font-bold">
<BrainCircuit size={18} />
Generate AI Plan
</Button>
</header>
{/* Free Tier Upgrade CTA Banner */}
<Card className="bg-primary/5 border-primary/20 relative overflow-hidden">
@@ -47,7 +42,7 @@ export default function DashboardPage() {
<Badge className="bg-primary text-primary-foreground font-bold tracking-widest uppercase mb-2">Upgrade to Pro</Badge>
<h2 className="text-2xl font-bold tracking-tight">Unlock Unlimited DPS & Mocks</h2>
<p className="text-muted-foreground max-w-xl font-medium">
You're currently on the Free Tier. Upgrade to Pro for ₹299/mo to access unlimited Daily Practice Sheets, weekly CBT tests, and advanced analytics.
You're currently on the Free Tier. Upgrade to Pro for ₹2499/yr to access unlimited Daily Practice Sheets, weekly CBT tests, and advanced analytics.
</p>
</div>
<Button size="lg" className="shrink-0 rounded-xl shadow-[0_0_20px_-5px_var(--color-primary)] font-bold">

View File

@@ -34,10 +34,14 @@ export default function RootLayout({
dangerouslySetInnerHTML={{
__html: `
try {
if (localStorage.getItem('theme') === 'dark' || (!localStorage.getItem('theme') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark')
if (window.location.pathname.startsWith('/dashboard') || window.location.pathname.startsWith('/cbt')) {
if (localStorage.getItem('theme') === 'dark' || (!localStorage.getItem('theme') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
} else {
document.documentElement.classList.remove('dark')
document.documentElement.classList.add('dark')
}
} catch (_) {}
`,

View File

@@ -7,9 +7,13 @@ import { BrainCircuit, Activity, Zap, CheckCircle2, Shield, ArrowRight, Play, Bo
import Image from "next/image"
import Link from "next/link"
import { motion, useScroll, useTransform, Variants } from "framer-motion"
import { useRef } from "react"
import { useRef, useEffect } from "react"
export default function MarketingPage() {
useEffect(() => {
document.documentElement.classList.add('dark');
}, []);
const containerRef = useRef(null)
const { scrollYProgress } = useScroll({
target: containerRef,
@@ -32,28 +36,28 @@ export default function MarketingPage() {
const itemVariants: Variants = {
hidden: { opacity: 0, y: 30 },
visible: {
opacity: 1,
y: 0,
visible: {
opacity: 1,
y: 0,
transition: { type: "spring", stiffness: 100, damping: 12 }
}
}
return (
<div ref={containerRef} className="min-h-screen bg-background text-foreground overflow-x-hidden selection:bg-primary/20 relative">
{/* Animated Deep Background */}
<div className="fixed inset-0 z-0 pointer-events-none bg-grid opacity-30"></div>
<div className="fixed inset-0 z-0 pointer-events-none" style={{ background: 'radial-gradient(circle at 50% 0%, rgba(209, 255, 77, 0.05) 0%, transparent 50%)' }}></div>
<div className="fixed inset-0 z-0 pointer-events-none" style={{ background: 'radial-gradient(circle at 100% 50%, rgba(0, 240, 255, 0.03) 0%, transparent 40%)' }}></div>
{/* Floating Orbs (Using Framer Motion) */}
<motion.div
<motion.div
className="fixed top-1/4 left-1/4 size-[400px] rounded-full bg-primary/20 blur-[120px] pointer-events-none mix-blend-screen -z-10"
animate={{ x: [0, 50, -20, 0], y: [0, -30, 40, 0] }}
transition={{ duration: 15, repeat: Infinity, ease: "linear" }}
/>
<motion.div
<motion.div
className="fixed bottom-1/4 right-1/4 size-[500px] rounded-full bg-[#00F0FF]/10 blur-[150px] pointer-events-none mix-blend-screen -z-10"
animate={{ x: [0, -60, 30, 0], y: [0, 40, -50, 0] }}
transition={{ duration: 20, repeat: Infinity, ease: "linear" }}
@@ -63,12 +67,12 @@ export default function MarketingPage() {
<nav className="fixed top-0 w-full z-50 bg-background/50 backdrop-blur-xl border-b border-border/50">
<div className="max-w-7xl mx-auto px-6 h-20 flex items-center justify-between">
<Link href="/" className="relative h-14 w-60 hover:opacity-90 transition-opacity block">
<Image
src="/PragatiVeer_Logo/SVG_Logos/Rectangular_light_background.svg"
<Image
src="/PragatiVeer_Logo/SVG_Logos/Rectangular_light_background.svg"
alt="PragatiVeer" fill className="object-contain object-left scale-135 origin-left dark:hidden" priority
/>
<Image
src="/PragatiVeer_Logo/SVG_Logos/Rectangular_dark_background.svg"
<Image
src="/PragatiVeer_Logo/SVG_Logos/Rectangular_dark_background.svg"
alt="PragatiVeer" fill className="object-contain object-left scale-135 origin-left hidden dark:block" priority
/>
</Link>
@@ -98,8 +102,8 @@ export default function MarketingPage() {
The Next Evolution of Prep
</Badge>
</motion.div>
<motion.h1
<motion.h1
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.8, type: "spring" }}
@@ -108,8 +112,8 @@ export default function MarketingPage() {
NEET/JEE Prep, <br className="hidden md:block" />
<span className="text-transparent bg-clip-text bg-gradient-to-r from-primary via-[#00F0FF] to-primary animate-gradient pb-2 drop-shadow-[0_0_20px_rgba(209,255,77,0.3)]">Re-engineered by AI.</span>
</motion.h1>
<motion.p
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.8, delay: 0.2 }}
@@ -117,8 +121,8 @@ export default function MarketingPage() {
>
Experience the most accurate CBT simulator combined with adaptive DPPs generated specifically for your weak areas in real-time.
</motion.p>
<motion.div
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.4 }}
@@ -153,7 +157,7 @@ export default function MarketingPage() {
</div>
<div className="hidden sm:flex items-center bg-background/80 border border-border/50 px-6 py-1.5 rounded-full text-xs font-mono text-muted-foreground w-80 justify-center gap-2">
<div className="size-2 rounded-full bg-[#00F0FF] animate-pulse"></div>
pragativeer.ai/dashboard/student
pragativeer.com/dashboard/student
</div>
<div className="flex items-center gap-2">
<Badge variant="outline" className="border-[#00F0FF]/30 text-[#00F0FF] bg-[#00F0FF]/5 px-2 py-0.5 rounded-md text-[10px] uppercase tracking-wider font-bold">
@@ -254,8 +258,8 @@ export default function MarketingPage() {
{/* Mini Sparkline Chart representation */}
<div className="flex items-center gap-1 h-8 pt-2">
{[30, 45, 35, 55, 70, 60, 85].map((val, i) => (
<div
key={i}
<div
key={i}
className="flex-1 bg-[#00F0FF]/20 hover:bg-[#00F0FF]/60 transition-colors rounded-t"
style={{ height: `${val}%` }}
></div>
@@ -319,7 +323,7 @@ export default function MarketingPage() {
{/* Animated DPP Engine Section */}
<section id="features" className="py-32 px-6 max-w-7xl mx-auto">
<motion.div
<motion.div
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
@@ -329,8 +333,8 @@ export default function MarketingPage() {
<h2 className="text-4xl md:text-6xl font-bold tracking-tighter mb-6">Adaptive Engine</h2>
<p className="text-muted-foreground text-xl max-w-2xl mx-auto">Our AI analyzes every keystroke and hesitation to generate the exact problems you need to solve next.</p>
</motion.div>
<motion.div
<motion.div
variants={containerVariants}
initial="hidden"
whileInView="visible"
@@ -393,8 +397,8 @@ export default function MarketingPage() {
{/* Animated Pricing */}
<section id="pricing" className="py-24 px-6 max-w-7xl mx-auto relative">
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 size-[600px] bg-primary/5 blur-[120px] rounded-full pointer-events-none -z-10"></div>
<motion.div
<motion.div
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
@@ -404,18 +408,41 @@ export default function MarketingPage() {
<h2 className="text-4xl md:text-6xl font-bold tracking-tighter mb-6">Simple, Transparent Pricing</h2>
<p className="text-muted-foreground text-xl max-w-2xl mx-auto">Unlock the ultimate unfair advantage for your NEET/JEE preparation.</p>
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-5xl mx-auto">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 max-w-6xl mx-auto">
{/* Free Plan */}
<motion.div
initial={{ opacity: 0, x: -50 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, type: "spring" }}
>
<Card className="bg-background/80 backdrop-blur-md border-border p-10 rounded-[2.5rem] flex flex-col hover:border-border/80 transition-colors h-full z-10 relative shadow-xl">
<Card className="bg-background/80 backdrop-blur-md border-border p-8 rounded-[2.5rem] flex flex-col hover:border-border/80 transition-colors h-full z-10 relative shadow-xl">
<h3 className="text-3xl font-bold mb-2">Free</h3>
<p className="text-muted-foreground mb-8 text-lg">Perfect to get started.</p>
<div className="text-6xl font-bold tracking-tighter mb-10">₹0<span className="text-xl text-muted-foreground font-medium">/yr</span></div>
<ul className="space-y-5 mb-10 flex-1 font-medium text-lg">
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-muted-foreground" /> 1 Daily Practice Sheet</li>
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-muted-foreground" /> Basic Performance Analytics</li>
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-muted-foreground" /> Community Access</li>
</ul>
<Link href="/signup">
<Button variant="outline" size="lg" className="w-full rounded-2xl h-16 border-border font-bold text-lg hover:bg-secondary hover:text-foreground">Get Started Free</Button>
</Link>
</Card>
</motion.div>
{/* Pro Plan */}
<motion.div
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, type: "spring", delay: 0.1 }}
>
<Card className="bg-background/80 backdrop-blur-md border-border p-8 rounded-[2.5rem] flex flex-col hover:border-border/80 transition-colors h-full z-10 relative shadow-xl">
<h3 className="text-3xl font-bold mb-2">Pro</h3>
<p className="text-muted-foreground mb-8 text-lg">Perfect for self-paced practice.</p>
<div className="text-6xl font-bold tracking-tighter mb-10">₹299<span className="text-xl text-muted-foreground font-medium">/mo</span></div>
<div className="text-6xl font-bold tracking-tighter mb-10">₹2,499<span className="text-xl text-muted-foreground font-medium">/yr</span></div>
<ul className="space-y-5 mb-10 flex-1 font-medium text-lg">
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-primary" /> Unlimited Daily Practice Sheets</li>
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-primary" /> Weekly Test Series & Mock Tests</li>
@@ -423,27 +450,30 @@ export default function MarketingPage() {
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-primary" /> Parent Dashboard Access</li>
</ul>
<Link href="/signup">
<Button variant="outline" size="lg" className="w-full rounded-2xl h-16 border-border font-bold text-lg hover:bg-secondary/50">Choose Pro</Button>
<Button variant="outline" size="lg" className="w-full rounded-2xl h-16 border-border font-bold text-lg hover:bg-secondary hover:text-foreground">Choose Pro</Button>
</Link>
</Card>
</motion.div>
{/* AI Coach Plan */}
<motion.div
initial={{ opacity: 0, x: 50 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, type: "spring", delay: 0.1 }}
className="relative"
transition={{ duration: 0.6, type: "spring", delay: 0.2 }}
className="relative mt-8 lg:mt-0"
>
<Card className="bg-secondary/30 backdrop-blur-xl border-primary p-10 rounded-[2.5rem] flex flex-col shadow-[0_0_60px_-15px_var(--color-primary)] hover:shadow-[0_0_80px_-15px_var(--color-primary)] transition-all h-full overflow-hidden">
<div className="absolute inset-0 -translate-x-full animate-shine bg-gradient-to-r from-transparent via-white/10 to-transparent z-0 pointer-events-none"></div>
<div className="absolute top-0 right-10 transform -translate-y-1/2 z-20">
<Badge className="bg-primary text-primary-foreground font-bold px-5 py-2 text-sm rounded-full shadow-[0_0_15px_-2px_var(--color-primary)]">MOST POPULAR</Badge>
<div className="absolute -top-4 left-1/2 -translate-x-1/2 z-20">
<Badge className="bg-primary text-primary-foreground font-extrabold px-6 py-2 text-sm rounded-full shadow-[0_0_20px_-2px_var(--color-primary)]">MOST POPULAR</Badge>
</div>
<Card className="bg-secondary/30 backdrop-blur-xl border-primary p-8 rounded-[2.5rem] flex flex-col shadow-[0_0_60px_-15px_var(--color-primary)] hover:shadow-[0_0_80px_-15px_var(--color-primary)] transition-all h-full relative">
<div className="absolute inset-0 rounded-[2.5rem] overflow-hidden">
<div className="absolute inset-0 -translate-x-full animate-shine bg-gradient-to-r from-transparent via-white/10 to-transparent z-0 pointer-events-none"></div>
</div>
<div className="relative z-10 flex flex-col h-full">
<div className="relative z-10 flex flex-col h-full mt-4">
<h3 className="text-3xl font-bold mb-2">AI Coach</h3>
<p className="text-muted-foreground mb-8 text-lg">The complete AI-driven preparation system.</p>
<div className="text-6xl font-bold tracking-tighter mb-10 text-primary drop-shadow-[0_0_10px_var(--color-primary)]">₹599<span className="text-xl text-foreground font-medium">/mo</span></div>
<p className="text-muted-foreground mb-8 text-lg">The complete AI-driven system.</p>
<div className="text-6xl font-bold tracking-tighter mb-10 text-primary drop-shadow-[0_0_10px_var(--color-primary)]">₹4,999<span className="text-xl text-foreground font-medium">/yr</span></div>
<ul className="space-y-5 mb-10 flex-1 font-medium text-lg">
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-[#00F0FF]" /> Everything in Pro</li>
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-[#00F0FF]" /> AI Spaced Repetition (Mistakes)</li>
@@ -466,12 +496,12 @@ export default function MarketingPage() {
<div className="grid grid-cols-1 md:grid-cols-4 gap-12 mb-16">
<div className="md:col-span-1 space-y-4">
<div className="relative h-16 w-64 mb-6">
<Image
src="/PragatiVeer_Logo/SVG_Logos/Rectangular_light_background.svg"
<Image
src="/PragatiVeer_Logo/SVG_Logos/Rectangular_light_background.svg"
alt="PragatiVeer" fill className="object-contain object-left scale-135 origin-left dark:hidden"
/>
<Image
src="/PragatiVeer_Logo/SVG_Logos/Rectangular_dark_background.svg"
<Image
src="/PragatiVeer_Logo/SVG_Logos/Rectangular_dark_background.svg"
alt="PragatiVeer" fill className="object-contain object-left scale-135 origin-left hidden dark:block"
/>
</div>
@@ -490,7 +520,7 @@ export default function MarketingPage() {
</Link>
</div>
</div>
<div>
<h4 className="font-bold mb-6 tracking-tight text-foreground">Product</h4>
<ul className="space-y-3 text-sm font-medium text-muted-foreground">
@@ -518,7 +548,7 @@ export default function MarketingPage() {
</ul>
</div>
</div>
<div className="border-t border-border pt-8 flex flex-col md:flex-row items-center justify-between gap-4">
<p className="text-sm text-muted-foreground font-medium">© {new Date().getFullYear()} PragatiVeer. All rights reserved.</p>
<div className="flex items-center gap-2 text-sm text-muted-foreground font-medium">