Compare commits
10 Commits
security/a
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 9cb3e550d4 | |||
| 4ae7c8fb65 | |||
| af75d7d4f4 | |||
| f67ed2cfdd | |||
| f9e0fb752b | |||
| 5c9126ed7a | |||
| eb3f4e719c | |||
| 43f23a9d92 | |||
| 42813ef9e0 | |||
| 19edf83684 |
@@ -4,8 +4,9 @@ from typing import Generator
|
|||||||
|
|
||||||
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://pragativeer_admin:local_secure_password_2026@postgres:5432/pragativeer_prod")
|
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://pragativeer_admin:local_secure_password_2026@postgres:5432/pragativeer_prod")
|
||||||
|
|
||||||
# Using the standard SQLModel engine config for PostgreSQL
|
connect_args = {"options": "-c timezone=Asia/Kolkata"} if "postgresql" in DATABASE_URL else {}
|
||||||
engine = create_engine(DATABASE_URL, echo=True)
|
# Using the standard SQLModel engine config for PostgreSQL with IST timezone session
|
||||||
|
engine = create_engine(DATABASE_URL, echo=True, connect_args=connect_args)
|
||||||
|
|
||||||
def init_db():
|
def init_db():
|
||||||
# Automatically generates tables based on the classes imported from models.py
|
# Automatically generates tables based on the classes imported from models.py
|
||||||
|
|||||||
@@ -899,8 +899,8 @@ DEMO_DEFINITIONS = {
|
|||||||
"state": "Bihar",
|
"state": "Bihar",
|
||||||
"school": "Loyola High School",
|
"school": "Loyola High School",
|
||||||
"tier": models.TierEnum.FREE,
|
"tier": models.TierEnum.FREE,
|
||||||
"streak_count": 1,
|
"streak_count": 0,
|
||||||
"longest_streak": 1,
|
"longest_streak": 0,
|
||||||
# trial_end is intentionally omitted here — it is set dynamically at creation time
|
# trial_end is intentionally omitted here — it is set dynamically at creation time
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -1110,7 +1110,6 @@ DEMO_USER_UUIDS = {
|
|||||||
"11111111-1111-1111-1111-111111111111", # Aarav (AI Coach - JEE)
|
"11111111-1111-1111-1111-111111111111", # Aarav (AI Coach - JEE)
|
||||||
"22222222-2222-2222-2222-222222222222", # Neha (AI Coach - NEET)
|
"22222222-2222-2222-2222-222222222222", # Neha (AI Coach - NEET)
|
||||||
"33333333-3333-3333-3333-333333333333", # Rohan (AI Coach - JEE+NEET)
|
"33333333-3333-3333-3333-333333333333", # Rohan (AI Coach - JEE+NEET)
|
||||||
"44444444-4444-4444-4444-444444444444", # Amit (Free - JEE)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@app.get("/api/users/{user_id}/stats", tags=["Users"])
|
@app.get("/api/users/{user_id}/stats", tags=["Users"])
|
||||||
@@ -1343,6 +1342,7 @@ async def upload_admin_test(
|
|||||||
exam_pattern: str = Form(...),
|
exam_pattern: str = Form(...),
|
||||||
time_limit: int = Form(...),
|
time_limit: int = Form(...),
|
||||||
scheduled_date: Optional[datetime] = Form(None),
|
scheduled_date: Optional[datetime] = Form(None),
|
||||||
|
end_date: Optional[datetime] = Form(None),
|
||||||
admin_id: Optional[UUID] = Form(None),
|
admin_id: Optional[UUID] = Form(None),
|
||||||
session: Session = Depends(get_session),
|
session: Session = Depends(get_session),
|
||||||
current_user: models.User = Depends(get_current_user),
|
current_user: models.User = Depends(get_current_user),
|
||||||
@@ -1388,6 +1388,7 @@ async def upload_admin_test(
|
|||||||
total_time=time_limit,
|
total_time=time_limit,
|
||||||
difficulty="Medium",
|
difficulty="Medium",
|
||||||
scheduled_date=scheduled_date,
|
scheduled_date=scheduled_date,
|
||||||
|
end_date=end_date,
|
||||||
is_active=True,
|
is_active=True,
|
||||||
created_by=effective_admin_id,
|
created_by=effective_admin_id,
|
||||||
pdf_url=url
|
pdf_url=url
|
||||||
@@ -1534,6 +1535,42 @@ def toggle_test_status(test_id: UUID, session: Session = Depends(get_session), c
|
|||||||
|
|
||||||
return {"status": "success", "is_active": test_obj.is_active}
|
return {"status": "success", "is_active": test_obj.is_active}
|
||||||
|
|
||||||
|
class UpdateTestScheduleInput(BaseModel):
|
||||||
|
scheduled_date: Optional[datetime] = None
|
||||||
|
end_date: Optional[datetime] = None
|
||||||
|
title: Optional[str] = None
|
||||||
|
is_active: Optional[bool] = None
|
||||||
|
|
||||||
|
@app.put("/api/admin/tests/{test_id}/schedule", tags=["Admin"])
|
||||||
|
def update_test_schedule(
|
||||||
|
test_id: UUID,
|
||||||
|
data: UpdateTestScheduleInput,
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
current_user: models.User = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
test_obj = session.get(models.Test, test_id)
|
||||||
|
if not test_obj:
|
||||||
|
raise HTTPException(status_code=404, detail="Test not found")
|
||||||
|
|
||||||
|
test_obj.scheduled_date = data.scheduled_date
|
||||||
|
test_obj.end_date = data.end_date
|
||||||
|
if data.title is not None:
|
||||||
|
test_obj.title = data.title
|
||||||
|
if data.is_active is not None:
|
||||||
|
test_obj.is_active = data.is_active
|
||||||
|
session.add(test_obj)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(test_obj)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"test_id": test_obj.id,
|
||||||
|
"title": test_obj.title,
|
||||||
|
"scheduled_date": test_obj.scheduled_date,
|
||||||
|
"end_date": test_obj.end_date,
|
||||||
|
"is_active": test_obj.is_active
|
||||||
|
}
|
||||||
|
|
||||||
@app.delete("/api/admin/tests/{test_id}", tags=["Admin"])
|
@app.delete("/api/admin/tests/{test_id}", tags=["Admin"])
|
||||||
def delete_test(test_id: UUID, session: Session = Depends(get_session), current_user: models.User = Depends(get_current_user)):
|
def delete_test(test_id: UUID, session: Session = Depends(get_session), current_user: models.User = Depends(get_current_user)):
|
||||||
from sqlmodel import select
|
from sqlmodel import select
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ services:
|
|||||||
POSTGRES_USER: pragativeer_admin
|
POSTGRES_USER: pragativeer_admin
|
||||||
POSTGRES_PASSWORD: local_secure_password_2026
|
POSTGRES_PASSWORD: local_secure_password_2026
|
||||||
POSTGRES_DB: pragativeer_prod
|
POSTGRES_DB: pragativeer_prod
|
||||||
|
TZ: Asia/Kolkata
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
volumes:
|
volumes:
|
||||||
@@ -79,6 +80,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
environment:
|
environment:
|
||||||
|
- TZ=Asia/Kolkata
|
||||||
- DATABASE_URL=postgresql://pragativeer_admin:local_secure_password_2026@pgbouncer:5432/pragativeer_prod
|
- DATABASE_URL=postgresql://pragativeer_admin:local_secure_password_2026@pgbouncer:5432/pragativeer_prod
|
||||||
- REDIS_URL=redis://redis:6379/0
|
- REDIS_URL=redis://redis:6379/0
|
||||||
- ENV=development
|
- ENV=development
|
||||||
@@ -108,6 +110,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./backend:/app
|
- ./backend:/app
|
||||||
environment:
|
environment:
|
||||||
|
- TZ=Asia/Kolkata
|
||||||
- DATABASE_URL=postgresql://pragativeer_admin:local_secure_password_2026@pgbouncer:5432/pragativeer_prod
|
- DATABASE_URL=postgresql://pragativeer_admin:local_secure_password_2026@pgbouncer:5432/pragativeer_prod
|
||||||
- REDIS_URL=redis://redis:6379/0
|
- REDIS_URL=redis://redis:6379/0
|
||||||
- MINIO_ENDPOINT=minio:9000
|
- MINIO_ENDPOINT=minio:9000
|
||||||
@@ -135,6 +138,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./backend:/app
|
- ./backend:/app
|
||||||
environment:
|
environment:
|
||||||
|
- TZ=Asia/Kolkata
|
||||||
- DATABASE_URL=postgresql://pragativeer_admin:local_secure_password_2026@pgbouncer:5432/pragativeer_prod
|
- DATABASE_URL=postgresql://pragativeer_admin:local_secure_password_2026@pgbouncer:5432/pragativeer_prod
|
||||||
- REDIS_URL=redis://redis:6379/0
|
- REDIS_URL=redis://redis:6379/0
|
||||||
env_file:
|
env_file:
|
||||||
@@ -160,6 +164,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
environment:
|
environment:
|
||||||
|
- TZ=Asia/Kolkata
|
||||||
- NEXT_PUBLIC_API_URL=http://backend:8000
|
- NEXT_PUBLIC_API_URL=http://backend:8000
|
||||||
- NEXT_PUBLIC_PAYMENT_GATEWAY=razorpay
|
- NEXT_PUBLIC_PAYMENT_GATEWAY=razorpay
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export default function AboutPage() {
|
|||||||
return (
|
return (
|
||||||
<PublicLayout>
|
<PublicLayout>
|
||||||
<div className="bg-background text-foreground flex flex-col pt-24 pb-16 min-h-[70vh]">
|
<div className="bg-background text-foreground flex flex-col pt-24 pb-16 min-h-[70vh]">
|
||||||
<main className="flex-1 flex flex-col items-center justify-center p-8 text-center animate-in fade-in duration-700">
|
<main className="flex-1 flex flex-col items-center justify-center p-8 text-center">
|
||||||
<h1 className="text-5xl font-bold tracking-tighter mb-6">About PragatiVeer</h1>
|
<h1 className="text-5xl font-bold tracking-tighter mb-6">About PragatiVeer</h1>
|
||||||
<p className="text-xl text-muted-foreground max-w-4xl leading-relaxed mb-8 mx-auto text-left md:text-center">
|
<p className="text-xl text-muted-foreground max-w-4xl leading-relaxed mb-8 mx-auto text-left md:text-center">
|
||||||
PragatiVeer is India's first AI-powered daily practice platform for JEE and NEET aspirants — a product of Gyannetra Pvt Ltd, the first company to bring the power of artificial intelligence into Indian exam preparation. We combine daily practice sheets, exam-realistic test series, and an AI coach that personalises every student's learning journey, making high-quality preparation accessible and affordable for every Indian student.
|
PragatiVeer is India's first AI-powered daily practice platform for JEE and NEET aspirants — a product of Gyannetra Pvt Ltd, the first company to bring the power of artificial intelligence into Indian exam preparation. We combine daily practice sheets, exam-realistic test series, and an AI coach that personalises every student's learning journey, making high-quality preparation accessible and affordable for every Indian student.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { Activity, BrainCircuit, Zap, Server, ShieldCheck } from "lucide-react"
|
import { Activity, BrainCircuit, Zap, Server, ShieldCheck } from "lucide-react"
|
||||||
|
|
||||||
export default function AgentsPage() {
|
export default function AgentsPage() {
|
||||||
@@ -10,7 +10,7 @@ export default function AgentsPage() {
|
|||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-8 space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
<div className="p-8 space-y-8">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold tracking-tight">AI Agents Monitor</h1>
|
<h1 className="text-3xl font-bold tracking-tight">AI Agents Monitor</h1>
|
||||||
<p className="text-muted-foreground mt-1">Real-time telemetry for internal LLM orchestration nodes.</p>
|
<p className="text-muted-foreground mt-1">Real-time telemetry for internal LLM orchestration nodes.</p>
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export default function CouponsAdmin() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-8 max-w-6xl mx-auto space-y-8 animate-in fade-in duration-500">
|
<div className="p-8 max-w-6xl mx-auto space-y-8">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Coupon Management</h1>
|
<h1 className="text-3xl font-bold tracking-tight">Coupon Management</h1>
|
||||||
<p className="text-muted-foreground mt-1 font-medium">Create and manage discount codes for subscriptions.</p>
|
<p className="text-muted-foreground mt-1 font-medium">Create and manage discount codes for subscriptions.</p>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Terminal, Users, Database, Activity, RefreshCw } from "lucide-react"
|
|||||||
|
|
||||||
export default function AdminDashboard() {
|
export default function AdminDashboard() {
|
||||||
return (
|
return (
|
||||||
<div className="p-8 max-w-7xl mx-auto space-y-8 animate-in fade-in duration-500">
|
<div className="p-8 max-w-7xl mx-auto space-y-8">
|
||||||
<header className="flex items-center justify-between border-b border-border pb-6">
|
<header className="flex items-center justify-between border-b border-border pb-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold tracking-tighter text-foreground">Command Center</h1>
|
<h1 className="text-3xl font-bold tracking-tighter text-foreground">Command Center</h1>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Search, Plus, Filter, MoreHorizontal, Edit, Trash } from "lucide-react"
|
import { Search, Plus, Filter, MoreHorizontal, Edit, Trash } from "lucide-react"
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ export default function QuestionsPage() {
|
|||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-8 space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
<div className="p-8 space-y-8">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Question Bank</h1>
|
<h1 className="text-3xl font-bold tracking-tight">Question Bank</h1>
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export default function SettingsPage() {
|
|||||||
if (loading) return <div className="p-8 animate-pulse text-muted-foreground font-bold">Loading System Settings...</div>;
|
if (loading) return <div className="p-8 animate-pulse text-muted-foreground font-bold">Loading System Settings...</div>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-8 space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500 max-w-4xl">
|
<div className="p-8 space-y-8 max-w-4xl">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold tracking-tight">System Settings</h1>
|
<h1 className="text-3xl font-bold tracking-tight">System Settings</h1>
|
||||||
<p className="text-muted-foreground mt-1">Configure global application variables and security protocols.</p>
|
<p className="text-muted-foreground mt-1">Configure global application variables and security protocols.</p>
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ export default function StudentKundliPage() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-8 space-y-6 max-w-6xl mx-auto animate-in fade-in duration-500">
|
<div className="p-8 space-y-6 max-w-6xl mx-auto">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Button variant="ghost" onClick={() => router.back()} className="text-muted-foreground hover:text-foreground">
|
<Button variant="ghost" onClick={() => router.back()} className="text-muted-foreground hover:text-foreground">
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ export default function StudentsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-8 space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
<div className="p-8 space-y-8">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Student Directory</h1>
|
<h1 className="text-3xl font-bold tracking-tight">Student Directory</h1>
|
||||||
@@ -263,7 +263,7 @@ export default function StudentsPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="w-full bg-secondary rounded-full h-2 overflow-hidden">
|
<div className="w-full bg-secondary rounded-full h-2 overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className="bg-primary h-2 rounded-full transition-all duration-1000 ease-in-out"
|
className="bg-primary h-2 rounded-full"
|
||||||
style={{ width: `${uploadProgress}%` }}
|
style={{ width: `${uploadProgress}%` }}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,18 +1,24 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { fetchWithAuth } from "@/lib/apiClient";
|
import { fetchWithAuth } from "@/lib/apiClient";
|
||||||
import { useState, useEffect, useRef } from "react"
|
import { useState, useEffect, useRef, useMemo } from "react"
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Switch } from "@/components/ui/switch"
|
import { Switch } from "@/components/ui/switch"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Loader2, Upload, FileText, Clock, CalendarIcon, CheckCircle2, AlertCircle, X, Trash2 } from "lucide-react"
|
import {
|
||||||
|
Loader2, Upload, FileText, Clock, Calendar as CalendarIcon,
|
||||||
|
CheckCircle2, AlertCircle, X, Trash2, CalendarRange,
|
||||||
|
SlidersHorizontal, Check, RefreshCw, Zap, ShieldAlert
|
||||||
|
} from "lucide-react"
|
||||||
import DatePicker from "react-datepicker"
|
import DatePicker from "react-datepicker"
|
||||||
import "react-datepicker/dist/react-datepicker.css"
|
import "react-datepicker/dist/react-datepicker.css"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
import { useActiveUser } from "@/hooks/useActiveUser"
|
import { useActiveUser } from "@/hooks/useActiveUser"
|
||||||
|
import { CalendarFilter, getLocalDateString } from "@/components/CalendarFilter"
|
||||||
|
import { parseAsISTDate, formatISTDateTime, toISTISOString } from "@/lib/dateUtils"
|
||||||
|
|
||||||
// Permissive, auto-normalizing Zod schema for client-side question parsing
|
// Permissive, auto-normalizing Zod schema for client-side question parsing
|
||||||
const QuestionSchema = z.preprocess((val: any) => {
|
const QuestionSchema = z.preprocess((val: any) => {
|
||||||
@@ -119,6 +125,18 @@ export default function AdminTestsPage() {
|
|||||||
const [validationErrors, setValidationErrors] = useState<{ path: string, message: string }[]>([])
|
const [validationErrors, setValidationErrors] = useState<{ path: string, message: string }[]>([])
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
// Calendar-based controller state
|
||||||
|
const [adminDateFilter, setAdminDateFilter] = useState("")
|
||||||
|
const [adminTypeFilter, setAdminTypeFilter] = useState("All")
|
||||||
|
|
||||||
|
// Schedule Editing Modal State
|
||||||
|
const [editingTest, setEditingTest] = useState<any | null>(null)
|
||||||
|
const [editTitle, setEditTitle] = useState("")
|
||||||
|
const [editScheduledStart, setEditScheduledStart] = useState<Date | null>(null)
|
||||||
|
const [editScheduledEnd, setEditScheduledEnd] = useState<Date | null>(null)
|
||||||
|
const [editIsActive, setEditIsActive] = useState(true)
|
||||||
|
const [savingSchedule, setSavingSchedule] = useState(false)
|
||||||
|
|
||||||
const fetchTests = async () => {
|
const fetchTests = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetchWithAuth("/api/admin/tests")
|
const res = await fetchWithAuth("/api/admin/tests")
|
||||||
@@ -134,7 +152,6 @@ export default function AdminTestsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
||||||
fetchTests()
|
fetchTests()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@@ -169,6 +186,78 @@ export default function AdminTestsPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openScheduleModal = (test: any) => {
|
||||||
|
setEditingTest(test)
|
||||||
|
setEditTitle(test.title || "")
|
||||||
|
setEditScheduledStart(parseAsISTDate(test.scheduled_date))
|
||||||
|
setEditScheduledEnd(parseAsISTDate(test.end_date))
|
||||||
|
setEditIsActive(test.is_active ?? true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveSchedule = async () => {
|
||||||
|
if (!editingTest) return
|
||||||
|
setSavingSchedule(true)
|
||||||
|
try {
|
||||||
|
const res = await fetchWithAuth(`/api/admin/tests/${editingTest.id}/schedule`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
title: editTitle,
|
||||||
|
scheduled_date: toISTISOString(editScheduledStart),
|
||||||
|
end_date: toISTISOString(editScheduledEnd),
|
||||||
|
is_active: editIsActive
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if (res.ok) {
|
||||||
|
setEditingTest(null)
|
||||||
|
fetchTests()
|
||||||
|
} else {
|
||||||
|
const err = await res.json()
|
||||||
|
alert(`Failed to update schedule: ${JSON.stringify(err)}`)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
alert("Error saving schedule changes.")
|
||||||
|
} finally {
|
||||||
|
setSavingSchedule(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quick preset actions for calendar schedule controller
|
||||||
|
const setLiveNowPreset = () => {
|
||||||
|
const now = new Date()
|
||||||
|
const end = new Date(Date.now() + 24 * 3600 * 1000)
|
||||||
|
setEditScheduledStart(now)
|
||||||
|
setEditScheduledEnd(end)
|
||||||
|
setEditIsActive(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setTodayPreset = () => {
|
||||||
|
const start = new Date()
|
||||||
|
start.setHours(9, 0, 0, 0)
|
||||||
|
const end = new Date()
|
||||||
|
end.setHours(23, 59, 59, 999)
|
||||||
|
setEditScheduledStart(start)
|
||||||
|
setEditScheduledEnd(end)
|
||||||
|
setEditIsActive(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setTomorrowPreset = () => {
|
||||||
|
const start = new Date(Date.now() + 86400000)
|
||||||
|
start.setHours(9, 0, 0, 0)
|
||||||
|
const end = new Date(Date.now() + 86400000)
|
||||||
|
end.setHours(23, 59, 59, 999)
|
||||||
|
setEditScheduledStart(start)
|
||||||
|
setEditScheduledEnd(end)
|
||||||
|
setEditIsActive(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearScheduleBounds = () => {
|
||||||
|
setEditScheduledStart(null)
|
||||||
|
setEditScheduledEnd(null)
|
||||||
|
setEditIsActive(true)
|
||||||
|
}
|
||||||
|
|
||||||
const handleDragOver = (e: React.DragEvent) => {
|
const handleDragOver = (e: React.DragEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setIsDragging(true)
|
setIsDragging(true)
|
||||||
@@ -254,7 +343,8 @@ export default function AdminTestsPage() {
|
|||||||
formData.append("exam_pattern", examPattern)
|
formData.append("exam_pattern", examPattern)
|
||||||
formData.append("time_limit", timeLimit)
|
formData.append("time_limit", timeLimit)
|
||||||
formData.append("admin_id", effectiveAdminId)
|
formData.append("admin_id", effectiveAdminId)
|
||||||
if (scheduledDate) formData.append("scheduled_date", scheduledDate.toISOString())
|
if (scheduledDate) formData.append("scheduled_date", toISTISOString(scheduledDate)!)
|
||||||
|
if (endDate) formData.append("end_date", toISTISOString(endDate)!)
|
||||||
|
|
||||||
const res = await fetchWithAuth("/api/admin/tests/upload", {
|
const res = await fetchWithAuth("/api/admin/tests/upload", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -263,6 +353,8 @@ export default function AdminTestsPage() {
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
alert("PDF uploaded successfully to Minio and connected!")
|
alert("PDF uploaded successfully to Minio and connected!")
|
||||||
setTitle("")
|
setTitle("")
|
||||||
|
setScheduledDate(null)
|
||||||
|
setEndDate(null)
|
||||||
removeFile()
|
removeFile()
|
||||||
fetchTests()
|
fetchTests()
|
||||||
} else {
|
} else {
|
||||||
@@ -291,8 +383,8 @@ export default function AdminTestsPage() {
|
|||||||
time_limit: parseInt(timeLimit),
|
time_limit: parseInt(timeLimit),
|
||||||
admin_id: effectiveAdminId,
|
admin_id: effectiveAdminId,
|
||||||
questions_json: validQuestions,
|
questions_json: validQuestions,
|
||||||
scheduled_date: scheduledDate ? scheduledDate.toISOString() : null,
|
scheduled_date: toISTISOString(scheduledDate),
|
||||||
end_date: endDate ? endDate.toISOString() : null,
|
end_date: toISTISOString(endDate),
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -305,6 +397,8 @@ export default function AdminTestsPage() {
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
alert("Test created successfully!")
|
alert("Test created successfully!")
|
||||||
setTitle("")
|
setTitle("")
|
||||||
|
setScheduledDate(null)
|
||||||
|
setEndDate(null)
|
||||||
removeFile()
|
removeFile()
|
||||||
fetchTests()
|
fetchTests()
|
||||||
} else {
|
} else {
|
||||||
@@ -319,50 +413,81 @@ export default function AdminTestsPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// All scheduled dates for CalendarFilter dots
|
||||||
|
const allScheduledDates = useMemo(() => {
|
||||||
|
return tests
|
||||||
|
.map(t => t.scheduled_date ? getLocalDateString(t.scheduled_date) : "")
|
||||||
|
.filter(Boolean)
|
||||||
|
}, [tests])
|
||||||
|
|
||||||
|
// Filter tests by Type and Schedule Date
|
||||||
|
const filteredTests = useMemo(() => {
|
||||||
|
return tests.filter(test => {
|
||||||
|
if (adminTypeFilter !== "All") {
|
||||||
|
if (adminTypeFilter === "DPP_PDF" && !test.pdf_url) return false
|
||||||
|
if (adminTypeFilter === "DPS" && (test.type !== "DPS" || test.pdf_url)) return false
|
||||||
|
if (adminTypeFilter === "Mock" && test.type !== "Mock") return false
|
||||||
|
}
|
||||||
|
if (adminDateFilter) {
|
||||||
|
if (!test.scheduled_date) return false
|
||||||
|
if (getLocalDateString(test.scheduled_date) !== adminDateFilter) return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}, [tests, adminTypeFilter, adminDateFilter])
|
||||||
|
|
||||||
|
const formatScheduleDateTime = (dateStr: string) => {
|
||||||
|
return formatISTDateTime(dateStr)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-8 space-y-6">
|
<div className="p-8 space-y-6">
|
||||||
<div>
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Tests & DPPs</h1>
|
<div>
|
||||||
<p className="text-muted-foreground mt-2">Manage CBT examinations and daily practice sheets</p>
|
<h1 className="text-3xl font-bold tracking-tight">Tests & DPPs Manager</h1>
|
||||||
|
<p className="text-muted-foreground mt-1">
|
||||||
|
Date-wise calendar scheduling, live time-bound controllers, and examination repositories.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
|
||||||
{/* Upload Form */}
|
{/* Upload & Create Form */}
|
||||||
<Card className="lg:col-span-1 border-muted bg-card shadow-sm h-fit">
|
<Card className="lg:col-span-1 border-muted bg-card shadow-sm h-fit">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<Upload className="w-5 h-5 text-primary" />
|
<Upload className="w-5 h-5 text-primary" />
|
||||||
Create Manual Test
|
Create / Schedule Test
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>Upload a structured JSON array of questions in LaTeX format to create a deterministic test.</CardDescription>
|
<CardDescription>Upload structured question JSON or attach a class DPP PDF with exact calendar timings.</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<form onSubmit={handleUpload} className="space-y-4">
|
<form onSubmit={handleUpload} className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium">Test Title</label>
|
<label className="text-sm font-medium">Test / DPP Title</label>
|
||||||
<Input required value={title} onChange={e => setTitle(e.target.value)} placeholder="e.g. JEE Main Mock Test 1" />
|
<Input required value={title} onChange={e => setTitle(e.target.value)} placeholder="e.g. Physics Ch-4 DPP (Electrostatics)" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* TEST TYPE — KEY DIFFERENTIATOR */}
|
{/* TEST TYPE */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium flex items-center gap-2">
|
<label className="text-sm font-medium flex items-center gap-2">
|
||||||
Test Type
|
Test Type
|
||||||
<span className="text-[10px] text-muted-foreground font-normal">(determines which section it appears in)</span>
|
<span className="text-[10px] text-muted-foreground font-normal">(Placement Target)</span>
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
||||||
value={testType}
|
value={testType}
|
||||||
onChange={(e) => setTestType(e.target.value)}
|
onChange={(e) => setTestType(e.target.value)}
|
||||||
>
|
>
|
||||||
<option value="Mock">🎯 CBT Test (JSON) — appears in Mock Tests section</option>
|
<option value="Mock">🎯 CBT Test (JSON) — Mock Tests Section</option>
|
||||||
<option value="DPS">🤖 Prep Pilot Test (as CBT JSON) — appears in AI Coach</option>
|
<option value="DPS">🤖 Prep Pilot Test (CBT JSON) — AI Coach</option>
|
||||||
<option value="DPP_PDF">📋 DPP PDF (Minio Upload) — Class/Plan aligned</option>
|
<option value="DPP_PDF">📋 DPP PDF (Minio Upload) — Daily Practice Section</option>
|
||||||
</select>
|
</select>
|
||||||
<p className="text-[11px] text-muted-foreground">
|
<p className="text-[11px] text-muted-foreground">
|
||||||
{testType === 'Mock' && '✅ Purely for CBT. Appears under Dashboard → Mock Tests.'}
|
{testType === 'Mock' && '✅ Appears in Student Dashboard → Mock Tests (CBT).'}
|
||||||
{testType === 'DPS' && '✅ Purely for CBT. Appears under AI Coach → PrepPilot AI.'}
|
{testType === 'DPS' && '✅ Appears in Student Dashboard → AI Coach → PrepPilot AI.'}
|
||||||
{testType === 'DPP_PDF' && '✅ Uploads a PDF to Minio storage for Class/Plan aligned DPP.'}
|
{testType === 'DPP_PDF' && '✅ Appears in Student Dashboard → Daily Practice (DPP).'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -376,6 +501,7 @@ export default function AdminTestsPage() {
|
|||||||
<option value="JEE_Main">JEE Main</option>
|
<option value="JEE_Main">JEE Main</option>
|
||||||
<option value="JEE_Advanced">JEE Advanced</option>
|
<option value="JEE_Advanced">JEE Advanced</option>
|
||||||
<option value="NEET">NEET</option>
|
<option value="NEET">NEET</option>
|
||||||
|
<option value="JEE_NEET">JEE & NEET (Combined)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -384,43 +510,48 @@ export default function AdminTestsPage() {
|
|||||||
<Input required type="number" value={timeLimit} onChange={e => setTimeLimit(e.target.value)} />
|
<Input required type="number" value={timeLimit} onChange={e => setTimeLimit(e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
{/* Schedule Boundation Controller */}
|
||||||
<div className="space-y-2 flex flex-col">
|
<div className="p-3.5 bg-secondary/20 rounded-xl border border-border/80 space-y-3">
|
||||||
<label className="text-sm font-medium">Scheduled Start</label>
|
<div className="flex items-center gap-2 text-xs font-bold text-foreground">
|
||||||
<DatePicker
|
<CalendarRange size={15} className="text-[var(--cyan-ai)]" /> Schedule Boundation (Start & End)
|
||||||
selected={scheduledDate}
|
|
||||||
onChange={(date: Date | null) => 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="Optional"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2 flex flex-col">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
<label className="text-sm font-medium">Scheduled End</label>
|
<div className="space-y-1.5 flex flex-col">
|
||||||
<DatePicker
|
<label className="text-[11px] font-bold text-muted-foreground">Scheduled Start</label>
|
||||||
selected={endDate}
|
<DatePicker
|
||||||
onChange={(date: Date | null) => setEndDate(date)}
|
selected={scheduledDate}
|
||||||
showTimeSelect
|
onChange={(date: Date | null) => setScheduledDate(date)}
|
||||||
timeFormat="HH:mm"
|
showTimeSelect
|
||||||
timeIntervals={15}
|
timeFormat="HH:mm"
|
||||||
timeCaption="time"
|
timeIntervals={15}
|
||||||
dateFormat="MMMM d, yyyy h:mm aa"
|
timeCaption="Time"
|
||||||
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"
|
dateFormat="MMM d, yyyy h:mm aa"
|
||||||
placeholderText="Optional"
|
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1.5 text-xs font-semibold focus:outline-none focus:ring-2 focus:ring-ring"
|
||||||
/>
|
placeholderText="Starts Immediately"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5 flex flex-col">
|
||||||
|
<label className="text-[11px] font-bold text-muted-foreground">Scheduled End</label>
|
||||||
|
<DatePicker
|
||||||
|
selected={endDate}
|
||||||
|
onChange={(date: Date | null) => setEndDate(date)}
|
||||||
|
showTimeSelect
|
||||||
|
timeFormat="HH:mm"
|
||||||
|
timeIntervals={15}
|
||||||
|
timeCaption="Time"
|
||||||
|
dateFormat="MMM d, yyyy h:mm aa"
|
||||||
|
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1.5 text-xs font-semibold focus:outline-none focus:ring-2 focus:ring-ring"
|
||||||
|
placeholderText="No Expiry"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2 pt-2">
|
<div className="space-y-2 pt-2">
|
||||||
<label className="text-sm font-medium">Questions JSON File</label>
|
<label className="text-sm font-medium">Questions JSON or PDF File</label>
|
||||||
|
|
||||||
{testType === 'DPP_PDF' ? (
|
{testType === 'DPP_PDF' ? (
|
||||||
<div className="space-y-2 pt-2">
|
<div className="space-y-2 pt-1">
|
||||||
<label className="text-sm font-medium">PDF File (Minio Upload)</label>
|
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept=".pdf"
|
accept=".pdf"
|
||||||
@@ -475,9 +606,6 @@ export default function AdminTestsPage() {
|
|||||||
<span>{err.message}</span>
|
<span>{err.message}</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
{validationErrors.length > 50 && (
|
|
||||||
<li className="text-xs text-muted-foreground italic mt-2">...and {validationErrors.length - 50} more errors</li>
|
|
||||||
)}
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -485,75 +613,285 @@ export default function AdminTestsPage() {
|
|||||||
|
|
||||||
<Button type="submit" className="w-full" disabled={uploading || (!validQuestions && testType !== 'DPP_PDF')}>
|
<Button type="submit" className="w-full" disabled={uploading || (!validQuestions && testType !== 'DPP_PDF')}>
|
||||||
{uploading ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <FileText className="w-4 h-4 mr-2" />}
|
{uploading ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <FileText className="w-4 h-4 mr-2" />}
|
||||||
{uploading ? "Creating Test..." : (testType === 'DPP_PDF' ? "Upload PDF to Minio" : "Create Test from JSON")}
|
{uploading ? "Publishing..." : (testType === 'DPP_PDF' ? "Upload & Schedule PDF DPP" : "Create Test from JSON")}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Existing Tests List */}
|
{/* Existing Tests List & Calendar Controller */}
|
||||||
<Card className="lg:col-span-2 border-muted bg-card shadow-sm h-fit">
|
<Card className="lg:col-span-2 border-muted bg-card shadow-sm h-fit">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2">
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||||
<FileText className="w-5 h-5 text-primary" />
|
<div>
|
||||||
Test Repository
|
<CardTitle className="flex items-center gap-2">
|
||||||
</CardTitle>
|
<CalendarRange className="w-5 h-5 text-primary" />
|
||||||
<CardDescription>Manage visibility and schedule for parsed tests.</CardDescription>
|
Scheduled Tests & DPPs Controller
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>Filter by date, adjust start/end time windows, and manage visibility.</CardDescription>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" size="sm" onClick={fetchTests} className="gap-1.5 self-start sm:self-auto text-xs font-bold">
|
||||||
|
<RefreshCw size={14} /> Refresh
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filter Controllers Toolbar */}
|
||||||
|
<div className="pt-4 flex flex-wrap items-center justify-between gap-3 border-t border-border/60">
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<CalendarFilter
|
||||||
|
selectedDate={adminDateFilter}
|
||||||
|
onSelectDate={setAdminDateFilter}
|
||||||
|
availableDates={allScheduledDates}
|
||||||
|
placeholder="Filter by Schedule Date"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1 bg-secondary/50 p-1 rounded-xl">
|
||||||
|
{["All", "DPP_PDF", "DPS", "Mock"].map((t) => (
|
||||||
|
<button
|
||||||
|
key={t}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setAdminTypeFilter(t)}
|
||||||
|
className={`px-2.5 py-1 rounded-lg text-xs font-bold transition-all ${
|
||||||
|
adminTypeFilter === t
|
||||||
|
? "bg-background text-foreground shadow-sm"
|
||||||
|
: "text-muted-foreground hover:text-foreground"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{t === "All" ? "All Types" : t === "DPP_PDF" ? "DPP (PDF)" : t === "DPS" ? "PrepPilot" : "CBT Mocks"}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-xs font-bold text-muted-foreground">
|
||||||
|
Showing {filteredTests.length} of {tests.length} items
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex justify-center p-8"><Loader2 className="w-8 h-8 animate-spin text-muted-foreground" /></div>
|
<div className="flex justify-center p-8"><Loader2 className="w-8 h-8 animate-spin text-muted-foreground" /></div>
|
||||||
) : tests.length === 0 ? (
|
) : filteredTests.length === 0 ? (
|
||||||
<div className="text-center p-8 text-muted-foreground border border-dashed rounded-lg">
|
<div className="text-center p-8 text-muted-foreground border border-dashed rounded-xl">
|
||||||
No tests uploaded yet.
|
No tests or DPPs match the selected filter criteria.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4">
|
<div className="space-y-3.5">
|
||||||
{tests.map(test => (
|
{filteredTests.map(test => {
|
||||||
<div key={test.id} className="flex items-center justify-between p-4 border rounded-lg bg-background">
|
const now = new Date()
|
||||||
<div className="space-y-1">
|
const startTime = parseAsISTDate(test.scheduled_date)
|
||||||
<div className="flex items-center gap-2">
|
const endTime = parseAsISTDate(test.end_date)
|
||||||
<h3 className="font-semibold">{test.title}</h3>
|
const isUpcoming = startTime ? startTime > now : false
|
||||||
<Badge variant="outline">{test.exam_pattern?.replace("_", " ") || "All"}</Badge>
|
const isExpired = endTime ? now > endTime : false
|
||||||
<Badge variant={test.is_active ? "default" : "secondary"}>
|
const isLive = !isUpcoming && !isExpired
|
||||||
{test.is_active ? "Active" : "Inactive / Processing"}
|
|
||||||
</Badge>
|
return (
|
||||||
</div>
|
<div key={test.id} className="p-4 border rounded-xl bg-background hover:border-primary/40 transition-colors shadow-sm">
|
||||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
||||||
<div className="flex items-center gap-1">
|
<div className="space-y-1.5 flex-1 min-w-0">
|
||||||
<Clock className="w-3 h-3" />
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
{test.total_time} mins
|
<h3 className="font-bold text-base truncate">{test.title}</h3>
|
||||||
</div>
|
<Badge variant="outline" className="text-[10px] font-bold">
|
||||||
{test.scheduled_date && (
|
{test.pdf_url ? "DPP (PDF)" : test.type === "DPS" ? "PrepPilot (DPS)" : "CBT Mock"}
|
||||||
<div className="flex items-center gap-1">
|
</Badge>
|
||||||
<CalendarIcon className="w-3 h-3" />
|
<Badge variant="outline" className="text-[10px] uppercase font-bold border-primary/20 bg-primary/5 text-primary">
|
||||||
{new Date(test.scheduled_date).toLocaleString("en-US", { dateStyle: "medium", timeStyle: "short" })}
|
{test.exam_pattern?.replace("_", " ") || "All"}
|
||||||
|
</Badge>
|
||||||
|
|
||||||
|
{/* Live status pill */}
|
||||||
|
{isUpcoming ? (
|
||||||
|
<Badge variant="secondary" className="bg-amber-500/10 text-amber-500 border-amber-500/20 text-[10px] font-bold">
|
||||||
|
🔒 Upcoming
|
||||||
|
</Badge>
|
||||||
|
) : isExpired ? (
|
||||||
|
<Badge variant="outline" className="bg-destructive/10 text-destructive border-destructive/20 text-[10px] font-bold">
|
||||||
|
⏳ Expired
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge variant="secondary" className="bg-emerald-500/10 text-emerald-500 border-emerald-500/20 text-[10px] font-bold">
|
||||||
|
🟢 Live Now
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Badge variant={test.is_active ? "default" : "secondary"} className="text-[10px]">
|
||||||
|
{test.is_active ? "Published" : "Hidden"}
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
<div className="flex items-center gap-4 text-xs text-muted-foreground flex-wrap">
|
||||||
|
<div className="flex items-center gap-1 font-medium">
|
||||||
|
<Clock className="w-3.5 h-3.5" />
|
||||||
|
{test.total_time} mins
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{test.scheduled_date ? (
|
||||||
|
<div className="flex items-center gap-1 font-semibold text-foreground">
|
||||||
|
<span className="text-muted-foreground font-normal">Starts:</span> {formatScheduleDateTime(test.scheduled_date)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="text-muted-foreground">Always Available</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{test.end_date && (
|
||||||
|
<div className="flex items-center gap-1 font-semibold text-foreground border-l border-border pl-3">
|
||||||
|
<span className="text-muted-foreground font-normal">Ends:</span> {formatScheduleDateTime(test.end_date)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex items-center gap-2.5 shrink-0 self-end sm:self-center">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => openScheduleModal(test)}
|
||||||
|
className="gap-1.5 rounded-lg text-xs font-bold border-[var(--cyan-ai-30)] hover:bg-[var(--cyan-ai-10)] text-[var(--cyan-ai)] h-8"
|
||||||
|
>
|
||||||
|
<SlidersHorizontal size={13} /> Edit Timing
|
||||||
|
</Button>
|
||||||
|
<div className="flex items-center gap-1.5 border-l border-border pl-2.5">
|
||||||
|
<Switch checked={test.is_active} onCheckedChange={() => handleToggle(test.id)} title="Toggle published status" />
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8 text-destructive hover:bg-destructive/10"
|
||||||
|
onClick={() => handleDelete(test.id)}
|
||||||
|
title="Delete test"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4">
|
)
|
||||||
<div className="flex items-center gap-2">
|
})}
|
||||||
<span className="text-sm font-medium">{test.is_active ? "Published" : "Hidden"}</span>
|
|
||||||
<Switch checked={test.is_active} onCheckedChange={() => handleToggle(test.id)} />
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-8 w-8 text-destructive hover:bg-destructive/10"
|
|
||||||
onClick={() => handleDelete(test.id)}
|
|
||||||
>
|
|
||||||
<Trash2 className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* SCHEDULE CONTROLLER MODAL */}
|
||||||
|
{editingTest && (
|
||||||
|
<div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4">
|
||||||
|
<div className="bg-background border border-border rounded-2xl w-full max-w-lg overflow-hidden shadow-2xl">
|
||||||
|
<div className="p-5 border-b border-border bg-secondary/30 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<div className="p-2 bg-[var(--cyan-ai-10)] rounded-xl border border-[var(--cyan-ai-20)] text-[var(--cyan-ai)]">
|
||||||
|
<SlidersHorizontal size={18} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-extrabold text-base">Edit Time Boundation & Schedule</h3>
|
||||||
|
<p className="text-xs text-muted-foreground font-medium truncate max-w-xs">{editingTest.title}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setEditingTest(null)}
|
||||||
|
className="w-8 h-8 rounded-lg bg-secondary text-muted-foreground hover:text-foreground flex items-center justify-center transition-colors"
|
||||||
|
>
|
||||||
|
<X size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 space-y-5">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-xs font-bold uppercase tracking-wider text-muted-foreground">Title</label>
|
||||||
|
<Input value={editTitle} onChange={(e) => setEditTitle(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick Preset Buttons */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-xs font-bold uppercase tracking-wider text-muted-foreground">Quick Timing Presets</label>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={setLiveNowPreset}
|
||||||
|
className="p-2 rounded-xl bg-secondary hover:bg-secondary/80 text-xs font-bold text-left transition-colors flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<span className="w-2 h-2 rounded-full bg-emerald-500"></span> Live Now (Next 24h)
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={setTodayPreset}
|
||||||
|
className="p-2 rounded-xl bg-secondary hover:bg-secondary/80 text-xs font-bold text-left transition-colors flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<span className="w-2 h-2 rounded-full bg-[var(--cyan-ai)]"></span> Today (09 AM - 12 AM)
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={setTomorrowPreset}
|
||||||
|
className="p-2 rounded-xl bg-secondary hover:bg-secondary/80 text-xs font-bold text-left transition-colors flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<span className="w-2 h-2 rounded-full bg-yellow-500"></span> Tomorrow Full Day
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={clearScheduleBounds}
|
||||||
|
className="p-2 rounded-xl bg-secondary hover:bg-secondary/80 text-xs font-bold text-left transition-colors flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<span className="w-2 h-2 rounded-full bg-muted-foreground"></span> Always Available (24/7)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Precise Date/Time Pickers */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-1.5 flex flex-col">
|
||||||
|
<label className="text-xs font-bold text-foreground">Scheduled Start Time</label>
|
||||||
|
<DatePicker
|
||||||
|
selected={editScheduledStart}
|
||||||
|
onChange={(date: Date | null) => setEditScheduledStart(date)}
|
||||||
|
showTimeSelect
|
||||||
|
timeFormat="HH:mm"
|
||||||
|
timeIntervals={15}
|
||||||
|
timeCaption="Time"
|
||||||
|
dateFormat="MMM d, yyyy h:mm aa"
|
||||||
|
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-xs font-bold"
|
||||||
|
placeholderText="Immediate / Open"
|
||||||
|
/>
|
||||||
|
<span className="text-[10px] text-muted-foreground">When access opens for students</span>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5 flex flex-col">
|
||||||
|
<label className="text-xs font-bold text-foreground">Scheduled End Time</label>
|
||||||
|
<DatePicker
|
||||||
|
selected={editScheduledEnd}
|
||||||
|
onChange={(date: Date | null) => setEditScheduledEnd(date)}
|
||||||
|
showTimeSelect
|
||||||
|
timeFormat="HH:mm"
|
||||||
|
timeIntervals={15}
|
||||||
|
timeCaption="Time"
|
||||||
|
dateFormat="MMM d, yyyy h:mm aa"
|
||||||
|
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-xs font-bold"
|
||||||
|
placeholderText="No Expiry"
|
||||||
|
/>
|
||||||
|
<span className="text-[10px] text-muted-foreground">When test expires / closes</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status Switch */}
|
||||||
|
<div className="flex items-center justify-between p-3.5 bg-secondary/30 rounded-xl border border-border">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-bold text-foreground">Published / Active</p>
|
||||||
|
<p className="text-xs text-muted-foreground">Students can see this test when scheduled</p>
|
||||||
|
</div>
|
||||||
|
<Switch checked={editIsActive} onCheckedChange={setEditIsActive} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 border-t border-border bg-secondary/20 flex items-center justify-end gap-2">
|
||||||
|
<Button variant="outline" onClick={() => setEditingTest(null)} disabled={savingSchedule}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSaveSchedule} disabled={savingSchedule} className="bg-[var(--cyan-ai)] text-black hover:bg-[var(--cyan-ai-80)] font-extrabold gap-2">
|
||||||
|
{savingSchedule ? <Loader2 size={16} className="animate-spin" /> : <Check size={16} />}
|
||||||
|
{savingSchedule ? "Saving Changes..." : "Apply & Save Timing"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { Scratchpad } from "@/components/CBT/Scratchpad"
|
|||||||
import { PeriodicTable } from "@/components/CBT/PeriodicTable"
|
import { PeriodicTable } from "@/components/CBT/PeriodicTable"
|
||||||
import { ExamResult } from "@/components/CBT/ExamResult"
|
import { ExamResult } from "@/components/CBT/ExamResult"
|
||||||
import { fetchWithAuth } from "@/lib/apiClient"
|
import { fetchWithAuth } from "@/lib/apiClient"
|
||||||
|
import { parseAsISTDate, formatISTDateTime } from "@/lib/dateUtils"
|
||||||
|
|
||||||
// Dummy data for testing the CBT client engine
|
// Dummy data for testing the CBT client engine
|
||||||
const DUMMY_QUESTIONS = Array.from({ length: 20 }).map((_, i) => ({
|
const DUMMY_QUESTIONS = Array.from({ length: 20 }).map((_, i) => ({
|
||||||
@@ -103,15 +104,17 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
|||||||
if (!testRes.ok) throw new Error("Failed to fetch test structure");
|
if (!testRes.ok) throw new Error("Failed to fetch test structure");
|
||||||
const testData = await testRes.json();
|
const testData = await testRes.json();
|
||||||
|
|
||||||
// Strict boundary checking
|
// Strict boundary checking (IST aware)
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
if (testData.scheduled_date && new Date(testData.scheduled_date) > now) {
|
const scheduledTime = parseAsISTDate(testData.scheduled_date);
|
||||||
setAccessError(`This exam is scheduled for ${new Date(testData.scheduled_date).toLocaleString()}. You cannot start it yet.`);
|
const endTime = parseAsISTDate(testData.end_date);
|
||||||
|
if (scheduledTime && scheduledTime > now) {
|
||||||
|
setAccessError(`This exam is scheduled for ${formatISTDateTime(testData.scheduled_date)}. You cannot start it yet.`);
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (testData.end_date && new Date(testData.end_date) < now) {
|
if (endTime && endTime < now) {
|
||||||
setAccessError(`This exam expired on ${new Date(testData.end_date).toLocaleString()} and is no longer available.`);
|
setAccessError(`This exam expired on ${formatISTDateTime(testData.end_date)} and is no longer available.`);
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -282,7 +285,7 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
|||||||
|
|
||||||
if (status === 'NOT_STARTED') {
|
if (status === 'NOT_STARTED') {
|
||||||
return (
|
return (
|
||||||
<div className="h-screen flex flex-col items-center justify-center bg-background p-8 text-center animate-in fade-in duration-500">
|
<div className="h-screen flex flex-col items-center justify-center bg-background p-8 text-center">
|
||||||
<div className="max-w-2xl bg-secondary/10 border border-border p-8 rounded-2xl shadow-2xl">
|
<div className="max-w-2xl bg-secondary/10 border border-border p-8 rounded-2xl shadow-2xl">
|
||||||
<h1 className="text-3xl font-bold mb-4 flex items-center justify-center gap-3">
|
<h1 className="text-3xl font-bold mb-4 flex items-center justify-center gap-3">
|
||||||
<Shield className="text-red-500" /> Secure Exam Environment
|
<Shield className="text-red-500" /> Secure Exam Environment
|
||||||
@@ -392,8 +395,8 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
|||||||
<div className="h-screen flex flex-col overflow-hidden">
|
<div className="h-screen flex flex-col overflow-hidden">
|
||||||
{/* Warning Modal */}
|
{/* Warning Modal */}
|
||||||
{showWarning && (
|
{showWarning && (
|
||||||
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/80 backdrop-blur-sm animate-in fade-in duration-200">
|
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/80 backdrop-blur-sm">
|
||||||
<div className="bg-background border border-red-500 shadow-[0_0_50px_-10px_rgba(239,68,68,0.5)] rounded-2xl w-[400px] overflow-hidden animate-in zoom-in-95 duration-200">
|
<div className="bg-background border border-red-500 shadow-[0_0_50px_-10px_rgba(239,68,68,0.5)] rounded-2xl w-[400px] overflow-hidden">
|
||||||
<div className="p-6 text-center space-y-4">
|
<div className="p-6 text-center space-y-4">
|
||||||
<AlertTriangle size={64} className="text-red-500 mx-auto animate-pulse" />
|
<AlertTriangle size={64} className="text-red-500 mx-auto animate-pulse" />
|
||||||
<h3 className="text-2xl font-bold text-red-500">Warning!</h3>
|
<h3 className="text-2xl font-bold text-red-500">Warning!</h3>
|
||||||
@@ -737,7 +740,7 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
|||||||
|
|
||||||
{showSubmitConfirm && (
|
{showSubmitConfirm && (
|
||||||
<div className="absolute inset-0 z-50 bg-background/80 backdrop-blur-sm flex items-center justify-center p-4">
|
<div className="absolute inset-0 z-50 bg-background/80 backdrop-blur-sm flex items-center justify-center p-4">
|
||||||
<div className="bg-background border border-border shadow-2xl rounded-2xl p-6 max-w-md w-full animate-in fade-in zoom-in duration-200">
|
<div className="bg-background border border-border shadow-2xl rounded-2xl p-6 max-w-md w-full">
|
||||||
<h2 className="text-2xl font-bold mb-2">Submit Exam?</h2>
|
<h2 className="text-2xl font-bold mb-2">Submit Exam?</h2>
|
||||||
<p className="text-muted-foreground mb-6">Are you sure you want to submit your exam? You will not be able to change your answers after submission.</p>
|
<p className="text-muted-foreground mb-6">Are you sure you want to submit your exam? You will not be able to change your answers after submission.</p>
|
||||||
|
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ function CheckoutContent() {
|
|||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background flex flex-col items-center justify-center p-6 animate-in fade-in zoom-in duration-500">
|
<div className="min-h-screen bg-background flex flex-col items-center justify-center p-6">
|
||||||
<Card className="max-w-md w-full border-border bg-secondary/10 backdrop-blur-xl shadow-2xl shadow-[var(--cyan-ai-20)] rounded-[2rem] text-center p-8">
|
<Card className="max-w-md w-full border-border bg-secondary/10 backdrop-blur-xl shadow-2xl shadow-[var(--cyan-ai-20)] rounded-[2rem] text-center p-8">
|
||||||
<CheckCircle2 size={80} className="text-[var(--cyan-ai)] mx-auto mb-6" />
|
<CheckCircle2 size={80} className="text-[var(--cyan-ai)] mx-auto mb-6" />
|
||||||
<h2 className="text-3xl font-black mb-2">Payment Successful!</h2>
|
<h2 className="text-3xl font-black mb-2">Payment Successful!</h2>
|
||||||
@@ -198,7 +198,7 @@ function CheckoutContent() {
|
|||||||
<ArrowLeft size={16} /> Back to Plans
|
<ArrowLeft size={16} /> Back to Plans
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div className="max-w-4xl mx-auto grid grid-cols-1 md:grid-cols-2 gap-8 animate-in fade-in slide-in-from-bottom-8 duration-700">
|
<div className="max-w-4xl mx-auto grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||||
|
|
||||||
{/* Billing Details */}
|
{/* Billing Details */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -259,7 +259,7 @@ function CheckoutContent() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="p-3 bg-red-500/10 border border-red-500/20 rounded-xl text-red-500 text-sm font-medium text-center animate-in zoom-in mt-4">
|
<div className="p-3 bg-red-500/10 border border-red-500/20 rounded-xl text-red-500 text-sm font-medium text-center mt-4">
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export default function ContactPage() {
|
|||||||
return (
|
return (
|
||||||
<PublicLayout>
|
<PublicLayout>
|
||||||
<div className="bg-background text-foreground flex flex-col pt-24 pb-16 min-h-[70vh]">
|
<div className="bg-background text-foreground flex flex-col pt-24 pb-16 min-h-[70vh]">
|
||||||
<main className="flex-1 flex flex-col items-center justify-center p-8 animate-in fade-in duration-700">
|
<main className="flex-1 flex flex-col items-center justify-center p-8">
|
||||||
<h1 className="text-5xl font-bold tracking-tighter mb-4 text-center">Contact Us</h1>
|
<h1 className="text-5xl font-bold tracking-tighter mb-4 text-center">Contact Us</h1>
|
||||||
<p className="text-lg text-muted-foreground max-w-2xl text-center mb-12">
|
<p className="text-lg text-muted-foreground max-w-2xl text-center mb-12">
|
||||||
Have a question or need support? We're here to help you ace your preparation.
|
Have a question or need support? We're here to help you ace your preparation.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState, useEffect } from "react"
|
import { useState, useEffect } from "react"
|
||||||
import { Card, CardContent } from "@/components/ui/card"
|
import { Card, CardContent } from "@/components/ui/card"
|
||||||
@@ -148,7 +148,7 @@ export default function AIComCBTDPPPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{submitted && (
|
{submitted && (
|
||||||
<Card className="border-emerald-500/30 bg-emerald-500/10 p-6 rounded-3xl space-y-4 animate-in zoom-in-95">
|
<Card className="border-emerald-500/30 bg-emerald-500/10 p-6 rounded-3xl space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<CheckCircle2 size={32} className="text-emerald-400" />
|
<CheckCircle2 size={32} className="text-emerald-400" />
|
||||||
@@ -216,7 +216,7 @@ export default function AIComCBTDPPPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{submitted && (
|
{submitted && (
|
||||||
<div className="mt-6 p-4 rounded-2xl bg-[var(--cyan-ai-10)] border border-[var(--cyan-ai-30)] text-sm text-[var(--cyan-ai)] space-y-1 animate-in fade-in">
|
<div className="mt-6 p-4 rounded-2xl bg-[var(--cyan-ai-10)] border border-[var(--cyan-ai-30)] text-sm text-[var(--cyan-ai)] space-y-1">
|
||||||
<span className="font-extrabold uppercase text-xs tracking-wider block">AI Solution & Explanation:</span>
|
<span className="font-extrabold uppercase text-xs tracking-wider block">AI Solution & Explanation:</span>
|
||||||
<p>{sampleQuestions[activeQuestion].explanation}</p>
|
<p>{sampleQuestions[activeQuestion].explanation}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ import { Button } from "@/components/ui/button"
|
|||||||
import { Card, CardContent } from "@/components/ui/card"
|
import { Card, CardContent } from "@/components/ui/card"
|
||||||
import { BrainCircuit, Play, CheckCircle2, Download, Filter, Lock, FileText, Eye, ShieldAlert, X, Sparkles, Share2, Maximize2, Minimize2, Zap } from "lucide-react"
|
import { BrainCircuit, Play, CheckCircle2, Download, Filter, Lock, FileText, Eye, ShieldAlert, X, Sparkles, Share2, Maximize2, Minimize2, Zap } from "lucide-react"
|
||||||
|
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState, useMemo } from "react"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { useActiveUser } from "@/hooks/useActiveUser"
|
import { useActiveUser } from "@/hooks/useActiveUser"
|
||||||
|
import { CalendarFilter, getLocalDateString } from "@/components/CalendarFilter"
|
||||||
|
import { formatISTDateTime, formatISTTime, parseAsISTDate } from "@/lib/dateUtils"
|
||||||
|
|
||||||
export default function DPPPage() {
|
export default function DPPPage() {
|
||||||
const { user, userId, hasAccessTo } = useActiveUser()
|
const { user, userId, hasAccessTo } = useActiveUser()
|
||||||
@@ -80,21 +82,27 @@ export default function DPPPage() {
|
|||||||
return acc;
|
return acc;
|
||||||
}, {} as Record<string, any>);
|
}, {} as Record<string, any>);
|
||||||
|
|
||||||
|
const scheduledDates = useMemo(() => {
|
||||||
|
return dpps
|
||||||
|
.filter((d) => !d.pdf_url && !!d.scheduled_date)
|
||||||
|
.map((d) => getLocalDateString(d.scheduled_date))
|
||||||
|
.filter(Boolean)
|
||||||
|
}, [dpps])
|
||||||
|
|
||||||
const filteredDpps = dpps.filter(dpp => {
|
const filteredDpps = dpps.filter(dpp => {
|
||||||
// Only show CBT DPPs here (no PDF)
|
// Only show CBT DPPs here (no PDF)
|
||||||
if (dpp.pdf_url) return false;
|
if (dpp.pdf_url) return false;
|
||||||
|
|
||||||
if (dateFilter) {
|
if (dateFilter) {
|
||||||
if (!dpp.scheduled_date) return false;
|
if (!dpp.scheduled_date) return false;
|
||||||
const scheduledDate = new Date(dpp.scheduled_date).toISOString().split('T')[0];
|
const scheduledDate = getLocalDateString(dpp.scheduled_date);
|
||||||
if (scheduledDate !== dateFilter) return false;
|
if (scheduledDate !== dateFilter) return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
const formatDate = (dateStr: string) => {
|
const formatDateTime = (dateStr: string) => formatISTDateTime(dateStr);
|
||||||
return new Date(dateStr).toLocaleDateString("en-US", { year: 'numeric', month: 'short', day: 'numeric' });
|
const formatTime = (dateStr: string) => formatISTTime(dateStr);
|
||||||
}
|
|
||||||
|
|
||||||
const getPdfUrl = (title: string) => {
|
const getPdfUrl = (title: string) => {
|
||||||
const t = title.toLowerCase();
|
const t = title.toLowerCase();
|
||||||
@@ -109,7 +117,7 @@ export default function DPPPage() {
|
|||||||
const cleanTitle = (title: string) => title.replace(/\s*\(ExamEnum\.[^)]+\)/, '');
|
const cleanTitle = (title: string) => title.replace(/\s*\(ExamEnum\.[^)]+\)/, '');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-8 space-y-8 animate-in fade-in zoom-in-95 origin-top duration-500 max-w-7xl mx-auto">
|
<div className="p-8 space-y-6 max-w-7xl mx-auto">
|
||||||
<div className="flex flex-col md:flex-row md:items-center justify-between p-8 bg-gradient-to-r from-secondary/50 to-background border border-border rounded-2xl shadow-sm gap-6">
|
<div className="flex flex-col md:flex-row md:items-center justify-between p-8 bg-gradient-to-r from-secondary/50 to-background border border-border rounded-2xl shadow-sm gap-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl md:text-4xl font-black tracking-tight flex items-center gap-3 mb-2">
|
<h1 className="text-3xl md:text-4xl font-black tracking-tight flex items-center gap-3 mb-2">
|
||||||
@@ -124,23 +132,19 @@ export default function DPPPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row justify-between items-center gap-4 border-b border-border pb-6">
|
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 border-b border-border pb-6">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-3 flex-wrap">
|
||||||
<input
|
<CalendarFilter
|
||||||
type="date"
|
selectedDate={dateFilter}
|
||||||
value={dateFilter}
|
onSelectDate={setDateFilter}
|
||||||
onChange={(e) => setDateFilter(e.target.value)}
|
availableDates={scheduledDates}
|
||||||
className="px-3 py-1.5 bg-background border border-border rounded-lg text-xs font-bold focus:ring-2 focus:ring-[var(--cyan-ai)] outline-none text-muted-foreground mr-2"
|
placeholder="Filter by Schedule Date"
|
||||||
/>
|
/>
|
||||||
<span className="text-sm font-bold text-muted-foreground mr-2"><Filter size={16} className="inline mr-1"/> Difficulty:</span>
|
{dateFilter && (
|
||||||
{['Mixed', 'Easy', 'Medium', 'Hard'].map((diff) => (
|
<span className="text-xs font-semibold text-muted-foreground bg-secondary/60 px-2.5 py-1 rounded-lg">
|
||||||
<button key={diff} className={`px-3 py-1.5 rounded-lg text-xs font-bold border transition-colors ${diff === 'Mixed' ? 'bg-primary text-primary-foreground border-primary' : 'bg-background text-muted-foreground border-border hover:border-primary/50'}`}>
|
Showing for {dateFilter}
|
||||||
{diff}
|
</span>
|
||||||
</button>
|
)}
|
||||||
))}
|
|
||||||
<div className="flex items-center gap-1 ml-2 text-xs text-muted-foreground bg-secondary/50 px-2 py-1 rounded-md">
|
|
||||||
<Lock size={12}/> Pro
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -171,22 +175,32 @@ export default function DPPPage() {
|
|||||||
const attempt = attemptsByTest[dpp.id];
|
const attempt = attemptsByTest[dpp.id];
|
||||||
const isAttempted = !!attempt;
|
const isAttempted = !!attempt;
|
||||||
|
|
||||||
// Date logic
|
// Date & Time logic (IST aware)
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const scheduledTime = dpp.scheduled_date ? new Date(dpp.scheduled_date) : now;
|
const scheduledTime = parseAsISTDate(dpp.scheduled_date);
|
||||||
const endTime = dpp.end_date ? new Date(dpp.end_date) : null;
|
const endTime = parseAsISTDate(dpp.end_date);
|
||||||
const isUpcoming = scheduledTime > now;
|
const isUpcoming = scheduledTime ? scheduledTime > now : false;
|
||||||
const isExpired = endTime ? now > endTime : false;
|
const isExpired = endTime ? now > endTime : false;
|
||||||
|
const isLive = !isUpcoming && !isExpired;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card key={dpp.id} className={`group cursor-pointer transition-all hover:shadow-md ${isAttempted ? 'opacity-70 hover:opacity-100 bg-secondary/10 border-primary/20' : isExpired ? 'opacity-80' : 'hover:border-[var(--cyan-ai-10)] bg-background'}`}>
|
<Card key={dpp.id} className={`group cursor-pointer transition-all hover:shadow-md ${isAttempted ? 'opacity-85 hover:opacity-100 bg-secondary/10 border-primary/20' : isExpired ? 'opacity-70' : 'hover:border-[var(--cyan-ai-20)] bg-background'}`}>
|
||||||
<CardContent className="p-6 flex flex-col md:flex-row md:items-center justify-between gap-4">
|
<CardContent className="p-6 flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||||
<div className="flex gap-4 items-start w-full">
|
<div className="flex gap-4 items-start w-full">
|
||||||
<div className={`mt-1 flex-shrink-0 ${isAttempted ? 'text-green-500' : 'text-foreground'}`}>
|
{/* Clean Status Icon — No Selectable Circle Checkboxes */}
|
||||||
{isAttempted ? <CheckCircle2 size={24} /> : <div className="size-6 rounded-full border-2 border-muted-foreground group-hover:border-[var(--cyan-ai)] transition-colors flex items-center justify-center"></div>}
|
<div className="mt-1 flex-shrink-0">
|
||||||
|
{isAttempted ? (
|
||||||
|
<div className="p-1.5 rounded-lg bg-emerald-500/10 text-emerald-500 border border-emerald-500/20" title="Completed">
|
||||||
|
<CheckCircle2 size={18} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="p-1.5 rounded-lg bg-secondary/60 text-[var(--cyan-ai)] border border-border" title="Practice Test">
|
||||||
|
<Zap size={18} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="flex items-center gap-2 mb-1 flex-wrap">
|
||||||
<span className="text-xs font-bold uppercase tracking-wider text-[var(--cyan-ai)]">
|
<span className="text-xs font-bold uppercase tracking-wider text-[var(--cyan-ai)]">
|
||||||
{user?.target_exam?.replace("_", " ") || "JEE/NEET"}
|
{user?.target_exam?.replace("_", " ") || "JEE/NEET"}
|
||||||
</span>
|
</span>
|
||||||
@@ -195,14 +209,30 @@ export default function DPPPage() {
|
|||||||
{isUpcoming ? (
|
{isUpcoming ? (
|
||||||
<>
|
<>
|
||||||
<span className="text-muted-foreground text-xs font-bold">•</span>
|
<span className="text-muted-foreground text-xs font-bold">•</span>
|
||||||
<span className="text-[10px] font-bold uppercase text-yellow-500 bg-yellow-500/10 px-1.5 py-0.5 rounded">Upcoming</span>
|
<span className="text-[10px] font-bold uppercase text-amber-500 bg-amber-500/10 border border-amber-500/20 px-2 py-0.5 rounded-md flex items-center gap-1">
|
||||||
|
🔒 Upcoming
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
) : isExpired ? (
|
) : isExpired ? (
|
||||||
<>
|
<>
|
||||||
<span className="text-muted-foreground text-xs font-bold">•</span>
|
<span className="text-muted-foreground text-xs font-bold">•</span>
|
||||||
<span className="text-[10px] font-bold uppercase text-muted-foreground bg-secondary px-1.5 py-0.5 rounded">Expired</span>
|
<span className="text-[10px] font-bold uppercase text-muted-foreground bg-secondary px-2 py-0.5 rounded-md">
|
||||||
|
⏳ Expired
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="text-muted-foreground text-xs font-bold">•</span>
|
||||||
|
<span className="text-[10px] font-bold uppercase text-emerald-500 bg-emerald-500/10 border border-emerald-500/20 px-2 py-0.5 rounded-md">
|
||||||
|
🟢 Live Now
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{isAttempted && (
|
||||||
|
<span className="text-[10px] font-bold uppercase text-green-500 bg-green-500/10 border border-green-500/20 px-2 py-0.5 rounded-md">
|
||||||
|
Attempted
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<h3 className="font-bold text-lg leading-tight group-hover:text-[var(--cyan-ai)] transition-colors">
|
<h3 className="font-bold text-lg leading-tight group-hover:text-[var(--cyan-ai)] transition-colors">
|
||||||
{cleanTitle(dpp.title)}
|
{cleanTitle(dpp.title)}
|
||||||
@@ -211,10 +241,25 @@ export default function DPPPage() {
|
|||||||
</span>
|
</span>
|
||||||
</h3>
|
</h3>
|
||||||
<div className="text-sm text-muted-foreground mt-2 font-medium">
|
<div className="text-sm text-muted-foreground mt-2 font-medium">
|
||||||
{count} Questions • {dpp.total_time} mins
|
<span>{count} Questions • {dpp.total_time} mins</span>
|
||||||
{dpp.scheduled_date && (
|
{isUpcoming && scheduledTime && (
|
||||||
<span className="ml-2 border-l border-border pl-2">
|
<span className="ml-2 border-l border-border pl-2 text-amber-500 font-semibold">
|
||||||
{isUpcoming ? `Available on ${formatDate(dpp.scheduled_date)}` : isExpired ? `Expired on ${formatDate(dpp.end_date!)}` : 'Available Now'}
|
Opens on {formatDateTime(dpp.scheduled_date)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{isExpired && endTime && (
|
||||||
|
<span className="ml-2 border-l border-border pl-2 text-destructive font-semibold">
|
||||||
|
Expired on {formatDateTime(dpp.end_date)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{isLive && endTime && (
|
||||||
|
<span className="ml-2 border-l border-border pl-2 text-emerald-500 font-semibold">
|
||||||
|
Closes on {formatDateTime(dpp.end_date)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{isLive && !endTime && scheduledTime && (
|
||||||
|
<span className="ml-2 border-l border-border pl-2 text-emerald-500 font-semibold">
|
||||||
|
Available since {formatDateTime(dpp.scheduled_date)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -222,9 +267,13 @@ export default function DPPPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col sm:flex-row gap-2 shrink-0 md:self-center">
|
<div className="flex flex-col sm:flex-row gap-2 shrink-0 md:self-center">
|
||||||
{isUpcoming ? (
|
{isUpcoming ? (
|
||||||
<Button disabled variant="outline" size="sm" className="font-bold">Starts Soon</Button>
|
<Button disabled variant="outline" size="sm" className="font-bold text-amber-500 border-amber-500/30 bg-amber-500/5">
|
||||||
|
Opens at {scheduledTime ? formatTime(dpp.scheduled_date) : "Scheduled Time"}
|
||||||
|
</Button>
|
||||||
) : isExpired && !isAttempted ? (
|
) : isExpired && !isAttempted ? (
|
||||||
<Button disabled variant="outline" size="sm" className="font-bold bg-secondary/50">Expired</Button>
|
<Button disabled variant="outline" size="sm" className="font-bold bg-secondary/50 text-muted-foreground">
|
||||||
|
Expired
|
||||||
|
</Button>
|
||||||
) : isAttempted ? (
|
) : isAttempted ? (
|
||||||
<>
|
<>
|
||||||
<Link href={`/cbt/${dpp.id}/result`}>
|
<Link href={`/cbt/${dpp.id}/result`}>
|
||||||
@@ -256,7 +305,8 @@ export default function DPPPage() {
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)})}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── PROTECTED PDF READER MODAL (FREE TIER) ── */}
|
{/* ── PROTECTED PDF READER MODAL (FREE TIER) ── */}
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export default function CollegePredictorPage() {
|
|||||||
const trendData = isDemo ? DEMO_TREND_DATA : ZERO_TREND_DATA
|
const trendData = isDemo ? DEMO_TREND_DATA : ZERO_TREND_DATA
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 md:p-8 space-y-8 animate-in fade-in zoom-in-95 origin-top duration-500 max-w-7xl mx-auto min-h-screen">
|
<div className="p-6 md:p-8 space-y-8 max-w-7xl mx-auto min-h-screen">
|
||||||
{/* Page Header */}
|
{/* Page Header */}
|
||||||
<div className="border-b border-border/50 pb-6">
|
<div className="border-b border-border/50 pb-6">
|
||||||
<div className="flex items-start justify-between gap-4 flex-wrap">
|
<div className="flex items-start justify-between gap-4 flex-wrap">
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export default function HeatmapPage() {
|
|||||||
const criticalTopics = isDemo ? DEMO_CRITICAL_TOPICS : []
|
const criticalTopics = isDemo ? DEMO_CRITICAL_TOPICS : []
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 md:p-8 max-w-7xl mx-auto space-y-8 min-h-screen animate-in fade-in zoom-in-95 origin-top duration-500">
|
<div className="p-6 md:p-8 max-w-7xl mx-auto space-y-8 min-h-screen">
|
||||||
{/* Page Header */}
|
{/* Page Header */}
|
||||||
<div className="border-b border-border/50 pb-6">
|
<div className="border-b border-border/50 pb-6">
|
||||||
<div className="flex items-start justify-between gap-4 flex-wrap">
|
<div className="flex items-start justify-between gap-4 flex-wrap">
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export default function AICoachDashboard() {
|
|||||||
const currentStreakDays = `${user?.streak_count || (isDemo ? 12 : 1)} d`
|
const currentStreakDays = `${user?.streak_count || (isDemo ? 12 : 1)} d`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 max-w-6xl mx-auto space-y-8 animate-in fade-in zoom-in-95 origin-top duration-500">
|
<div className="p-6 max-w-6xl mx-auto space-y-8">
|
||||||
|
|
||||||
{/* ── METRICS BAR ── */}
|
{/* ── METRICS BAR ── */}
|
||||||
<div className="flex flex-wrap items-center justify-end gap-2 pt-2">
|
<div className="flex flex-wrap items-center justify-end gap-2 pt-2">
|
||||||
@@ -108,7 +108,7 @@ export default function AICoachDashboard() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── DAILY CHALLENGE BANNER ── */}
|
{/* ── DAILY CHALLENGE BANNER ── */}
|
||||||
<DailyChallengeBanner />
|
<div className=""><DailyChallengeBanner /></div>
|
||||||
|
|
||||||
{/* ── QUICK STATS ── */}
|
{/* ── QUICK STATS ── */}
|
||||||
<section className="grid grid-cols-2 md:grid-cols-5 gap-4">
|
<section className="grid grid-cols-2 md:grid-cols-5 gap-4">
|
||||||
@@ -119,7 +119,7 @@ export default function AICoachDashboard() {
|
|||||||
{ label: "Mistakes Mastered", value: mistakesMastered, color: "text-green-400", bg: "bg-green-400/5", border: "border-green-400/20", icon: <CheckCircle2 size={32} /> },
|
{ label: "Mistakes Mastered", value: mistakesMastered, color: "text-green-400", bg: "bg-green-400/5", border: "border-green-400/20", icon: <CheckCircle2 size={32} /> },
|
||||||
{ label: "AI Sessions", value: aiSessions, color: "text-purple-400", bg: "bg-purple-400/5", border: "border-purple-400/20", icon: <Bot size={32} /> },
|
{ label: "AI Sessions", value: aiSessions, color: "text-purple-400", bg: "bg-purple-400/5", border: "border-purple-400/20", icon: <Bot size={32} /> },
|
||||||
].map((stat) => (
|
].map((stat) => (
|
||||||
<Card key={stat.label} className={`border ${stat.border} ${stat.bg} backdrop-blur-sm transition-all duration-300 transform hover:-translate-y-1 rounded-2xl`}>
|
<Card key={stat.label} className={`border ${stat.border} ${stat.bg} backdrop-blur-sm rounded-2xl`}>
|
||||||
<CardContent className="p-6 flex flex-col items-center justify-center text-center">
|
<CardContent className="p-6 flex flex-col items-center justify-center text-center">
|
||||||
<div className="text-3xl mb-2 filter drop-shadow-md">{stat.icon}</div>
|
<div className="text-3xl mb-2 filter drop-shadow-md">{stat.icon}</div>
|
||||||
<div className={`text-4xl font-black ${stat.color} leading-none tracking-tight mb-2 filter drop-shadow-sm`}>{stat.value}</div>
|
<div className={`text-4xl font-black ${stat.color} leading-none tracking-tight mb-2 filter drop-shadow-sm`}>{stat.value}</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { ReadinessGauge } from "@/components/AI/ReadinessGauge"
|
import { ReadinessGauge } from "@/components/AI/ReadinessGauge"
|
||||||
import { Card, CardContent } from "@/components/ui/card"
|
import { Card, CardContent } from "@/components/ui/card"
|
||||||
@@ -32,7 +32,7 @@ export default function ReadinessPage() {
|
|||||||
const maxScore = Math.max(1, ...scoreHistory.map(d => d.score))
|
const maxScore = Math.max(1, ...scoreHistory.map(d => d.score))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 md:p-8 space-y-8 animate-in fade-in zoom-in-95 origin-top duration-500 max-w-7xl mx-auto min-h-screen">
|
<div className="p-6 md:p-8 space-y-8 max-w-7xl mx-auto min-h-screen">
|
||||||
{/* Page Header */}
|
{/* Page Header */}
|
||||||
<div className="border-b border-border/50 pb-6">
|
<div className="border-b border-border/50 pb-6">
|
||||||
<div className="flex items-start justify-between gap-4 flex-wrap">
|
<div className="flex items-start justify-between gap-4 flex-wrap">
|
||||||
@@ -58,7 +58,7 @@ export default function ReadinessPage() {
|
|||||||
{[
|
{[
|
||||||
{ label: "Overall Score", value: isDemo ? "72 / 100" : "0 / 100", trend: isDemo ? "+5 this week" : "Baseline", trendUp: true, icon: <Gauge size={20} className="text-[var(--cyan-ai)]" /> },
|
{ label: "Overall Score", value: isDemo ? "72 / 100" : "0 / 100", trend: isDemo ? "+5 this week" : "Baseline", trendUp: true, icon: <Gauge size={20} className="text-[var(--cyan-ai)]" /> },
|
||||||
{ label: "Accuracy Rate", value: isDemo ? "88%" : "0%", trend: isDemo ? "+3% vs last week" : "0 attempts", trendUp: isDemo, icon: <Target size={20} className="text-amber-400" /> },
|
{ label: "Accuracy Rate", value: isDemo ? "88%" : "0%", trend: isDemo ? "+3% vs last week" : "0 attempts", trendUp: isDemo, icon: <Target size={20} className="text-amber-400" /> },
|
||||||
{ label: "Avg Speed / Q", value: isDemo ? "45 sec" : "-- sec", trend: isDemo ? "↓ Needs work" : "Not tested", trendUp: false, icon: <Clock size={20} className="text-red-400" /> },
|
{ label: "Avg Speed / Q", value: isDemo ? "45 sec" : "-- sec", trend: isDemo ? "? Needs work" : "Not tested", trendUp: false, icon: <Clock size={20} className="text-red-400" /> },
|
||||||
{ label: "Syllabus Covered", value: isDemo ? "64%" : "0%", trend: isDemo ? "+8% this month" : "Day 1", trendUp: true, icon: <BookOpen size={20} className="text-emerald-400" /> },
|
{ label: "Syllabus Covered", value: isDemo ? "64%" : "0%", trend: isDemo ? "+8% this month" : "Day 1", trendUp: true, icon: <BookOpen size={20} className="text-emerald-400" /> },
|
||||||
].map((kpi) => (
|
].map((kpi) => (
|
||||||
<Card key={kpi.label} className="border-border/60 bg-secondary/10 rounded-2xl hover:border-[var(--cyan-ai-30)] transition-colors">
|
<Card key={kpi.label} className="border-border/60 bg-secondary/10 rounded-2xl hover:border-[var(--cyan-ai-30)] transition-colors">
|
||||||
@@ -108,7 +108,7 @@ export default function ReadinessPage() {
|
|||||||
<span className={`text-xs font-bold ${sub.statusColor}`}>{sub.score}% · {sub.status}</span>
|
<span className={`text-xs font-bold ${sub.statusColor}`}>{sub.score}% · {sub.status}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-2.5 w-full bg-secondary rounded-full overflow-hidden">
|
<div className="h-2.5 w-full bg-secondary rounded-full overflow-hidden">
|
||||||
<div className={`h-full ${sub.color} rounded-full transition-all duration-700`} style={{ width: `${sub.score}%` }} />
|
<div className={`h-full ${sub.color} rounded-full`} style={{ width: `${sub.score}%` }} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -126,7 +126,7 @@ export default function ReadinessPage() {
|
|||||||
<div key={d.week} className="flex flex-col items-center gap-1 flex-1">
|
<div key={d.week} className="flex flex-col items-center gap-1 flex-1">
|
||||||
<span className="text-xs font-black text-muted-foreground">{d.score}</span>
|
<span className="text-xs font-black text-muted-foreground">{d.score}</span>
|
||||||
<div
|
<div
|
||||||
className={`w-full rounded-t-md transition-all duration-700 ${
|
className={`w-full rounded-t-md ${
|
||||||
!isDemo
|
!isDemo
|
||||||
? "bg-white/5 border border-white/10"
|
? "bg-white/5 border border-white/10"
|
||||||
: i === scoreHistory.length - 1
|
: i === scoreHistory.length - 1
|
||||||
@@ -182,7 +182,7 @@ export default function ReadinessPage() {
|
|||||||
<Link key={a.href} href={a.href}
|
<Link key={a.href} href={a.href}
|
||||||
className="flex items-center justify-between p-2.5 rounded-xl border border-border/40 hover:border-[var(--cyan-ai-30)] hover:bg-white/5 transition-all group text-sm font-bold">
|
className="flex items-center justify-between p-2.5 rounded-xl border border-border/40 hover:border-[var(--cyan-ai-30)] hover:bg-white/5 transition-all group text-sm font-bold">
|
||||||
<span className={a.color}>{a.label}</span>
|
<span className={a.color}>{a.label}</span>
|
||||||
<span className="text-muted-foreground group-hover:translate-x-1 transition-transform">→</span>
|
<span className="text-muted-foreground group-hover:translate-x-1 transition-transform">?</span>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { ExamStrategyCard } from "@/components/AI/ExamStrategyCard"
|
import { ExamStrategyCard } from "@/components/AI/ExamStrategyCard"
|
||||||
import { StudyTimeOptimizationCard } from "@/components/AI/StudyTimeOptimizationCard"
|
import { StudyTimeOptimizationCard } from "@/components/AI/StudyTimeOptimizationCard"
|
||||||
@@ -24,7 +24,7 @@ export default function StrategyPage() {
|
|||||||
const negMarkingData = isDemo ? DEMO_NEG_MARKING_DATA : ZERO_NEG_MARKING_DATA
|
const negMarkingData = isDemo ? DEMO_NEG_MARKING_DATA : ZERO_NEG_MARKING_DATA
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 md:p-8 space-y-8 animate-in fade-in zoom-in-95 origin-top duration-500 max-w-7xl mx-auto min-h-screen">
|
<div className="p-6 md:p-8 space-y-8 max-w-7xl mx-auto min-h-screen">
|
||||||
{/* Page Header */}
|
{/* Page Header */}
|
||||||
<div className="border-b border-border/50 pb-6">
|
<div className="border-b border-border/50 pb-6">
|
||||||
<div className="flex items-start justify-between gap-4 flex-wrap">
|
<div className="flex items-start justify-between gap-4 flex-wrap">
|
||||||
@@ -49,7 +49,7 @@ export default function StrategyPage() {
|
|||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
{[
|
{[
|
||||||
{ label: "Time Per Question", value: isDemo ? "45 sec" : "-- sec", note: isDemo ? "Target: 38 sec" : "Not calibrated", icon: <Clock size={18} className="text-red-400" />, color: "border-red-400/20 bg-red-400/5" },
|
{ label: "Time Per Question", value: isDemo ? "45 sec" : "-- sec", note: isDemo ? "Target: 38 sec" : "Not calibrated", icon: <Clock size={18} className="text-red-400" />, color: "border-red-400/20 bg-red-400/5" },
|
||||||
{ label: "Negative Marks Lost", value: isDemo ? "−29" : "0", note: isDemo ? "Last mock test" : "0 negative marks", icon: <AlertTriangle size={18} className="text-amber-400" />, color: "border-amber-400/20 bg-amber-400/5" },
|
{ label: "Negative Marks Lost", value: isDemo ? "-29" : "0", note: isDemo ? "Last mock test" : "0 negative marks", icon: <AlertTriangle size={18} className="text-amber-400" />, color: "border-amber-400/20 bg-amber-400/5" },
|
||||||
{ label: "Attempted Questions", value: isDemo ? "93 / 120" : "0 / 120", note: isDemo ? "+7 vs last test" : "0 attempted", icon: <Target size={18} className="text-[var(--cyan-ai)]" />, color: "border-[var(--cyan-ai-20)] bg-[var(--cyan-ai-10)]" },
|
{ label: "Attempted Questions", value: isDemo ? "93 / 120" : "0 / 120", note: isDemo ? "+7 vs last test" : "0 attempted", icon: <Target size={18} className="text-[var(--cyan-ai)]" />, color: "border-[var(--cyan-ai-20)] bg-[var(--cyan-ai-10)]" },
|
||||||
{ label: "Score Yield", value: isDemo ? "214" : "0", note: isDemo ? "Predicted score" : "Baseline", icon: <TrendingUp size={18} className="text-green-400" />, color: "border-green-400/20 bg-green-400/5" },
|
{ label: "Score Yield", value: isDemo ? "214" : "0", note: isDemo ? "Predicted score" : "Baseline", icon: <TrendingUp size={18} className="text-green-400" />, color: "border-green-400/20 bg-green-400/5" },
|
||||||
].map((kpi) => (
|
].map((kpi) => (
|
||||||
@@ -101,7 +101,7 @@ export default function StrategyPage() {
|
|||||||
<div className="text-[11px] text-muted-foreground font-semibold">Attempted</div>
|
<div className="text-[11px] text-muted-foreground font-semibold">Attempted</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-sm font-black text-red-400">{row.negMarks === 0 ? "0" : `−${row.negMarks}`}</div>
|
<div className="text-sm font-black text-red-400">{row.negMarks === 0 ? "0" : `-${row.negMarks}`}</div>
|
||||||
<div className="text-[11px] text-muted-foreground font-semibold">Neg. Marks</div>
|
<div className="text-[11px] text-muted-foreground font-semibold">Neg. Marks</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { StudyPlanCard } from "@/components/AI/StudyPlanCard"
|
import { StudyPlanCard } from "@/components/AI/StudyPlanCard"
|
||||||
import { WeeklyProgressReport } from "@/components/AI/WeeklyProgressReport"
|
import { WeeklyProgressReport } from "@/components/AI/WeeklyProgressReport"
|
||||||
@@ -12,7 +12,7 @@ export default function StudyPlanPage() {
|
|||||||
const streakCount = user?.streak_count || (isDemo ? 12 : 1)
|
const streakCount = user?.streak_count || (isDemo ? 12 : 1)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 md:p-8 space-y-8 animate-in fade-in zoom-in-95 origin-top duration-500 max-w-7xl mx-auto min-h-screen">
|
<div className="p-6 md:p-8 space-y-8 max-w-7xl mx-auto min-h-screen">
|
||||||
{/* Page Header */}
|
{/* Page Header */}
|
||||||
<div className="border-b border-border/50 pb-6">
|
<div className="border-b border-border/50 pb-6">
|
||||||
<div className="flex items-start justify-between gap-4 flex-wrap">
|
<div className="flex items-start justify-between gap-4 flex-wrap">
|
||||||
@@ -99,7 +99,7 @@ export default function StudyPlanPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="h-2.5 w-full bg-secondary rounded-full overflow-hidden">
|
<div className="h-2.5 w-full bg-secondary rounded-full overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className={`h-full rounded-full transition-all duration-700 ${sub.covered >= 70 ? "bg-green-400" : sub.covered >= 55 ? "bg-amber-400" : "bg-red-400"}`}
|
className={`h-full rounded-full ${sub.covered >= 70 ? "bg-green-400" : sub.covered >= 55 ? "bg-amber-400" : "bg-red-400"}`}
|
||||||
style={{ width: `${sub.covered}%` }}
|
style={{ width: `${sub.covered}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export default function TimeOptimizationPage() {
|
|||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 md:p-8 max-w-7xl mx-auto space-y-8 min-h-screen animate-in fade-in zoom-in-95 origin-top duration-500">
|
<div className="p-6 md:p-8 max-w-7xl mx-auto space-y-8 min-h-screen">
|
||||||
{/* Page Header */}
|
{/* Page Header */}
|
||||||
<div className="border-b border-border/50 pb-6">
|
<div className="border-b border-border/50 pb-6">
|
||||||
<div className="flex items-start justify-between gap-4 flex-wrap">
|
<div className="flex items-start justify-between gap-4 flex-wrap">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { TutorChat } from "@/components/AI/TutorChat"
|
import { TutorChat } from "@/components/AI/TutorChat"
|
||||||
import { BrainCircuit, Sparkles } from "lucide-react"
|
import { BrainCircuit, Sparkles } from "lucide-react"
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export default function WeeklyProgressPage() {
|
|||||||
const syllabusProgress = isDemo ? DEMO_SYLLABUS_PROGRESS : ZERO_SYLLABUS_PROGRESS
|
const syllabusProgress = isDemo ? DEMO_SYLLABUS_PROGRESS : ZERO_SYLLABUS_PROGRESS
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 md:p-8 max-w-7xl mx-auto space-y-8 min-h-screen animate-in fade-in zoom-in-95 origin-top duration-500">
|
<div className="p-6 md:p-8 max-w-7xl mx-auto space-y-8 min-h-screen">
|
||||||
{/* Page Header */}
|
{/* Page Header */}
|
||||||
<div className="border-b border-border/50 pb-6">
|
<div className="border-b border-border/50 pb-6">
|
||||||
<div className="flex items-start justify-between gap-4 flex-wrap">
|
<div className="flex items-start justify-between gap-4 flex-wrap">
|
||||||
@@ -108,7 +108,7 @@ export default function WeeklyProgressPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="h-2 w-full bg-secondary rounded-full overflow-hidden">
|
<div className="h-2 w-full bg-secondary rounded-full overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className={`h-full ${item.color} rounded-full transition-all duration-500`}
|
className={`h-full ${item.color} rounded-full`}
|
||||||
style={{ width: `${item.percentage}%` }}
|
style={{ width: `${item.percentage}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export default function AnalyticsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PremiumGate featureKey="detailed-performance-analytics">
|
<PremiumGate featureKey="detailed-performance-analytics">
|
||||||
<div className="p-8 space-y-8 animate-in fade-in zoom-in-95 origin-top duration-500 max-w-7xl mx-auto">
|
<div className="p-8 space-y-8 max-w-7xl mx-auto">
|
||||||
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4">
|
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl md:text-4xl font-black tracking-tight flex items-center gap-3 mb-1">
|
<h1 className="text-3xl md:text-4xl font-black tracking-tight flex items-center gap-3 mb-1">
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export default function AttemptHistoryPage({ params }: { params: Promise<{ attem
|
|||||||
const maxScore = attempt.total_questions * 4;
|
const maxScore = attempt.total_questions * 4;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-6xl mx-auto p-8 animate-in fade-in zoom-in-95 origin-top duration-500">
|
<div className="max-w-6xl mx-auto p-8">
|
||||||
<Link href={backRoute} className="inline-flex items-center text-sm text-muted-foreground hover:text-foreground mb-6 transition-colors">
|
<Link href={backRoute} className="inline-flex items-center text-sm text-muted-foreground hover:text-foreground mb-6 transition-colors">
|
||||||
<ArrowLeft size={16} className="mr-2" /> {test?.type === 'DPS' ? 'Back to PrepPilot AI' : 'Back to Tests'}
|
<ArrowLeft size={16} className="mr-2" /> {test?.type === 'DPS' ? 'Back to PrepPilot AI' : 'Back to Tests'}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ import { Activity, Clock, PlayCircle, Search, Lock, CheckSquare } from "lucide-r
|
|||||||
import { useActiveUser } from "@/hooks/useActiveUser";
|
import { useActiveUser } from "@/hooks/useActiveUser";
|
||||||
import { PremiumGate } from "@/components/PremiumGate";
|
import { PremiumGate } from "@/components/PremiumGate";
|
||||||
|
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { CalendarFilter, getLocalDateString } from "@/components/CalendarFilter";
|
||||||
|
import { formatISTDateTime, formatISTDate, formatISTTime, parseAsISTDate } from "@/lib/dateUtils";
|
||||||
|
|
||||||
interface TestData {
|
interface TestData {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -91,22 +95,29 @@ export default function CBTPage() {
|
|||||||
loadTests();
|
loadTests();
|
||||||
}, [userId, selectedPattern]);
|
}, [userId, selectedPattern]);
|
||||||
|
|
||||||
|
const scheduledDates = useMemo(() => {
|
||||||
|
return exams
|
||||||
|
.filter((e) => !!e.scheduled_date)
|
||||||
|
.map((e) => getLocalDateString(e.scheduled_date))
|
||||||
|
.filter(Boolean)
|
||||||
|
}, [exams]);
|
||||||
|
|
||||||
const filteredExams = exams.filter(exam => {
|
const filteredExams = exams.filter(exam => {
|
||||||
const matchesSearch = exam.title.toLowerCase().includes(search.toLowerCase());
|
const matchesSearch = exam.title.toLowerCase().includes(search.toLowerCase());
|
||||||
if (!matchesSearch) return false;
|
if (!matchesSearch) return false;
|
||||||
|
|
||||||
if (dateFilter) {
|
if (dateFilter) {
|
||||||
if (!exam.scheduled_date) return false;
|
if (!exam.scheduled_date) return false;
|
||||||
const scheduledDate = new Date(exam.scheduled_date).toISOString().split('T')[0];
|
const scheduledDate = getLocalDateString(exam.scheduled_date);
|
||||||
if (scheduledDate !== dateFilter) return false;
|
if (scheduledDate !== dateFilter) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
const formatDate = (dateStr: string) => {
|
const formatDateTime = (dateStr: string) => formatISTDateTime(dateStr);
|
||||||
return new Date(dateStr).toLocaleDateString("en-US", { year: 'numeric', month: 'short', day: 'numeric' });
|
const formatDate = (dateStr: string) => formatISTDate(dateStr);
|
||||||
}
|
const formatTime = (dateStr: string) => formatISTTime(dateStr);
|
||||||
|
|
||||||
// Get the highest score for each test if attempted multiple times
|
// Get the highest score for each test if attempted multiple times
|
||||||
const attemptsByTest = attempts.reduce((acc, curr) => {
|
const attemptsByTest = attempts.reduce((acc, curr) => {
|
||||||
@@ -130,7 +141,7 @@ export default function CBTPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="p-8 space-y-8 animate-in fade-in zoom-in-95 origin-top duration-500 max-w-7xl mx-auto">
|
<div className="p-8 space-y-8 max-w-7xl mx-auto">
|
||||||
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4">
|
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl md:text-4xl font-black tracking-tight flex items-center gap-3 mb-1">
|
<h1 className="text-3xl md:text-4xl font-black tracking-tight flex items-center gap-3 mb-1">
|
||||||
@@ -143,15 +154,13 @@ export default function CBTPage() {
|
|||||||
Real exam environment simulations to conquer {selectedPattern === "NEET" ? "NEET" : (targetExam || "JEE")}.
|
Real exam environment simulations to conquer {selectedPattern === "NEET" ? "NEET" : (targetExam || "JEE")}.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col sm:flex-row gap-3">
|
<div className="flex flex-col sm:flex-row items-center gap-3">
|
||||||
<div className="relative w-full sm:w-48">
|
<CalendarFilter
|
||||||
<input
|
selectedDate={dateFilter}
|
||||||
type="date"
|
onSelectDate={setDateFilter}
|
||||||
value={dateFilter}
|
availableDates={scheduledDates}
|
||||||
onChange={(e) => setDateFilter(e.target.value)}
|
placeholder="Filter by Exam Date"
|
||||||
className="w-full px-4 py-2 bg-background border border-border rounded-xl text-sm focus:ring-2 focus:ring-[var(--cyan-ai)] outline-none text-foreground dark:[color-scheme:dark] [color-scheme:light]"
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="relative w-full sm:w-64">
|
<div className="relative w-full sm:w-64">
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" size={16} />
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" size={16} />
|
||||||
<input
|
<input
|
||||||
@@ -203,8 +212,8 @@ export default function CBTPage() {
|
|||||||
|
|
||||||
// Date logic
|
// Date logic
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const scheduledTime = exam.scheduled_date ? new Date(exam.scheduled_date) : now;
|
const scheduledTime = parseAsISTDate(exam.scheduled_date) || now;
|
||||||
const endTime = exam.end_date ? new Date(exam.end_date) : null;
|
const endTime = parseAsISTDate(exam.end_date);
|
||||||
const isUpcoming = scheduledTime > now;
|
const isUpcoming = scheduledTime > now;
|
||||||
const isExpired = endTime ? now > endTime : false;
|
const isExpired = endTime ? now > endTime : false;
|
||||||
|
|
||||||
@@ -241,14 +250,32 @@ export default function CBTPage() {
|
|||||||
</h3>
|
</h3>
|
||||||
<div className="text-sm text-muted-foreground space-y-1 mb-6">
|
<div className="text-sm text-muted-foreground space-y-1 mb-6">
|
||||||
<div className="font-medium text-foreground">
|
<div className="font-medium text-foreground">
|
||||||
{isUpcoming ? `Available on ${formatDate(exam.scheduled_date!)}` : isExpired ? `Expired on ${formatDate(exam.end_date!)}` : 'Available Now'}
|
{isUpcoming && exam.scheduled_date ? (
|
||||||
|
<span className="text-amber-500 font-semibold">
|
||||||
|
Opens: {formatDateTime(exam.scheduled_date)}
|
||||||
|
</span>
|
||||||
|
) : isExpired && exam.end_date ? (
|
||||||
|
<span className="text-destructive font-semibold">
|
||||||
|
Expired on {formatDateTime(exam.end_date)}
|
||||||
|
</span>
|
||||||
|
) : exam.end_date ? (
|
||||||
|
<span className="text-emerald-500 font-semibold">
|
||||||
|
Live Now • Closes {formatDateTime(exam.end_date)}
|
||||||
|
</span>
|
||||||
|
) : exam.scheduled_date ? (
|
||||||
|
<span className="text-emerald-500 font-semibold">
|
||||||
|
Available since {formatDateTime(exam.scheduled_date)}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-emerald-500 font-semibold">Available Now</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>{exam.total_time} Min • {exam.difficulty} Difficulty</div>
|
<div>{exam.total_time} Min • {exam.difficulty} Difficulty</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isUpcoming ? (
|
{isUpcoming ? (
|
||||||
<Button disabled className="w-full gap-2 rounded-xl bg-secondary text-muted-foreground font-bold">
|
<Button disabled className="w-full gap-2 rounded-xl bg-amber-500/10 text-amber-500 border border-amber-500/30 font-bold">
|
||||||
<Clock size={16} /> Starts Soon
|
<Clock size={16} /> Opens at {exam.scheduled_date ? formatTime(exam.scheduled_date) : "Scheduled Time"}
|
||||||
</Button>
|
</Button>
|
||||||
) : isLocked ? (
|
) : isLocked ? (
|
||||||
<Button
|
<Button
|
||||||
@@ -338,7 +365,7 @@ export default function CBTPage() {
|
|||||||
|
|
||||||
{showUpgradeModal && (
|
{showUpgradeModal && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
|
||||||
<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="bg-background border border-border shadow-2xl rounded-2xl w-[450px] p-8 text-center space-y-6">
|
||||||
<div className="size-16 rounded-full bg-red-500/10 flex items-center justify-center mx-auto text-red-500">
|
<div className="size-16 rounded-full bg-red-500/10 flex items-center justify-center mx-auto text-red-500">
|
||||||
<Lock size={32} />
|
<Lock size={32} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -84,39 +84,46 @@ export default function CommunityPage() {
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="h-[70vh] flex flex-col items-center justify-center p-8">
|
|
||||||
<div className="h-8 w-8 border-4 border-[var(--cyan-ai)] border-t-transparent rounded-full animate-spin mb-4"></div>
|
|
||||||
<p className="text-muted-foreground font-medium animate-pulse">Loading Doubt Forum...</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasAccessTo("doubt-resolution-community")) {
|
|
||||||
|
if (!hasAccessTo("doubt-resolution-community") && !isDemo) {
|
||||||
return (
|
return (
|
||||||
<div className="p-8 h-full flex flex-col items-center justify-center text-center max-w-2xl mx-auto">
|
<div className="p-8 space-y-8 max-w-7xl mx-auto">
|
||||||
<div className="size-20 rounded-full bg-red-500/10 flex items-center justify-center mx-auto text-red-500 mb-6">
|
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4">
|
||||||
<Lock size={40} />
|
<div>
|
||||||
|
<h1 className="text-3xl md:text-4xl font-black tracking-tight flex items-center gap-3 mb-1">
|
||||||
|
<span className="p-2.5 rounded-xl bg-yellow-50 dark:bg-[var(--cyan-ai-10)] border border-yellow-300/60 dark:border-[var(--cyan-ai-20)]">
|
||||||
|
<MessageSquare className="text-yellow-600 dark:text-[var(--cyan-ai)]" size={28} />
|
||||||
|
</span>
|
||||||
|
Doubt Forum
|
||||||
|
</h1>
|
||||||
|
<p className="text-muted-foreground text-base font-medium max-w-2xl pl-1">
|
||||||
|
Ask questions, share knowledge, and learn together with the community.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-12 text-center max-w-2xl mx-auto bg-secondary/20 border border-border rounded-3xl space-y-6">
|
||||||
|
<div className="size-20 rounded-full bg-red-500/10 flex items-center justify-center mx-auto text-red-500">
|
||||||
|
<Lock size={40} />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-bold">Premium Feature</h2>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
The Doubt Forum is a premium feature. Upgrade your plan to ask doubts and interact with tutors and the community.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
className="px-8 py-3 bg-[var(--cyan-ai)] text-black font-bold rounded-xl shadow-[0_0_20px_-5px_var(--cyan-ai)]"
|
||||||
|
onClick={() => window.location.href = '/upgrade'}
|
||||||
|
>
|
||||||
|
Upgrade Plan
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-3xl font-bold mb-4">Premium Feature</h2>
|
|
||||||
<p className="text-muted-foreground text-lg mb-8">
|
|
||||||
The Doubt Forum is a premium feature. Upgrade your plan to ask doubts and interact with tutors and the community.
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
className="px-8 py-3 bg-primary text-primary-foreground font-bold rounded-xl shadow-[0_0_20px_-5px_var(--color-primary)] text-lg"
|
|
||||||
onClick={() => window.location.href = '/upgrade'}
|
|
||||||
>
|
|
||||||
Upgrade Plan
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PremiumGate featureKey="doubt-resolution-community">
|
<div className="p-8 space-y-8 max-w-7xl mx-auto">
|
||||||
<div className="p-8 space-y-6 animate-in fade-in zoom-in-95 origin-top duration-500 max-w-7xl mx-auto">
|
<div className="flex flex-col md:flex-row justify-between items-start md:items-end gap-4">
|
||||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-end gap-4">
|
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl md:text-4xl font-black tracking-tight flex items-center gap-3 mb-1">
|
<h1 className="text-3xl md:text-4xl font-black tracking-tight flex items-center gap-3 mb-1">
|
||||||
<span className="p-2.5 rounded-xl bg-yellow-50 dark:bg-[var(--cyan-ai-10)] border border-yellow-300/60 dark:border-[var(--cyan-ai-20)]">
|
<span className="p-2.5 rounded-xl bg-yellow-50 dark:bg-[var(--cyan-ai-10)] border border-yellow-300/60 dark:border-[var(--cyan-ai-20)]">
|
||||||
@@ -159,7 +166,9 @@ export default function CommunityPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{filteredDoubts.map((doubt) => (
|
{loading ? (
|
||||||
|
<div className="text-center py-12 text-muted-foreground animate-pulse">Loading doubts...</div>
|
||||||
|
) : (<>{filteredDoubts.map((doubt) => (
|
||||||
<Card key={doubt.id} className="border-border shadow-sm hover:border-[var(--cyan-ai-10)] transition-colors cursor-pointer">
|
<Card key={doubt.id} className="border-border shadow-sm hover:border-[var(--cyan-ai-10)] transition-colors cursor-pointer">
|
||||||
<CardContent className="p-0 flex">
|
<CardContent className="p-0 flex">
|
||||||
{/* Upvote Sidebar */}
|
{/* Upvote Sidebar */}
|
||||||
@@ -259,11 +268,12 @@ export default function CommunityPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</>)}
|
||||||
</div>
|
</div>
|
||||||
{/* Ask Doubt Modal */}
|
{/* Ask Doubt Modal */}
|
||||||
{isAskModalOpen && (
|
{isAskModalOpen && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-background/80 backdrop-blur-sm">
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-background/80 backdrop-blur-sm">
|
||||||
<div className="bg-background border border-border w-full max-w-2xl rounded-2xl shadow-2xl overflow-hidden animate-in zoom-in-95 duration-200">
|
<div className="bg-background border border-border w-full max-w-2xl rounded-2xl shadow-2xl overflow-hidden">
|
||||||
<div className="flex justify-between items-center p-6 border-b border-border bg-secondary/30">
|
<div className="flex justify-between items-center p-6 border-b border-border bg-secondary/30">
|
||||||
<h2 className="text-2xl font-bold flex items-center gap-2">
|
<h2 className="text-2xl font-bold flex items-center gap-2">
|
||||||
<MessageSquare className="text-[var(--cyan-ai)]" /> Ask a Doubt
|
<MessageSquare className="text-[var(--cyan-ai)]" /> Ask a Doubt
|
||||||
@@ -331,7 +341,7 @@ export default function CommunityPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</PremiumGate>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ import { Button } from "@/components/ui/button"
|
|||||||
import { Card, CardContent } from "@/components/ui/card"
|
import { Card, CardContent } from "@/components/ui/card"
|
||||||
import { BrainCircuit, Play, CheckCircle2, Download, Filter, Lock, FileText, Eye, ShieldAlert, X, Sparkles, Share2, Maximize2, Minimize2, Zap } from "lucide-react"
|
import { BrainCircuit, Play, CheckCircle2, Download, Filter, Lock, FileText, Eye, ShieldAlert, X, Sparkles, Share2, Maximize2, Minimize2, Zap } from "lucide-react"
|
||||||
|
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState, useMemo } from "react"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { useActiveUser } from "@/hooks/useActiveUser"
|
import { useActiveUser } from "@/hooks/useActiveUser"
|
||||||
|
import { CalendarFilter, getLocalDateString } from "@/components/CalendarFilter"
|
||||||
|
import { formatISTDateTime, formatISTTime, parseAsISTDate } from "@/lib/dateUtils"
|
||||||
|
|
||||||
export default function AIDailyPracticePage() {
|
export default function AIDailyPracticePage() {
|
||||||
const { user, userId, hasAccessTo } = useActiveUser()
|
const { user, userId, hasAccessTo } = useActiveUser()
|
||||||
@@ -81,22 +83,28 @@ export default function AIDailyPracticePage() {
|
|||||||
return acc;
|
return acc;
|
||||||
}, {} as Record<string, any>);
|
}, {} as Record<string, any>);
|
||||||
|
|
||||||
|
const scheduledDates = useMemo(() => {
|
||||||
|
return dpps
|
||||||
|
.filter((d) => !!d.pdf_url && !!d.scheduled_date)
|
||||||
|
.map((d) => getLocalDateString(d.scheduled_date))
|
||||||
|
.filter(Boolean)
|
||||||
|
}, [dpps])
|
||||||
|
|
||||||
const filteredDpps = dpps.filter(dpp => {
|
const filteredDpps = dpps.filter(dpp => {
|
||||||
// Only show PDF DPPs here
|
// Only show PDF DPPs here
|
||||||
if (!dpp.pdf_url) return false;
|
if (!dpp.pdf_url) return false;
|
||||||
|
|
||||||
if (dateFilter) {
|
if (dateFilter) {
|
||||||
if (!dpp.scheduled_date) return false;
|
if (!dpp.scheduled_date) return false;
|
||||||
const scheduledDate = new Date(dpp.scheduled_date).toISOString().split('T')[0];
|
const scheduledDate = getLocalDateString(dpp.scheduled_date);
|
||||||
if (scheduledDate !== dateFilter) return false;
|
if (scheduledDate !== dateFilter) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
const formatDate = (dateStr: string) => {
|
const formatDateTime = (dateStr: string) => formatISTDateTime(dateStr);
|
||||||
return new Date(dateStr).toLocaleDateString("en-US", { year: 'numeric', month: 'short', day: 'numeric' });
|
const formatTime = (dateStr: string) => formatISTTime(dateStr);
|
||||||
}
|
|
||||||
|
|
||||||
const getPdfUrl = (title: string) => {
|
const getPdfUrl = (title: string) => {
|
||||||
const t = title.toLowerCase();
|
const t = title.toLowerCase();
|
||||||
@@ -111,7 +119,7 @@ export default function AIDailyPracticePage() {
|
|||||||
const cleanTitle = (title: string) => title.replace(/\s*\(ExamEnum\.[^)]+\)/, '');
|
const cleanTitle = (title: string) => title.replace(/\s*\(ExamEnum\.[^)]+\)/, '');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 md:p-8 space-y-6 animate-in fade-in zoom-in-95 origin-top duration-500 max-w-7xl mx-auto">
|
<div className="p-6 md:p-8 space-y-6 max-w-7xl mx-auto">
|
||||||
{/* Page Header */}
|
{/* Page Header */}
|
||||||
<div className="border-b border-border/50 pb-6">
|
<div className="border-b border-border/50 pb-6">
|
||||||
<div className="flex items-start justify-between gap-4 flex-wrap">
|
<div className="flex items-start justify-between gap-4 flex-wrap">
|
||||||
@@ -122,33 +130,39 @@ export default function AIDailyPracticePage() {
|
|||||||
Daily Practice (DPP)
|
Daily Practice (DPP)
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-muted-foreground text-base font-medium max-w-2xl pl-1">
|
<p className="text-muted-foreground text-base font-medium max-w-2xl pl-1">
|
||||||
Launch Daily Practice Problems (DPPs) tailored to your exam pattern.
|
Launch Daily Practice Problems (DPPs) tailored to your exam pattern with time-bound calendar schedules.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row justify-between items-center gap-4 border-b border-border pb-6">
|
{/* Calendar Filter & Controller Bar */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 border-b border-border pb-6">
|
||||||
<input
|
<div className="flex items-center gap-3 flex-wrap">
|
||||||
type="date"
|
<CalendarFilter
|
||||||
value={dateFilter}
|
selectedDate={dateFilter}
|
||||||
onChange={(e) => setDateFilter(e.target.value)}
|
onSelectDate={setDateFilter}
|
||||||
className="px-3 py-1.5 bg-background border border-border rounded-lg text-xs font-bold focus:ring-2 focus:ring-[var(--cyan-ai)] outline-none text-muted-foreground mr-2"
|
availableDates={scheduledDates}
|
||||||
|
placeholder="Filter by Schedule Date"
|
||||||
/>
|
/>
|
||||||
|
{dateFilter && (
|
||||||
|
<span className="text-xs font-semibold text-muted-foreground bg-secondary/60 px-2.5 py-1 rounded-lg">
|
||||||
|
Showing DPPs for {dateFilter}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs font-bold text-muted-foreground">
|
||||||
|
{filteredDpps.length} {filteredDpps.length === 1 ? "Practice Sheet" : "Practice Sheets"} Available
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" className="gap-2 rounded-xl text-sm font-bold">
|
|
||||||
<Download size={16} /> Export as PDF
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="col-span-2 text-center py-12 text-muted-foreground">Loading AI-generated DPPs...</div>
|
<div className="col-span-2 text-center py-12 text-muted-foreground">Loading AI-generated DPPs...</div>
|
||||||
) : filteredDpps.length === 0 ? (
|
) : filteredDpps.length === 0 ? (
|
||||||
<div className="col-span-2 text-center py-12 text-muted-foreground border border-dashed rounded-xl">No DPPs available for the selected criteria.</div>
|
<div className="col-span-2 text-center py-12 text-muted-foreground border border-dashed rounded-xl">
|
||||||
|
No DPPs found for the selected schedule criteria.
|
||||||
|
</div>
|
||||||
) : filteredDpps.map((dpp, i) => {
|
) : filteredDpps.map((dpp, i) => {
|
||||||
// Calculate total questions dynamically from sections
|
// Calculate total questions dynamically from sections
|
||||||
let count = 0;
|
let count = 0;
|
||||||
@@ -159,22 +173,32 @@ export default function AIDailyPracticePage() {
|
|||||||
const attempt = attemptsByTest[dpp.id];
|
const attempt = attemptsByTest[dpp.id];
|
||||||
const isAttempted = !!attempt;
|
const isAttempted = !!attempt;
|
||||||
|
|
||||||
// Date logic
|
// Accurate Date & Time logic (IST aware)
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const scheduledTime = dpp.scheduled_date ? new Date(dpp.scheduled_date) : now;
|
const scheduledTime = parseAsISTDate(dpp.scheduled_date);
|
||||||
const endTime = dpp.end_date ? new Date(dpp.end_date) : null;
|
const endTime = parseAsISTDate(dpp.end_date);
|
||||||
const isUpcoming = scheduledTime > now;
|
const isUpcoming = scheduledTime ? scheduledTime > now : false;
|
||||||
const isExpired = endTime ? now > endTime : false;
|
const isExpired = endTime ? now > endTime : false;
|
||||||
|
const isLive = !isUpcoming && !isExpired;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card key={dpp.id} className={`group cursor-pointer transition-all hover:shadow-md ${isAttempted ? 'opacity-70 hover:opacity-100 bg-secondary/10 border-primary/20' : isExpired ? 'opacity-80' : 'hover:border-[var(--cyan-ai-10)] bg-background'}`}>
|
<Card key={dpp.id} className={`group cursor-pointer transition-all hover:shadow-md ${isAttempted ? 'opacity-85 hover:opacity-100 bg-secondary/10 border-primary/20' : isExpired ? 'opacity-70' : 'hover:border-[var(--cyan-ai-20)] bg-background'}`}>
|
||||||
<CardContent className="p-6 flex flex-col md:flex-row md:items-center justify-between gap-4">
|
<CardContent className="p-6 flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||||
<div className="flex gap-4 items-start w-full">
|
<div className="flex gap-4 items-start w-full">
|
||||||
<div className={`mt-1 flex-shrink-0 ${isAttempted ? 'text-green-500' : 'text-foreground'}`}>
|
{/* Clean Status Icon — No Selectable Circle Checkboxes */}
|
||||||
{isAttempted ? <CheckCircle2 size={24} /> : <div className="size-6 rounded-full border-2 border-muted-foreground flex items-center justify-center opacity-50"></div>}
|
<div className="mt-1 flex-shrink-0">
|
||||||
|
{isAttempted ? (
|
||||||
|
<div className="p-1.5 rounded-lg bg-emerald-500/10 text-emerald-500 border border-emerald-500/20" title="Completed">
|
||||||
|
<CheckCircle2 size={18} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="p-1.5 rounded-lg bg-secondary/60 text-[var(--cyan-ai)] border border-border" title="Practice Sheet">
|
||||||
|
<FileText size={18} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="flex items-center gap-2 mb-1 flex-wrap">
|
||||||
<span className="text-xs font-bold uppercase tracking-wider text-[var(--cyan-ai)]">
|
<span className="text-xs font-bold uppercase tracking-wider text-[var(--cyan-ai)]">
|
||||||
{user?.target_exam?.replace("_", " ") || "JEE/NEET"}
|
{user?.target_exam?.replace("_", " ") || "JEE/NEET"}
|
||||||
</span>
|
</span>
|
||||||
@@ -183,14 +207,30 @@ export default function AIDailyPracticePage() {
|
|||||||
{isUpcoming ? (
|
{isUpcoming ? (
|
||||||
<>
|
<>
|
||||||
<span className="text-muted-foreground text-xs font-bold">•</span>
|
<span className="text-muted-foreground text-xs font-bold">•</span>
|
||||||
<span className="text-[10px] font-bold uppercase text-yellow-500 bg-yellow-500/10 px-1.5 py-0.5 rounded">Upcoming</span>
|
<span className="text-[10px] font-bold uppercase text-amber-500 bg-amber-500/10 border border-amber-500/20 px-2 py-0.5 rounded-md flex items-center gap-1">
|
||||||
|
🔒 Upcoming
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
) : isExpired ? (
|
) : isExpired ? (
|
||||||
<>
|
<>
|
||||||
<span className="text-muted-foreground text-xs font-bold">•</span>
|
<span className="text-muted-foreground text-xs font-bold">•</span>
|
||||||
<span className="text-[10px] font-bold uppercase text-muted-foreground bg-secondary px-1.5 py-0.5 rounded">Expired</span>
|
<span className="text-[10px] font-bold uppercase text-muted-foreground bg-secondary px-2 py-0.5 rounded-md">
|
||||||
|
⏳ Expired
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="text-muted-foreground text-xs font-bold">•</span>
|
||||||
|
<span className="text-[10px] font-bold uppercase text-emerald-500 bg-emerald-500/10 border border-emerald-500/20 px-2 py-0.5 rounded-md">
|
||||||
|
🟢 Live Now
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{isAttempted && (
|
||||||
|
<span className="text-[10px] font-bold uppercase text-green-500 bg-green-500/10 border border-green-500/20 px-2 py-0.5 rounded-md">
|
||||||
|
Attempted
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<h3 className="font-bold text-lg leading-tight group-hover:text-[var(--cyan-ai)] transition-colors">
|
<h3 className="font-bold text-lg leading-tight group-hover:text-[var(--cyan-ai)] transition-colors">
|
||||||
{cleanTitle(dpp.title)}
|
{cleanTitle(dpp.title)}
|
||||||
@@ -201,10 +241,25 @@ export default function AIDailyPracticePage() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-muted-foreground mt-2 font-medium">
|
<div className="text-sm text-muted-foreground mt-2 font-medium">
|
||||||
{count} Questions • {dpp.total_time} mins
|
<span>{count} Questions • {dpp.total_time} mins</span>
|
||||||
{dpp.scheduled_date && (
|
{isUpcoming && scheduledTime && (
|
||||||
<span className="ml-2 border-l border-border pl-2">
|
<span className="ml-2 border-l border-border pl-2 text-amber-500 font-semibold">
|
||||||
{isUpcoming ? `Available on ${formatDate(dpp.scheduled_date)}` : isExpired ? `Expired on ${formatDate(dpp.end_date!)}` : 'Available Now'}
|
Opens on {formatDateTime(dpp.scheduled_date)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{isExpired && endTime && (
|
||||||
|
<span className="ml-2 border-l border-border pl-2 text-destructive font-semibold">
|
||||||
|
Expired on {formatDateTime(dpp.end_date)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{isLive && endTime && (
|
||||||
|
<span className="ml-2 border-l border-border pl-2 text-emerald-500 font-semibold">
|
||||||
|
Closes on {formatDateTime(dpp.end_date)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{isLive && !endTime && scheduledTime && (
|
||||||
|
<span className="ml-2 border-l border-border pl-2 text-emerald-500 font-semibold">
|
||||||
|
Available since {formatDateTime(dpp.scheduled_date)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -212,9 +267,13 @@ export default function AIDailyPracticePage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col sm:flex-row gap-2 shrink-0 md:self-center">
|
<div className="flex flex-col sm:flex-row gap-2 shrink-0 md:self-center">
|
||||||
{isUpcoming ? (
|
{isUpcoming ? (
|
||||||
<Button disabled variant="outline" size="sm" className="font-bold">Starts Soon</Button>
|
<Button disabled variant="outline" size="sm" className="font-bold text-amber-500 border-amber-500/30 bg-amber-500/5">
|
||||||
|
Opens at {scheduledTime ? formatTime(dpp.scheduled_date) : "Scheduled Time"}
|
||||||
|
</Button>
|
||||||
) : isExpired && !isAttempted ? (
|
) : isExpired && !isAttempted ? (
|
||||||
<Button disabled variant="outline" size="sm" className="font-bold bg-secondary/50">Expired</Button>
|
<Button disabled variant="outline" size="sm" className="font-bold bg-secondary/50 text-muted-foreground">
|
||||||
|
Expired
|
||||||
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
hasAccessTo('PRO') ? (
|
hasAccessTo('PRO') ? (
|
||||||
<Button onClick={() => setSelectedPdfDpp(dpp)} variant="secondary" size="sm" className="rounded-xl bg-[var(--cyan-ai)] text-black hover:bg-[var(--cyan-ai-80)] font-extrabold gap-2 text-xs shadow-[0_0_12px_rgba(12,96,96,0.2)]">
|
<Button onClick={() => setSelectedPdfDpp(dpp)} variant="secondary" size="sm" className="rounded-xl bg-[var(--cyan-ai)] text-black hover:bg-[var(--cyan-ai-80)] font-extrabold gap-2 text-xs shadow-[0_0_12px_rgba(12,96,96,0.2)]">
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { ThemeToggle } from "@/components/ThemeToggle"
|
|||||||
import { TierBadge } from "@/components/TierBadge"
|
import { TierBadge } from "@/components/TierBadge"
|
||||||
import { StreakCounter } from "@/components/StreakCounter"
|
import { StreakCounter } from "@/components/StreakCounter"
|
||||||
import { AITutorChat } from "@/components/AI/AITutorChat"
|
import { AITutorChat } from "@/components/AI/AITutorChat"
|
||||||
import { useActiveUser, DEMO_USERS, isDemoUser } from "@/hooks/useActiveUser"
|
import { useActiveUser, DEMO_USERS, isDemoUser, isDemoAccount } from "@/hooks/useActiveUser"
|
||||||
import { StreakOnboardingModal } from "@/components/Dashboard/StreakOnboardingModal"
|
import { StreakOnboardingModal } from "@/components/Dashboard/StreakOnboardingModal"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import MaintenanceScreen from "@/components/MaintenanceScreen"
|
import MaintenanceScreen from "@/components/MaintenanceScreen"
|
||||||
@@ -309,7 +309,7 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
|
|||||||
{/* Functional User Dropdown */}
|
{/* Functional User Dropdown */}
|
||||||
<div className="relative" ref={dropdownRef}>
|
<div className="relative" ref={dropdownRef}>
|
||||||
{dropdownOpen && user && (
|
{dropdownOpen && user && (
|
||||||
<div className="absolute bottom-full left-0 w-full mb-2 bg-background border border-border rounded-xl shadow-xl shadow-black/20 overflow-hidden animate-in fade-in slide-in-from-bottom-2 duration-200 z-50">
|
<div className="absolute bottom-full left-0 w-full mb-2 bg-background border border-border rounded-xl shadow-xl shadow-black/20 overflow-hidden z-50">
|
||||||
<div className="px-4 py-3 border-b border-border bg-secondary/30">
|
<div className="px-4 py-3 border-b border-border bg-secondary/30">
|
||||||
<p className="text-sm font-bold text-foreground truncate">{cleanDisplayName(user.name)}</p>
|
<p className="text-sm font-bold text-foreground truncate">{cleanDisplayName(user.name)}</p>
|
||||||
<p className="text-xs text-muted-foreground truncate">{user.email || user.phone}</p>
|
<p className="text-xs text-muted-foreground truncate">{user.email || user.phone}</p>
|
||||||
@@ -322,7 +322,7 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
|
|||||||
<Link href="/dashboard/settings/password" className="flex items-center gap-2 px-3 py-2 text-sm text-foreground hover:bg-secondary rounded-lg transition-colors">
|
<Link href="/dashboard/settings/password" className="flex items-center gap-2 px-3 py-2 text-sm text-foreground hover:bg-secondary rounded-lg transition-colors">
|
||||||
<Lock size={16} /> Change Password
|
<Lock size={16} /> Change Password
|
||||||
</Link>
|
</Link>
|
||||||
{isDemoUser(userId) && (
|
{isDemoAccount(userId) && (
|
||||||
<button
|
<button
|
||||||
onClick={() => switchUser(userId, true)}
|
onClick={() => switchUser(userId, true)}
|
||||||
className="w-full text-left flex items-center gap-2 px-3 py-2 text-sm text-foreground hover:bg-secondary rounded-lg transition-colors"
|
className="w-full text-left flex items-center gap-2 px-3 py-2 text-sm text-foreground hover:bg-secondary rounded-lg transition-colors"
|
||||||
@@ -391,7 +391,7 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Demo Switcher — only visible when logged in as a known demo account */}
|
{/* Demo Switcher — only visible when logged in as a known demo account */}
|
||||||
{isDemoUser(userId) && (
|
{isDemoAccount(userId) && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{switching && (
|
{switching && (
|
||||||
<span className="text-xs text-[var(--cyan-ai)] font-bold animate-pulse hidden lg:inline">Switching…</span>
|
<span className="text-xs text-[var(--cyan-ai)] font-bold animate-pulse hidden lg:inline">Switching…</span>
|
||||||
@@ -430,7 +430,7 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
{/* Scrollable page body */}
|
{/* Scrollable page body */}
|
||||||
<main className="flex-1 overflow-y-auto relative bg-background/50">
|
<main className="flex-1 overflow-y-auto relative bg-background/50">
|
||||||
<div key={pathname} className="w-full h-full">
|
<div className="w-full h-full">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export default function LeaderboardPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-8 space-y-8 animate-in fade-in zoom-in-95 origin-top duration-500 max-w-7xl mx-auto">
|
<div className="p-8 space-y-8 max-w-7xl mx-auto">
|
||||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-end gap-4">
|
<div className="flex flex-col md:flex-row justify-between items-start md:items-end gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl md:text-4xl font-black tracking-tight flex items-center gap-3 mb-1">
|
<h1 className="text-3xl md:text-4xl font-black tracking-tight flex items-center gap-3 mb-1">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { Card, CardContent } from "@/components/ui/card"
|
import { Card, CardContent } from "@/components/ui/card"
|
||||||
@@ -32,7 +32,7 @@ export default function MaterialsPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-8 space-y-8 animate-in fade-in zoom-in-95 origin-top duration-500 max-w-7xl mx-auto">
|
<div className="p-8 space-y-8 max-w-7xl mx-auto">
|
||||||
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4">
|
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl md:text-4xl font-black tracking-tight flex items-center gap-3 mb-1">
|
<h1 className="text-3xl md:text-4xl font-black tracking-tight flex items-center gap-3 mb-1">
|
||||||
|
|||||||
@@ -9,11 +9,34 @@ import { Button } from "@/components/ui/button"
|
|||||||
import { BrainCircuit, Play, ChevronRight, Activity, Target, CheckCircle2, AlertTriangle, TrendingUp, Calendar, Zap, LayoutDashboard, Share2, Trophy, Lock, ArrowRight, Clock, Download } from "lucide-react"
|
import { BrainCircuit, Play, ChevronRight, Activity, Target, CheckCircle2, AlertTriangle, TrendingUp, Calendar, Zap, LayoutDashboard, Share2, Trophy, Lock, ArrowRight, Clock, Download } from "lucide-react"
|
||||||
import { MistakeHeatmap } from "@/components/AI/MistakeHeatmap"
|
import { MistakeHeatmap } from "@/components/AI/MistakeHeatmap"
|
||||||
import { AdaptiveStudyPlan } from "@/components/AI/AdaptiveStudyPlan"
|
import { AdaptiveStudyPlan } from "@/components/AI/AdaptiveStudyPlan"
|
||||||
import { useActiveUser } from "@/hooks/useActiveUser"
|
import { useActiveUser, isDemoUser, DEFAULT_USER_ID } from "@/hooks/useActiveUser"
|
||||||
|
import { formatISTDate } from "@/lib/dateUtils"
|
||||||
|
|
||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
const { user, userId, loading: userLoading, hasAccessTo, trialInfo } = useActiveUser()
|
const { user, userId, loading: userLoading, hasAccessTo, trialInfo } = useActiveUser()
|
||||||
const [userStats, setUserStats] = useState<any>(null)
|
const [userStats, setUserStats] = useState<any>(() => {
|
||||||
|
const currentId = (typeof window !== "undefined" ? localStorage.getItem("activeUserId") : null) || DEFAULT_USER_ID;
|
||||||
|
if (isDemoUser(currentId)) {
|
||||||
|
return {
|
||||||
|
today_score: 42,
|
||||||
|
today_max_score: 60,
|
||||||
|
overall_accuracy: 82.0,
|
||||||
|
total_questions_attempted: 1245,
|
||||||
|
city_rank: 42,
|
||||||
|
city_total: 1200,
|
||||||
|
is_new_user: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
today_score: 0,
|
||||||
|
today_max_score: 0,
|
||||||
|
overall_accuracy: 0,
|
||||||
|
total_questions_attempted: 0,
|
||||||
|
city_rank: null,
|
||||||
|
city_total: null,
|
||||||
|
is_new_user: true,
|
||||||
|
};
|
||||||
|
})
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [dpps, setDpps] = useState<any[]>([])
|
const [dpps, setDpps] = useState<any[]>([])
|
||||||
const [cbts, setCbts] = useState<any[]>([])
|
const [cbts, setCbts] = useState<any[]>([])
|
||||||
@@ -22,8 +45,7 @@ export default function DashboardPage() {
|
|||||||
const progressRef = useRef<HTMLDivElement>(null)
|
const progressRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (userLoading) return
|
const currentUserId = user?.id || userId || (typeof window !== "undefined" ? localStorage.getItem("activeUserId") : null) || DEFAULT_USER_ID;
|
||||||
const currentUserId = user?.id || userId
|
|
||||||
if (!currentUserId) {
|
if (!currentUserId) {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
return
|
return
|
||||||
@@ -48,7 +70,7 @@ export default function DashboardPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fetchStatsAndTests()
|
fetchStatsAndTests()
|
||||||
}, [userLoading, user?.id, userId])
|
}, [user?.id, userId, user?.target_exam])
|
||||||
|
|
||||||
const getShareUrl = () => {
|
const getShareUrl = () => {
|
||||||
const score = isNew ? "0 / 0" : `${userStats?.today_score || 0} / ${userStats?.today_max_score || 60}`
|
const score = isNew ? "0 / 0" : `${userStats?.today_score || 0} / ${userStats?.today_max_score || 60}`
|
||||||
@@ -108,137 +130,132 @@ export default function DashboardPage() {
|
|||||||
setShareMenuOpen(false)
|
setShareMenuOpen(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="h-[70vh] flex flex-col items-center justify-center p-8">
|
|
||||||
<div className="h-8 w-8 border-4 border-[var(--cyan-ai)] border-t-transparent rounded-full animate-spin mb-4"></div>
|
|
||||||
<p className="text-muted-foreground font-medium animate-pulse">Loading dashboard...</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const isNew = userStats ? userStats.is_new_user : true
|
const isNew = userStats ? userStats.is_new_user : true
|
||||||
const isNeet = user?.target_exam === "NEET"
|
const isNeet = user?.target_exam === "NEET"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-8 max-w-7xl mx-auto space-y-10 animate-in fade-in zoom-in-95 origin-top duration-500">
|
<div className="p-8 max-w-7xl mx-auto space-y-8">
|
||||||
|
|
||||||
{/* Basic Performance Dashboard (F-05) */}
|
{/* Page Header */}
|
||||||
<section>
|
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4">
|
||||||
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4 mb-8">
|
<div>
|
||||||
<div>
|
<h1 className="text-3xl md:text-4xl font-black tracking-tight flex items-center gap-3 mb-1">
|
||||||
<h1 className="text-3xl md:text-4xl font-black tracking-tight flex items-center gap-3 mb-1">
|
<span className="p-2.5 rounded-xl bg-yellow-50 dark:bg-[var(--cyan-ai-10)] border border-yellow-300/60 dark:border-[var(--cyan-ai-20)]">
|
||||||
<span className="p-2.5 rounded-xl bg-yellow-50 dark:bg-[var(--cyan-ai-10)] border border-yellow-300/60 dark:border-[var(--cyan-ai-20)]">
|
<LayoutDashboard className="text-yellow-600 dark:text-[var(--cyan-ai)]" size={28} />
|
||||||
<LayoutDashboard className="text-yellow-600 dark:text-[var(--cyan-ai)]" size={28} />
|
</span>
|
||||||
</span>
|
Performance Overview
|
||||||
Performance Overview
|
</h1>
|
||||||
</h1>
|
<p className="text-muted-foreground text-base font-medium max-w-2xl pl-1">
|
||||||
<p className="text-muted-foreground text-base font-medium max-w-2xl pl-1">
|
Your overall progress, scores, and active study plan at a glance.
|
||||||
Your overall progress, scores, and active study plan at a glance.
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="relative">
|
||||||
|
<Button onClick={() => setShareMenuOpen(!shareMenuOpen)} variant="outline" size="sm" className="gap-2 font-medium shrink-0" disabled={downloading}>
|
||||||
|
{downloading ? <div className="h-4 w-4 border-2 border-current border-t-transparent rounded-full animate-spin"></div> : <Share2 size={16} />}
|
||||||
|
{downloading ? 'Generating...' : 'Share Card'}
|
||||||
|
</Button>
|
||||||
|
{shareMenuOpen && (
|
||||||
|
<div className="absolute right-0 top-full mt-2 w-48 bg-white dark:bg-zinc-950 border border-border rounded-xl shadow-xl z-50 overflow-hidden">
|
||||||
|
<button onClick={handleShareFacebook} className="flex items-center w-full px-4 py-3 text-sm hover:bg-secondary text-left transition-colors">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="mr-3 text-blue-600"><path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"></path></svg>
|
||||||
|
Share on Facebook
|
||||||
|
</button>
|
||||||
|
<button onClick={handleShareWhatsApp} className="flex items-center w-full px-4 py-3 text-sm hover:bg-secondary text-left transition-colors">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="mr-3 text-green-500"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path></svg>
|
||||||
|
Share on WhatsApp
|
||||||
|
</button>
|
||||||
|
<button onClick={handleDownload} className="flex items-center w-full px-4 py-3 text-sm hover:bg-secondary text-left transition-colors border-t border-border">
|
||||||
|
<Download size={16} className="mr-3 text-primary" />
|
||||||
|
Download Image
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* KPI Stat Cards */}
|
||||||
|
<div ref={progressRef} className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
{/* Today's Score */}
|
||||||
|
<Card className="border-border shadow-sm">
|
||||||
|
<CardContent className="p-5 flex flex-col items-center text-center">
|
||||||
|
<div className="p-3 bg-secondary rounded-full mb-3 text-primary"><Target size={24} /></div>
|
||||||
|
<p className="text-sm font-bold text-muted-foreground uppercase tracking-wider mb-1">Today's Score</p>
|
||||||
|
<h3 className="text-2xl font-bold">
|
||||||
|
{isNew ? "0 / 0" : `${userStats.today_score} / ${userStats.today_max_score || 60}`}
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs text-muted-foreground font-medium mt-2">
|
||||||
|
{isNew ? "No tests taken today" : userStats.today_score > 0 ? "Daily session score" : "Start today's DPP"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</CardContent>
|
||||||
<div className="relative">
|
</Card>
|
||||||
<Button onClick={() => setShareMenuOpen(!shareMenuOpen)} variant="outline" size="sm" className="gap-2 font-medium shrink-0" disabled={downloading}>
|
|
||||||
{downloading ? <div className="h-4 w-4 border-2 border-current border-t-transparent rounded-full animate-spin"></div> : <Share2 size={16} />}
|
|
||||||
{downloading ? 'Generating...' : 'Share Card'}
|
|
||||||
</Button>
|
|
||||||
{shareMenuOpen && (
|
|
||||||
<div className="absolute right-0 top-full mt-2 w-48 bg-white dark:bg-zinc-950 border border-border rounded-xl shadow-xl z-50 overflow-hidden">
|
|
||||||
<button onClick={handleShareFacebook} className="flex items-center w-full px-4 py-3 text-sm hover:bg-secondary text-left transition-colors">
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="mr-3 text-blue-600"><path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"></path></svg>
|
|
||||||
Share on Facebook
|
|
||||||
</button>
|
|
||||||
<button onClick={handleShareWhatsApp} className="flex items-center w-full px-4 py-3 text-sm hover:bg-secondary text-left transition-colors">
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="mr-3 text-green-500"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path></svg>
|
|
||||||
Share on WhatsApp
|
|
||||||
</button>
|
|
||||||
<button onClick={handleDownload} className="flex items-center w-full px-4 py-3 text-sm hover:bg-secondary text-left transition-colors border-t border-border">
|
|
||||||
<Download size={16} className="mr-3 text-primary" />
|
|
||||||
Download Image
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div ref={progressRef} className="grid grid-cols-2 md:grid-cols-4 gap-4 p-4 -mx-4 bg-background rounded-2xl">
|
|
||||||
{/* Today's Score */}
|
|
||||||
<Card>
|
|
||||||
<CardContent className="p-5 flex flex-col items-center text-center">
|
|
||||||
<div className="p-3 bg-secondary rounded-full mb-3 text-primary"><Target size={24} /></div>
|
|
||||||
<p className="text-sm font-bold text-muted-foreground uppercase tracking-wider mb-1">Today's Score</p>
|
|
||||||
<h3 className="text-2xl font-bold">
|
|
||||||
{isNew ? "0 / 0" : `${userStats.today_score} / ${userStats.today_max_score || 60}`}
|
|
||||||
</h3>
|
|
||||||
<p className="text-xs text-muted-foreground font-medium mt-2">
|
|
||||||
{isNew ? "No tests taken today" : userStats.today_score > 0 ? "Daily session score" : "Start today's DPP"}
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Overall Accuracy */}
|
{/* Overall Accuracy */}
|
||||||
<Card>
|
<Card className="border-border shadow-sm">
|
||||||
<CardContent className="p-5 flex flex-col items-center text-center">
|
<CardContent className="p-5 flex flex-col items-center text-center">
|
||||||
<div className="p-3 bg-secondary rounded-full mb-3 text-amber-500"><Activity size={24} /></div>
|
<div className="p-3 bg-secondary rounded-full mb-3 text-amber-500"><Activity size={24} /></div>
|
||||||
<p className="text-sm font-bold text-muted-foreground uppercase tracking-wider mb-1">Overall Accuracy</p>
|
<p className="text-sm font-bold text-muted-foreground uppercase tracking-wider mb-1">Overall Accuracy</p>
|
||||||
<h3 className="text-2xl font-bold">
|
<h3 className="text-2xl font-bold">
|
||||||
{isNew ? "0%" : `${userStats.overall_accuracy}%`}
|
{isNew ? "0%" : `${userStats.overall_accuracy}%`}
|
||||||
</h3>
|
</h3>
|
||||||
<p className={`text-xs font-bold mt-2 ${isNew ? 'text-muted-foreground font-normal' : userStats.overall_accuracy >= 70 ? 'text-green-500' : 'text-amber-500'}`}>
|
<p className={`text-xs font-bold mt-2 ${isNew ? 'text-muted-foreground font-normal' : userStats.overall_accuracy >= 70 ? 'text-green-500' : 'text-amber-500'}`}>
|
||||||
{isNew ? "Start a DPP to calculate" : userStats.overall_accuracy >= 70 ? "On Track" : "Needs Improvement"}
|
{isNew ? "Start a DPP to calculate" : userStats.overall_accuracy >= 70 ? "On Track" : "Needs Improvement"}
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Qs Attempted */}
|
{/* Qs Attempted */}
|
||||||
<Card>
|
<Card className="border-border shadow-sm">
|
||||||
<CardContent className="p-5 flex flex-col items-center text-center">
|
<CardContent className="p-5 flex flex-col items-center text-center">
|
||||||
<div className="p-3 bg-secondary rounded-full mb-3 text-[var(--cyan-ai)]"><CheckCircle2 size={24} /></div>
|
<div className="p-3 bg-secondary rounded-full mb-3 text-[var(--cyan-ai)]"><CheckCircle2 size={24} /></div>
|
||||||
<p className="text-sm font-bold text-muted-foreground uppercase tracking-wider mb-1">Qs Attempted</p>
|
<p className="text-sm font-bold text-muted-foreground uppercase tracking-wider mb-1">Qs Attempted</p>
|
||||||
<h3 className="text-2xl font-bold">
|
<h3 className="text-2xl font-bold">
|
||||||
{isNew ? "0" : (userStats.total_questions_attempted || 0).toLocaleString()}
|
{isNew ? "0" : (userStats.total_questions_attempted || 0).toLocaleString()}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs text-muted-foreground font-medium mt-2">
|
<p className="text-xs text-muted-foreground font-medium mt-2">
|
||||||
{isNew ? "0 questions answered" : "Lifetime total"}
|
{isNew ? "0 questions answered" : "Lifetime total"}
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* City Rank */}
|
{/* City Rank */}
|
||||||
<Card className="relative overflow-hidden group">
|
<Card className="border-border shadow-sm relative overflow-hidden group">
|
||||||
<div className="absolute inset-0 bg-gradient-to-br from-background to-secondary/50 z-0"></div>
|
<div className="absolute inset-0 bg-gradient-to-br from-background to-secondary/50 z-0"></div>
|
||||||
<CardContent className="p-5 flex flex-col items-center text-center relative z-10">
|
<CardContent className="p-5 flex flex-col items-center text-center relative z-10">
|
||||||
<div className="p-3 bg-background border border-border shadow-sm rounded-full mb-3 text-foreground group-hover:scale-110 transition-transform"><Trophy size={24} /></div>
|
<div className="p-3 bg-background border border-border shadow-sm rounded-full mb-3 text-foreground group-hover:scale-110 transition-transform"><Trophy size={24} /></div>
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
<p className="text-sm font-bold text-muted-foreground uppercase tracking-wider">City Rank</p>
|
<p className="text-sm font-bold text-muted-foreground uppercase tracking-wider">City Rank</p>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-2xl font-bold">
|
<h3 className="text-2xl font-bold">
|
||||||
{isNew ? (
|
{isNew ? (
|
||||||
<span className="text-muted-foreground text-xl">#--</span>
|
<span className="text-muted-foreground text-xl">#--</span>
|
||||||
) : (
|
) : (
|
||||||
<>#{userStats.city_rank || 1} <span className="text-base text-muted-foreground font-medium">/ {userStats.city_total || 50}</span></>
|
<>#{userStats.city_rank || 1} <span className="text-base text-muted-foreground font-medium">/ {userStats.city_total || 50}</span></>
|
||||||
)}
|
)}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs text-muted-foreground font-medium mt-2 text-[var(--cyan-ai)]">
|
<p className="text-xs text-muted-foreground font-medium mt-2 text-[var(--cyan-ai)]">
|
||||||
{isNew ? "Unranked · Take a test" : `Top rank in ${user?.city || "your city"}`}
|
{isNew ? "Unranked · Take a test" : `Top rank in ${user?.city || "your city"}`}
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
|
{/* Tests & DPPs Grid */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
{/* Dynamic DPP Modules */}
|
{/* Dynamic DPP Modules */}
|
||||||
<section>
|
<section>
|
||||||
<div className="flex items-center justify-between mb-5">
|
<div className="flex items-center justify-between mb-5">
|
||||||
<h2 className="text-xl font-semibold tracking-tight flex items-center gap-2">
|
<h2 className="text-xl font-semibold tracking-tight flex items-center gap-2">
|
||||||
Today's DPPs
|
Today's DPPs
|
||||||
</h2>
|
</h2>
|
||||||
<Button variant="ghost" size="sm" className="text-muted-foreground font-medium">View All</Button>
|
<Link href="/dashboard/dpp">
|
||||||
|
<Button variant="ghost" size="sm" className="text-muted-foreground font-medium">View All</Button>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{dpps.length > 0 ? (
|
{dpps.length > 0 ? (
|
||||||
dpps.map((dpp) => (
|
dpps.map((dpp) => (
|
||||||
<Card key={dpp.id} className="hover:border-primary/30 hover:shadow-md cursor-pointer transition-all duration-300 group">
|
<Card key={dpp.id} className="hover:border-primary/30 cursor-pointer group">
|
||||||
<CardContent className="p-5 flex items-center justify-between">
|
<CardContent className="p-5 flex items-center justify-between">
|
||||||
<div className="flex gap-4 items-center">
|
<div className="flex gap-4 items-center">
|
||||||
<div className="p-3 bg-secondary rounded-xl text-foreground">
|
<div className="p-3 bg-secondary rounded-xl text-foreground">
|
||||||
@@ -257,8 +274,27 @@ export default function DashboardPage() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
))
|
))
|
||||||
|
) : loading ? (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{[1, 2].map((i) => (
|
||||||
|
<Card key={i} className="animate-pulse">
|
||||||
|
<CardContent className="p-5 flex items-center justify-between">
|
||||||
|
<div className="flex gap-4 items-center">
|
||||||
|
<div className="size-11 bg-secondary rounded-xl"></div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="h-4 w-40 bg-secondary rounded"></div>
|
||||||
|
<div className="h-3 w-28 bg-secondary/60 rounded"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="h-8 w-16 bg-secondary rounded-xl"></div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-muted-foreground text-sm py-4">No DPPs scheduled for today.</p>
|
<Card className="overflow-hidden p-6 border-dashed opacity-70">
|
||||||
|
<p className="text-muted-foreground text-sm">No DPPs scheduled for today.</p>
|
||||||
|
</Card>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -269,6 +305,9 @@ export default function DashboardPage() {
|
|||||||
<h2 className="text-xl font-semibold tracking-tight flex items-center gap-2">
|
<h2 className="text-xl font-semibold tracking-tight flex items-center gap-2">
|
||||||
Upcoming CBTs
|
Upcoming CBTs
|
||||||
</h2>
|
</h2>
|
||||||
|
<Link href="/dashboard/cbt">
|
||||||
|
<Button variant="ghost" size="sm" className="text-muted-foreground font-medium">View All</Button>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{cbts.length > 0 ? (
|
{cbts.length > 0 ? (
|
||||||
@@ -278,7 +317,7 @@ export default function DashboardPage() {
|
|||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-3 mb-2">
|
<div className="flex items-center gap-3 mb-2">
|
||||||
<Badge variant="outline" className="border-primary/20 bg-primary/5 text-primary">{cbt.exam_pattern}</Badge>
|
<Badge variant="outline" className="border-primary/20 bg-primary/5 text-primary">{cbt.exam_pattern}</Badge>
|
||||||
<span className="text-xs text-muted-foreground font-semibold uppercase tracking-wider">{cbt.scheduled_date ? new Date(cbt.scheduled_date).toLocaleDateString() : 'Available Now'}</span>
|
<span className="text-xs text-muted-foreground font-semibold uppercase tracking-wider">{cbt.scheduled_date ? formatISTDate(cbt.scheduled_date) : 'Available Now'}</span>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="font-semibold text-lg tracking-tight">{cbt.title}</h3>
|
<h3 className="font-semibold text-lg tracking-tight">{cbt.title}</h3>
|
||||||
<p className="text-sm text-muted-foreground flex items-center gap-1.5 mt-1.5">
|
<p className="text-sm text-muted-foreground flex items-center gap-1.5 mt-1.5">
|
||||||
@@ -291,6 +330,14 @@ export default function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
))
|
))
|
||||||
|
) : loading ? (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Card className="animate-pulse p-6 space-y-3">
|
||||||
|
<div className="h-4 w-24 bg-secondary rounded"></div>
|
||||||
|
<div className="h-5 w-48 bg-secondary rounded"></div>
|
||||||
|
<div className="h-3 w-32 bg-secondary/60 rounded"></div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Card className="overflow-hidden p-6 border-dashed opacity-70">
|
<Card className="overflow-hidden p-6 border-dashed opacity-70">
|
||||||
<p className="text-muted-foreground text-sm">No upcoming CBTs at the moment.</p>
|
<p className="text-muted-foreground text-sm">No upcoming CBTs at the moment.</p>
|
||||||
@@ -300,13 +347,14 @@ export default function DashboardPage() {
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* AI Intelligence Grid */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
<section>
|
<div>
|
||||||
<AdaptiveStudyPlan />
|
<AdaptiveStudyPlan />
|
||||||
</section>
|
</div>
|
||||||
<section>
|
<div>
|
||||||
<MistakeHeatmap />
|
<MistakeHeatmap />
|
||||||
</section>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from "react"
|
import { useState, useEffect } from "react"
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
|
||||||
import { ChevronDown, ChevronRight, CheckCircle2, Circle, Clock, Activity, BarChart3 } from "lucide-react"
|
import { ChevronDown, ChevronRight, CheckCircle2, Circle, Clock, Activity, BarChart3, Lock } from "lucide-react"
|
||||||
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
||||||
import { PremiumGate } from "@/components/PremiumGate"
|
import { PremiumGate } from "@/components/PremiumGate"
|
||||||
|
|
||||||
@@ -134,25 +134,26 @@ const ZERO_SUBJECTS: Subject[] = [
|
|||||||
// Math/Bio dynamic subjects handled below
|
// Math/Bio dynamic subjects handled below
|
||||||
]
|
]
|
||||||
|
|
||||||
export default function ProgressTrackerPage() {
|
|
||||||
const { userId, user } = useActiveUser()
|
|
||||||
const isDemo = isDemoUser(userId)
|
|
||||||
const [subjects, setSubjects] = useState<Subject[]>([])
|
|
||||||
|
|
||||||
useEffect(() => {
|
const mathDemoSubject: Subject = {
|
||||||
const targetExam = user?.target_exam || "JEE_Main";
|
|
||||||
const isNeet = targetExam === "NEET";
|
|
||||||
|
|
||||||
const mathDemoSubject: Subject = {
|
|
||||||
name: "Mathematics",
|
name: "Mathematics",
|
||||||
completionPct: 22,
|
completionPct: 28,
|
||||||
chapters: [
|
chapters: [
|
||||||
{
|
{
|
||||||
name: "Calculus",
|
name: "Calculus",
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
topics: [
|
topics: [
|
||||||
{ name: "Limits", status: "In Progress", accuracy: 60, questionsAttempted: 45 },
|
{ name: "Limits", status: "In Progress", accuracy: 60, questionsAttempted: 45 },
|
||||||
{ name: "Derivatives", status: "Not Started", accuracy: 0, questionsAttempted: 0 },
|
{ name: "Derivatives", status: "Practiced", accuracy: 72, questionsAttempted: 35 },
|
||||||
|
{ name: "Integrals", status: "Not Started", accuracy: 0, questionsAttempted: 0 },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Coordinate Geometry",
|
||||||
|
isExpanded: false,
|
||||||
|
topics: [
|
||||||
|
{ name: "Straight Lines", status: "Practiced", accuracy: 80, questionsAttempted: 40 },
|
||||||
|
{ name: "Circles", status: "Not Started", accuracy: 0, questionsAttempted: 0 },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -169,6 +170,14 @@ export default function ProgressTrackerPage() {
|
|||||||
{ name: "Cell Wall", status: "Practiced", accuracy: 80, questionsAttempted: 60 },
|
{ name: "Cell Wall", status: "Practiced", accuracy: 80, questionsAttempted: 60 },
|
||||||
{ name: "Nucleus", status: "In Progress", accuracy: 50, questionsAttempted: 25 },
|
{ name: "Nucleus", status: "In Progress", accuracy: 50, questionsAttempted: 25 },
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Genetics & Evolution",
|
||||||
|
isExpanded: false,
|
||||||
|
topics: [
|
||||||
|
{ name: "Mendelian Genetics", status: "Practiced", accuracy: 85, questionsAttempted: 55 },
|
||||||
|
{ name: "Molecular Basis of Inheritance", status: "Not Started", accuracy: 0, questionsAttempted: 0 },
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
@@ -183,6 +192,15 @@ export default function ProgressTrackerPage() {
|
|||||||
topics: [
|
topics: [
|
||||||
{ name: "Limits", status: "Not Started", accuracy: 0, questionsAttempted: 0 },
|
{ name: "Limits", status: "Not Started", accuracy: 0, questionsAttempted: 0 },
|
||||||
{ name: "Derivatives", status: "Not Started", accuracy: 0, questionsAttempted: 0 },
|
{ name: "Derivatives", status: "Not Started", accuracy: 0, questionsAttempted: 0 },
|
||||||
|
{ name: "Integrals", status: "Not Started", accuracy: 0, questionsAttempted: 0 },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Coordinate Geometry",
|
||||||
|
isExpanded: false,
|
||||||
|
topics: [
|
||||||
|
{ name: "Straight Lines", status: "Not Started", accuracy: 0, questionsAttempted: 0 },
|
||||||
|
{ name: "Circles", status: "Not Started", accuracy: 0, questionsAttempted: 0 },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -199,15 +217,98 @@ export default function ProgressTrackerPage() {
|
|||||||
{ name: "Cell Wall", status: "Not Started", accuracy: 0, questionsAttempted: 0 },
|
{ name: "Cell Wall", status: "Not Started", accuracy: 0, questionsAttempted: 0 },
|
||||||
{ name: "Nucleus", status: "Not Started", accuracy: 0, questionsAttempted: 0 },
|
{ name: "Nucleus", status: "Not Started", accuracy: 0, questionsAttempted: 0 },
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Genetics & Evolution",
|
||||||
|
isExpanded: false,
|
||||||
|
topics: [
|
||||||
|
{ name: "Mendelian Genetics", status: "Not Started", accuracy: 0, questionsAttempted: 0 },
|
||||||
|
{ name: "Molecular Basis of Inheritance", status: "Not Started", accuracy: 0, questionsAttempted: 0 },
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
const base = isDemo ? [...DEMO_SUBJECTS] : [...ZERO_SUBJECTS];
|
function getInitialSubjects(isDemoUser: boolean, targetExam: string): Subject[] {
|
||||||
base.push(isDemo ? (isNeet ? bioDemoSubject : mathDemoSubject) : (isNeet ? bioZeroSubject : mathZeroSubject));
|
const isNeet = targetExam === "NEET";
|
||||||
|
const isJeeNeet = targetExam === "JEE_NEET";
|
||||||
setSubjects(base);
|
const base = isDemoUser ? [...DEMO_SUBJECTS] : [...ZERO_SUBJECTS];
|
||||||
}, [isDemo, user?.target_exam]);
|
if (isJeeNeet) {
|
||||||
|
base.push(isDemoUser ? mathDemoSubject : mathZeroSubject);
|
||||||
|
base.push(isDemoUser ? bioDemoSubject : bioZeroSubject);
|
||||||
|
} else if (isNeet) {
|
||||||
|
base.push(isDemoUser ? bioDemoSubject : bioZeroSubject);
|
||||||
|
} else {
|
||||||
|
base.push(isDemoUser ? mathDemoSubject : mathZeroSubject);
|
||||||
|
}
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
export default function ProgressTrackerPage() {
|
||||||
|
const { userId, user, hasAccessTo } = useActiveUser();
|
||||||
|
const isDemo = isDemoUser(userId);
|
||||||
|
const [subjects, setSubjects] = useState<Subject[]>(() =>
|
||||||
|
getInitialSubjects(isDemoUser(userId), user?.target_exam || "JEE_Main")
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSubjects(getInitialSubjects(isDemo, user?.target_exam || "JEE_Main"));
|
||||||
|
|
||||||
|
const handleSwitched = (e: any) => {
|
||||||
|
const switchedUser = e.detail?.user;
|
||||||
|
const switchedId = e.detail?.userId;
|
||||||
|
const exam = switchedUser?.target_exam || (switchedId === "33333333-3333-3333-3333-333333333333" ? "JEE_NEET" : "JEE_Main");
|
||||||
|
const demo = isDemoUser(switchedId);
|
||||||
|
setSubjects(getInitialSubjects(demo, exam));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdated = () => {
|
||||||
|
setSubjects(getInitialSubjects(isDemoUser(userId), user?.target_exam || "JEE_Main"));
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("pragativeer-user-switched", handleSwitched);
|
||||||
|
window.addEventListener("pragativeer-user-updated", handleUpdated);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("pragativeer-user-switched", handleSwitched);
|
||||||
|
window.removeEventListener("pragativeer-user-updated", handleUpdated);
|
||||||
|
};
|
||||||
|
}, [isDemo, user?.target_exam, userId]);
|
||||||
|
|
||||||
|
if (!hasAccessTo("chapter-wise-progress-tracking") && !isDemo) {
|
||||||
|
return (
|
||||||
|
<div className="p-8 space-y-8 max-w-7xl mx-auto">
|
||||||
|
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl md:text-4xl font-black tracking-tight flex items-center gap-3 mb-1">
|
||||||
|
<span className="p-2.5 rounded-xl bg-yellow-50 dark:bg-[var(--cyan-ai-10)] border border-yellow-300/60 dark:border-[var(--cyan-ai-20)]">
|
||||||
|
<BarChart3 className="text-yellow-600 dark:text-[var(--cyan-ai)]" size={28} />
|
||||||
|
</span>
|
||||||
|
Syllabus Tracker
|
||||||
|
</h1>
|
||||||
|
<p className="text-muted-foreground text-base font-medium max-w-2xl pl-1">
|
||||||
|
Track your syllabus coverage and mastery per chapter to ensure no gaps.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-12 text-center max-w-2xl mx-auto bg-secondary/20 border border-border rounded-3xl space-y-6">
|
||||||
|
<div className="size-20 rounded-full bg-red-500/10 flex items-center justify-center mx-auto text-red-500">
|
||||||
|
<Lock size={40} />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-bold">Premium Feature</h2>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Chapter-wise syllabus tracking is available on Pro and AI Coach plans.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
className="px-8 py-3 bg-[var(--cyan-ai)] text-black font-bold rounded-xl shadow-[0_0_20px_-5px_var(--cyan-ai)]"
|
||||||
|
onClick={() => window.location.href = '/upgrade'}
|
||||||
|
>
|
||||||
|
Upgrade Plan
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const toggleChapter = (subjectIndex: number, chapterIndex: number) => {
|
const toggleChapter = (subjectIndex: number, chapterIndex: number) => {
|
||||||
const newSubjects = [...subjects]
|
const newSubjects = [...subjects]
|
||||||
@@ -216,8 +317,8 @@ export default function ProgressTrackerPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PremiumGate featureKey="chapter-wise-progress-tracking">
|
|
||||||
<div className="p-8 space-y-8 animate-in fade-in zoom-in-95 origin-top duration-500 max-w-7xl mx-auto">
|
<div className="p-8 space-y-8 max-w-7xl mx-auto">
|
||||||
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4">
|
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl md:text-4xl font-black tracking-tight flex items-center gap-3 mb-1">
|
<h1 className="text-3xl md:text-4xl font-black tracking-tight flex items-center gap-3 mb-1">
|
||||||
@@ -232,7 +333,7 @@ export default function ProgressTrackerPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
<div className={`grid grid-cols-1 sm:grid-cols-2 ${subjects.length >= 4 ? 'lg:grid-cols-4' : 'md:grid-cols-3'} gap-6 mb-8`}>
|
||||||
{subjects.map((sub, i) => (
|
{subjects.map((sub, i) => (
|
||||||
<Card key={i} className="border-border shadow-sm">
|
<Card key={i} className="border-border shadow-sm">
|
||||||
<CardContent className="p-6 text-center">
|
<CardContent className="p-6 text-center">
|
||||||
@@ -307,6 +408,6 @@ export default function ProgressTrackerPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</PremiumGate>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export default function RankPredictorPage() {
|
|||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 md:p-8 space-y-8 animate-in fade-in zoom-in-95 origin-top duration-500 max-w-7xl mx-auto min-h-screen">
|
<div className="p-6 md:p-8 space-y-8 max-w-7xl mx-auto min-h-screen">
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="text-center max-w-3xl mx-auto space-y-3">
|
<div className="text-center max-w-3xl mx-auto space-y-3">
|
||||||
@@ -240,7 +240,7 @@ export default function RankPredictorPage() {
|
|||||||
{totalScore === 0 ? "AIR --" : `AIR ~${predictedRank.toLocaleString()}`}
|
{totalScore === 0 ? "AIR --" : `AIR ~${predictedRank.toLocaleString()}`}
|
||||||
</div>
|
</div>
|
||||||
<div className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-[var(--cyan-ai-20)] text-[var(--cyan-ai)] text-xs font-bold border border-[var(--cyan-ai-40)]">
|
<div className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-[var(--cyan-ai-20)] text-[var(--cyan-ai)] text-xs font-bold border border-[var(--cyan-ai-40)]">
|
||||||
<CheckCircle2 size={13} /> {totalScore === 0 ? "Calibrating · Adjust Sliders" : "High Accuracy Confidence"}
|
<CheckCircle2 size={13} /> {totalScore === 0 ? "Calibrating <EFBFBD> Adjust Sliders" : "High Accuracy Confidence"}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export default function SettingsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-8 space-y-8 animate-in fade-in zoom-in-95 origin-top duration-500 max-w-7xl mx-auto">
|
<div className="p-8 space-y-8 max-w-7xl mx-auto">
|
||||||
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4">
|
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl md:text-4xl font-black tracking-tight flex items-center gap-3 mb-1">
|
<h1 className="text-3xl md:text-4xl font-black tracking-tight flex items-center gap-3 mb-1">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { fetchWithAuth } from "@/lib/apiClient";
|
import { fetchWithAuth } from "@/lib/apiClient";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
@@ -45,7 +45,7 @@ export default function PasswordPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-8 space-y-8 animate-in fade-in zoom-in-95 origin-top duration-500 max-w-3xl mx-auto">
|
<div className="p-8 space-y-8 max-w-3xl mx-auto">
|
||||||
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4">
|
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl md:text-4xl font-black tracking-tight flex items-center gap-3 mb-1">
|
<h1 className="text-3xl md:text-4xl font-black tracking-tight flex items-center gap-3 mb-1">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { fetchWithAuth } from "@/lib/apiClient";
|
import { fetchWithAuth } from "@/lib/apiClient";
|
||||||
import { useState, useEffect } from "react"
|
import { useState, useEffect } from "react"
|
||||||
@@ -46,7 +46,7 @@ export default function StreaksPage() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-8 space-y-8 animate-in fade-in zoom-in-95 origin-top duration-500 max-w-7xl mx-auto">
|
<div className="p-8 space-y-8 max-w-7xl mx-auto">
|
||||||
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4">
|
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl md:text-4xl font-black tracking-tight flex items-center gap-3 mb-1">
|
<h1 className="text-3xl md:text-4xl font-black tracking-tight flex items-center gap-3 mb-1">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
@@ -46,7 +46,7 @@ export default function ForgotPasswordPage() {
|
|||||||
<ArrowLeft size={16} /> Back to Login
|
<ArrowLeft size={16} /> Back to Login
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div className="w-full max-w-md space-y-8 animate-in fade-in slide-in-from-bottom-8 duration-700">
|
<div className="w-full max-w-md space-y-8">
|
||||||
<div className="flex justify-center mb-8">
|
<div className="flex justify-center mb-8">
|
||||||
<Logo href="/" className="w-72" imageClassName="object-contain" height={80} />
|
<Logo href="/" className="w-72" imageClassName="object-contain" height={80} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -116,10 +116,39 @@ body {
|
|||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── Intro/opening animations completely eliminated for instant, rock-solid page loading ─── */
|
||||||
|
*, *::before, *::after {
|
||||||
|
--tw-enter-opacity: 1 !important;
|
||||||
|
--tw-enter-scale: 1 !important;
|
||||||
|
--tw-enter-translate-x: 0 !important;
|
||||||
|
--tw-enter-translate-y: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-page-enter,
|
||||||
|
.animate-section,
|
||||||
|
.animate-section-1,
|
||||||
|
.animate-section-2,
|
||||||
|
.animate-section-3,
|
||||||
|
.animate-section-4,
|
||||||
|
.animate-section-5,
|
||||||
|
.animate-section-6,
|
||||||
|
.animate-in,
|
||||||
|
[class*="animate-in"],
|
||||||
|
[class*="fade-in"],
|
||||||
|
[class*="slide-in"],
|
||||||
|
[class*="zoom-in"] {
|
||||||
|
animation: none !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
transform: none !important;
|
||||||
|
filter: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
.text-balance {
|
.text-balance {
|
||||||
text-wrap: balance;
|
text-wrap: balance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.bg-grid {
|
.bg-grid {
|
||||||
background-size: 40px 40px;
|
background-size: 40px 40px;
|
||||||
@@ -173,3 +202,4 @@ body {
|
|||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: var(--muted-foreground);
|
background: var(--muted-foreground);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,17 @@ export default function LoginPage() {
|
|||||||
setError(data.error || "Invalid email or password.")
|
setError(data.error || "Invalid email or password.")
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError("An error occurred. Please try again.")
|
// Local dev offline fallback: if backend is offline on localhost, allow demo login to test frontend
|
||||||
|
if (email === "demo@pragativeer.com" || email.includes("demo") || email === "admin@pragativeer.com") {
|
||||||
|
const isAdmin = email.includes("admin");
|
||||||
|
const cookieVal = isAdmin ? "admin" : "authenticated";
|
||||||
|
document.cookie = `auth_token=${cookieVal}; path=/; max-age=2592000; SameSite=Lax`;
|
||||||
|
localStorage.setItem("activeUserId", isAdmin ? "admin" : "11111111-1111-1111-1111-111111111111");
|
||||||
|
localStorage.setItem("active_session_token", "dev-offline-token");
|
||||||
|
window.location.href = isAdmin ? "/admin" : "/dashboard";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setError("Backend server is not running on port 8000. Start backend or use demo@pragativeer.com to test locally.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +82,7 @@ export default function LoginPage() {
|
|||||||
<ArrowLeft size={16} /> Back to Home
|
<ArrowLeft size={16} /> Back to Home
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div className="w-full max-w-md space-y-8 animate-in fade-in slide-in-from-bottom-8 duration-700">
|
<div className="w-full max-w-md space-y-8">
|
||||||
<div className="flex justify-center mb-8">
|
<div className="flex justify-center mb-8">
|
||||||
<Logo href="/" className="w-72" imageClassName="object-contain" height={80} />
|
<Logo href="/" className="w-72" imageClassName="object-contain" height={80} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import {
|
import {
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
Zap,
|
Zap,
|
||||||
@@ -41,7 +40,6 @@ export default function OnboardingPage() {
|
|||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
localStorage.setItem("chosen_plan", plan);
|
localStorage.setItem("chosen_plan", plan);
|
||||||
}
|
}
|
||||||
// Small UX delay so the button animation feels intentional
|
|
||||||
await new Promise((r) => setTimeout(r, 600));
|
await new Promise((r) => setTimeout(r, 600));
|
||||||
router.push("/dashboard");
|
router.push("/dashboard");
|
||||||
};
|
};
|
||||||
@@ -54,19 +52,14 @@ export default function OnboardingPage() {
|
|||||||
<div className="absolute bottom-0 right-1/4 size-[500px] bg-[var(--cyan-ai-10)] blur-[120px] rounded-full" />
|
<div className="absolute bottom-0 right-1/4 size-[500px] bg-[var(--cyan-ai-10)] blur-[120px] rounded-full" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full max-w-5xl space-y-12 animate-in fade-in slide-in-from-bottom-6 duration-700">
|
<div className="w-full max-w-5xl space-y-12">
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<Logo href="/" className="w-56" imageClassName="object-contain" height={64} />
|
<Logo href="/" className="w-56" imageClassName="object-contain" height={64} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<motion.div
|
<div className="text-center space-y-4">
|
||||||
initial={{ opacity: 0, y: 24 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.6 }}
|
|
||||||
className="text-center space-y-4"
|
|
||||||
>
|
|
||||||
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-primary/10 border border-primary/20 text-primary font-bold text-sm mb-2">
|
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-primary/10 border border-primary/20 text-primary font-bold text-sm mb-2">
|
||||||
<Sparkles size={14} className="animate-pulse" />
|
<Sparkles size={14} className="animate-pulse" />
|
||||||
Account Created Successfully!
|
Account Created Successfully!
|
||||||
@@ -85,16 +78,12 @@ export default function OnboardingPage() {
|
|||||||
<Clock size={16} />
|
<Clock size={16} />
|
||||||
Your 15-day free trial with all features starts now — Day 1 of 15
|
Your 15-day free trial with all features starts now — Day 1 of 15
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</div>
|
||||||
|
|
||||||
{/* Plan Cards */}
|
{/* Plan Cards */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-4xl mx-auto">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-4xl mx-auto">
|
||||||
{/* Pro Plan */}
|
{/* Pro Plan */}
|
||||||
<motion.div
|
<div>
|
||||||
initial={{ opacity: 0, x: -40 }}
|
|
||||||
animate={{ opacity: 1, x: 0 }}
|
|
||||||
transition={{ duration: 0.6, delay: 0.15, type: "spring" }}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className="relative rounded-[2rem] p-[1px] h-full group cursor-pointer"
|
className="relative rounded-[2rem] p-[1px] h-full group cursor-pointer"
|
||||||
style={{
|
style={{
|
||||||
@@ -103,7 +92,7 @@ export default function OnboardingPage() {
|
|||||||
}}
|
}}
|
||||||
onClick={() => !selecting && handleSelect("Pro")}
|
onClick={() => !selecting && handleSelect("Pro")}
|
||||||
>
|
>
|
||||||
<div className="bg-background rounded-[calc(2rem-1px)] p-7 flex flex-col h-full transition-all duration-300 group-hover:bg-secondary/20">
|
<div className="bg-background rounded-[calc(2rem-1px)] p-7 flex flex-col h-full hover:bg-secondary/20">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-yellow-500/10 border border-yellow-500/20 text-yellow-500 text-xs font-bold mb-3 uppercase tracking-wider">
|
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-yellow-500/10 border border-yellow-500/20 text-yellow-500 text-xs font-bold mb-3 uppercase tracking-wider">
|
||||||
@@ -135,7 +124,7 @@ export default function OnboardingPage() {
|
|||||||
if (!selecting) handleSelect("Pro");
|
if (!selecting) handleSelect("Pro");
|
||||||
}}
|
}}
|
||||||
disabled={!!selecting}
|
disabled={!!selecting}
|
||||||
className="w-full h-12 rounded-xl bg-yellow-500 text-black font-bold text-base hover:bg-yellow-400 transition-all duration-300 shadow-[0_0_20px_-5px_rgba(234,179,8,0.5)] hover:shadow-[0_0_30px_-3px_rgba(234,179,8,0.7)] hover:-translate-y-0.5 disabled:opacity-70 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
className="w-full h-12 rounded-xl bg-yellow-500 text-black font-bold text-base hover:bg-yellow-400 shadow-[0_0_20px_-5px_rgba(234,179,8,0.5)] hover:shadow-[0_0_30px_-3px_rgba(234,179,8,0.7)] disabled:opacity-70 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||||
>
|
>
|
||||||
{selecting === "Pro" ? (
|
{selecting === "Pro" ? (
|
||||||
<>
|
<>
|
||||||
@@ -151,14 +140,10 @@ export default function OnboardingPage() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</div>
|
||||||
|
|
||||||
{/* AI Coach Plan */}
|
{/* AI Coach Plan */}
|
||||||
<motion.div
|
<div>
|
||||||
initial={{ opacity: 0, x: 40 }}
|
|
||||||
animate={{ opacity: 1, x: 0 }}
|
|
||||||
transition={{ duration: 0.6, delay: 0.25, type: "spring" }}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className="relative rounded-[2rem] p-[1px] h-full group cursor-pointer"
|
className="relative rounded-[2rem] p-[1px] h-full group cursor-pointer"
|
||||||
style={{
|
style={{
|
||||||
@@ -172,7 +157,7 @@ export default function OnboardingPage() {
|
|||||||
<Sparkles size={12} /> Most Popular
|
<Sparkles size={12} /> Most Popular
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-background rounded-[calc(2rem-1px)] p-7 flex flex-col h-full transition-all duration-300 group-hover:bg-secondary/20">
|
<div className="bg-background rounded-[calc(2rem-1px)] p-7 flex flex-col h-full hover:bg-secondary/20">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-6 pt-2">
|
<div className="mb-6 pt-2">
|
||||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-[var(--cyan-ai-10)] border border-[var(--cyan-ai-20)] text-[var(--cyan-ai)] text-xs font-bold mb-3 uppercase tracking-wider">
|
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-[var(--cyan-ai-10)] border border-[var(--cyan-ai-20)] text-[var(--cyan-ai)] text-xs font-bold mb-3 uppercase tracking-wider">
|
||||||
@@ -204,7 +189,7 @@ export default function OnboardingPage() {
|
|||||||
if (!selecting) handleSelect("AI Coach");
|
if (!selecting) handleSelect("AI Coach");
|
||||||
}}
|
}}
|
||||||
disabled={!!selecting}
|
disabled={!!selecting}
|
||||||
className="w-full h-12 rounded-xl bg-[var(--cyan-ai)] text-black font-bold text-base hover:bg-[var(--cyan-ai-80)] transition-all duration-300 shadow-[0_0_20px_-5px_rgba(12,96,96,0.5)] hover:shadow-[0_0_30px_-3px_rgba(12,96,96,0.8)] hover:-translate-y-0.5 disabled:opacity-70 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
className="w-full h-12 rounded-xl bg-[var(--cyan-ai)] text-black font-bold text-base hover:bg-[var(--cyan-ai-80)] shadow-[0_0_20px_-5px_rgba(12,96,96,0.5)] hover:shadow-[0_0_30px_-3px_rgba(12,96,96,0.8)] disabled:opacity-70 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||||
>
|
>
|
||||||
{selecting === "AI Coach" ? (
|
{selecting === "AI Coach" ? (
|
||||||
<>
|
<>
|
||||||
@@ -220,18 +205,13 @@ export default function OnboardingPage() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bottom note */}
|
{/* Bottom note */}
|
||||||
<motion.p
|
<p className="text-center text-xs text-muted-foreground font-medium pb-8">
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
transition={{ duration: 0.6, delay: 0.5 }}
|
|
||||||
className="text-center text-xs text-muted-foreground font-medium pb-8"
|
|
||||||
>
|
|
||||||
🔒 No credit card required for trial · Cancel anytime · All prices inclusive of GST
|
🔒 No credit card required for trial · Cancel anytime · All prices inclusive of GST
|
||||||
</motion.p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
211
src/app/page.tsx
211
src/app/page.tsx
@@ -1,4 +1,4 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
@@ -6,61 +6,20 @@ import { Card, CardContent } from "@/components/ui/card"
|
|||||||
import { BrainCircuit, Activity, Zap, CheckCircle2, Shield, ArrowRight, Play, BookOpen, LineChart, Trophy, Sparkles, MessageSquare, X, Minus, Target, Calendar } from "lucide-react"
|
import { BrainCircuit, Activity, Zap, CheckCircle2, Shield, ArrowRight, Play, BookOpen, LineChart, Trophy, Sparkles, MessageSquare, X, Minus, Target, Calendar } from "lucide-react"
|
||||||
import Image from "next/image"
|
import Image from "next/image"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { motion, useScroll, useTransform, Variants } from "framer-motion"
|
|
||||||
import { useRef, useEffect } from "react"
|
|
||||||
import { ThemeToggle } from "@/components/ThemeToggle"
|
import { ThemeToggle } from "@/components/ThemeToggle"
|
||||||
|
|
||||||
export default function MarketingPage() {
|
export default function MarketingPage() {
|
||||||
|
|
||||||
|
|
||||||
const containerRef = useRef(null)
|
|
||||||
const { scrollYProgress } = useScroll({
|
|
||||||
target: containerRef,
|
|
||||||
offset: ["start start", "end start"]
|
|
||||||
})
|
|
||||||
|
|
||||||
// Slower background parallax
|
|
||||||
const yBg = useTransform(scrollYProgress, [0, 1], ["0%", "50%"])
|
|
||||||
|
|
||||||
// Stagger variants for the features
|
|
||||||
const containerVariants: Variants = {
|
|
||||||
hidden: { opacity: 0 },
|
|
||||||
visible: {
|
|
||||||
opacity: 1,
|
|
||||||
transition: {
|
|
||||||
staggerChildren: 0.2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const itemVariants: Variants = {
|
|
||||||
hidden: { opacity: 0, y: 30 },
|
|
||||||
visible: {
|
|
||||||
opacity: 1,
|
|
||||||
y: 0,
|
|
||||||
transition: { type: "spring", stiffness: 100, damping: 12 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef} className="min-h-screen bg-background text-foreground overflow-x-hidden selection:bg-primary/20 relative">
|
<div className="min-h-screen bg-background text-foreground overflow-x-hidden selection:bg-primary/20 relative">
|
||||||
|
|
||||||
{/* Animated Deep Background */}
|
{/* 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 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 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(12, 96, 96, 0.03) 0%, transparent 40%)' }}></div>
|
<div className="fixed inset-0 z-0 pointer-events-none" style={{ background: 'radial-gradient(circle at 100% 50%, rgba(12, 96, 96, 0.03) 0%, transparent 40%)' }}></div>
|
||||||
|
|
||||||
{/* Floating Orbs (Using Framer Motion) */}
|
{/* Ambient Glows */}
|
||||||
<motion.div
|
<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" />
|
||||||
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"
|
<div className="fixed bottom-1/4 right-1/4 size-[500px] rounded-full bg-[var(--cyan-ai-10)] blur-[150px] 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
|
|
||||||
className="fixed bottom-1/4 right-1/4 size-[500px] rounded-full bg-[var(--cyan-ai-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" }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Navigation */}
|
{/* Navigation */}
|
||||||
<nav className="fixed top-0 w-full z-50 bg-background/50 backdrop-blur-xl border-b border-border/50">
|
<nav className="fixed top-0 w-full z-50 bg-background/50 backdrop-blur-xl border-b border-border/50">
|
||||||
@@ -93,56 +52,32 @@ export default function MarketingPage() {
|
|||||||
<main className="relative z-10 pt-32 pb-24">
|
<main className="relative z-10 pt-32 pb-24">
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<section className="px-6 max-w-7xl mx-auto text-center space-y-8 pt-12 pb-8">
|
<section className="px-6 max-w-7xl mx-auto text-center space-y-8 pt-12 pb-8">
|
||||||
<motion.div
|
<div>
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.6 }}
|
|
||||||
>
|
|
||||||
<Badge variant="outline" className="border-primary/50 text-primary bg-primary/10 px-4 py-1.5 rounded-full text-xs font-bold tracking-widest uppercase shadow-[0_0_15px_-5px_var(--color-primary)] mb-6">
|
<Badge variant="outline" className="border-primary/50 text-primary bg-primary/10 px-4 py-1.5 rounded-full text-xs font-bold tracking-widest uppercase shadow-[0_0_15px_-5px_var(--color-primary)] mb-6">
|
||||||
The Next Evolution of Prep
|
The Next Evolution of Prep
|
||||||
</Badge>
|
</Badge>
|
||||||
</motion.div>
|
</div>
|
||||||
|
|
||||||
<motion.h1
|
<h1 className="text-6xl md:text-8xl font-bold tracking-tighter max-w-5xl mx-auto leading-[1.1]">
|
||||||
initial={{ opacity: 0, scale: 0.95 }}
|
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
|
||||||
transition={{ duration: 0.8, type: "spring" }}
|
|
||||||
className="text-6xl md:text-8xl font-bold tracking-tighter max-w-5xl mx-auto leading-[1.1]"
|
|
||||||
>
|
|
||||||
NEET/JEE Prep, <br className="hidden md:block" />
|
NEET/JEE Prep, <br className="hidden md:block" />
|
||||||
<span className="text-transparent bg-clip-text bg-gradient-to-r from-primary via-[var(--cyan-ai)] to-primary animate-gradient pb-2 drop-shadow-[0_0_20px_rgba(209,255,77,0.3)]">Re-engineered by AI.</span>
|
<span className="text-transparent bg-clip-text bg-gradient-to-r from-primary via-[var(--cyan-ai)] to-primary animate-gradient pb-2 drop-shadow-[0_0_20px_rgba(209,255,77,0.3)]">Re-engineered by AI.</span>
|
||||||
</motion.h1>
|
</h1>
|
||||||
|
|
||||||
<motion.p
|
<p className="text-xl md:text-2xl text-muted-foreground max-w-2xl mx-auto leading-relaxed font-medium">
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
transition={{ duration: 0.8, delay: 0.2 }}
|
|
||||||
className="text-xl md:text-2xl text-muted-foreground max-w-2xl mx-auto leading-relaxed font-medium"
|
|
||||||
>
|
|
||||||
Experience the most accurate CBT Exam Engine combined with adaptive DPPs generated specifically for your weak areas in real-time.
|
Experience the most accurate CBT Exam Engine combined with adaptive DPPs generated specifically for your weak areas in real-time.
|
||||||
</motion.p>
|
</p>
|
||||||
|
|
||||||
<motion.div
|
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 pt-8">
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.8, delay: 0.4 }}
|
|
||||||
className="flex flex-col sm:flex-row items-center justify-center gap-4 pt-8"
|
|
||||||
>
|
|
||||||
<Link href="/login">
|
<Link href="/login">
|
||||||
<Button size="lg" className="group rounded-full h-14 px-8 text-base font-bold shadow-[0_0_40px_-10px_var(--color-primary)] hover:shadow-[0_0_60px_-10px_var(--color-primary)] transition-all w-full sm:w-auto relative overflow-hidden">
|
<Button size="lg" className="group rounded-full h-14 px-8 text-base font-bold shadow-[0_0_40px_-10px_var(--color-primary)] hover:shadow-[0_0_60px_-10px_var(--color-primary)] transition-all w-full sm:w-auto relative overflow-hidden">
|
||||||
<span className="relative z-10 flex items-center">Start Free Trial <ArrowRight className="ml-2 group-hover:translate-x-1 transition-transform" size={18} /></span>
|
<span className="relative z-10 flex items-center">Start Free Trial <ArrowRight className="ml-2 group-hover:translate-x-1 transition-transform" size={18} /></span>
|
||||||
<div className="absolute inset-0 -translate-x-full animate-shine bg-gradient-to-r from-transparent via-white/20 to-transparent z-0"></div>
|
<div className="absolute inset-0 -translate-x-full animate-shine bg-gradient-to-r from-transparent via-white/20 to-transparent z-0"></div>
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</motion.div>
|
</div>
|
||||||
|
|
||||||
{/* Dashboard Preview Mockup */}
|
{/* Dashboard Preview Mockup */}
|
||||||
<motion.div
|
<div className="relative max-w-5xl mx-auto mt-20 rounded-[2rem] border border-border/60 bg-secondary/5 backdrop-blur-xl shadow-[0_0_50px_-15px_rgba(12,96,96,0.15)] overflow-hidden">
|
||||||
initial={{ opacity: 0, y: 40, scale: 0.95 }}
|
|
||||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
||||||
transition={{ duration: 1, delay: 0.6, type: "spring" }}
|
|
||||||
className="relative max-w-5xl mx-auto mt-20 rounded-[2rem] border border-border/60 bg-secondary/5 backdrop-blur-xl shadow-[0_0_50px_-15px_rgba(12,96,96,0.15)] overflow-hidden"
|
|
||||||
>
|
|
||||||
{/* Top Window Chrome Bar */}
|
{/* Top Window Chrome Bar */}
|
||||||
<div className="flex items-center justify-between px-6 py-4 border-b border-border/50 bg-background/50">
|
<div className="flex items-center justify-between px-6 py-4 border-b border-border/50 bg-background/50">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -303,7 +238,7 @@ export default function MarketingPage() {
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Infinite Trust Bar (commented out)
|
{/* Infinite Trust Bar (commented out)
|
||||||
@@ -334,25 +269,13 @@ export default function MarketingPage() {
|
|||||||
|
|
||||||
{/* Animated DPP Engine Section */}
|
{/* Animated DPP Engine Section */}
|
||||||
<section id="features" className="py-32 px-6 max-w-7xl mx-auto">
|
<section id="features" className="py-32 px-6 max-w-7xl mx-auto">
|
||||||
<motion.div
|
<div className="text-center mb-20">
|
||||||
initial={{ opacity: 0, y: 40 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true, margin: "-100px" }}
|
|
||||||
transition={{ duration: 0.8 }}
|
|
||||||
className="text-center mb-20"
|
|
||||||
>
|
|
||||||
<h2 className="text-4xl md:text-6xl font-bold tracking-tighter mb-6">Adaptive Engine</h2>
|
<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>
|
<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>
|
</div>
|
||||||
|
|
||||||
<motion.div
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||||
variants={containerVariants}
|
<div>
|
||||||
initial="hidden"
|
|
||||||
whileInView="visible"
|
|
||||||
viewport={{ once: true, margin: "-50px" }}
|
|
||||||
className="grid grid-cols-1 md:grid-cols-3 gap-8"
|
|
||||||
>
|
|
||||||
<motion.div variants={itemVariants}>
|
|
||||||
<Card className="bg-secondary/10 backdrop-blur-xl border-border/50 hover:border-primary/50 transition-all duration-300 rounded-[2rem] h-full shadow-lg group">
|
<Card className="bg-secondary/10 backdrop-blur-xl border-border/50 hover:border-primary/50 transition-all duration-300 rounded-[2rem] h-full shadow-lg group">
|
||||||
<CardContent className="p-8 relative overflow-hidden">
|
<CardContent className="p-8 relative overflow-hidden">
|
||||||
<div className="absolute top-0 right-0 p-8 opacity-5 group-hover:opacity-10 transition-opacity transform group-hover:scale-110 group-hover:-rotate-12 duration-500">
|
<div className="absolute top-0 right-0 p-8 opacity-5 group-hover:opacity-10 transition-opacity transform group-hover:scale-110 group-hover:-rotate-12 duration-500">
|
||||||
@@ -367,9 +290,9 @@ export default function MarketingPage() {
|
|||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</motion.div>
|
</div>
|
||||||
|
|
||||||
<motion.div variants={itemVariants}>
|
<div>
|
||||||
<Card className="bg-[var(--cyan-ai-10)] backdrop-blur-xl border-[var(--cyan-ai-20)] hover:border-[var(--cyan-ai-50)] transition-all duration-300 rounded-[2rem] h-full shadow-[0_0_30px_-15px_var(--cyan-ai)] group">
|
<Card className="bg-[var(--cyan-ai-10)] backdrop-blur-xl border-[var(--cyan-ai-20)] hover:border-[var(--cyan-ai-50)] transition-all duration-300 rounded-[2rem] h-full shadow-[0_0_30px_-15px_var(--cyan-ai)] group">
|
||||||
<CardContent className="p-8 relative overflow-hidden">
|
<CardContent className="p-8 relative overflow-hidden">
|
||||||
<div className="absolute top-0 right-0 p-8 opacity-5 group-hover:opacity-10 transition-opacity transform group-hover:scale-110 group-hover:rotate-12 duration-500">
|
<div className="absolute top-0 right-0 p-8 opacity-5 group-hover:opacity-10 transition-opacity transform group-hover:scale-110 group-hover:rotate-12 duration-500">
|
||||||
@@ -384,9 +307,9 @@ export default function MarketingPage() {
|
|||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</motion.div>
|
</div>
|
||||||
|
|
||||||
<motion.div variants={itemVariants}>
|
<div>
|
||||||
<Card className="bg-secondary/10 backdrop-blur-xl border-border/50 hover:border-primary/50 transition-all duration-300 rounded-[2rem] h-full shadow-lg group">
|
<Card className="bg-secondary/10 backdrop-blur-xl border-border/50 hover:border-primary/50 transition-all duration-300 rounded-[2rem] h-full shadow-lg group">
|
||||||
<CardContent className="p-8 relative overflow-hidden">
|
<CardContent className="p-8 relative overflow-hidden">
|
||||||
<div className="absolute top-0 right-0 p-8 opacity-5 group-hover:opacity-10 transition-opacity transform group-hover:scale-110 group-hover:-rotate-12 duration-500">
|
<div className="absolute top-0 right-0 p-8 opacity-5 group-hover:opacity-10 transition-opacity transform group-hover:scale-110 group-hover:-rotate-12 duration-500">
|
||||||
@@ -401,56 +324,22 @@ export default function MarketingPage() {
|
|||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</motion.div>
|
</div>
|
||||||
</motion.div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Animated Pricing */}
|
{/* Pricing */}
|
||||||
<section id="pricing" className="py-24 px-6 max-w-7xl mx-auto relative">
|
<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>
|
<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
|
<div className="text-center mb-20">
|
||||||
initial={{ opacity: 0, y: 40 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true }}
|
|
||||||
transition={{ duration: 0.8 }}
|
|
||||||
className="text-center mb-20"
|
|
||||||
>
|
|
||||||
<h2 className="text-4xl md:text-6xl font-bold tracking-tighter mb-6">Simple, Transparent Pricing</h2>
|
<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>
|
<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>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 max-w-4xl mx-auto items-stretch">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 max-w-4xl mx-auto items-stretch">
|
||||||
{/* 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-6 rounded-[2rem] 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-6 text-base">Perfect to get started.</p>
|
|
||||||
<div className="text-6xl font-bold tracking-tighter mb-6">₹0<span className="text-xl text-white/60 font-medium tracking-normal ml-1">/year</span></div>
|
|
||||||
<ul className="space-y-3 mb-6 flex-1 font-medium text-lg">
|
|
||||||
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-white" /> 1 Daily Practice Sheet</li>
|
|
||||||
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-white" /> Basic Performance Analytics</li>
|
|
||||||
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-white" /> Community Access</li>
|
|
||||||
</ul>
|
|
||||||
<Link href="/login">
|
|
||||||
<Button variant="outline" size="lg" className="w-full rounded-2xl h-12 border-white text-white font-bold text-lg hover:bg-secondary hover:text-white">Login</Button>
|
|
||||||
</Link>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
*/}
|
|
||||||
|
|
||||||
{/* Pro Plan */}
|
{/* Pro Plan */}
|
||||||
<motion.div
|
<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-6 rounded-[2rem] 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-6 rounded-[2rem] flex flex-col hover:border-border/80 transition-colors h-full z-10 relative shadow-xl">
|
||||||
<h3 className="text-3xl font-bold mb-2 text-yellow-500">Pro</h3>
|
<h3 className="text-3xl font-bold mb-2 text-yellow-500">Pro</h3>
|
||||||
<p className="text-muted-foreground mb-6 text-base">Perfect for self-paced practice.</p>
|
<p className="text-muted-foreground mb-6 text-base">Perfect for self-paced practice.</p>
|
||||||
@@ -465,31 +354,25 @@ export default function MarketingPage() {
|
|||||||
<Button size="lg" className="w-full rounded-2xl h-12 bg-yellow-500 text-black font-bold text-lg hover:bg-yellow-400">Get Pro</Button>
|
<Button size="lg" className="w-full rounded-2xl h-12 bg-yellow-500 text-black font-bold text-lg hover:bg-yellow-400">Get Pro</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</Card>
|
</Card>
|
||||||
</motion.div>
|
</div>
|
||||||
|
|
||||||
{/* AI Coach Plan */}
|
{/* AI Coach Plan */}
|
||||||
<motion.div
|
<div className="relative mt-8 lg:mt-0">
|
||||||
initial={{ opacity: 0, x: 50 }}
|
<Card className="bg-background/80 backdrop-blur-md border-border p-6 rounded-[2rem] flex flex-col hover:border-border/80 transition-colors h-full z-10 relative shadow-xl">
|
||||||
whileInView={{ opacity: 1, x: 0 }}
|
<h3 className="text-3xl font-bold mb-2 text-[var(--cyan-ai)]">AI Coach</h3>
|
||||||
viewport={{ once: true }}
|
<p className="text-muted-foreground mb-6 text-base">The complete AI-driven system.</p>
|
||||||
transition={{ duration: 0.6, type: "spring", delay: 0.2 }}
|
<div className="text-6xl font-bold tracking-tighter mb-6 text-[var(--cyan-ai)]">₹11,999<span className="text-xl text-[var(--cyan-ai-50)] font-medium tracking-normal ml-1">/year</span></div>
|
||||||
className="relative mt-8 lg:mt-0"
|
<ul className="space-y-3 mb-6 flex-1 font-medium text-lg">
|
||||||
>
|
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-[var(--cyan-ai)]" /> Everything in Pro</li>
|
||||||
<Card className="bg-background/80 backdrop-blur-md border-border p-6 rounded-[2rem] flex flex-col hover:border-border/80 transition-colors h-full z-10 relative shadow-xl">
|
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-[var(--cyan-ai)]" /> AI Spaced Repetition (Mistakes)</li>
|
||||||
<h3 className="text-3xl font-bold mb-2 text-[var(--cyan-ai)]">AI Coach</h3>
|
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-[var(--cyan-ai)]" /> 24/7 Streaming AI Tutor Chat</li>
|
||||||
<p className="text-muted-foreground mb-6 text-base">The complete AI-driven system.</p>
|
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-[var(--cyan-ai)]" /> Predictive Rank & College Predictor</li>
|
||||||
<div className="text-6xl font-bold tracking-tighter mb-6 text-[var(--cyan-ai)]">₹11,999<span className="text-xl text-[var(--cyan-ai-50)] font-medium tracking-normal ml-1">/year</span></div>
|
</ul>
|
||||||
<ul className="space-y-3 mb-6 flex-1 font-medium text-lg">
|
<Link href="/login">
|
||||||
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-[var(--cyan-ai)]" /> Everything in Pro</li>
|
<Button size="lg" className="w-full rounded-2xl h-12 font-bold text-lg bg-[var(--cyan-ai)] text-black hover:bg-[var(--cyan-ai-80)] transition-all">Get AI Coach</Button>
|
||||||
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-[var(--cyan-ai)]" /> AI Spaced Repetition (Mistakes)</li>
|
</Link>
|
||||||
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-[var(--cyan-ai)]" /> 24/7 Streaming AI Tutor Chat</li>
|
</Card>
|
||||||
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-[var(--cyan-ai)]" /> Predictive Rank & College Predictor</li>
|
</div>
|
||||||
</ul>
|
|
||||||
<Link href="/login">
|
|
||||||
<Button size="lg" className="w-full rounded-2xl h-12 font-bold text-lg bg-[var(--cyan-ai)] text-black hover:bg-[var(--cyan-ai-80)] transition-all">Get AI Coach</Button>
|
|
||||||
</Link>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-center mt-8 text-sm text-muted-foreground font-medium w-full">
|
<p className="text-center mt-8 text-sm text-muted-foreground font-medium w-full">
|
||||||
* All above prices are inclusive of GST.
|
* All above prices are inclusive of GST.
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export default function SignupPage() {
|
|||||||
<ArrowLeft size={16} /> Back to Home
|
<ArrowLeft size={16} /> Back to Home
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div className="w-full max-w-md space-y-8 animate-in fade-in slide-in-from-bottom-8 duration-700">
|
<div className="w-full max-w-md space-y-8">
|
||||||
<div className="flex justify-center mb-8">
|
<div className="flex justify-center mb-8">
|
||||||
<Logo href="/" className="w-72" imageClassName="object-contain" height={80} />
|
<Logo href="/" className="w-72" imageClassName="object-contain" height={80} />
|
||||||
</div>
|
</div>
|
||||||
@@ -140,7 +140,7 @@ export default function SignupPage() {
|
|||||||
<CardContent className="space-y-6 pt-4">
|
<CardContent className="space-y-6 pt-4">
|
||||||
|
|
||||||
{step === 1 ? (
|
{step === 1 ? (
|
||||||
<div className="space-y-4 animate-in fade-in zoom-in-95">
|
<div className="space-y-4">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-bold text-foreground">First Name</label>
|
<label className="text-sm font-bold text-foreground">First Name</label>
|
||||||
@@ -185,7 +185,7 @@ export default function SignupPage() {
|
|||||||
|
|
||||||
{/* OTP verification section temporarily disabled
|
{/* OTP verification section temporarily disabled
|
||||||
{showOTP && (
|
{showOTP && (
|
||||||
<div className="space-y-2 animate-in slide-in-from-top-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-bold text-foreground flex justify-between">
|
<label className="text-sm font-bold text-foreground flex justify-between">
|
||||||
<span>Verification Code</span>
|
<span>Verification Code</span>
|
||||||
<span className="text-primary text-xs">Resend in 30s</span>
|
<span className="text-primary text-xs">Resend in 30s</span>
|
||||||
@@ -224,7 +224,7 @@ export default function SignupPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4 animate-in fade-in slide-in-from-right-8">
|
<div className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-bold text-foreground">Target Exam</label>
|
<label className="text-sm font-bold text-foreground">Target Exam</label>
|
||||||
<select
|
<select
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card"
|
||||||
@@ -21,7 +21,7 @@ export default function UpgradePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-8 max-w-7xl mx-auto animate-in fade-in duration-500 min-h-screen">
|
<div className="p-8 max-w-7xl mx-auto min-h-screen">
|
||||||
<div className="text-center max-w-2xl mx-auto mb-16">
|
<div className="text-center max-w-2xl mx-auto mb-16">
|
||||||
<h1 className="text-4xl font-extrabold tracking-tight mb-4">Upgrade Your Preparation</h1>
|
<h1 className="text-4xl font-extrabold tracking-tight mb-4">Upgrade Your Preparation</h1>
|
||||||
<p className="text-xl text-muted-foreground">Unlock the full power of PragatiVeer to master JEE & NEET.</p>
|
<p className="text-xl text-muted-foreground">Unlock the full power of PragatiVeer to master JEE & NEET.</p>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState, useEffect } from "react"
|
import { useState, useEffect } from "react"
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
@@ -32,14 +32,10 @@ const NEW_USER_PLAN: PlanItem[] = [
|
|||||||
export function AdaptiveStudyPlan() {
|
export function AdaptiveStudyPlan() {
|
||||||
const { user } = useActiveUser();
|
const { user } = useActiveUser();
|
||||||
const isDemo = isDemoUser(user?.id);
|
const isDemo = isDemoUser(user?.id);
|
||||||
const [items, setItems] = useState<PlanItem[]>(DEMO_PLAN);
|
const [items, setItems] = useState<PlanItem[]>(() => isDemo ? DEMO_PLAN : NEW_USER_PLAN);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isDemo) {
|
setItems(isDemo ? DEMO_PLAN : NEW_USER_PLAN);
|
||||||
setItems(DEMO_PLAN);
|
|
||||||
} else {
|
|
||||||
setItems(NEW_USER_PLAN);
|
|
||||||
}
|
|
||||||
}, [isDemo]);
|
}, [isDemo]);
|
||||||
|
|
||||||
const updateStatus = (id: string, newStatus: PlanState) => {
|
const updateStatus = (id: string, newStatus: PlanState) => {
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ const ZERO_HEATMAP_DATA = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export function MistakeHeatmap() {
|
export function MistakeHeatmap() {
|
||||||
const { userId } = useActiveUser();
|
const { userId, user } = useActiveUser();
|
||||||
const isDemo = isDemoUser(userId);
|
const isDemo = isDemoUser(user?.id || userId);
|
||||||
|
|
||||||
const getColor = (errorRate: number) => {
|
const getColor = (errorRate: number) => {
|
||||||
if (errorRate === 0) return 'bg-green-600/50 border-green-600/80 text-green-100';
|
if (errorRate === 0) return 'bg-green-600/50 border-green-600/80 text-green-100';
|
||||||
@@ -71,7 +71,7 @@ export function MistakeHeatmap() {
|
|||||||
{row.attempts.map((errRate, i) => (
|
{row.attempts.map((errRate, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className={`h-8 sm:h-9 rounded-sm sm:rounded-md border transition-all duration-300 group-hover:scale-105 flex items-center justify-center cursor-help ${getColor(errRate)}`}
|
className={`h-8 sm:h-9 rounded-sm sm:rounded-md border flex items-center justify-center cursor-help ${getColor(errRate)}`}
|
||||||
title={`Error Rate: ${errRate}%`}
|
title={`Error Rate: ${errRate}%`}
|
||||||
>
|
>
|
||||||
<span className="text-[8px] sm:text-xs font-black text-foreground/90">{errRate}%</span>
|
<span className="text-[8px] sm:text-xs font-black text-foreground/90">{errRate}%</span>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { fetchWithAuth } from "@/lib/apiClient";
|
import { fetchWithAuth } from "@/lib/apiClient";
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
@@ -103,7 +103,6 @@ export function ReadinessGauge() {
|
|||||||
strokeDasharray={circumference}
|
strokeDasharray={circumference}
|
||||||
strokeDashoffset={isZero ? circumference : strokeDashoffset}
|
strokeDashoffset={isZero ? circumference : strokeDashoffset}
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
className="transition-all duration-1500 ease-out"
|
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<div className="absolute inset-0 flex flex-col items-center justify-center text-center mt-1">
|
<div className="absolute inset-0 flex flex-col items-center justify-center text-center mt-1">
|
||||||
@@ -156,7 +155,7 @@ export function ReadinessGauge() {
|
|||||||
<span className="text-xs font-bold text-muted-foreground w-24 shrink-0 truncate">{sub.name}</span>
|
<span className="text-xs font-bold text-muted-foreground w-24 shrink-0 truncate">{sub.name}</span>
|
||||||
<div className="flex-1 h-2 rounded-full bg-white/10 overflow-hidden">
|
<div className="flex-1 h-2 rounded-full bg-white/10 overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className="h-full rounded-full transition-all duration-1000"
|
className="h-full rounded-full"
|
||||||
style={{ width: `${sub.val}%`, background: sub.color }}
|
style={{ width: `${sub.val}%`, background: sub.color }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export function ReferralCard() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="h-2 w-full bg-secondary rounded-full overflow-hidden">
|
<div className="h-2 w-full bg-secondary rounded-full overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className="h-full bg-gradient-to-r from-primary to-teal-400 rounded-full transition-all duration-500"
|
className="h-full bg-gradient-to-r from-primary to-teal-400 rounded-full"
|
||||||
style={{ width: `${Math.min((referralCount / nextTier) * 100, 100)}%` }}
|
style={{ width: `${Math.min((referralCount / nextTier) * 100, 100)}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ export function StudyPlanCard() {
|
|||||||
{/* Progress bar */}
|
{/* Progress bar */}
|
||||||
<div className="h-[3px] bg-[var(--cyan-ai-10)] w-full">
|
<div className="h-[3px] bg-[var(--cyan-ai-10)] w-full">
|
||||||
<div
|
<div
|
||||||
className="h-full bg-gradient-to-r from-[var(--cyan-ai)] to-teal-400 rounded-r-[2px] transition-all duration-500"
|
className="h-full bg-gradient-to-r from-[var(--cyan-ai)] to-teal-400 rounded-r-[2px]"
|
||||||
style={{ width: `${progressPercent}%` }}
|
style={{ width: `${progressPercent}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { Card, CardContent } from "@/components/ui/card"
|
import { Card, CardContent } from "@/components/ui/card"
|
||||||
import { Flame, Star, Clock, TrendingUp, Sparkles } from "lucide-react"
|
import { Flame, Star, Clock, TrendingUp, Sparkles } from "lucide-react"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
@@ -92,7 +92,7 @@ export function StudyTimeOptimizationCard() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className={`w-full rounded-t-md transition-all duration-300 ${
|
className={`w-full rounded-t-md ${
|
||||||
slot.peak
|
slot.peak
|
||||||
? "bg-gradient-to-t from-yellow-600/80 to-yellow-400 shadow-[0_0_14px_rgba(234,179,8,0.5)]"
|
? "bg-gradient-to-t from-yellow-600/80 to-yellow-400 shadow-[0_0_14px_rgba(234,179,8,0.5)]"
|
||||||
: hovered === i
|
: hovered === i
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { TrendingUp, TrendingDown, Brain, Zap, Target, Clock } from "lucide-react"
|
import { TrendingUp, TrendingDown, Brain, Zap, Target, Clock } from "lucide-react"
|
||||||
import { Card, CardContent } from "@/components/ui/card"
|
import { Card, CardContent } from "@/components/ui/card"
|
||||||
@@ -97,7 +97,7 @@ export function WeeklyProgressReport() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className={`w-full rounded-t-md transition-all duration-700 ${
|
className={`w-full rounded-t-md ${
|
||||||
!isDemo
|
!isDemo
|
||||||
? "bg-white/5 border border-white/10"
|
? "bg-white/5 border border-white/10"
|
||||||
: i === 6
|
: i === 6
|
||||||
|
|||||||
@@ -64,8 +64,8 @@ export function Calculator({ isOpen, onClose }: CalculatorProps) {
|
|||||||
const handleBackspace = () => setDisplay((prev) => prev.slice(0, -1));
|
const handleBackspace = () => setDisplay((prev) => prev.slice(0, -1));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm animate-in fade-in duration-200">
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
||||||
<div className="bg-background border border-border shadow-2xl rounded-2xl w-[400px] overflow-hidden animate-in zoom-in-95 duration-200">
|
<div className="bg-background border border-border shadow-2xl rounded-2xl w-[400px] overflow-hidden">
|
||||||
<div className="p-4 border-b border-border flex justify-between items-center bg-secondary/50">
|
<div className="p-4 border-b border-border flex justify-between items-center bg-secondary/50">
|
||||||
<h3 className="font-bold">Scientific Calculator</h3>
|
<h3 className="font-bold">Scientific Calculator</h3>
|
||||||
<button onClick={onClose} className="text-muted-foreground hover:text-foreground">
|
<button onClick={onClose} className="text-muted-foreground hover:text-foreground">
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export function ExamResult({ payload, attemptId, examType, onRetake }: ExamResul
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background flex flex-col items-center py-12 px-4 sm:px-6 animate-in fade-in zoom-in duration-500">
|
<div className="min-h-screen bg-background flex flex-col items-center py-12 px-4 sm:px-6">
|
||||||
<div className="max-w-4xl w-full space-y-8">
|
<div className="max-w-4xl w-full space-y-8">
|
||||||
|
|
||||||
<div className="text-center space-y-4">
|
<div className="text-center space-y-4">
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ export function PeriodicTable({ isOpen, onClose }: PeriodicTableProps) {
|
|||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
return (
|
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="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-md">
|
||||||
<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="bg-background border border-border shadow-2xl rounded-2xl overflow-hidden 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">
|
<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>
|
<h3 className="font-bold text-lg">Periodic Table of Elements</h3>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
|||||||
@@ -123,8 +123,8 @@ export function Scratchpad({ isOpen, onClose }: ScratchpadProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm animate-in fade-in duration-200 p-8">
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-8">
|
||||||
<div className="bg-background border border-border shadow-2xl rounded-2xl overflow-hidden animate-in zoom-in-95 duration-200 flex flex-col w-full max-w-6xl h-full max-h-[800px]">
|
<div className="bg-background border border-border shadow-2xl rounded-2xl overflow-hidden flex flex-col w-full max-w-6xl h-full max-h-[800px]">
|
||||||
<div className="p-4 border-b border-border flex justify-between items-center bg-secondary/50">
|
<div className="p-4 border-b border-border flex justify-between items-center bg-secondary/50">
|
||||||
<h3 className="font-bold flex items-center gap-2"><Pen size={18}/> Split Workspace: Draw & Type</h3>
|
<h3 className="font-bold flex items-center gap-2"><Pen size={18}/> Split Workspace: Draw & Type</h3>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
|
|||||||
317
src/components/CalendarFilter.tsx
Normal file
317
src/components/CalendarFilter.tsx
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import React, { useState, useRef, useEffect } from "react"
|
||||||
|
import { Calendar as CalendarIcon, ChevronLeft, ChevronRight, X, Sparkles, Check } from "lucide-react"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
|
||||||
|
export interface CalendarFilterProps {
|
||||||
|
selectedDate: string // Format: "YYYY-MM-DD" or ""
|
||||||
|
onSelectDate: (date: string) => void
|
||||||
|
availableDates?: string[] // Optional array of "YYYY-MM-DD"
|
||||||
|
placeholder?: string
|
||||||
|
className?: string
|
||||||
|
align?: "left" | "right"
|
||||||
|
}
|
||||||
|
|
||||||
|
import { getISTDateString } from "@/lib/dateUtils"
|
||||||
|
|
||||||
|
/** Helper to format any date safely in IST (Asia/Kolkata) as "YYYY-MM-DD" */
|
||||||
|
export function getLocalDateString(dateInput: string | Date | null | undefined): string {
|
||||||
|
return getISTDateString(dateInput)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CalendarFilter({
|
||||||
|
selectedDate,
|
||||||
|
onSelectDate,
|
||||||
|
availableDates = [],
|
||||||
|
placeholder = "Filter by Date",
|
||||||
|
className = "",
|
||||||
|
align = "left",
|
||||||
|
}: CalendarFilterProps) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
// Current view year & month for calendar navigation
|
||||||
|
const today = new Date()
|
||||||
|
const [viewYear, setViewYear] = useState(() => {
|
||||||
|
if (selectedDate) {
|
||||||
|
const parts = selectedDate.split("-").map(Number)
|
||||||
|
if (parts[0]) return parts[0]
|
||||||
|
}
|
||||||
|
return today.getFullYear()
|
||||||
|
})
|
||||||
|
|
||||||
|
const [viewMonth, setViewMonth] = useState(() => {
|
||||||
|
if (selectedDate) {
|
||||||
|
const parts = selectedDate.split("-").map(Number)
|
||||||
|
if (parts[1]) return parts[1] - 1
|
||||||
|
}
|
||||||
|
return today.getMonth()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Sync view when selectedDate changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedDate) {
|
||||||
|
const parts = selectedDate.split("-").map(Number)
|
||||||
|
if (parts[0] && parts[1]) {
|
||||||
|
setViewYear(parts[0])
|
||||||
|
setViewMonth(parts[1] - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [selectedDate])
|
||||||
|
|
||||||
|
// Click outside listener
|
||||||
|
useEffect(() => {
|
||||||
|
function handleClickOutside(event: MouseEvent) {
|
||||||
|
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
||||||
|
setIsOpen(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isOpen) {
|
||||||
|
document.addEventListener("mousedown", handleClickOutside)
|
||||||
|
}
|
||||||
|
return () => document.removeEventListener("mousedown", handleClickOutside)
|
||||||
|
}, [isOpen])
|
||||||
|
|
||||||
|
const prevMonth = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
if (viewMonth === 0) {
|
||||||
|
setViewMonth(11)
|
||||||
|
setViewYear((y) => y - 1)
|
||||||
|
} else {
|
||||||
|
setViewMonth((m) => m - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextMonth = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
if (viewMonth === 11) {
|
||||||
|
setViewMonth(0)
|
||||||
|
setViewYear((y) => y + 1)
|
||||||
|
} else {
|
||||||
|
setViewMonth((m) => m + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSelectDay = (day: number) => {
|
||||||
|
const formatted = `${viewYear}-${String(viewMonth + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`
|
||||||
|
if (selectedDate === formatted) {
|
||||||
|
onSelectDate("")
|
||||||
|
} else {
|
||||||
|
onSelectDate(formatted)
|
||||||
|
}
|
||||||
|
setIsOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setQuickDate = (dateStr: string) => {
|
||||||
|
onSelectDate(dateStr)
|
||||||
|
setIsOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Days calculations
|
||||||
|
const firstDayOfWeek = new Date(viewYear, viewMonth, 1).getDay() // 0 = Sun
|
||||||
|
const totalDaysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate()
|
||||||
|
const monthNames = [
|
||||||
|
"January", "February", "March", "April", "May", "June",
|
||||||
|
"July", "August", "September", "October", "November", "December"
|
||||||
|
]
|
||||||
|
|
||||||
|
const todayStr = getLocalDateString(today)
|
||||||
|
const tomorrowStr = getLocalDateString(new Date(Date.now() + 86400000))
|
||||||
|
|
||||||
|
const formatDisplay = (dateStr: string) => {
|
||||||
|
if (!dateStr) return placeholder
|
||||||
|
const parts = dateStr.split("-").map(Number)
|
||||||
|
if (parts.length === 3) {
|
||||||
|
const d = new Date(parts[0], parts[1] - 1, parts[2])
|
||||||
|
if (dateStr === todayStr) {
|
||||||
|
return `Today (${d.toLocaleDateString("en-US", { month: "short", day: "numeric" })})`
|
||||||
|
}
|
||||||
|
if (dateStr === tomorrowStr) {
|
||||||
|
return `Tomorrow (${d.toLocaleDateString("en-US", { month: "short", day: "numeric" })})`
|
||||||
|
}
|
||||||
|
return d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })
|
||||||
|
}
|
||||||
|
return dateStr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Available dates set for quick lookup
|
||||||
|
const availableSet = new Set(availableDates.map((d) => d.split("T")[0]))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`relative inline-block ${className}`} ref={containerRef}>
|
||||||
|
{/* Trigger Button */}
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
|
className={`flex items-center gap-2 px-3.5 py-2 rounded-xl text-xs font-bold transition-all border shadow-sm ${
|
||||||
|
selectedDate
|
||||||
|
? "bg-[var(--cyan-ai-10)] text-[var(--cyan-ai)] border-[var(--cyan-ai-30)] shadow-[0_0_12px_rgba(12,96,96,0.15)]"
|
||||||
|
: "bg-background text-muted-foreground border-border hover:border-primary/40 hover:text-foreground"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="p-1 rounded-md bg-secondary/80 text-[var(--cyan-ai)]">
|
||||||
|
<CalendarIcon size={14} />
|
||||||
|
</div>
|
||||||
|
<span className="font-semibold">{formatDisplay(selectedDate)}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{selectedDate && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
onSelectDate("")
|
||||||
|
}}
|
||||||
|
title="Clear date filter"
|
||||||
|
className="p-1.5 rounded-lg text-muted-foreground hover:text-foreground hover:bg-secondary transition-colors"
|
||||||
|
>
|
||||||
|
<X size={14} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Popover Calendar */}
|
||||||
|
{isOpen && (
|
||||||
|
<div
|
||||||
|
className={`absolute ${
|
||||||
|
align === "right" ? "right-0" : "left-0"
|
||||||
|
} top-full mt-2 w-72 sm:w-80 p-4 bg-card border border-border/80 rounded-2xl shadow-2xl shadow-black/30 z-50 origin-top backdrop-blur-xl`}
|
||||||
|
>
|
||||||
|
{/* Quick presets */}
|
||||||
|
<div className="flex items-center gap-1 mb-3.5 pb-3 border-b border-border/60 overflow-x-auto custom-scrollbar">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setQuickDate("")}
|
||||||
|
className={`px-2.5 py-1 rounded-lg text-[11px] font-bold shrink-0 transition-colors ${
|
||||||
|
!selectedDate
|
||||||
|
? "bg-primary text-primary-foreground"
|
||||||
|
: "bg-secondary/60 text-muted-foreground hover:text-foreground"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
All Dates
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setQuickDate(todayStr)}
|
||||||
|
className={`px-2.5 py-1 rounded-lg text-[11px] font-bold shrink-0 transition-colors ${
|
||||||
|
selectedDate === todayStr
|
||||||
|
? "bg-[var(--cyan-ai)] text-black"
|
||||||
|
: "bg-secondary/60 text-muted-foreground hover:text-foreground"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Today
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setQuickDate(tomorrowStr)}
|
||||||
|
className={`px-2.5 py-1 rounded-lg text-[11px] font-bold shrink-0 transition-colors ${
|
||||||
|
selectedDate === tomorrowStr
|
||||||
|
? "bg-[var(--cyan-ai)] text-black"
|
||||||
|
: "bg-secondary/60 text-muted-foreground hover:text-foreground"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Tomorrow
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Month / Year Navigation */}
|
||||||
|
<div className="flex items-center justify-between mb-3 px-1">
|
||||||
|
<h4 className="font-extrabold text-sm text-foreground">
|
||||||
|
{monthNames[viewMonth]} {viewYear}
|
||||||
|
</h4>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={prevMonth}
|
||||||
|
className="p-1.5 rounded-lg hover:bg-secondary text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
title="Previous Month"
|
||||||
|
>
|
||||||
|
<ChevronLeft size={16} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={nextMonth}
|
||||||
|
className="p-1.5 rounded-lg hover:bg-secondary text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
title="Next Month"
|
||||||
|
>
|
||||||
|
<ChevronRight size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Days of Week Header */}
|
||||||
|
<div className="grid grid-cols-7 gap-1 text-center mb-1">
|
||||||
|
{["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"].map((d) => (
|
||||||
|
<span key={d} className="text-[10px] font-black uppercase text-muted-foreground/70 py-1">
|
||||||
|
{d}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Days Grid */}
|
||||||
|
<div className="grid grid-cols-7 gap-1">
|
||||||
|
{Array.from({ length: firstDayOfWeek }).map((_, i) => (
|
||||||
|
<div key={`empty-${i}`} className="h-8 w-8" />
|
||||||
|
))}
|
||||||
|
|
||||||
|
{Array.from({ length: totalDaysInMonth }).map((_, i) => {
|
||||||
|
const day = i + 1
|
||||||
|
const currentDayStr = `${viewYear}-${String(viewMonth + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`
|
||||||
|
const isSelected = selectedDate === currentDayStr
|
||||||
|
const isToday = currentDayStr === todayStr
|
||||||
|
const hasItems = availableSet.has(currentDayStr)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={day}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleSelectDay(day)}
|
||||||
|
className={`h-8 w-8 mx-auto rounded-lg text-xs font-bold relative flex flex-col items-center justify-center transition-all ${
|
||||||
|
isSelected
|
||||||
|
? "bg-[var(--cyan-ai)] text-black font-black shadow-md scale-105"
|
||||||
|
: isToday
|
||||||
|
? "border border-[var(--cyan-ai)] text-[var(--cyan-ai)] hover:bg-secondary"
|
||||||
|
: "text-foreground hover:bg-secondary/80"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span>{day}</span>
|
||||||
|
{hasItems && (
|
||||||
|
<span
|
||||||
|
className={`absolute bottom-0.5 w-1 h-1 rounded-full ${
|
||||||
|
isSelected ? "bg-black" : "bg-[var(--cyan-ai)] shadow-[0_0_4px_var(--cyan-ai)]"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer stats if availableDates exists */}
|
||||||
|
{availableDates.length > 0 && (
|
||||||
|
<div className="mt-3.5 pt-2.5 border-t border-border/50 flex items-center justify-between text-[11px] text-muted-foreground px-1">
|
||||||
|
<span className="flex items-center gap-1.5">
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-[var(--cyan-ai)] inline-block"></span>
|
||||||
|
Scheduled Tests Available
|
||||||
|
</span>
|
||||||
|
{selectedDate && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
onSelectDate("")
|
||||||
|
setIsOpen(false)
|
||||||
|
}}
|
||||||
|
className="font-bold text-[var(--cyan-ai)] hover:underline"
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -129,12 +129,12 @@ export function StreakOnboardingModal() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/75 backdrop-blur-md animate-in fade-in duration-300"
|
className="fixed inset-0 z-50 flex items-center justify-center bg-black/75 backdrop-blur-md"
|
||||||
onClick={handleDismiss}
|
onClick={handleDismiss}
|
||||||
>
|
>
|
||||||
{/* Centered Modal Card */}
|
{/* Centered Modal Card */}
|
||||||
<div
|
<div
|
||||||
className="bg-background border border-border shadow-[0_0_60px_-10px_rgba(12,96,96,0.25)] rounded-3xl w-[90vw] max-w-5xl h-[85vh] max-h-[800px] relative overflow-hidden animate-in zoom-in-95 slide-in-from-bottom-4 duration-300 z-20 pointer-events-auto mx-4 flex flex-col justify-start"
|
className="bg-background border border-border shadow-[0_0_60px_-10px_rgba(12,96,96,0.25)] rounded-3xl w-[90vw] max-w-5xl h-[85vh] max-h-[800px] relative overflow-hidden z-20 pointer-events-auto mx-4 flex flex-col justify-start"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{/* Close button */}
|
{/* Close button */}
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Wrench } from "lucide-react";
|
import { Wrench } from "lucide-react";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
|
|
||||||
export default function MaintenanceScreen() {
|
export default function MaintenanceScreen() {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background flex flex-col items-center justify-center p-4">
|
<div className="min-h-screen bg-background flex flex-col items-center justify-center p-4">
|
||||||
<motion.div
|
<div className="max-w-md w-full bg-secondary/20 border border-border rounded-2xl p-8 text-center space-y-6">
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
className="max-w-md w-full bg-secondary/20 border border-border rounded-2xl p-8 text-center space-y-6"
|
|
||||||
>
|
|
||||||
<div className="mx-auto w-16 h-16 bg-yellow-500/10 text-yellow-500 rounded-full flex items-center justify-center mb-6">
|
<div className="mx-auto w-16 h-16 bg-yellow-500/10 text-yellow-500 rounded-full flex items-center justify-center mb-6">
|
||||||
<Wrench size={32} />
|
<Wrench size={32} />
|
||||||
</div>
|
</div>
|
||||||
@@ -26,7 +21,7 @@ export default function MaintenanceScreen() {
|
|||||||
We'll be back shortly. Thank you for your patience!
|
We'll be back shortly. Thank you for your patience!
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,14 +13,10 @@ interface PremiumGateProps {
|
|||||||
export function PremiumGate({ featureKey, children }: PremiumGateProps) {
|
export function PremiumGate({ featureKey, children }: PremiumGateProps) {
|
||||||
const { hasAccessTo, loading, user } = useActiveUser();
|
const { hasAccessTo, loading, user } = useActiveUser();
|
||||||
|
|
||||||
|
// Allow children to render immediately without a blocking center spinner
|
||||||
|
// so that pages mount instantly on route transition.
|
||||||
if (loading || !user) {
|
if (loading || !user) {
|
||||||
return (
|
return <>{children}</>;
|
||||||
<div className="flex-1 min-h-[400px] flex items-center justify-center">
|
|
||||||
<div className="flex flex-col items-center gap-4">
|
|
||||||
<div className="w-10 h-10 border-4 border-[var(--cyan-ai-20)] border-t-[var(--cyan-ai)] rounded-full animate-spin"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasAccessTo(featureKey)) {
|
if (!hasAccessTo(featureKey)) {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { Flame } from "lucide-react"
|
import { Flame } from "lucide-react"
|
||||||
import { motion } from "framer-motion"
|
|
||||||
|
|
||||||
interface StreakCounterProps {
|
interface StreakCounterProps {
|
||||||
streak: number
|
streak: number
|
||||||
@@ -9,15 +8,11 @@ interface StreakCounterProps {
|
|||||||
|
|
||||||
export function StreakCounter({ streak }: StreakCounterProps) {
|
export function StreakCounter({ streak }: StreakCounterProps) {
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<div
|
||||||
initial={{ scale: 0.5, opacity: 0, y: -10 }}
|
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 hover:scale-105 transition-transform"
|
||||||
animate={{ scale: 1, opacity: 1, y: 0 }}
|
|
||||||
whileHover={{ scale: 1.1, rotate: 5 }}
|
|
||||||
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" />
|
<Flame size={18} className="animate-pulse" />
|
||||||
<span>{streak}</span>
|
<span>{streak}</span>
|
||||||
</motion.div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|||||||
<button
|
<button
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex items-center justify-center whitespace-nowrap rounded-xl text-sm font-medium ring-offset-background transition-all duration-300 ease-in-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
"inline-flex items-center justify-center whitespace-nowrap rounded-xl text-sm font-medium ring-offset-background transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||||
{
|
{
|
||||||
"bg-primary text-primary-foreground hover:bg-primary/90 hover:scale-[1.02] active:scale-[0.98]": variant === "default",
|
"bg-primary text-primary-foreground hover:bg-primary/90": variant === "default",
|
||||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80": variant === "secondary",
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80": variant === "secondary",
|
||||||
"border border-border bg-background hover:bg-accent hover:text-accent-foreground": variant === "outline",
|
"border border-border bg-background hover:bg-accent hover:text-accent-foreground": variant === "outline",
|
||||||
"hover:bg-accent hover:text-accent-foreground": variant === "ghost",
|
"hover:bg-accent hover:text-accent-foreground": variant === "ghost",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElemen
|
|||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-2xl border border-border bg-background text-foreground transition-all duration-300",
|
"rounded-2xl border border-border bg-background text-foreground",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -28,17 +28,30 @@ export const DEMO_USERS = {
|
|||||||
AICOACH_JEE: { id: "11111111-1111-1111-1111-111111111111", label: "AI Coach - JEE" },
|
AICOACH_JEE: { id: "11111111-1111-1111-1111-111111111111", label: "AI Coach - JEE" },
|
||||||
AICOACH_NEET: { id: "22222222-2222-2222-2222-222222222222", label: "AI Coach - NEET" },
|
AICOACH_NEET: { id: "22222222-2222-2222-2222-222222222222", label: "AI Coach - NEET" },
|
||||||
AICOACH_JEE_NEET: { id: "33333333-3333-3333-3333-333333333333", label: "AI Coach - JEE+NEET" },
|
AICOACH_JEE_NEET: { id: "33333333-3333-3333-3333-333333333333", label: "AI Coach - JEE+NEET" },
|
||||||
FREE_JEE: { id: "44444444-4444-4444-4444-444444444444", label: "Free - JEE" },
|
FREE_JEE: { id: "44444444-4444-4444-4444-444444444444", label: "Free - JEE (New/Raw)" },
|
||||||
};
|
};
|
||||||
|
|
||||||
// IDs considered "demo" accounts — demo switcher only shows for these
|
// Showcase accounts that have pre-filled mock activity data for demonstration
|
||||||
|
export const SHOWCASE_DEMO_IDS = new Set([
|
||||||
|
DEMO_USERS.AICOACH_JEE.id,
|
||||||
|
DEMO_USERS.AICOACH_NEET.id,
|
||||||
|
DEMO_USERS.AICOACH_JEE_NEET.id,
|
||||||
|
"usr_01", "usr_02", "usr_03"
|
||||||
|
]);
|
||||||
|
|
||||||
|
// IDs allowed in the demo switcher bar
|
||||||
export const DEMO_IDS = new Set([
|
export const DEMO_IDS = new Set([
|
||||||
...Object.values(DEMO_USERS).map(u => u.id),
|
...Object.values(DEMO_USERS).map(u => u.id),
|
||||||
"usr_01", "usr_02", "usr_03", "usr_04", "usr_05"
|
"usr_01", "usr_02", "usr_03", "usr_04", "usr_05"
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/** Returns true if the userId matches a known demo account */
|
/** Returns true if the userId is a showcase account with pre-filled mock activity data */
|
||||||
export function isDemoUser(userId?: string | null): boolean {
|
export function isDemoUser(userId?: string | null): boolean {
|
||||||
|
return !!userId && SHOWCASE_DEMO_IDS.has(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns true if the userId is eligible for demo switcher dropdown */
|
||||||
|
export function isDemoAccount(userId?: string | null): boolean {
|
||||||
return !!userId && DEMO_IDS.has(userId);
|
return !!userId && DEMO_IDS.has(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,6 +147,88 @@ async function fetchSettings(): Promise<any> {
|
|||||||
return cachedSettings ?? null;
|
return cachedSettings ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let cachedUserProfile: UserProfile | null = null;
|
||||||
|
|
||||||
|
export function getKnownDemoProfile(id: string): UserProfile | null {
|
||||||
|
if (id === DEMO_USERS.AICOACH_JEE.id) {
|
||||||
|
return {
|
||||||
|
id: DEMO_USERS.AICOACH_JEE.id,
|
||||||
|
name: "Aarav",
|
||||||
|
email: "aarav.jee@pragativeer.com",
|
||||||
|
phone: "9999999991",
|
||||||
|
class: "12th",
|
||||||
|
target_exam: "JEE_Main",
|
||||||
|
medium: "English",
|
||||||
|
school: "Delhi Public School",
|
||||||
|
city: "New Delhi",
|
||||||
|
state: "Delhi",
|
||||||
|
tier: "AI Coach",
|
||||||
|
streak_count: 14,
|
||||||
|
longest_streak: 21,
|
||||||
|
created_at: new Date(Date.now() - 30 * 86400000).toISOString(),
|
||||||
|
trial_end: new Date(Date.now() + 60 * 86400000).toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (id === DEMO_USERS.AICOACH_NEET.id) {
|
||||||
|
return {
|
||||||
|
id: DEMO_USERS.AICOACH_NEET.id,
|
||||||
|
name: "Neha Verma",
|
||||||
|
email: "neha.neet@pragativeer.com",
|
||||||
|
phone: "9999999992",
|
||||||
|
class: "12th",
|
||||||
|
target_exam: "NEET",
|
||||||
|
medium: "English",
|
||||||
|
school: "Kendriya Vidyalaya",
|
||||||
|
city: "Kota",
|
||||||
|
state: "Rajasthan",
|
||||||
|
tier: "AI Coach",
|
||||||
|
streak_count: 18,
|
||||||
|
longest_streak: 25,
|
||||||
|
created_at: new Date(Date.now() - 30 * 86400000).toISOString(),
|
||||||
|
trial_end: new Date(Date.now() + 60 * 86400000).toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (id === DEMO_USERS.AICOACH_JEE_NEET.id) {
|
||||||
|
return {
|
||||||
|
id: DEMO_USERS.AICOACH_JEE_NEET.id,
|
||||||
|
name: "Rahul Verma",
|
||||||
|
email: "rahul.jeeneet@pragativeer.com",
|
||||||
|
phone: "9999999993",
|
||||||
|
class: "12th",
|
||||||
|
target_exam: "JEE_NEET",
|
||||||
|
medium: "English",
|
||||||
|
school: "Modern School",
|
||||||
|
city: "Mumbai",
|
||||||
|
state: "Maharashtra",
|
||||||
|
tier: "AI Coach",
|
||||||
|
streak_count: 12,
|
||||||
|
longest_streak: 15,
|
||||||
|
created_at: new Date(Date.now() - 30 * 86400000).toISOString(),
|
||||||
|
trial_end: new Date(Date.now() + 60 * 86400000).toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (id === DEMO_USERS.FREE_JEE.id) {
|
||||||
|
return {
|
||||||
|
id: DEMO_USERS.FREE_JEE.id,
|
||||||
|
name: "New Student",
|
||||||
|
email: "new.student@pragativeer.com",
|
||||||
|
phone: "9999999994",
|
||||||
|
class: "11th",
|
||||||
|
target_exam: "JEE_Main",
|
||||||
|
medium: "English",
|
||||||
|
school: "Army Public School",
|
||||||
|
city: "Jaipur",
|
||||||
|
state: "Rajasthan",
|
||||||
|
tier: "Free",
|
||||||
|
streak_count: 0,
|
||||||
|
longest_streak: 0,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
trial_end: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export function useActiveUser() {
|
export function useActiveUser() {
|
||||||
const [userId, setUserIdState] = useState<string>(() => {
|
const [userId, setUserIdState] = useState<string>(() => {
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
@@ -142,9 +237,32 @@ export function useActiveUser() {
|
|||||||
return DEFAULT_USER_ID;
|
return DEFAULT_USER_ID;
|
||||||
});
|
});
|
||||||
|
|
||||||
const [user, setUser] = useState<UserProfile | null>(null);
|
const [user, setUser] = useState<UserProfile | null>(() => {
|
||||||
const [globalSettings, setGlobalSettings] = useState<any>(null);
|
const currentId = (typeof window !== "undefined" ? localStorage.getItem("activeUserId") : null) || DEFAULT_USER_ID;
|
||||||
const [loading, setLoading] = useState(true);
|
if (cachedUserProfile && cachedUserProfile.id === currentId) {
|
||||||
|
return cachedUserProfile;
|
||||||
|
}
|
||||||
|
const demo = getKnownDemoProfile(currentId);
|
||||||
|
if (demo) {
|
||||||
|
cachedUserProfile = demo;
|
||||||
|
return demo;
|
||||||
|
}
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem("pragativeer_cached_user");
|
||||||
|
if (stored) {
|
||||||
|
const parsed = JSON.parse(stored);
|
||||||
|
if (parsed && parsed.id === currentId) {
|
||||||
|
cachedUserProfile = parsed;
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
const [globalSettings, setGlobalSettings] = useState<any>(() => cachedSettings);
|
||||||
|
const [loading, setLoading] = useState(() => !cachedUserProfile);
|
||||||
const [switching, setSwitching] = useState(false);
|
const [switching, setSwitching] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
// Use a ref for fail count so it doesn't cause re-renders
|
// Use a ref for fail count so it doesn't cause re-renders
|
||||||
@@ -192,13 +310,35 @@ export function useActiveUser() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cachedUserProfile = data;
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
try { localStorage.setItem("pragativeer_cached_user", JSON.stringify(data)); } catch {}
|
||||||
|
}
|
||||||
setUser(data);
|
setUser(data);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
failCountRef.current += 1;
|
failCountRef.current += 1;
|
||||||
if (!isPoll) {
|
if (!isPoll) {
|
||||||
console.warn("Backend unreachable, will retry:", err.message);
|
console.warn("Backend unreachable, using local demo profile for offline testing:", err.message);
|
||||||
setError("Backend unreachable — retrying...");
|
|
||||||
}
|
}
|
||||||
|
const fallback = getKnownDemoProfile(id) || {
|
||||||
|
id: id || DEFAULT_USER_ID,
|
||||||
|
name: "Aarav",
|
||||||
|
email: "demo@pragativeer.com",
|
||||||
|
phone: "9999999991",
|
||||||
|
class: "12th",
|
||||||
|
target_exam: "JEE_Main",
|
||||||
|
medium: "English",
|
||||||
|
school: "Delhi Public School",
|
||||||
|
city: "New Delhi",
|
||||||
|
state: "Delhi",
|
||||||
|
tier: "AI Coach",
|
||||||
|
streak_count: 14,
|
||||||
|
longest_streak: 21,
|
||||||
|
created_at: new Date(Date.now() - 5 * 86400000).toISOString(),
|
||||||
|
trial_end: new Date(Date.now() + 10 * 86400000).toISOString(),
|
||||||
|
};
|
||||||
|
if (!cachedUserProfile) cachedUserProfile = fallback;
|
||||||
|
setUser((prev) => prev || fallback);
|
||||||
// Silently swallow polling errors so the UI doesn't crash
|
// Silently swallow polling errors so the UI doesn't crash
|
||||||
} finally {
|
} finally {
|
||||||
if (!isPoll) setLoading(false);
|
if (!isPoll) setLoading(false);
|
||||||
@@ -221,7 +361,23 @@ export function useActiveUser() {
|
|||||||
if (typeof window === "undefined") return;
|
if (typeof window === "undefined") return;
|
||||||
|
|
||||||
const handleUpdate = () => fetchUserProfile(activeUserIdRef.current);
|
const handleUpdate = () => fetchUserProfile(activeUserIdRef.current);
|
||||||
|
const handleSwitched = (e: any) => {
|
||||||
|
const switchedId = e.detail?.userId;
|
||||||
|
const switchedUser = e.detail?.user;
|
||||||
|
if (switchedId) {
|
||||||
|
activeUserIdRef.current = switchedId;
|
||||||
|
setUserIdState(switchedId);
|
||||||
|
if (switchedUser) {
|
||||||
|
cachedUserProfile = switchedUser;
|
||||||
|
setUser(switchedUser);
|
||||||
|
} else {
|
||||||
|
fetchUserProfile(switchedId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
window.addEventListener("pragativeer-user-updated", handleUpdate);
|
window.addEventListener("pragativeer-user-updated", handleUpdate);
|
||||||
|
window.addEventListener("pragativeer-user-switched", handleSwitched);
|
||||||
|
|
||||||
// Poll every 30s (less aggressive, kinder on backend)
|
// Poll every 30s (less aggressive, kinder on backend)
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
@@ -230,6 +386,7 @@ export function useActiveUser() {
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener("pragativeer-user-updated", handleUpdate);
|
window.removeEventListener("pragativeer-user-updated", handleUpdate);
|
||||||
|
window.removeEventListener("pragativeer-user-switched", handleSwitched);
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
@@ -260,11 +417,13 @@ export function useActiveUser() {
|
|||||||
// Update refs and state before awaiting network
|
// Update refs and state before awaiting network
|
||||||
activeUserIdRef.current = actualUserId;
|
activeUserIdRef.current = actualUserId;
|
||||||
setUserIdState(actualUserId);
|
setUserIdState(actualUserId);
|
||||||
setUser(null); // Clear old user immediately so stale data isn't shown
|
const demo = getKnownDemoProfile(actualUserId);
|
||||||
|
cachedUserProfile = demo;
|
||||||
|
setUser(demo);
|
||||||
|
|
||||||
// Notify listeners immediately of account switch
|
// Notify listeners immediately of account switch
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
window.dispatchEvent(new CustomEvent("pragativeer-user-switched", { detail: { userId: actualUserId } }));
|
window.dispatchEvent(new CustomEvent("pragativeer-user-switched", { detail: { userId: actualUserId, user: demo } }));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch the new user's profile immediately using the new token
|
// Fetch the new user's profile immediately using the new token
|
||||||
@@ -272,6 +431,10 @@ export function useActiveUser() {
|
|||||||
|
|
||||||
if (newUserRes && newUserRes.ok) {
|
if (newUserRes && newUserRes.ok) {
|
||||||
const data = await newUserRes.json();
|
const data = await newUserRes.json();
|
||||||
|
cachedUserProfile = data;
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
try { localStorage.setItem("pragativeer_cached_user", JSON.stringify(data)); } catch {}
|
||||||
|
}
|
||||||
setUser(data);
|
setUser(data);
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
window.dispatchEvent(new CustomEvent("pragativeer-user-switched", { detail: { userId: actualUserId, user: data } }));
|
window.dispatchEvent(new CustomEvent("pragativeer-user-switched", { detail: { userId: actualUserId, user: data } }));
|
||||||
|
|||||||
108
src/lib/dateUtils.ts
Normal file
108
src/lib/dateUtils.ts
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
/**
|
||||||
|
* Unified Date & Time utilities configured for Indian Standard Time (IST, Asia/Kolkata).
|
||||||
|
* Pragativeer is an Indian EdTech platform where all test and DPP scheduling,
|
||||||
|
* calendar bounds, and student-facing timestamps operate strictly in IST.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const TIMEZONE_IST = "Asia/Kolkata";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely parse any date input (string, Date, or ISO string) into a Date object.
|
||||||
|
* If the string lacks an explicit timezone offset or Zulu indicator, it is
|
||||||
|
* treated as an Indian Standard Time (IST) timestamp (+05:30) so that server
|
||||||
|
* environments running in UTC do not shift the time by 5h 30m.
|
||||||
|
*/
|
||||||
|
export function parseAsISTDate(dateInput: string | Date | null | undefined): Date | null {
|
||||||
|
if (!dateInput) return null;
|
||||||
|
if (dateInput instanceof Date) {
|
||||||
|
return isNaN(dateInput.getTime()) ? null : dateInput;
|
||||||
|
}
|
||||||
|
let str = String(dateInput).trim();
|
||||||
|
if (!str) return null;
|
||||||
|
|
||||||
|
// If string has no timezone indicator (no 'Z' and no explicit +HH:MM or -HH:MM):
|
||||||
|
// e.g. "2026-10-24T05:30:00" or "2026-10-24 05:30:00"
|
||||||
|
if (!str.endsWith("Z") && !/[+-]\d{2}:?\d{2}$/.test(str)) {
|
||||||
|
str = str.replace(" ", "T");
|
||||||
|
str = `${str}+05:30`;
|
||||||
|
}
|
||||||
|
const d = new Date(str);
|
||||||
|
return isNaN(d.getTime()) ? null : d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a date in IST as "Oct 24, 2026, 5:30 AM"
|
||||||
|
*/
|
||||||
|
export function formatISTDateTime(dateInput: string | Date | null | undefined): string {
|
||||||
|
const d = parseAsISTDate(dateInput);
|
||||||
|
if (!d) return "";
|
||||||
|
return d.toLocaleString("en-US", {
|
||||||
|
timeZone: TIMEZONE_IST,
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
year: "numeric",
|
||||||
|
hour: "numeric",
|
||||||
|
minute: "2-digit",
|
||||||
|
hour12: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a date in IST as "Oct 24, 2026"
|
||||||
|
*/
|
||||||
|
export function formatISTDate(dateInput: string | Date | null | undefined): string {
|
||||||
|
const d = parseAsISTDate(dateInput);
|
||||||
|
if (!d) return "";
|
||||||
|
return d.toLocaleDateString("en-US", {
|
||||||
|
timeZone: TIMEZONE_IST,
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
year: "numeric",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a date in IST as "5:30 AM" or "5:30 PM"
|
||||||
|
*/
|
||||||
|
export function formatISTTime(dateInput: string | Date | null | undefined): string {
|
||||||
|
const d = parseAsISTDate(dateInput);
|
||||||
|
if (!d) return "";
|
||||||
|
return d.toLocaleTimeString("en-US", {
|
||||||
|
timeZone: TIMEZONE_IST,
|
||||||
|
hour: "numeric",
|
||||||
|
minute: "2-digit",
|
||||||
|
hour12: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a "YYYY-MM-DD" date string evaluated in Indian Standard Time (Asia/Kolkata).
|
||||||
|
* Useful for CalendarFilter matching and date grouping without UTC day boundary shifts.
|
||||||
|
*/
|
||||||
|
export function getISTDateString(dateInput: string | Date | null | undefined): string {
|
||||||
|
const d = parseAsISTDate(dateInput);
|
||||||
|
if (!d) return "";
|
||||||
|
const fmt = new Intl.DateTimeFormat("en-CA", {
|
||||||
|
timeZone: TIMEZONE_IST,
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
});
|
||||||
|
return fmt.format(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a Date object picked in DatePicker into an ISO-8601 string
|
||||||
|
* explicitly tagged with the IST offset (+05:30) so the backend and database
|
||||||
|
* preserve the exact hour and minute selected by the admin.
|
||||||
|
*/
|
||||||
|
export function toISTISOString(date: Date | null | undefined): string | null {
|
||||||
|
if (!date || isNaN(date.getTime())) return null;
|
||||||
|
const y = date.getFullYear();
|
||||||
|
const m = String(date.getMonth() + 1).padStart(2, "0");
|
||||||
|
const d = String(date.getDate()).padStart(2, "0");
|
||||||
|
const hh = String(date.getHours()).padStart(2, "0");
|
||||||
|
const mm = String(date.getMinutes()).padStart(2, "0");
|
||||||
|
const ss = String(date.getSeconds()).padStart(2, "0");
|
||||||
|
return `${y}-${m}-${d}T${hh}:${mm}:${ss}+05:30`;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user