Redesign college predictor, swap DPP contents, reorder sidebar, fix imports, add sql dump
This commit is contained in:
BIN
server_tests.sql
Normal file
BIN
server_tests.sql
Normal file
Binary file not shown.
@@ -1,21 +1,20 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { fetchWithAuth } from "@/lib/apiClient";
|
import { fetchWithAuth } from "@/lib/apiClient";
|
||||||
import { Button } from "@/components/ui/button"
|
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 } from "lucide-react"
|
||||||
|
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { useActiveUser } from "@/hooks/useActiveUser"
|
import { useActiveUser } from "@/hooks/useActiveUser"
|
||||||
|
|
||||||
export default function AIDailyPracticePage() {
|
export default function DPPPage() {
|
||||||
const { user, userId } = useActiveUser()
|
const { user, userId } = useActiveUser()
|
||||||
const [dpps, setDpps] = useState<any[]>([])
|
const [dpps, setDpps] = useState<any[]>([])
|
||||||
const [attempts, setAttempts] = useState<any[]>([])
|
const [attempts, setAttempts] = useState<any[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [dateFilter, setDateFilter] = useState("")
|
const [dateFilter, setDateFilter] = useState("")
|
||||||
const [diffFilter, setDiffFilter] = useState("Mixed")
|
|
||||||
const [selectedPdfDpp, setSelectedPdfDpp] = useState<any | null>(null)
|
const [selectedPdfDpp, setSelectedPdfDpp] = useState<any | null>(null)
|
||||||
const [isFullScreen, setIsFullScreen] = useState(false)
|
const [isFullScreen, setIsFullScreen] = useState(false)
|
||||||
const [copiedShare, setCopiedShare] = useState(false)
|
const [copiedShare, setCopiedShare] = useState(false)
|
||||||
@@ -74,9 +73,10 @@ export default function AIDailyPracticePage() {
|
|||||||
const scheduledDate = new Date(dpp.scheduled_date).toISOString().split('T')[0];
|
const scheduledDate = new Date(dpp.scheduled_date).toISOString().split('T')[0];
|
||||||
if (scheduledDate !== dateFilter) return false;
|
if (scheduledDate !== dateFilter) return false;
|
||||||
}
|
}
|
||||||
if (diffFilter !== "Mixed" && dpp.difficulty !== diffFilter) {
|
const isNeet = user?.target_exam === "NEET";
|
||||||
return false;
|
const title = dpp.title.toLowerCase();
|
||||||
}
|
if (isNeet && (title.includes("math") || title.includes("mth"))) return false;
|
||||||
|
if (!isNeet && title.includes("bio")) return false;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -97,26 +97,18 @@ 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-8 space-y-8 animate-in fade-in zoom-in-95 origin-top duration-500 max-w-7xl mx-auto">
|
||||||
{/* Page Header */}
|
<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="border-b border-border/50 pb-6">
|
<div>
|
||||||
<div className="flex items-start justify-between gap-4 flex-wrap">
|
<h1 className="text-3xl md:text-4xl font-black tracking-tight flex items-center gap-3 mb-2">
|
||||||
<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-cyan-50 dark:bg-[var(--cyan-ai-10)] border border-cyan-300/60 dark:border-[var(--cyan-ai-20)]">
|
<span className="p-2.5 rounded-xl bg-cyan-50 dark:bg-[var(--cyan-ai-10)] border border-cyan-300/60 dark:border-[var(--cyan-ai-20)]">
|
||||||
<Zap className="text-cyan-700 dark:text-[var(--cyan-ai)]" size={28} />
|
<Zap className="text-cyan-700 dark:text-[var(--cyan-ai)]" size={28} />
|
||||||
</span>
|
</span>
|
||||||
PrepPilot AI
|
PrepPilot AI
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-muted-foreground text-base font-medium max-w-2xl pl-1">
|
<p className="text-muted-foreground font-medium max-w-xl">
|
||||||
Launch AI-generated Daily Practice Problems (DPPs) tailored to your exam pattern and weak areas.
|
Hyper-personalized problem sets generated daily by our AI Evaluator to target your specific weak areas based on past exam performance.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 shrink-0">
|
|
||||||
<span className="text-xs font-bold uppercase tracking-wider text-[var(--cyan-ai)] bg-cyan-50 dark:bg-[var(--cyan-ai-10)] border border-cyan-300/60 dark:border-[var(--cyan-ai-20)] px-3 py-1.5 rounded-full flex items-center gap-1.5">
|
|
||||||
<BrainCircuit size={12} /> AI Powered
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -134,9 +126,21 @@ export default function AIDailyPracticePage() {
|
|||||||
{diff}
|
{diff}
|
||||||
</button>
|
</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 variant="outline" className="gap-2 rounded-xl text-sm font-bold">
|
<Button
|
||||||
<Download size={16} /> Export as PDF
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(window.location.origin + '/dashboard/dpp');
|
||||||
|
setCopiedShare(true);
|
||||||
|
setTimeout(() => setCopiedShare(false), 2000);
|
||||||
|
}}
|
||||||
|
className="gap-2 rounded-xl text-sm font-bold border-primary/30 hover:bg-primary/10 text-primary transition-all shadow-sm"
|
||||||
|
>
|
||||||
|
{copiedShare ? <CheckCircle2 size={16} className="text-green-500" /> : <Share2 size={16} />}
|
||||||
|
{copiedShare ? "Share Link Copied!" : "Share DPPs Link"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -190,12 +194,10 @@ export default function AIDailyPracticePage() {
|
|||||||
</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)}
|
||||||
</h3>
|
<span className={`ml-2 text-[10px] uppercase px-2 py-0.5 rounded-md border font-bold tracking-wider ${getDifficultyColor(dpp.difficulty)}`}>
|
||||||
<div className="mt-1">
|
|
||||||
<span className={`text-[10px] uppercase px-2 py-0.5 rounded-md border font-bold tracking-wider ${getDifficultyColor(dpp.difficulty)}`}>
|
|
||||||
{dpp.difficulty}
|
{dpp.difficulty}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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
|
{count} Questions • {dpp.total_time} mins
|
||||||
{dpp.scheduled_date && (
|
{dpp.scheduled_date && (
|
||||||
@@ -217,19 +219,15 @@ export default function AIDailyPracticePage() {
|
|||||||
<Button variant="outline" size="sm" className="font-bold border-border">Result</Button>
|
<Button variant="outline" size="sm" className="font-bold border-border">Result</Button>
|
||||||
</Link>
|
</Link>
|
||||||
{!isExpired && (
|
{!isExpired && (
|
||||||
<Link href={`/cbt/${dpp.id}`}>
|
<Button onClick={() => setSelectedPdfDpp(dpp)} variant="secondary" size="sm" className="font-bold bg-[var(--cyan-ai-10)] text-[var(--cyan-ai)] hover:bg-[var(--cyan-ai-20)] gap-2">
|
||||||
<Button variant="secondary" size="sm" className="font-bold bg-[var(--cyan-ai-10)] text-[var(--cyan-ai)] hover:bg-[var(--cyan-ai-20)] gap-2">
|
<Eye size={14} /> Read PDF
|
||||||
<Play size={14} className="fill-current" /> Retake Test
|
</Button>
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Link href={`/cbt/${dpp.id}`}>
|
<Button onClick={() => setSelectedPdfDpp(dpp)} variant="secondary" className="rounded-xl bg-primary/10 text-primary hover:bg-primary/20 font-bold gap-2 text-xs">
|
||||||
<Button 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)]">
|
<Eye size={14} /> Open PDF
|
||||||
<Play size={14} className="fill-black" /> Launch Practice Test
|
</Button>
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -249,7 +247,7 @@ export default function AIDailyPracticePage() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-black text-base sm:text-lg flex items-center gap-2">
|
<h3 className="font-black text-base sm:text-lg flex items-center gap-2">
|
||||||
{selectedPdfDpp.title}
|
{cleanTitle(selectedPdfDpp.title)}
|
||||||
<span className="text-[10px] bg-yellow-500/10 text-yellow-500 border border-yellow-500/20 px-2 py-0.5 rounded-full uppercase tracking-wider font-extrabold hidden md:inline-block">
|
<span className="text-[10px] bg-yellow-500/10 text-yellow-500 border border-yellow-500/20 px-2 py-0.5 rounded-full uppercase tracking-wider font-extrabold hidden md:inline-block">
|
||||||
Protected In-App PDF View
|
Protected In-App PDF View
|
||||||
</span>
|
</span>
|
||||||
@@ -264,7 +262,7 @@ export default function AIDailyPracticePage() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(window.location.origin + '/dashboard/ai-coach/cbt-dpp');
|
navigator.clipboard.writeText(window.location.origin + '/dashboard/dpp');
|
||||||
setCopiedShare(true);
|
setCopiedShare(true);
|
||||||
setTimeout(() => setCopiedShare(false), 2000);
|
setTimeout(() => setCopiedShare(false), 2000);
|
||||||
}}
|
}}
|
||||||
@@ -298,7 +296,7 @@ export default function AIDailyPracticePage() {
|
|||||||
</span>
|
</span>
|
||||||
<Link href="/upgrade" className="hidden sm:block shrink-0 ml-2">
|
<Link href="/upgrade" className="hidden sm:block shrink-0 ml-2">
|
||||||
<Button size="sm" className="bg-amber-500 text-black hover:bg-amber-400 font-extrabold h-7 text-[11px] rounded-lg">
|
<Button size="sm" className="bg-amber-500 text-black hover:bg-amber-400 font-extrabold h-7 text-[11px] rounded-lg">
|
||||||
Upgrade for Interactive Practice Mode →
|
Upgrade for Interactive CBT Mode →
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@@ -332,7 +330,7 @@ export default function AIDailyPracticePage() {
|
|||||||
</Button>
|
</Button>
|
||||||
<Link href="/upgrade">
|
<Link href="/upgrade">
|
||||||
<Button className="bg-[var(--cyan-ai)] text-black hover:bg-[var(--cyan-ai-80)] font-extrabold rounded-xl shadow-[0_0_15px_rgba(12,96,96,0.2)] text-xs h-9">
|
<Button className="bg-[var(--cyan-ai)] text-black hover:bg-[var(--cyan-ai-80)] font-extrabold rounded-xl shadow-[0_0_15px_rgba(12,96,96,0.2)] text-xs h-9">
|
||||||
<Sparkles size={14} className="mr-1.5" /> Upgrade to AI Coach
|
<Sparkles size={14} className="mr-1.5" /> Upgrade to AI Coach for CBT
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,39 +1,45 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState, useEffect } from "react"
|
import { useState, useEffect } from "react"
|
||||||
import { CollegePredictorCard } from "@/components/AI/CollegePredictorCard"
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
|
||||||
import { Card, CardContent } from "@/components/ui/card"
|
import { GraduationCap, BrainCircuit, LineChart as LineChartIcon } from "lucide-react"
|
||||||
import { GraduationCap, Building2, Award, CheckCircle2, TrendingUp, MapPin, Users, BarChart2 } from "lucide-react"
|
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'
|
||||||
import Link from "next/link"
|
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
||||||
|
|
||||||
const DEMO_IDS = new Set([
|
const DEMO_TREND_DATA = [
|
||||||
"11111111-1111-1111-1111-111111111111",
|
{ name: 'Week 1', rank: 4500, label: 'NIT Trichy CSE' },
|
||||||
"22222222-2222-2222-2222-222222222222",
|
{ name: 'Week 2', rank: 4100 },
|
||||||
"33333333-3333-3333-3333-333333333333",
|
{ name: 'Week 3', rank: 3800 },
|
||||||
"44444444-4444-4444-4444-444444444444"
|
{ name: 'Week 4', rank: 3300 },
|
||||||
]);
|
{ name: 'Week 5', rank: 2900 },
|
||||||
|
{ name: 'Week 6', rank: 2750, label: 'IIT Roorkee' },
|
||||||
function isDemoUser(id: string): boolean {
|
|
||||||
return !!id && DEMO_IDS.has(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
const CUTOFF_TRENDS = [
|
|
||||||
{ year: "2021", rank: 3800 },
|
|
||||||
{ year: "2022", rank: 3500 },
|
|
||||||
{ year: "2023", rank: 3200 },
|
|
||||||
{ year: "2024", rank: 2950 },
|
|
||||||
{ year: "2025", rank: 2750 },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const ZERO_TREND_DATA = [
|
||||||
|
{ name: 'Week 1', rank: 0 },
|
||||||
|
{ name: 'Week 2', rank: 0 },
|
||||||
|
{ name: 'Week 3', rank: 0 },
|
||||||
|
{ name: 'Week 4', rank: 0 },
|
||||||
|
{ name: 'Week 5', rank: 0 },
|
||||||
|
{ name: 'Week 6', rank: 0 },
|
||||||
|
]
|
||||||
|
|
||||||
|
// Custom Label to permanently show text on the graph (not on hover)
|
||||||
|
const CustomLabel = (props: any) => {
|
||||||
|
const { x, y, stroke, value, index, data } = props;
|
||||||
|
const point = data[index];
|
||||||
|
if (!point || !point.label) return null;
|
||||||
|
return (
|
||||||
|
<text x={x} y={y - 15} fill="#fff" fontSize={12} fontWeight="bold" textAnchor="middle">
|
||||||
|
{point.label}
|
||||||
|
</text>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default function CollegePredictorPage() {
|
export default function CollegePredictorPage() {
|
||||||
const [isDemo, setIsDemo] = useState(false);
|
const { userId } = useActiveUser()
|
||||||
useEffect(() => {
|
const isDemo = isDemoUser(userId)
|
||||||
if (typeof window !== 'undefined') {
|
const trendData = isDemo ? DEMO_TREND_DATA : ZERO_TREND_DATA
|
||||||
const id = localStorage.getItem('activeUserId');
|
|
||||||
setIsDemo(isDemoUser(id ?? ''));
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
const maxRank = Math.max(...CUTOFF_TRENDS.map(d => d.rank))
|
|
||||||
|
|
||||||
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 animate-in fade-in zoom-in-95 origin-top duration-500 max-w-7xl mx-auto min-h-screen">
|
||||||
@@ -45,10 +51,10 @@ export default function CollegePredictorPage() {
|
|||||||
<span className="p-2.5 rounded-xl bg-cyan-50 dark:bg-[var(--cyan-ai-10)] border border-cyan-300/60 dark:border-[var(--cyan-ai-20)]">
|
<span className="p-2.5 rounded-xl bg-cyan-50 dark:bg-[var(--cyan-ai-10)] border border-cyan-300/60 dark:border-[var(--cyan-ai-20)]">
|
||||||
<GraduationCap className="text-cyan-700 dark:text-[var(--cyan-ai)]" size={28} />
|
<GraduationCap className="text-cyan-700 dark:text-[var(--cyan-ai)]" size={28} />
|
||||||
</span>
|
</span>
|
||||||
AI College & Cutoff Predictor
|
College Predictor
|
||||||
</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">
|
||||||
Discover your probable target medical & engineering colleges based on current mock exam scores and historical cutoffs.
|
Discover your probable target medical & engineering colleges based on current mock exam scores and historical cutoffs.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs font-bold uppercase tracking-wider text-[var(--cyan-ai)] bg-cyan-50 dark:bg-[var(--cyan-ai-10)] border border-cyan-300/60 dark:border-[var(--cyan-ai-20)] px-3 py-1.5 rounded-full flex items-center gap-1.5 shrink-0 self-start">
|
<span className="text-xs font-bold uppercase tracking-wider text-[var(--cyan-ai)] bg-cyan-50 dark:bg-[var(--cyan-ai-10)] border border-cyan-300/60 dark:border-[var(--cyan-ai-20)] px-3 py-1.5 rounded-full flex items-center gap-1.5 shrink-0 self-start">
|
||||||
@@ -57,97 +63,69 @@ export default function CollegePredictorPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Summary KPI Strip */}
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
{/* Left Box: Graph */}
|
||||||
{[
|
<Card className="col-span-1 lg:col-span-2 border-border shadow-sm bg-background">
|
||||||
{ label: "Predicted Rank", value: isDemo ? "~4,200" : "--", icon: <TrendingUp size={18} className="text-[var(--cyan-ai)]" />, color: "border-[var(--cyan-ai-20)] bg-[var(--cyan-ai-10)]" },
|
<CardHeader>
|
||||||
{ label: "Target Colleges", value: isDemo ? "12" : "0", icon: <Building2 size={18} className="text-amber-400" />, color: "border-amber-400/20 bg-amber-400/5" },
|
<CardTitle className="flex items-center gap-2"><LineChartIcon className="text-[var(--cyan-ai)]" size={20} /> College & Branch Legacy Prediction</CardTitle>
|
||||||
{ label: "Reach Colleges", value: isDemo ? "5" : "0", icon: <MapPin size={18} className="text-purple-400" />, color: "border-purple-400/20 bg-purple-400/5" },
|
<CardDescription>
|
||||||
{ label: "Safe Colleges", value: isDemo ? "18" : "0", icon: <CheckCircle2 size={18} className="text-green-400" />, color: "border-green-400/20 bg-green-400/5" },
|
{isDemo ? "Your projected admission chances mapped over your prep weeks." : "Your college trajectory will plot here as you complete tests."}
|
||||||
].map((kpi) => (
|
</CardDescription>
|
||||||
<Card key={kpi.label} className={`rounded-2xl border ${kpi.color} hover:scale-[1.02] transition-transform`}>
|
</CardHeader>
|
||||||
<CardContent className="p-5 flex flex-col gap-2">
|
<CardContent className="h-[350px] mt-4">
|
||||||
{kpi.icon}
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<div className="text-2xl font-black">{kpi.value}</div>
|
<LineChart data={trendData} margin={{ top: 25, right: 30, bottom: 5, left: 10 }}>
|
||||||
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">{kpi.label}</div>
|
{/* User said static line, not dotted. So removed strokeDasharray from grid? Let's make it solid if they mean grid, or just the line. Line is monotone. */}
|
||||||
</CardContent>
|
<CartesianGrid stroke="#333" vertical={false} />
|
||||||
</Card>
|
<XAxis dataKey="name" stroke="#888" tick={{ fill: '#888' }} />
|
||||||
))}
|
<YAxis stroke="#888" tick={{ fill: '#888' }} domain={[0, 5000]} reversed={true} />
|
||||||
</div>
|
<Tooltip
|
||||||
|
contentStyle={{ backgroundColor: '#1f2937', borderColor: '#374151', borderRadius: '8px' }}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
itemStyle={{ color: 'var(--cyan-ai)' }}
|
||||||
<div className="lg:col-span-2 space-y-8">
|
formatter={(value: any) => [`Rank: ${value}`, 'Prediction']}
|
||||||
{/* College Predictor */}
|
/>
|
||||||
<CollegePredictorCard />
|
<Line
|
||||||
|
type="linear"
|
||||||
{/* Cutoff Trend Chart */}
|
dataKey="rank"
|
||||||
<Card className="p-6 bg-secondary/10 border-border/60 rounded-3xl">
|
stroke="var(--cyan-ai)"
|
||||||
<div className="flex items-center gap-2 mb-6">
|
strokeWidth={3}
|
||||||
<BarChart2 size={20} className="text-[var(--cyan-ai)]" />
|
dot={{ r: 4, fill: 'var(--cyan-ai)' }}
|
||||||
<h3 className="text-lg font-extrabold tracking-tight">NIT Trichy CSE — Closing Rank Trend</h3>
|
activeDot={{ r: 6 }}
|
||||||
<span className="ml-auto text-xs font-bold text-green-400 bg-green-400/10 border border-green-400/20 px-2.5 py-1 rounded-full flex items-center gap-1">
|
label={<CustomLabel data={trendData} />}
|
||||||
<TrendingUp size={11} /> Improving Cutoffs
|
/>
|
||||||
</span>
|
</LineChart>
|
||||||
</div>
|
</ResponsiveContainer>
|
||||||
<div className="flex items-end gap-4 h-32 px-2">
|
</CardContent>
|
||||||
{CUTOFF_TRENDS.map((d, i) => (
|
</Card>
|
||||||
<div key={d.year} className="flex flex-col items-center gap-1.5 flex-1">
|
|
||||||
<span className="text-xs font-black text-muted-foreground">{d.rank.toLocaleString()}</span>
|
|
||||||
<div
|
|
||||||
className={`w-full rounded-t-md transition-all duration-700 ${i === CUTOFF_TRENDS.length - 1 ? "bg-gradient-to-t from-[var(--cyan-ai-50)] to-[var(--cyan-ai)]" : "bg-gradient-to-t from-white/10 to-white/25"}`}
|
|
||||||
style={{ height: `${(d.rank / maxRank) * 80}px` }}
|
|
||||||
/>
|
|
||||||
<span className="text-xs text-muted-foreground font-semibold">{d.year}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground mt-4 pl-1">Closing rank trend: cutoffs are improving, meaning competition is easing. Your predicted rank of ~4,200 is within Target range.</p>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
{/* Right Box: AI Analysis (Text Only) */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<Card className="p-6 bg-gradient-to-br from-[var(--cyan-ai-10)] via-secondary/20 to-background border border-[var(--cyan-ai-30)] rounded-3xl space-y-4">
|
<Card className="border-[var(--cyan-ai-30)] bg-[var(--cyan-ai-10)] h-full">
|
||||||
<h3 className="text-base font-black tracking-tight text-[var(--cyan-ai)] flex items-center gap-2">
|
<CardHeader className="pb-2">
|
||||||
<Award size={18} /> Cutoff Modelling Parameters
|
<CardTitle className="text-lg flex items-center gap-2"><BrainCircuit size={18} className="text-[var(--cyan-ai)]"/> AI Analysis</CardTitle>
|
||||||
</h3>
|
</CardHeader>
|
||||||
<ul className="space-y-3 text-sm font-medium text-muted-foreground">
|
<CardContent>
|
||||||
<li className="flex items-start gap-2">
|
{isDemo ? (
|
||||||
<CheckCircle2 size={16} className="text-[var(--cyan-ai)] shrink-0 mt-0.5" />
|
<ul className="space-y-4 text-sm text-foreground/90 font-medium">
|
||||||
<span>Trained on <strong className="text-foreground">100,000+ candidate rank trends</strong> across 2021–2025 counselling rounds.</span>
|
<li className="flex items-start gap-3">
|
||||||
</li>
|
<span className="text-amber-400 font-black shrink-0">+15 marks</span>
|
||||||
<li className="flex items-start gap-2">
|
<span>in Maths → upgrades you from NIT Trichy to <strong className="text-foreground">IIT Roorkee</strong></span>
|
||||||
<CheckCircle2 size={16} className="text-[var(--cyan-ai)] shrink-0 mt-0.5" />
|
</li>
|
||||||
<span>Supports <strong className="text-foreground">All India Quota (AIQ)</strong> & State Quota seat adjustments.</span>
|
<li className="flex items-start gap-3">
|
||||||
</li>
|
<span className="text-[var(--cyan-ai)] font-black shrink-0">Fix Optics</span>
|
||||||
<li className="flex items-start gap-2">
|
<span>errors to recover ~8 marks in Physics alone</span>
|
||||||
<CheckCircle2 size={16} className="text-[var(--cyan-ai)] shrink-0 mt-0.5" />
|
</li>
|
||||||
<span>Simulates <strong className="text-foreground">category reservation cutoffs</strong>: General, OBC, SC, ST, EWS.</span>
|
<li className="flex items-start gap-3">
|
||||||
</li>
|
<span className="text-green-400 font-black shrink-0">Boost Speed</span>
|
||||||
<li className="flex items-start gap-2">
|
<span>from 45s ? 38s/Q gives you ~12 extra attempt opportunities</span>
|
||||||
<CheckCircle2 size={16} className="text-[var(--cyan-ai)] shrink-0 mt-0.5" />
|
</li>
|
||||||
<span>Score updated daily as you complete more mock tests.</span>
|
</ul>
|
||||||
</li>
|
) : (
|
||||||
</ul>
|
<p className="text-sm text-foreground/80 leading-relaxed font-medium">
|
||||||
</Card>
|
Complete your first diagnostic test or DPP to unlock personalized AI college predictions and target recommendations.
|
||||||
|
</p>
|
||||||
<Card className="p-6 bg-secondary/10 border-border/60 rounded-3xl space-y-3">
|
)}
|
||||||
<h3 className="text-base font-black text-muted-foreground uppercase tracking-wider flex items-center gap-2">
|
</CardContent>
|
||||||
<Users size={16} className="text-amber-400" /> Improve Your Rank
|
|
||||||
</h3>
|
|
||||||
<ul className="space-y-2.5 text-sm text-muted-foreground font-medium">
|
|
||||||
<li className="flex items-start gap-2">
|
|
||||||
<span className="text-amber-400 font-black shrink-0">+15 marks</span>
|
|
||||||
<span>in Maths → upgrades you from NIT Trichy to <strong className="text-foreground">IIT Roorkee</strong></span>
|
|
||||||
</li>
|
|
||||||
<li className="flex items-start gap-2">
|
|
||||||
<span className="text-[var(--cyan-ai)] font-black shrink-0">Fix Optics</span>
|
|
||||||
<span>errors to recover ~8 marks in Physics alone</span>
|
|
||||||
</li>
|
|
||||||
<li className="flex items-start gap-2">
|
|
||||||
<span className="text-green-400 font-black shrink-0">Boost Speed</span>
|
|
||||||
<span>from 45s → 38s/Q gives you ~12 extra attempt opportunities</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { fetchWithAuth } from "@/lib/apiClient";
|
import { fetchWithAuth } from "@/lib/apiClient";
|
||||||
import { Button } from "@/components/ui/button"
|
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 } 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 } from "react"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { useActiveUser } from "@/hooks/useActiveUser"
|
import { useActiveUser } from "@/hooks/useActiveUser"
|
||||||
|
|
||||||
export default function DPPPage() {
|
export default function AIDailyPracticePage() {
|
||||||
const { user, userId } = useActiveUser()
|
const { user, userId } = useActiveUser()
|
||||||
const [dpps, setDpps] = useState<any[]>([])
|
const [dpps, setDpps] = useState<any[]>([])
|
||||||
const [attempts, setAttempts] = useState<any[]>([])
|
const [attempts, setAttempts] = useState<any[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [dateFilter, setDateFilter] = useState("")
|
const [dateFilter, setDateFilter] = useState("")
|
||||||
|
|
||||||
const [selectedPdfDpp, setSelectedPdfDpp] = useState<any | null>(null)
|
const [selectedPdfDpp, setSelectedPdfDpp] = useState<any | null>(null)
|
||||||
const [isFullScreen, setIsFullScreen] = useState(false)
|
const [isFullScreen, setIsFullScreen] = useState(false)
|
||||||
const [copiedShare, setCopiedShare] = useState(false)
|
const [copiedShare, setCopiedShare] = useState(false)
|
||||||
@@ -73,10 +74,7 @@ export default function DPPPage() {
|
|||||||
const scheduledDate = new Date(dpp.scheduled_date).toISOString().split('T')[0];
|
const scheduledDate = new Date(dpp.scheduled_date).toISOString().split('T')[0];
|
||||||
if (scheduledDate !== dateFilter) return false;
|
if (scheduledDate !== dateFilter) return false;
|
||||||
}
|
}
|
||||||
const isNeet = user?.target_exam === "NEET";
|
|
||||||
const title = dpp.title.toLowerCase();
|
|
||||||
if (isNeet && (title.includes("math") || title.includes("mth"))) return false;
|
|
||||||
if (!isNeet && title.includes("bio")) return false;
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -97,13 +95,21 @@ 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-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="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">
|
{/* Page Header */}
|
||||||
<div>
|
<div className="border-b border-border/50 pb-6">
|
||||||
<h1 className="text-3xl font-bold tracking-tight mb-2">Daily Practice Problems (DPP)</h1>
|
<div className="flex items-start justify-between gap-4 flex-wrap">
|
||||||
<p className="text-muted-foreground font-medium max-w-xl">
|
<div>
|
||||||
Hyper-personalized problem sets generated daily by our AI Evaluator to target your specific weak areas based on past exam performance.
|
<h1 className="text-3xl md:text-4xl font-black tracking-tight flex items-center gap-3 mb-1">
|
||||||
</p>
|
<span className="p-2.5 rounded-xl bg-cyan-50 dark:bg-[var(--cyan-ai-10)] border border-cyan-300/60 dark:border-[var(--cyan-ai-20)]">
|
||||||
|
<FileText className="text-primary" size={28} /></span>
|
||||||
|
Daily Practice (DPP)
|
||||||
|
</h1>
|
||||||
|
<p className="text-muted-foreground text-base font-medium max-w-2xl pl-1">
|
||||||
|
Launch Daily Practice Problems (DPPs) tailored to your exam pattern.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -115,27 +121,10 @@ export default function DPPPage() {
|
|||||||
onChange={(e) => setDateFilter(e.target.value)}
|
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"
|
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"
|
||||||
/>
|
/>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button variant="outline" className="gap-2 rounded-xl text-sm font-bold">
|
||||||
variant="outline"
|
<Download size={16} /> Export as PDF
|
||||||
onClick={() => {
|
|
||||||
navigator.clipboard.writeText(window.location.origin + '/dashboard/dpp');
|
|
||||||
setCopiedShare(true);
|
|
||||||
setTimeout(() => setCopiedShare(false), 2000);
|
|
||||||
}}
|
|
||||||
className="gap-2 rounded-xl text-sm font-bold border-primary/30 hover:bg-primary/10 text-primary transition-all shadow-sm"
|
|
||||||
>
|
|
||||||
{copiedShare ? <CheckCircle2 size={16} className="text-green-500" /> : <Share2 size={16} />}
|
|
||||||
{copiedShare ? "Share Link Copied!" : "Share DPPs Link"}
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -189,10 +178,12 @@ export default function DPPPage() {
|
|||||||
</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)}
|
||||||
<span className={`ml-2 text-[10px] uppercase px-2 py-0.5 rounded-md border font-bold tracking-wider ${getDifficultyColor(dpp.difficulty)}`}>
|
</h3>
|
||||||
|
<div className="mt-1">
|
||||||
|
<span className={`text-[10px] uppercase px-2 py-0.5 rounded-md border font-bold tracking-wider ${getDifficultyColor(dpp.difficulty)}`}>
|
||||||
{dpp.difficulty}
|
{dpp.difficulty}
|
||||||
</span>
|
</span>
|
||||||
</h3>
|
</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
|
{count} Questions • {dpp.total_time} mins
|
||||||
{dpp.scheduled_date && (
|
{dpp.scheduled_date && (
|
||||||
@@ -214,15 +205,19 @@ export default function DPPPage() {
|
|||||||
<Button variant="outline" size="sm" className="font-bold border-border">Result</Button>
|
<Button variant="outline" size="sm" className="font-bold border-border">Result</Button>
|
||||||
</Link>
|
</Link>
|
||||||
{!isExpired && (
|
{!isExpired && (
|
||||||
<Button onClick={() => setSelectedPdfDpp(dpp)} variant="secondary" size="sm" className="font-bold bg-[var(--cyan-ai-10)] text-[var(--cyan-ai)] hover:bg-[var(--cyan-ai-20)] gap-2">
|
<Link href={`/cbt/${dpp.id}`}>
|
||||||
<Eye size={14} /> Read PDF
|
<Button variant="secondary" size="sm" className="font-bold bg-[var(--cyan-ai-10)] text-[var(--cyan-ai)] hover:bg-[var(--cyan-ai-20)] gap-2">
|
||||||
</Button>
|
<Play size={14} className="fill-current" /> Retake Test
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Button onClick={() => setSelectedPdfDpp(dpp)} variant="secondary" className="rounded-xl bg-primary/10 text-primary hover:bg-primary/20 font-bold gap-2 text-xs">
|
<Link href={`/cbt/${dpp.id}`}>
|
||||||
<Eye size={14} /> Open PDF
|
<Button 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>
|
<Play size={14} className="fill-black" /> Launch Practice Test
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -242,7 +237,7 @@ export default function DPPPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-black text-base sm:text-lg flex items-center gap-2">
|
<h3 className="font-black text-base sm:text-lg flex items-center gap-2">
|
||||||
{cleanTitle(selectedPdfDpp.title)}
|
{selectedPdfDpp.title}
|
||||||
<span className="text-[10px] bg-yellow-500/10 text-yellow-500 border border-yellow-500/20 px-2 py-0.5 rounded-full uppercase tracking-wider font-extrabold hidden md:inline-block">
|
<span className="text-[10px] bg-yellow-500/10 text-yellow-500 border border-yellow-500/20 px-2 py-0.5 rounded-full uppercase tracking-wider font-extrabold hidden md:inline-block">
|
||||||
Protected In-App PDF View
|
Protected In-App PDF View
|
||||||
</span>
|
</span>
|
||||||
@@ -257,7 +252,7 @@ export default function DPPPage() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(window.location.origin + '/dashboard/dpp');
|
navigator.clipboard.writeText(window.location.origin + '/dashboard/ai-coach/cbt-dpp');
|
||||||
setCopiedShare(true);
|
setCopiedShare(true);
|
||||||
setTimeout(() => setCopiedShare(false), 2000);
|
setTimeout(() => setCopiedShare(false), 2000);
|
||||||
}}
|
}}
|
||||||
@@ -291,7 +286,7 @@ export default function DPPPage() {
|
|||||||
</span>
|
</span>
|
||||||
<Link href="/upgrade" className="hidden sm:block shrink-0 ml-2">
|
<Link href="/upgrade" className="hidden sm:block shrink-0 ml-2">
|
||||||
<Button size="sm" className="bg-amber-500 text-black hover:bg-amber-400 font-extrabold h-7 text-[11px] rounded-lg">
|
<Button size="sm" className="bg-amber-500 text-black hover:bg-amber-400 font-extrabold h-7 text-[11px] rounded-lg">
|
||||||
Upgrade for Interactive CBT Mode →
|
Upgrade for Interactive Practice Mode →
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@@ -325,7 +320,7 @@ export default function DPPPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
<Link href="/upgrade">
|
<Link href="/upgrade">
|
||||||
<Button className="bg-[var(--cyan-ai)] text-black hover:bg-[var(--cyan-ai-80)] font-extrabold rounded-xl shadow-[0_0_15px_rgba(12,96,96,0.2)] text-xs h-9">
|
<Button className="bg-[var(--cyan-ai)] text-black hover:bg-[var(--cyan-ai-80)] font-extrabold rounded-xl shadow-[0_0_15px_rgba(12,96,96,0.2)] text-xs h-9">
|
||||||
<Sparkles size={14} className="mr-1.5" /> Upgrade to AI Coach for CBT
|
<Sparkles size={14} className="mr-1.5" /> Upgrade to AI Coach
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -144,7 +144,14 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<NavItem section="pro" href="/dashboard" icon={<LayoutDashboard size={17} />} label="Overview" active={pathname === "/dashboard"} />
|
<NavItem section="pro" href="/dashboard" icon={<LayoutDashboard size={17} />} label="Overview" active={pathname === "/dashboard"} />
|
||||||
<NavItem section="pro" href="/dashboard/leaderboard" icon={<Trophy size={17} />} label="Leaderboard" active={pathname.startsWith("/dashboard/leaderboard")} isLocked={!hasAccessTo("chapter-wise-progress-tracking")} />
|
<NavItem
|
||||||
|
section="pro"
|
||||||
|
href="/dashboard/dpp"
|
||||||
|
icon={<FileText size={17} />}
|
||||||
|
label="Daily Practice (DPP)"
|
||||||
|
active={pathname.startsWith("/dashboard/dpp")}
|
||||||
|
isLocked={!hasAccessTo("chapter-wise-progress-tracking")}
|
||||||
|
/>
|
||||||
<NavItem
|
<NavItem
|
||||||
section="pro"
|
section="pro"
|
||||||
href="/dashboard/progress"
|
href="/dashboard/progress"
|
||||||
@@ -161,15 +168,7 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
|
|||||||
active={pathname.startsWith("/dashboard/cbt")}
|
active={pathname.startsWith("/dashboard/cbt")}
|
||||||
isLocked={!hasAccessTo("weekly-test-series")}
|
isLocked={!hasAccessTo("weekly-test-series")}
|
||||||
/>
|
/>
|
||||||
{/* <NavItem
|
<NavItem section="pro" href="/dashboard/leaderboard" icon={<Trophy size={17} />} label="Leaderboard" active={pathname.startsWith("/dashboard/leaderboard")} isLocked={!hasAccessTo("chapter-wise-progress-tracking")} />
|
||||||
section="pro"
|
|
||||||
href="/dashboard/dpp"
|
|
||||||
icon={<FileText size={17} />}
|
|
||||||
label="Daily Practice (DPP)"
|
|
||||||
active={pathname.startsWith("/dashboard/dpp")}
|
|
||||||
isLocked={!hasAccessTo("chapter-wise-progress-tracking")}
|
|
||||||
/> */}
|
|
||||||
|
|
||||||
<NavItem
|
<NavItem
|
||||||
section="pro"
|
section="pro"
|
||||||
href="/dashboard/community"
|
href="/dashboard/community"
|
||||||
|
|||||||
Reference in New Issue
Block a user