feat: complete comprehensive zero-state metrics across all dashboard pages for new accounts
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { CheckCircle2, Clock, SkipForward, BrainCircuit } from "lucide-react"
|
||||
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
||||
|
||||
type PlanState = 'PENDING' | 'DONE' | 'SKIPPED';
|
||||
|
||||
@@ -16,14 +17,30 @@ interface PlanItem {
|
||||
status: PlanState;
|
||||
}
|
||||
|
||||
const INITIAL_PLAN: PlanItem[] = [
|
||||
const DEMO_PLAN: PlanItem[] = [
|
||||
{ id: '1', title: 'Revise Thermodynamics Concepts', duration: '45 min', type: 'Theory', status: 'PENDING' },
|
||||
{ id: '2', title: 'Solve 15 Rotational Dynamics DPPs', duration: '30 min', type: 'Practice', status: 'PENDING' },
|
||||
{ id: '3', title: 'Review Mistakes in Optics (T-3)', duration: '20 min', type: 'Review', status: 'PENDING' },
|
||||
];
|
||||
|
||||
const NEW_USER_PLAN: PlanItem[] = [
|
||||
{ id: '1', title: 'Take Baseline Diagnostic CBT Test', duration: '45 min', type: 'Diagnostic', status: 'PENDING' },
|
||||
{ id: '2', title: 'Attempt Today\'s 15 DPP Practice Questions', duration: '30 min', type: 'Practice', status: 'PENDING' },
|
||||
{ id: '3', title: 'Calibrate Target Exam & Daily Time Slot', duration: '15 min', type: 'Setup', status: 'PENDING' },
|
||||
];
|
||||
|
||||
export function AdaptiveStudyPlan() {
|
||||
const [items, setItems] = useState<PlanItem[]>(INITIAL_PLAN);
|
||||
const { user } = useActiveUser();
|
||||
const isDemo = isDemoUser(user?.id);
|
||||
const [items, setItems] = useState<PlanItem[]>(DEMO_PLAN);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDemo) {
|
||||
setItems(DEMO_PLAN);
|
||||
} else {
|
||||
setItems(NEW_USER_PLAN);
|
||||
}
|
||||
}, [isDemo]);
|
||||
|
||||
const updateStatus = (id: string, newStatus: PlanState) => {
|
||||
setItems(items.map(item => item.id === id ? { ...item, status: newStatus } : item));
|
||||
@@ -48,7 +65,9 @@ export function AdaptiveStudyPlan() {
|
||||
<Clock size={14} className="text-primary" /> Today: {totalTime} min
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">Dynamically generated based on your weak areas.</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{isDemo ? "Dynamically generated based on your weak areas." : "Day 1 Onboarding Plan · Calibrating AI recommendations based on your initial performance."}
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 p-6">
|
||||
<div className="relative space-y-6 before:absolute before:inset-0 before:ml-5 before:-translate-x-px md:before:mx-auto md:before:translate-x-0 before:h-full before:w-0.5 before:bg-gradient-to-b before:from-transparent before:via-border before:to-transparent">
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
"use client"
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Flame, Info } from "lucide-react"
|
||||
import { Flame, Info, CheckCircle2 } from "lucide-react"
|
||||
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
||||
|
||||
const HEATMAP_DATA = [
|
||||
{ topic: "Rotational Dynamics", attempts: [80, 90, 40, 20, 10] }, // 10 is critical error rate
|
||||
@@ -10,14 +13,26 @@ const HEATMAP_DATA = [
|
||||
{ topic: "Modern Physics", attempts: [10, 5, 2, 0, 0] }
|
||||
];
|
||||
|
||||
const ZERO_HEATMAP_DATA = [
|
||||
{ topic: "Physics (Core Mechanics)", attempts: [0, 0, 0, 0, 0] },
|
||||
{ topic: "Chemistry (Physical & Organic)", attempts: [0, 0, 0, 0, 0] },
|
||||
{ topic: "Mathematics / Biology", attempts: [0, 0, 0, 0, 0] },
|
||||
];
|
||||
|
||||
export function MistakeHeatmap() {
|
||||
const { userId } = useActiveUser();
|
||||
const isDemo = isDemoUser(userId);
|
||||
|
||||
const getColor = (errorRate: number) => {
|
||||
if (errorRate === 0) return 'bg-white/5 border-white/10 text-muted-foreground';
|
||||
if (errorRate >= 50) return 'bg-red-500/80 border-red-500 shadow-[0_0_10px_-2px_rgba(239,68,68,0.5)]'; // Critical
|
||||
if (errorRate >= 30) return 'bg-amber-500/60 border-amber-500'; // Warning
|
||||
if (errorRate >= 10) return 'bg-yellow-500/30 border-yellow-500/50'; // Moderate
|
||||
return 'bg-green-500/20 border-green-500/30'; // Good
|
||||
};
|
||||
|
||||
const rows = isDemo ? HEATMAP_DATA : ZERO_HEATMAP_DATA;
|
||||
|
||||
return (
|
||||
<Card className="border-border bg-background shadow-sm h-full min-h-[540px] flex flex-col justify-between">
|
||||
<CardHeader className="pb-4">
|
||||
@@ -28,7 +43,9 @@ export function MistakeHeatmap() {
|
||||
</CardTitle>
|
||||
<Info size={16} className="text-muted-foreground" />
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">Topic-wise error rate across last 5 tests.</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{isDemo ? "Topic-wise error rate across last 5 tests." : "Topic-wise error rate across your recent test sessions."}
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 p-4 md:p-6 flex flex-col justify-between">
|
||||
<div className="w-full">
|
||||
@@ -46,7 +63,7 @@ export function MistakeHeatmap() {
|
||||
|
||||
{/* Data rows */}
|
||||
<div className="space-y-3">
|
||||
{HEATMAP_DATA.map((row, idx) => (
|
||||
{rows.map((row, idx) => (
|
||||
<div key={idx} className="flex items-center group">
|
||||
<div className="w-32 sm:w-36 md:w-40 shrink-0 text-xs sm:text-sm md:text-base font-bold truncate pr-2 group-hover:text-primary transition-colors" title={row.topic}>
|
||||
{row.topic}
|
||||
@@ -65,11 +82,22 @@ export function MistakeHeatmap() {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{!isDemo && (
|
||||
<div className="mt-6 p-4 rounded-xl bg-secondary/30 border border-border/50 text-center space-y-1">
|
||||
<div className="flex items-center justify-center gap-1.5 text-xs font-bold text-green-400">
|
||||
<CheckCircle2 size={14} /> Clean Slate · 0 Errors Recorded
|
||||
</div>
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
As you attempt DPPs and CBT mock tests, questions you get wrong will automatically be tracked and heatmapped here.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Legend */}
|
||||
<div className="mt-6 pt-4 border-t border-border/50 flex flex-wrap items-center justify-center gap-x-4 gap-y-2 text-xs font-bold text-muted-foreground">
|
||||
<div className="flex items-center gap-1.5"><div className="size-3 rounded-sm bg-green-500/20 border border-green-500/30"></div> {"< 10%"}</div>
|
||||
<div className="flex items-center gap-1.5"><div className="size-3 rounded-sm bg-green-500/20 border border-green-500/30"></div> {"0-10% (Optimal)"}</div>
|
||||
<div className="flex items-center gap-1.5"><div className="size-3 rounded-sm bg-yellow-500/30 border border-yellow-500/50"></div> {"10-30%"}</div>
|
||||
<div className="flex items-center gap-1.5"><div className="size-3 rounded-sm bg-amber-500/60 border border-amber-500"></div> {"30-50%"}</div>
|
||||
<div className="flex items-center gap-1.5"><div className="size-3 rounded-sm bg-red-500/80 border border-red-500"></div> {"> 50%"}</div>
|
||||
@@ -78,3 +106,4 @@ export function MistakeHeatmap() {
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,13 @@ import { Card, CardContent } from "@/components/ui/card"
|
||||
import { TrendingUp, Flame, RefreshCw } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
|
||||
import { useActiveUser } from "@/hooks/useActiveUser"
|
||||
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
||||
|
||||
export function ReadinessGauge() {
|
||||
const [data, setData] = useState<any>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const { userId, loading: userLoading } = useActiveUser();
|
||||
const isDemo = isDemoUser(userId);
|
||||
|
||||
useEffect(() => {
|
||||
if (userLoading || !userId) return;
|
||||
@@ -22,23 +23,35 @@ export function ReadinessGauge() {
|
||||
const json = await res.json()
|
||||
setData(json)
|
||||
} catch (err) {
|
||||
console.warn("Using mock fallback data for Readiness Gauge.")
|
||||
setData({
|
||||
score: 72,
|
||||
breakdown: {
|
||||
accuracy: 30,
|
||||
speed: 15,
|
||||
consistency: 8,
|
||||
recovery: 5,
|
||||
coverage: 14
|
||||
}
|
||||
})
|
||||
if (isDemo) {
|
||||
setData({
|
||||
score: 72,
|
||||
breakdown: {
|
||||
accuracy: 30,
|
||||
speed: 15,
|
||||
consistency: 8,
|
||||
recovery: 5,
|
||||
coverage: 14
|
||||
}
|
||||
})
|
||||
} else {
|
||||
setData({
|
||||
score: 0,
|
||||
breakdown: {
|
||||
accuracy: 0,
|
||||
speed: 0,
|
||||
consistency: 0,
|
||||
recovery: 0,
|
||||
coverage: 0
|
||||
}
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
fetchReadiness()
|
||||
}, [userId, userLoading])
|
||||
}, [userId, userLoading, isDemo])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -49,16 +62,10 @@ export function ReadinessGauge() {
|
||||
}
|
||||
|
||||
const score = data?.score || 0
|
||||
const breakdown = data?.breakdown || {}
|
||||
const isZero = score === 0 && !isDemo
|
||||
const circumference = 2 * Math.PI * 45 // r=45
|
||||
const strokeDashoffset = circumference - (score / 100) * circumference
|
||||
|
||||
const getColor = (s: number) => {
|
||||
if (s >= 75) return "text-green-500"
|
||||
if (s >= 50) return "text-yellow-500"
|
||||
return "text-red-500"
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
|
||||
@@ -67,10 +74,12 @@ export function ReadinessGauge() {
|
||||
<div className="absolute -top-1/2 -right-1/4 w-48 h-48 bg-[radial-gradient(circle,rgba(94,196,184,0.06)_0%,transparent_70%)] rounded-full pointer-events-none" />
|
||||
|
||||
<div className="flex justify-between items-start mb-4 relative z-10">
|
||||
<span className="text-[11px] font-bold text-muted-foreground uppercase tracking-widest">Updated daily • Today</span>
|
||||
<span className="flex items-center gap-1 text-xs font-bold text-green-500 bg-green-500/10 px-2.5 py-0.5 rounded-full">
|
||||
<span className="text-[11px] font-bold text-muted-foreground uppercase tracking-widest">
|
||||
{isZero ? "Algorithmic Baseline" : "Updated daily • Today"}
|
||||
</span>
|
||||
<span className={`flex items-center gap-1 text-xs font-bold px-2.5 py-0.5 rounded-full ${isZero ? "text-muted-foreground bg-white/5" : "text-green-500 bg-green-500/10"}`}>
|
||||
<TrendingUp size={14} className="stroke-[2.5px]" />
|
||||
+5 this week
|
||||
{isZero ? "Day 1 Baseline" : "+5 this week"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -91,7 +100,7 @@ export function ReadinessGauge() {
|
||||
strokeWidth="6"
|
||||
fill="transparent"
|
||||
strokeDasharray={circumference}
|
||||
strokeDashoffset={strokeDashoffset}
|
||||
strokeDashoffset={isZero ? circumference : strokeDashoffset}
|
||||
strokeLinecap="round"
|
||||
className="transition-all duration-1500 ease-out"
|
||||
/>
|
||||
@@ -105,19 +114,31 @@ export function ReadinessGauge() {
|
||||
{/* Quick metric grid beside ring */}
|
||||
<div className="flex-1 grid grid-cols-2 gap-2.5 w-full">
|
||||
<div className="bg-white/5 rounded-lg p-2.5">
|
||||
<div className="text-base font-black flex items-baseline gap-1">88% <span className="text-xs text-green-500">↑</span></div>
|
||||
<div className="text-base font-black flex items-baseline gap-1">
|
||||
{isZero ? "0%" : "88%"}
|
||||
{!isZero && <span className="text-xs text-green-500">↑</span>}
|
||||
</div>
|
||||
<div className="text-xs font-bold text-muted-foreground">Accuracy</div>
|
||||
</div>
|
||||
<div className="bg-white/5 rounded-lg p-2.5">
|
||||
<div className="text-base font-black flex items-baseline gap-1">45s <span className="text-xs text-red-500">↓</span></div>
|
||||
<div className="text-base font-black flex items-baseline gap-1">
|
||||
{isZero ? "-- s" : "45s"}
|
||||
{!isZero && <span className="text-xs text-red-500">↓</span>}
|
||||
</div>
|
||||
<div className="text-xs font-bold text-muted-foreground">Speed/Q</div>
|
||||
</div>
|
||||
<div className="bg-white/5 rounded-lg p-2.5">
|
||||
<div className="text-base font-black flex items-baseline gap-1">High <span className="text-xs text-green-500">↑</span></div>
|
||||
<div className="text-base font-black flex items-baseline gap-1">
|
||||
{isZero ? "0%" : "High"}
|
||||
{!isZero && <span className="text-xs text-green-500">↑</span>}
|
||||
</div>
|
||||
<div className="text-xs font-bold text-muted-foreground">Consistency</div>
|
||||
</div>
|
||||
<div className="bg-white/5 rounded-lg p-2.5">
|
||||
<div className="text-base font-black flex items-baseline gap-1">64% <span className="text-xs text-green-500">↑</span></div>
|
||||
<div className="text-base font-black flex items-baseline gap-1">
|
||||
{isZero ? "0%" : "64%"}
|
||||
{!isZero && <span className="text-xs text-green-500">↑</span>}
|
||||
</div>
|
||||
<div className="text-xs font-bold text-muted-foreground">Syllabus</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -126,12 +147,12 @@ export function ReadinessGauge() {
|
||||
{/* Subject Bars */}
|
||||
<div className="space-y-2 mb-4 relative z-10">
|
||||
{[
|
||||
{ name: "Physics", val: 68, color: "#00F0FF" },
|
||||
{ name: "Chemistry", val: 74, color: "#F59E0B" },
|
||||
{ name: "Biology", val: 82, color: "#10B981" },
|
||||
{ name: "Physics", val: isZero ? 0 : 68, color: "#00F0FF" },
|
||||
{ name: "Chemistry", val: isZero ? 0 : 74, color: "#F59E0B" },
|
||||
{ name: "Biology / Maths", val: isZero ? 0 : 82, color: "#10B981" },
|
||||
].map(sub => (
|
||||
<div key={sub.name} className="flex items-center gap-3">
|
||||
<span className="text-xs font-bold text-muted-foreground w-20 shrink-0">{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="h-full rounded-full transition-all duration-1000"
|
||||
@@ -147,15 +168,20 @@ export function ReadinessGauge() {
|
||||
<div className="flex items-center justify-between px-3 py-2 bg-white/5 rounded-lg mb-4 relative z-10 text-xs font-bold">
|
||||
<span className="text-muted-foreground">National Avg</span>
|
||||
<span className="text-muted-foreground">58 / 100</span>
|
||||
<span className="text-green-500">You: +{score - 58} above avg</span>
|
||||
<span className={isZero ? "text-muted-foreground" : "text-green-500"}>
|
||||
{isZero ? "Pending test data" : `You: +${score - 58} above avg`}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-auto px-3.5 py-3 bg-[#00F0FF]/10 border border-[#00F0FF]/20 rounded-lg text-xs md:text-sm text-[#00F0FF] font-bold flex items-center gap-2 relative z-10">
|
||||
<Flame size={18} className="fill-[#00F0FF]/20 shrink-0" />
|
||||
Your speed has dropped slightly. Focus on time-bound practice today.
|
||||
{isZero
|
||||
? "Complete your first diagnostic test to establish your personalized readiness score."
|
||||
: "Your speed has dropped slightly. Focus on time-bound practice today."}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"use client"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Flame, Star, Clock, TrendingUp } from "lucide-react"
|
||||
import { Flame, Star, Clock, TrendingUp, Sparkles } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
||||
|
||||
const SLOTS = [
|
||||
const DEMO_SLOTS = [
|
||||
{ label: "6 AM", h: 30, score: 52, peak: false },
|
||||
{ label: "9 AM", h: 55, score: 68, peak: false },
|
||||
{ label: "12 PM", h: 65, score: 74, peak: false },
|
||||
@@ -15,9 +16,25 @@ const SLOTS = [
|
||||
{ label: "11 PM", h: 32, score: 48, peak: false },
|
||||
]
|
||||
|
||||
const ZERO_SLOTS = [
|
||||
{ label: "6 AM", h: 0, score: 0, peak: false },
|
||||
{ label: "9 AM", h: 0, score: 0, peak: false },
|
||||
{ label: "12 PM", h: 0, score: 0, peak: false },
|
||||
{ label: "2 PM", h: 0, score: 0, peak: false },
|
||||
{ label: "3 PM", h: 0, score: 0, peak: false },
|
||||
{ label: "5 PM", h: 0, score: 0, peak: false },
|
||||
{ label: "7 PM", h: 0, score: 0, peak: false },
|
||||
{ label: "9 PM", h: 0, score: 0, peak: false },
|
||||
{ label: "11 PM", h: 0, score: 0, peak: false },
|
||||
]
|
||||
|
||||
export function StudyTimeOptimizationCard() {
|
||||
const { userId } = useActiveUser()
|
||||
const isDemo = isDemoUser(userId)
|
||||
const [hovered, setHovered] = useState<number | null>(null)
|
||||
const maxH = Math.max(...SLOTS.map(s => s.h))
|
||||
|
||||
const slots = isDemo ? DEMO_SLOTS : ZERO_SLOTS
|
||||
const maxH = Math.max(1, ...slots.map(s => s.h))
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -29,11 +46,11 @@ export function StudyTimeOptimizationCard() {
|
||||
<Star size={15} className="fill-yellow-500 text-yellow-500" />
|
||||
<span className="text-sm font-bold text-yellow-500">Peak Window:</span>
|
||||
<span className="text-sm font-bold bg-yellow-500/10 border border-yellow-500/20 px-2.5 py-0.5 rounded-full">
|
||||
2 PM – 5 PM
|
||||
{isDemo ? "2 PM – 5 PM" : "Calibrating..."}
|
||||
</span>
|
||||
</div>
|
||||
<div className="ml-auto flex items-center gap-1.5 text-xs font-bold text-green-400 bg-green-400/10 border border-green-400/20 px-2.5 py-1 rounded-full">
|
||||
<TrendingUp size={11} /> 95% efficiency at peak
|
||||
<div className={`ml-auto flex items-center gap-1.5 text-xs font-bold px-2.5 py-1 rounded-full border ${isDemo ? "text-green-400 bg-green-400/10 border-green-400/20" : "text-muted-foreground bg-white/5 border-white/10"}`}>
|
||||
<TrendingUp size={11} /> {isDemo ? "95% efficiency at peak" : "0% baseline"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -58,7 +75,7 @@ export function StudyTimeOptimizationCard() {
|
||||
/>
|
||||
))}
|
||||
<div className="flex items-end gap-1 h-full absolute inset-0">
|
||||
{SLOTS.map((slot, i) => (
|
||||
{slots.map((slot, i) => (
|
||||
<div
|
||||
key={slot.label}
|
||||
className="flex-1 flex flex-col items-center justify-end h-full cursor-pointer relative group/bar"
|
||||
@@ -69,7 +86,9 @@ export function StudyTimeOptimizationCard() {
|
||||
{hovered === i && (
|
||||
<div className="absolute bottom-full mb-1.5 left-1/2 -translate-x-1/2 bg-background border border-border rounded-lg px-2.5 py-1.5 text-xs font-bold whitespace-nowrap shadow-lg z-10 pointer-events-none">
|
||||
<div className="text-foreground">{slot.label}</div>
|
||||
<div className={slot.peak ? "text-yellow-400" : "text-[#00F0FF]"}>Score: {slot.score}%</div>
|
||||
<div className={slot.peak ? "text-yellow-400" : "text-[#00F0FF]"}>
|
||||
Score: {slot.score}%
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
@@ -78,9 +97,11 @@ export function StudyTimeOptimizationCard() {
|
||||
? "bg-gradient-to-t from-yellow-600/80 to-yellow-400 shadow-[0_0_14px_rgba(234,179,8,0.5)]"
|
||||
: hovered === i
|
||||
? "bg-gradient-to-t from-[#00F0FF]/70 to-[#00F0FF]"
|
||||
: "bg-gradient-to-t from-[#00F0FF]/20 to-[#00F0FF]/50"
|
||||
: isDemo
|
||||
? "bg-gradient-to-t from-[#00F0FF]/20 to-[#00F0FF]/50"
|
||||
: "bg-white/5 border border-white/10"
|
||||
}`}
|
||||
style={{ height: `${(slot.h / maxH) * 100}%` }}
|
||||
style={{ height: isDemo ? `${(slot.h / maxH) * 100}%` : "4px" }}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
@@ -88,7 +109,7 @@ export function StudyTimeOptimizationCard() {
|
||||
</div>
|
||||
{/* X-axis labels */}
|
||||
<div className="flex gap-1 pt-1">
|
||||
{SLOTS.map((slot) => (
|
||||
{slots.map((slot) => (
|
||||
<div key={slot.label} className="flex-1 text-center">
|
||||
<span className="text-[10px] text-muted-foreground font-bold whitespace-nowrap">{slot.label}</span>
|
||||
</div>
|
||||
@@ -100,9 +121,9 @@ export function StudyTimeOptimizationCard() {
|
||||
{/* Efficiency breakdown */}
|
||||
<div className="grid grid-cols-3 gap-2 text-center">
|
||||
{[
|
||||
{ label: "Morning", time: "6–9 AM", eff: 60, color: "text-muted-foreground" },
|
||||
{ label: "Peak", time: "2–5 PM", eff: 95, color: "text-yellow-400" },
|
||||
{ label: "Evening", time: "7–9 PM", eff: 73, color: "text-[#00F0FF]" },
|
||||
{ label: "Morning", time: "6–9 AM", eff: isDemo ? 60 : 0, color: "text-muted-foreground" },
|
||||
{ label: "Peak", time: "2–5 PM", eff: isDemo ? 95 : 0, color: isDemo ? "text-yellow-400" : "text-muted-foreground" },
|
||||
{ label: "Evening", time: "7–9 PM", eff: isDemo ? 73 : 0, color: isDemo ? "text-[#00F0FF]" : "text-muted-foreground" },
|
||||
].map(slot => (
|
||||
<div key={slot.label} className="p-2.5 rounded-xl bg-white/5 border border-border/30">
|
||||
<div className={`text-base font-black ${slot.color}`}>{slot.eff}%</div>
|
||||
@@ -114,10 +135,13 @@ export function StudyTimeOptimizationCard() {
|
||||
|
||||
<div className="px-3.5 py-3 bg-yellow-500/10 border border-yellow-500/20 rounded-xl text-xs md:text-sm text-yellow-500 font-bold flex items-center gap-2">
|
||||
<Flame size={18} className="fill-yellow-500 shrink-0" />
|
||||
You perform best 2–5 PM. Schedule your hardest mock tests and DPPs in this window.
|
||||
{isDemo
|
||||
? "You perform best 2–5 PM. Schedule your hardest mock tests and DPPs in this window."
|
||||
: "Circadian AI calibrating: Complete practice sessions at different times of day to reveal your peak study window."}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,24 +3,33 @@
|
||||
import { TrendingUp, TrendingDown, Brain, Zap, Target, Clock } from "lucide-react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { useState } from "react"
|
||||
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
||||
|
||||
const DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
|
||||
const QUESTIONS = [42, 58, 35, 71, 65, 48, 60]
|
||||
const ACCURACY = [82, 78, 91, 88, 85, 79, 93]
|
||||
const HOURS = [2.5, 3.5, 2, 4.5, 4, 3, 3.8]
|
||||
const DEMO_QUESTIONS = [42, 58, 35, 71, 65, 48, 60]
|
||||
const DEMO_ACCURACY = [82, 78, 91, 88, 85, 79, 93]
|
||||
const DEMO_HOURS = [2.5, 3.5, 2, 4.5, 4, 3, 3.8]
|
||||
|
||||
const ZERO_DATA = [0, 0, 0, 0, 0, 0, 0]
|
||||
|
||||
export function WeeklyProgressReport() {
|
||||
const { userId } = useActiveUser()
|
||||
const isDemo = isDemoUser(userId)
|
||||
const [metric, setMetric] = useState<"questions" | "accuracy" | "hours">("accuracy")
|
||||
const [hovered, setHovered] = useState<number | null>(null)
|
||||
|
||||
const QUESTIONS = isDemo ? DEMO_QUESTIONS : ZERO_DATA
|
||||
const ACCURACY = isDemo ? DEMO_ACCURACY : ZERO_DATA
|
||||
const HOURS = isDemo ? DEMO_HOURS : ZERO_DATA
|
||||
|
||||
const DATA = metric === "questions" ? QUESTIONS : metric === "accuracy" ? ACCURACY : HOURS
|
||||
const maxVal = Math.max(...DATA)
|
||||
const maxVal = Math.max(1, ...DATA)
|
||||
const avgVal = Math.round((DATA.reduce((a, b) => a + b, 0) / DATA.length) * 10) / 10
|
||||
const trend = DATA[DATA.length - 1] - DATA[0]
|
||||
const trend = isDemo ? DATA[DATA.length - 1] - DATA[0] : 0
|
||||
const trendUp = trend >= 0
|
||||
|
||||
const UNIT = metric === "accuracy" ? "%" : metric === "hours" ? "h" : ""
|
||||
const avgAccuracy = Math.round(ACCURACY.reduce((a, b) => a + b, 0) / ACCURACY.length)
|
||||
const avgAccuracy = isDemo ? Math.round(ACCURACY.reduce((a, b) => a + b, 0) / ACCURACY.length) : 0
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
@@ -38,7 +47,7 @@ export function WeeklyProgressReport() {
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-xs font-bold px-2.5 py-1 rounded-full bg-yellow-500/10 text-yellow-500 border border-yellow-500/20">
|
||||
<Zap size={12} />
|
||||
7/7 days active
|
||||
{isDemo ? "7/7 days active" : "Day 1 Active"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -89,13 +98,15 @@ export function WeeklyProgressReport() {
|
||||
)}
|
||||
<div
|
||||
className={`w-full rounded-t-md transition-all duration-700 ${
|
||||
i === 6
|
||||
!isDemo
|
||||
? "bg-white/5 border border-white/10"
|
||||
: i === 6
|
||||
? "bg-gradient-to-t from-[#00F0FF]/60 to-[#00F0FF]"
|
||||
: hovered === i
|
||||
? "bg-gradient-to-t from-[#00F0FF]/40 to-[#00F0FF]/80"
|
||||
: "bg-gradient-to-t from-white/10 to-white/25"
|
||||
}`}
|
||||
style={{ height: `${(DATA[i] / maxVal) * 100}%` }}
|
||||
style={{ height: isDemo ? `${(DATA[i] / maxVal) * 100}%` : "4px" }}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
@@ -116,8 +127,8 @@ export function WeeklyProgressReport() {
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{[
|
||||
{ label: "Avg Accuracy", value: `${avgAccuracy}%`, icon: <Target size={12} className="text-green-400" /> },
|
||||
{ label: "Questions / Day", value: `${Math.round(QUESTIONS.reduce((a, b) => a + b, 0) / 7)}`, icon: <Zap size={12} className="text-[#00F0FF]" /> },
|
||||
{ label: "Study Hrs / Day", value: `${(HOURS.reduce((a, b) => a + b, 0) / 7).toFixed(1)}h`, icon: <Clock size={12} className="text-amber-400" /> },
|
||||
{ label: "Questions / Day", value: isDemo ? `${Math.round(QUESTIONS.reduce((a, b) => a + b, 0) / 7)}` : "0", icon: <Zap size={12} className="text-[#00F0FF]" /> },
|
||||
{ label: "Study Hrs / Day", value: isDemo ? `${(HOURS.reduce((a, b) => a + b, 0) / 7).toFixed(1)}h` : "0.0h", icon: <Clock size={12} className="text-amber-400" /> },
|
||||
].map(stat => (
|
||||
<div key={stat.label} className="p-2.5 rounded-xl bg-white/5 border border-border/30 text-center">
|
||||
<div className="flex items-center justify-center gap-1 mb-0.5">{stat.icon}</div>
|
||||
@@ -131,8 +142,16 @@ export function WeeklyProgressReport() {
|
||||
<div className="px-3 py-2.5 bg-[#00F0FF]/10 border border-[#00F0FF]/20 rounded-xl text-xs text-[#00F0FF] font-medium flex items-start gap-2.5">
|
||||
<Brain size={15} className="shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<span className="font-black text-white">Sign errors in Thermodynamics</span> — detected in your last 3 attempts (W = −PΔV misapplied). We've queued 3 targeted practice questions for tomorrow.{" "}
|
||||
<span className="text-[#00F0FF]/80"><strong className="text-white">Tip:</strong> You peak at 7–9 PM. Schedule the hardest topics then.</span>
|
||||
{isDemo ? (
|
||||
<>
|
||||
<span className="font-black text-white">Sign errors in Thermodynamics</span> — detected in your last 3 attempts (W = −PΔV misapplied). We've queued 3 targeted practice questions for tomorrow.{" "}
|
||||
<span className="text-[#00F0FF]/80"><strong className="text-white">Tip:</strong> You peak at 7–9 PM. Schedule the hardest topics then.</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="font-black text-white">AI Progress Engine Calibrating</span> — Complete your first CBT mock test or DPP to begin mapping subject retention, speed trends, and error velocity.
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -140,3 +159,4 @@ export function WeeklyProgressReport() {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user