feat(ui): add new ui
This commit is contained in:
@ -7,134 +7,326 @@ import {
|
||||
Trophy,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "../../components/ui/card";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useExamConfigStore } from "../../stores/useExamConfigStore";
|
||||
|
||||
const DOTS = [
|
||||
{ size: 10, color: "#f97316", top: "8%", left: "5%", delay: "0s" },
|
||||
{ size: 7, color: "#a855f7", top: "30%", left: "2%", delay: "1.2s" },
|
||||
{ size: 9, color: "#22c55e", top: "62%", left: "4%", delay: "0.6s" },
|
||||
{ size: 12, color: "#3b82f6", top: "12%", right: "4%", delay: "1.8s" },
|
||||
{ size: 7, color: "#f43f5e", top: "48%", right: "3%", delay: "0.9s" },
|
||||
{ size: 9, color: "#eab308", top: "78%", right: "6%", delay: "0.4s" },
|
||||
];
|
||||
|
||||
const STYLES = `
|
||||
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;800;900&family=Nunito+Sans:wght@400;600;700&display=swap');
|
||||
|
||||
.pr-screen {
|
||||
min-height: 100vh;
|
||||
background: #fffbf4;
|
||||
font-family: 'Nunito', sans-serif;
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* ── Blobs ── */
|
||||
.pr-blob { position: fixed; pointer-events: none; z-index: 0; filter: blur(48px); opacity: 0.35; }
|
||||
.pr-blob-1 { width:240px;height:240px;background:#fde68a;top:-80px;left:-80px;border-radius:60% 40% 70% 30%/50% 60% 40% 50%;animation:prWobble1 14s ease-in-out infinite; }
|
||||
.pr-blob-2 { width:190px;height:190px;background:#a5f3c0;bottom:-50px;left:6%;border-radius:40% 60% 30% 70%/60% 40% 60% 40%;animation:prWobble2 16s ease-in-out infinite; }
|
||||
.pr-blob-3 { width:210px;height:210px;background:#fbcfe8;top:15%;right:-60px;border-radius:70% 30% 50% 50%/40% 60% 40% 60%;animation:prWobble1 18s ease-in-out infinite reverse; }
|
||||
.pr-blob-4 { width:150px;height:150px;background:#bfdbfe;bottom:12%;right:2%;border-radius:50% 50% 30% 70%/60% 40% 60% 40%;animation:prWobble2 12s ease-in-out infinite; }
|
||||
|
||||
@keyframes prWobble1 {
|
||||
0%,100%{border-radius:60% 40% 70% 30%/50% 60% 40% 50%;transform:translate(0,0) rotate(0deg);}
|
||||
50%{border-radius:40% 60% 30% 70%/60% 40% 60% 40%;transform:translate(12px,16px) rotate(8deg);}
|
||||
}
|
||||
@keyframes prWobble2 {
|
||||
0%,100%{border-radius:40% 60% 30% 70%/60% 40% 60% 40%;transform:translate(0,0) rotate(0deg);}
|
||||
50%{border-radius:60% 40% 70% 30%/40% 60% 40% 60%;transform:translate(-10px,12px) rotate(-6deg);}
|
||||
}
|
||||
|
||||
/* ── Floating dots ── */
|
||||
.pr-dot { position:fixed;border-radius:50%;pointer-events:none;z-index:0;opacity:0.3;animation:prFloat 7s ease-in-out infinite; }
|
||||
@keyframes prFloat {
|
||||
0%,100%{transform:translateY(0) rotate(0deg);}
|
||||
50%{transform:translateY(-12px) rotate(180deg);}
|
||||
}
|
||||
|
||||
/* ── Inner container ── */
|
||||
.pr-inner {
|
||||
position: relative; z-index: 1;
|
||||
max-width: 580px; margin: 0 auto;
|
||||
padding: 2rem 1.25rem 4rem;
|
||||
display: flex; flex-direction: column; gap: 1.5rem;
|
||||
}
|
||||
|
||||
/* ── Animations ── */
|
||||
@keyframes prPopIn {
|
||||
from { opacity:0; transform: scale(0.92) translateY(12px); }
|
||||
to { opacity:1; transform: scale(1) translateY(0); }
|
||||
}
|
||||
.pr-anim { animation: prPopIn 0.4s cubic-bezier(0.34,1.56,0.64,1) both; }
|
||||
.pr-anim-1 { animation-delay: 0.05s; }
|
||||
.pr-anim-2 { animation-delay: 0.1s; }
|
||||
.pr-anim-3 { animation-delay: 0.15s; }
|
||||
.pr-anim-4 { animation-delay: 0.2s; }
|
||||
|
||||
/* ── Header ── */
|
||||
.pr-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
animation: prPopIn 0.4s cubic-bezier(0.34,1.56,0.64,1) both;
|
||||
}
|
||||
.pr-logo-btn {
|
||||
width: 44px; height: 44px; border-radius: 14px;
|
||||
background: linear-gradient(135deg, #a855f7, #7c3aed);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
box-shadow: 0 4px 0 #5b21b644;
|
||||
}
|
||||
.pr-xp-chip {
|
||||
display: flex; align-items: center; gap: 0.5rem;
|
||||
background: white; border: 2.5px solid #e9d5ff;
|
||||
border-radius: 100px; padding: 0.45rem 1rem;
|
||||
font-size: 0.85rem; font-weight: 800; color: #7c3aed;
|
||||
box-shadow: 0 3px 10px rgba(0,0,0,0.05);
|
||||
}
|
||||
.pr-xp-dot {
|
||||
width: 8px; height: 8px; border-radius: 50%;
|
||||
background: linear-gradient(135deg, #a855f7, #7c3aed);
|
||||
}
|
||||
|
||||
/* ── Hero banner ── */
|
||||
.pr-hero {
|
||||
border-radius: 24px;
|
||||
background: linear-gradient(135deg, #7c3aed, #a855f7);
|
||||
padding: 1.5rem;
|
||||
position: relative; overflow: hidden;
|
||||
box-shadow: 0 8px 0 #5b21b644, 0 12px 32px rgba(124,58,237,0.25);
|
||||
display: flex; flex-direction: column; gap: 0.75rem;
|
||||
}
|
||||
.pr-hero-icon-bg {
|
||||
position: absolute; right: -40px; top: -30px;
|
||||
opacity: 0.15; transform: rotate(-30deg);
|
||||
pointer-events: none;
|
||||
}
|
||||
.pr-hero-eyebrow {
|
||||
font-size: 0.65rem; font-weight: 800; letter-spacing: 0.16em;
|
||||
text-transform: uppercase; color: #e9d5ff;
|
||||
}
|
||||
.pr-hero-title {
|
||||
font-size: clamp(1.6rem, 5vw, 2rem); font-weight: 900;
|
||||
color: white; letter-spacing: -0.02em; line-height: 1.15;
|
||||
position: relative; z-index: 1;
|
||||
}
|
||||
.pr-hero-sub {
|
||||
font-family: 'Nunito Sans', sans-serif;
|
||||
font-size: 0.85rem; font-weight: 600; color: #ddd6fe;
|
||||
position: relative; z-index: 1;
|
||||
}
|
||||
.pr-hero-btn {
|
||||
display: inline-flex; align-items: center; gap: 0.4rem;
|
||||
background: white; border: none; border-radius: 100px;
|
||||
padding: 0.7rem 1.4rem; cursor: pointer;
|
||||
font-family: 'Nunito', sans-serif; font-size: 0.88rem; font-weight: 800;
|
||||
color: #7c3aed;
|
||||
box-shadow: 0 4px 0 rgba(0,0,0,0.15);
|
||||
transition: transform 0.1s ease, box-shadow 0.1s ease;
|
||||
width: fit-content; position: relative; z-index: 1;
|
||||
}
|
||||
.pr-hero-btn:hover { transform:translateY(-2px); box-shadow:0 6px 0 rgba(0,0,0,0.15); }
|
||||
.pr-hero-btn:active { transform:translateY(2px); box-shadow:0 2px 0 rgba(0,0,0,0.15); }
|
||||
|
||||
/* ── Section title ── */
|
||||
.pr-section-title {
|
||||
font-size: 1.15rem; font-weight: 900; color: #1e1b4b;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
/* ── Practice mode grid ── */
|
||||
.pr-grid {
|
||||
display: grid; grid-template-columns: 1fr;
|
||||
gap: 0.85rem;
|
||||
}
|
||||
@media(min-width: 480px){ .pr-grid { grid-template-columns: 1fr 1fr; } }
|
||||
|
||||
/* ── Mode card ── */
|
||||
.pr-mode-card {
|
||||
background: white; border: 2.5px solid #f3f4f6; border-radius: 22px;
|
||||
padding: 1.1rem 1.25rem;
|
||||
box-shadow: 0 4px 14px rgba(0,0,0,0.04);
|
||||
cursor: pointer; display: flex; flex-direction: column; gap: 0.85rem;
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
|
||||
position: relative; overflow: hidden;
|
||||
}
|
||||
.pr-mode-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 10px 24px rgba(0,0,0,0.08);
|
||||
}
|
||||
.pr-mode-card:active { transform: translateY(1px); box-shadow: 0 3px 8px rgba(0,0,0,0.06); }
|
||||
|
||||
.pr-mode-card.red { border-color: #fecaca; }
|
||||
.pr-mode-card.red:hover { border-color: #fca5a5; }
|
||||
.pr-mode-card.cyan { border-color: #a5f3fc; }
|
||||
.pr-mode-card.cyan:hover { border-color: #67e8f9; }
|
||||
.pr-mode-card.lime { border-color: #d9f99d; }
|
||||
.pr-mode-card.lime:hover { border-color: #bef264; }
|
||||
|
||||
.pr-mode-top {
|
||||
display: flex; align-items: flex-start; justify-content: space-between;
|
||||
}
|
||||
.pr-mode-icon {
|
||||
width: 44px; height: 44px; border-radius: 14px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.pr-mode-icon.red { background: linear-gradient(135deg, #f87171, #ef4444); box-shadow: 0 4px 0 #b91c1c44; }
|
||||
.pr-mode-icon.cyan { background: linear-gradient(135deg, #22d3ee, #06b6d4); box-shadow: 0 4px 0 #0e7490aa; }
|
||||
.pr-mode-icon.lime { background: linear-gradient(135deg, #a3e635, #84cc16); box-shadow: 0 4px 0 #4d7c0f44; }
|
||||
|
||||
.pr-mode-badge {
|
||||
width: 36px; height: 36px; border-radius: 50%;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.pr-mode-badge.red { background: #fff5f5; }
|
||||
.pr-mode-badge.cyan { background: #ecfeff; }
|
||||
.pr-mode-badge.lime { background: #f7ffe4; }
|
||||
|
||||
.pr-mode-title {
|
||||
font-size: 1rem; font-weight: 900; color: #1e1b4b;
|
||||
}
|
||||
.pr-mode-desc {
|
||||
font-family: 'Nunito Sans', sans-serif;
|
||||
font-size: 0.78rem; font-weight: 600; color: #9ca3af;
|
||||
}
|
||||
.pr-mode-arrow {
|
||||
font-size: 0.75rem; font-weight: 800; margin-top: auto;
|
||||
display: flex; align-items: center; gap: 0.25rem;
|
||||
transition: gap 0.2s ease;
|
||||
}
|
||||
.pr-mode-card:hover .pr-mode-arrow { gap: 0.5rem; }
|
||||
.pr-mode-arrow.red { color: #ef4444; }
|
||||
.pr-mode-arrow.cyan { color: #06b6d4; }
|
||||
.pr-mode-arrow.lime { color: #84cc16; }
|
||||
`;
|
||||
|
||||
const MODE_CARDS = [
|
||||
{
|
||||
color: "red",
|
||||
icon: <Target size={20} color="white" />,
|
||||
badge: <Loader2 size={22} color="#ef4444" />,
|
||||
title: "Targeted Practice",
|
||||
desc: "Focus on your weak spots and improve fast",
|
||||
route: "/student/practice/targeted-practice",
|
||||
arrow: "Practice →",
|
||||
},
|
||||
{
|
||||
color: "cyan",
|
||||
icon: <Zap size={20} color="white" />,
|
||||
badge: <Clock size={22} color="#06b6d4" />,
|
||||
title: "Drills",
|
||||
desc: "Train speed and accuracy under pressure",
|
||||
route: "/student/practice/drills",
|
||||
arrow: "Drill →",
|
||||
},
|
||||
{
|
||||
color: "lime",
|
||||
icon: <Trophy size={20} color="white" />,
|
||||
badge: <BookOpen size={22} color="#84cc16" />,
|
||||
title: "Hard Modules",
|
||||
desc: "Push yourself with the toughest questions",
|
||||
route: "/student/practice/hard-test-modules",
|
||||
arrow: "Challenge →",
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const Practice = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const userXp = useExamConfigStore.getState().userXp;
|
||||
return (
|
||||
<div className="px-8 py-8 space-y-4">
|
||||
<header className="flex justify-between items-center ">
|
||||
<div className="w-fit bg-linear-to-br from-indigo-500 to-indigo-600 p-3 rounded-2xl">
|
||||
<BookOpen size={20} color="white" />
|
||||
</div>
|
||||
<div className="bg-indigo-100 rounded-full w-fit py-2 px-4 flex items-center gap-2">
|
||||
<div className="h-2 w-2 bg-linear-to-br from-indigo-400 to-indigo-500 rounded-full"></div>
|
||||
<span className="font-satoshi-bold text-md">{userXp}</span>
|
||||
</div>
|
||||
</header>
|
||||
<section>
|
||||
<Card
|
||||
className="relative bg-linear-to-br from-indigo-500 to-indigo-600 rounded-4xl
|
||||
flex-row"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<CardHeader className="w-[65%] md:w-full">
|
||||
<CardTitle className="font-satoshi-bold tracking-tight text-3xl text-white">
|
||||
See where you stand
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="w-2/3 md:w-full">
|
||||
<p className="font-satoshi text-white">
|
||||
Test your knowledge with an adaptive practice test.
|
||||
</p>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="bg-gray-50 drop-shadow-xl p-6 text-md font-satoshi text-black rounded-full">
|
||||
Take a practice test
|
||||
</Button>
|
||||
</CardFooter>
|
||||
return (
|
||||
<div className="pr-screen">
|
||||
<style>{STYLES}</style>
|
||||
|
||||
{/* Blobs */}
|
||||
<div className="pr-blob pr-blob-1" />
|
||||
<div className="pr-blob pr-blob-2" />
|
||||
<div className="pr-blob pr-blob-3" />
|
||||
<div className="pr-blob pr-blob-4" />
|
||||
|
||||
{/* Dots */}
|
||||
{DOTS.map((d, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="pr-dot"
|
||||
style={
|
||||
{
|
||||
width: d.size,
|
||||
height: d.size,
|
||||
background: d.color,
|
||||
top: d.top,
|
||||
left: d.left,
|
||||
right: d.right,
|
||||
animationDelay: d.delay,
|
||||
animationDuration: `${5 + i * 0.5}s`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
))}
|
||||
|
||||
<div className="pr-inner">
|
||||
{/* ── Header ── */}
|
||||
<header className="pr-header">
|
||||
<div className="pr-logo-btn">
|
||||
<BookOpen size={20} color="white" />
|
||||
</div>
|
||||
<div className="overflow-hidden opacity-30 -rotate-45 absolute -top-10 -right-20">
|
||||
<DraftingCompass size={300} color="white" />
|
||||
<div className="pr-xp-chip">
|
||||
<div className="pr-xp-dot" />
|
||||
<span>⚡ {userXp} XP</span>
|
||||
</div>
|
||||
</Card>
|
||||
</section>
|
||||
<section className="flex flex-col gap-6">
|
||||
<h1 className="font-satoshi-black text-2xl">Practice your way</h1>
|
||||
<div className="md:grid md:grid-cols-2 md:gap-6 space-y-6 md:space-y-0">
|
||||
<Card
|
||||
onClick={() => navigate("/student/practice/targeted-practice")}
|
||||
className="rounded-4xl cursor-pointer hover:bg-gray-50 active:bg-gray-50 active:translate-y-1"
|
||||
>
|
||||
<CardHeader className="space-y-3">
|
||||
<div className="w-fit bg-linear-to-br from-red-400 to-red-500 p-3 rounded-2xl">
|
||||
<Target size={20} color="white" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<CardTitle className="font-satoshi">
|
||||
Targeted Practice
|
||||
</CardTitle>
|
||||
<CardDescription className="font-satoshi">
|
||||
Focus on what matters
|
||||
</CardDescription>
|
||||
</div>
|
||||
<CardAction>
|
||||
<div className="w-fit bg-red-100 p-2 rounded-full">
|
||||
<Loader2 size={30} color="#fa6969" />
|
||||
</div>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card
|
||||
onClick={() => navigate("/student/practice/drills")}
|
||||
className="rounded-4xl cursor-pointer hover:bg-gray-50 active:bg-gray-50 active:translate-y-1"
|
||||
>
|
||||
<CardHeader className="space-y-3">
|
||||
<div className="w-fit bg-linear-to-br from-cyan-400 to-cyan-500 p-3 rounded-2xl">
|
||||
<Zap size={20} color="white" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<CardTitle className="font-satoshi">Drills</CardTitle>
|
||||
<CardDescription className="font-satoshi">
|
||||
Train speed and accuracy
|
||||
</CardDescription>
|
||||
</div>
|
||||
<CardAction>
|
||||
<div className="w-fit bg-cyan-100 p-3 rounded-full">
|
||||
<Clock size={26} color="oklch(71.5% 0.143 215.221)" />
|
||||
</div>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card
|
||||
onClick={() => navigate("/student/practice/hard-test-modules")}
|
||||
className="rounded-4xl cursor-pointer hover:bg-gray-50 active:bg-gray-50 active:translate-y-1"
|
||||
>
|
||||
<CardHeader className="space-y-3">
|
||||
<div className="w-fit bg-linear-to-br from-lime-400 to-lime-500 p-3 rounded-2xl">
|
||||
<Trophy size={20} color="white" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<CardTitle className="font-satoshi">
|
||||
Hard Test Modules
|
||||
</CardTitle>
|
||||
<CardDescription className="font-satoshi">
|
||||
Focus on what matters
|
||||
</CardDescription>
|
||||
</div>
|
||||
<CardAction>
|
||||
<div className="w-fit bg-lime-100 p-3 rounded-full">
|
||||
<BookOpen size={26} color="oklch(76.8% 0.233 130.85)" />
|
||||
</div>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</header>
|
||||
|
||||
{/* ── Hero banner ── */}
|
||||
<div className="pr-hero pr-anim pr-anim-1">
|
||||
<div className="pr-hero-icon-bg">
|
||||
<DraftingCompass size={240} color="white" />
|
||||
</div>
|
||||
<p className="pr-hero-eyebrow">🎯 Full Practice Test</p>
|
||||
<h2 className="pr-hero-title">See where you stand</h2>
|
||||
<p className="pr-hero-sub">
|
||||
Take a full adaptive test and benchmark your SAT readiness.
|
||||
</p>
|
||||
<button className="pr-hero-btn">Take a practice test →</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── Practice modes ── */}
|
||||
<section className="pr-anim pr-anim-2">
|
||||
<p className="pr-section-title" style={{ marginBottom: "0.85rem" }}>
|
||||
Practice your way
|
||||
</p>
|
||||
<div className="pr-grid">
|
||||
{MODE_CARDS.map((card) => (
|
||||
<div
|
||||
key={card.route}
|
||||
className={`pr-mode-card ${card.color}`}
|
||||
onClick={() => navigate(card.route)}
|
||||
>
|
||||
<div className="pr-mode-top">
|
||||
<div className={`pr-mode-icon ${card.color}`}>
|
||||
{card.icon}
|
||||
</div>
|
||||
<div className={`pr-mode-badge ${card.color}`}>
|
||||
{card.badge}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="pr-mode-title">{card.title}</p>
|
||||
<p className="pr-mode-desc">{card.desc}</p>
|
||||
</div>
|
||||
<p className={`pr-mode-arrow ${card.color}`}>{card.arrow}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user