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:
@@ -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) ── */}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user