feat: admin date-time calendar controller, dpp ui cleanup, 4-subject jee+neet tracker, calendar filter, and clean new account state

This commit is contained in:
2026-10-23 12:23:36 +05:30
parent 8a5ea57feb
commit 19edf83684
10 changed files with 1143 additions and 200 deletions

View File

@@ -5,9 +5,10 @@ import { Button } from "@/components/ui/button"
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 { useEffect, useState } from "react"
import { useEffect, useState, useMemo } from "react"
import Link from "next/link"
import { useActiveUser } from "@/hooks/useActiveUser"
import { CalendarFilter, getLocalDateString } from "@/components/CalendarFilter"
export default function DPPPage() {
const { user, userId, hasAccessTo } = useActiveUser()
@@ -80,20 +81,42 @@ export default function DPPPage() {
return acc;
}, {} 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 => {
// Only show CBT DPPs here (no PDF)
if (dpp.pdf_url) return false;
if (dateFilter) {
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;
}
return true;
});
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleDateString("en-US", { year: 'numeric', month: 'short', day: 'numeric' });
const formatDateTime = (dateStr: string) => {
return new Date(dateStr).toLocaleString("en-US", {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: '2-digit',
hour12: true
});
}
const formatTime = (dateStr: string) => {
return new Date(dateStr).toLocaleTimeString("en-US", {
hour: 'numeric',
minute: '2-digit',
hour12: true
});
}
const getPdfUrl = (title: string) => {
@@ -124,23 +147,19 @@ export default function DPPPage() {
</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 items-center gap-2">
<input
type="date"
value={dateFilter}
onChange={(e) => setDateFilter(e.target.value)}
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"
<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-3 flex-wrap">
<CalendarFilter
selectedDate={dateFilter}
onSelectDate={setDateFilter}
availableDates={scheduledDates}
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>
{['Mixed', 'Easy', 'Medium', 'Hard'].map((diff) => (
<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'}`}>
{diff}
</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>
{dateFilter && (
<span className="text-xs font-semibold text-muted-foreground bg-secondary/60 px-2.5 py-1 rounded-lg">
Showing for {dateFilter}
</span>
)}
</div>
<Button
variant="outline"
@@ -171,22 +190,32 @@ export default function DPPPage() {
const attempt = attemptsByTest[dpp.id];
const isAttempted = !!attempt;
// Date logic
// Date & Time logic
const now = new Date();
const scheduledTime = dpp.scheduled_date ? new Date(dpp.scheduled_date) : now;
const scheduledTime = dpp.scheduled_date ? new Date(dpp.scheduled_date) : null;
const endTime = dpp.end_date ? new Date(dpp.end_date) : null;
const isUpcoming = scheduledTime > now;
const isUpcoming = scheduledTime ? scheduledTime > now : false;
const isExpired = endTime ? now > endTime : false;
const isLive = !isUpcoming && !isExpired;
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">
<div className="flex gap-4 items-start w-full">
<div className={`mt-1 flex-shrink-0 ${isAttempted ? 'text-green-500' : 'text-foreground'}`}>
{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>}
{/* Clean Status Icon — No Selectable Circle Checkboxes */}
<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 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)]">
{user?.target_exam?.replace("_", " ") || "JEE/NEET"}
</span>
@@ -195,14 +224,30 @@ export default function DPPPage() {
{isUpcoming ? (
<>
<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 ? (
<>
<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>
<h3 className="font-bold text-lg leading-tight group-hover:text-[var(--cyan-ai)] transition-colors">
{cleanTitle(dpp.title)}
@@ -211,10 +256,25 @@ export default function DPPPage() {
</span>
</h3>
<div className="text-sm text-muted-foreground mt-2 font-medium">
{count} Questions • {dpp.total_time} mins
{dpp.scheduled_date && (
<span className="ml-2 border-l border-border pl-2">
{isUpcoming ? `Available on ${formatDate(dpp.scheduled_date)}` : isExpired ? `Expired on ${formatDate(dpp.end_date!)}` : 'Available Now'}
<span>{count} Questions • {dpp.total_time} mins</span>
{isUpcoming && scheduledTime && (
<span className="ml-2 border-l border-border pl-2 text-amber-500 font-semibold">
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>
)}
</div>
@@ -222,9 +282,13 @@ export default function DPPPage() {
</div>
<div className="flex flex-col sm:flex-row gap-2 shrink-0 md:self-center">
{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 ? (
<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 ? (
<>
<Link href={`/cbt/${dpp.id}/result`}>
@@ -256,7 +320,8 @@ export default function DPPPage() {
</div>
</CardContent>
</Card>
)})}
);
})}
</div>
{/* ── PROTECTED PDF READER MODAL (FREE TIER) ── */}

View File

@@ -5,7 +5,7 @@ import { BrainCircuit, Sparkles } from "lucide-react"
export default function TutorChatPage() {
return (
<div className="h-[calc(100vh-100px)] w-full flex flex-col pt-4 max-w-7xl mx-auto px-4 gap-4">
<div className="h-[calc(100vh-100px)] w-full flex flex-col pt-4 max-w-7xl mx-auto px-4 gap-4 animate-in fade-in zoom-in-95 origin-top duration-500">
<div className="border-b border-border/50 pb-4">
<div className="flex items-start justify-between gap-4 flex-wrap">
<div>