feat: complete UI tasks, backend optimizations, coupon admin, user kundli

This commit is contained in:
2026-07-10 12:36:56 +05:30
parent 149d365218
commit d5199682e3
16 changed files with 782 additions and 78 deletions

View File

@@ -9,9 +9,11 @@ import Link from "next/link"
import { useActiveUser } from "@/hooks/useActiveUser"
export default function DPPPage() {
const { user } = useActiveUser()
const { user, userId } = useActiveUser()
const [dpps, setDpps] = useState<any[]>([])
const [attempts, setAttempts] = useState<any[]>([])
const [loading, setLoading] = useState(true)
const [dateFilter, setDateFilter] = useState("")
useEffect(() => {
async function fetchDpps() {
@@ -25,6 +27,14 @@ export default function DPPPage() {
const data = await res.json()
setDpps(data)
}
if (userId) {
const attemptsRes = await fetch(`/api/users/${userId}/attempts`);
if (attemptsRes.ok) {
const attemptsData = await attemptsRes.json();
setAttempts(attemptsData);
}
}
} catch (err) {
console.error("Failed to fetch DPPs", err)
} finally {
@@ -43,6 +53,29 @@ export default function DPPPage() {
}
}
const attemptsByTest = attempts.reduce((acc, curr) => {
// Isolate analytics to first attempt only - ignore retakes
if (curr.is_retake) return acc;
if (!acc[curr.source_id] || acc[curr.source_id].score < curr.score) {
acc[curr.source_id] = curr;
}
return acc;
}, {} as Record<string, any>);
const filteredDpps = dpps.filter(dpp => {
if (dateFilter) {
if (!dpp.scheduled_date) return false;
const scheduledDate = new Date(dpp.scheduled_date).toISOString().split('T')[0];
if (scheduledDate !== dateFilter) return false;
}
return true;
});
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleDateString("en-US", { year: 'numeric', month: 'short', day: 'numeric' });
}
return (
<div className="p-8 space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500 max-w-5xl 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">
@@ -56,6 +89,12 @@ export default function DPPPage() {
<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-[#00F0FF] outline-none text-muted-foreground mr-2"
/>
<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'}`}>
@@ -74,23 +113,31 @@ export default function DPPPage() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{loading ? (
<div className="col-span-2 text-center py-12 text-muted-foreground">Loading AI-generated DPPs...</div>
) : dpps.length === 0 ? (
<div className="col-span-2 text-center py-12 text-muted-foreground border border-dashed rounded-xl">No DPPs available yet. Upload some PDFs in the Admin Panel!</div>
) : dpps.map((dpp, i) => {
) : 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>
) : filteredDpps.map((dpp, i) => {
// Calculate total questions dynamically from sections
let count = 0;
if (dpp.sections) {
Object.values(dpp.sections).forEach((s: any) => { count += (s.questions?.length || 0) });
}
// Temporary mock for status
let status: string = "Pending"
const attempt = attemptsByTest[dpp.id];
const isAttempted = !!attempt;
// Date logic
const now = new Date();
const scheduledTime = dpp.scheduled_date ? new Date(dpp.scheduled_date) : now;
const endTime = dpp.end_date ? new Date(dpp.end_date) : null;
const isUpcoming = scheduledTime > now;
const isExpired = endTime ? now > endTime : false;
return (
<Card key={dpp.id} className={`group cursor-pointer transition-all hover:shadow-md ${status === 'Completed' ? 'opacity-70 hover:opacity-100 bg-secondary/10' : 'hover:border-[#00F0FF]/50 bg-background'}`}>
<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-[#00F0FF]/50 bg-background'}`}>
<CardContent className="p-6 flex items-center justify-between">
<div className="flex gap-4">
<div className={`mt-1 flex-shrink-0 ${status === 'Completed' ? 'text-green-500' : 'text-foreground'}`}>
{status === 'Completed' ? <CheckCircle2 size={24} /> : <div className="size-6 rounded-full border-2 border-muted-foreground group-hover:border-[#00F0FF] transition-colors flex items-center justify-center"></div>}
<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-[#00F0FF] transition-colors flex items-center justify-center"></div>}
</div>
<div>
<div className="flex items-center gap-2 mb-1">
@@ -99,6 +146,17 @@ export default function DPPPage() {
</span>
<span className="text-muted-foreground text-xs font-bold">•</span>
<span className="text-xs font-bold uppercase tracking-wider text-muted-foreground">{dpp.difficulty} Level</span>
{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>
</>
) : 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>
</>
) : null}
</div>
<h3 className="font-bold text-lg leading-tight group-hover:text-[#00F0FF] transition-colors flex items-center gap-2">
{dpp.title}
@@ -108,14 +166,30 @@ export default function DPPPage() {
</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>
)}
</div>
</div>
</div>
<div className="text-muted-foreground group-hover:text-foreground transition-colors pl-4 shrink-0">
{status === 'Completed' ? (
<Link href={`/dashboard/cbt/history/${dpp.id}`}>
<Button variant="ghost" size="sm" className="font-bold">Review</Button>
</Link>
<div className="flex flex-col sm:flex-row gap-2 shrink-0">
{isUpcoming ? (
<Button disabled variant="outline" size="sm" className="font-bold">Starts Soon</Button>
) : isExpired && !isAttempted ? (
<Button disabled variant="outline" size="sm" className="font-bold bg-secondary/50">Expired</Button>
) : isAttempted ? (
<>
<Link href={`/cbt/${dpp.id}/result`}>
<Button variant="outline" size="sm" className="font-bold border-border">Result</Button>
</Link>
{!isExpired && (
<Link href={`/cbt/${dpp.id}`}>
<Button variant="secondary" size="sm" className="font-bold bg-[#00F0FF]/10 text-[#00F0FF] hover:bg-[#00F0FF]/20">Retake</Button>
</Link>
)}
</>
) : (
<Link href={`/cbt/${dpp.id}`}>
<Button variant="secondary" size="icon" className="rounded-full group-hover:bg-primary group-hover:text-primary-foreground hover:bg-primary hover:text-primary-foreground shadow-sm transition-all duration-300">