fix(dashboard): DPP tags properly dynamic based on user target exam
This commit is contained in:
@@ -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 */}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 (_) {}
|
||||
`,
|
||||
|
||||
122
src/app/page.tsx
122
src/app/page.tsx
@@ -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">
|
||||
|
||||
@@ -115,7 +115,11 @@ export function ExamResult({ payload, onRetake }: ExamResultProps) {
|
||||
<RotateCcw className="size-5" /> Retake Mock Test
|
||||
</Button>
|
||||
<Link href="/dashboard/cbt">
|
||||
<Button size="lg" className="rounded-xl h-14 px-8 font-bold text-lg shadow-[0_0_20px_-5px_var(--color-primary)] gap-2 w-full sm:w-auto">
|
||||
<Button
|
||||
size="lg"
|
||||
className="rounded-xl h-14 px-8 font-bold text-lg shadow-[0_0_20px_-5px_var(--color-primary)] gap-2 w-full sm:w-auto"
|
||||
onClick={() => document.fullscreenElement && document.exitFullscreen()}
|
||||
>
|
||||
Dashboard <ArrowRight className="size-5" />
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
@@ -14,19 +14,17 @@ export function PeriodicTable({ isOpen, onClose }: PeriodicTableProps) {
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-md animate-in fade-in duration-300">
|
||||
<div className="bg-background border border-border shadow-2xl rounded-2xl overflow-hidden animate-in zoom-in-95 duration-300 flex flex-col w-[90vw] max-w-5xl h-[85vh]">
|
||||
<div className="bg-background border border-border shadow-2xl rounded-2xl overflow-hidden animate-in zoom-in-95 duration-300 flex flex-col w-[95vw] max-w-[1400px] h-[95vh]">
|
||||
<div className="p-4 border-b border-border flex justify-between items-center bg-secondary/80">
|
||||
<h3 className="font-bold text-lg">Periodic Table of Elements</h3>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm"><ExternalLink size={14} className="mr-2"/> Open Fullscreen</Button>
|
||||
<button onClick={onClose} className="text-muted-foreground hover:text-foreground ml-2 p-2">
|
||||
<X size={24} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 bg-zinc-950 p-6 flex items-center justify-center relative overflow-auto">
|
||||
{/* We will use a high quality remote SVG/image for the periodic table reference */}
|
||||
<div className="relative w-full h-full min-w-[800px] min-h-[500px]">
|
||||
<div className="flex-1 bg-white p-4 flex items-start justify-start relative overflow-auto">
|
||||
<div className="relative w-[1800px] h-[1100px] shrink-0">
|
||||
{/* Using a reliable wikimedia SVG for Periodic Table as a reference */}
|
||||
<Image
|
||||
src="https://upload.wikimedia.org/wikipedia/commons/4/4d/Periodic_table_large.svg"
|
||||
|
||||
@@ -23,9 +23,11 @@ export function StreakOnboardingModal() {
|
||||
|
||||
const streak = user.streak_count;
|
||||
|
||||
const targetExam = user.target_exam?.replace("_", " ") || "JEE/NEET";
|
||||
|
||||
// Streak-based wishes and titles
|
||||
let wishTitle = "Welcome to PragatiVeer! 🌟";
|
||||
let wishMessage = "Prepare yourself to conquer JEE/NEET. Your daily practice starts here!";
|
||||
let wishMessage = `Prepare yourself to conquer ${targetExam}. Your daily practice starts here!`;
|
||||
let iconTheme = "text-[#00F0FF] bg-[#00F0FF]/10";
|
||||
let iconComponent = <Sparkles className="size-10" />;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { Flame } from "lucide-react"
|
||||
import { motion } from "framer-motion"
|
||||
|
||||
interface StreakCounterProps {
|
||||
streak: number
|
||||
@@ -8,9 +9,15 @@ interface StreakCounterProps {
|
||||
|
||||
export function StreakCounter({ streak }: StreakCounterProps) {
|
||||
return (
|
||||
<div className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-orange-500/10 border border-orange-500/20 text-orange-500 font-bold shadow-[0_0_10px_-2px_rgba(249,115,22,0.5)] cursor-default transition-transform hover:scale-105">
|
||||
<motion.div
|
||||
initial={{ scale: 0.5, opacity: 0, y: -10 }}
|
||||
animate={{ scale: 1, opacity: 1, y: 0 }}
|
||||
whileHover={{ scale: 1.1, rotate: [0, -5, 5, -5, 0] }}
|
||||
transition={{ type: "spring", stiffness: 300, damping: 15 }}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-orange-500/10 border border-orange-500/20 text-orange-500 font-bold shadow-[0_0_10px_-2px_rgba(249,115,22,0.5)] cursor-default"
|
||||
>
|
||||
<Flame size={18} className="animate-pulse" />
|
||||
<span>{streak}</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ export interface ExamAttemptPayload {
|
||||
question_id: string
|
||||
selected_answer: string | null
|
||||
is_correct: boolean // Typically evaluated on server, but requested in payload structure
|
||||
is_subjective: boolean
|
||||
time_spent: number
|
||||
}>
|
||||
score: number
|
||||
@@ -200,16 +201,17 @@ export const useCBTStore = create<CBTState>()(
|
||||
const timeTaken = Math.floor((Date.now() - state.startTime) / 1000);
|
||||
const answerList = Object.values(state.answers);
|
||||
|
||||
// Mock evaluation (In reality, server does this)
|
||||
let correctCount = 0;
|
||||
// Backend will perform secure evaluation
|
||||
let correctCount = 0; // Removing mock count
|
||||
const finalAnswers = answerList.map(a => {
|
||||
// Assuming 'A' is correct for dummy data
|
||||
const isCorrect = a.selected_answer === 'A';
|
||||
if (isCorrect) correctCount++;
|
||||
const q = state.questions.find(quest => quest.id === a.question_id);
|
||||
const isSubjective = q?.type === 'Subjective' || q?.type === 'Numerical';
|
||||
|
||||
return {
|
||||
question_id: a.question_id,
|
||||
selected_answer: a.selected_answer,
|
||||
is_correct: isCorrect,
|
||||
is_correct: false,
|
||||
is_subjective: isSubjective,
|
||||
time_spent: a.time_spent
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user