feat(ui): improve ui for test, drills and htm screens
This commit is contained in:
@ -5,8 +5,8 @@ import {
|
||||
Pilcrow,
|
||||
Superscript,
|
||||
WholeWord,
|
||||
Trophy,
|
||||
} from "lucide-react";
|
||||
import { Card, CardContent } from "../../../components/ui/card";
|
||||
import { useState } from "react";
|
||||
import { useAuthStore } from "../../../stores/authStore";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
@ -14,106 +14,425 @@ import { useExamConfigStore } from "../../../stores/useExamConfigStore";
|
||||
|
||||
type Module = "EBRW" | "MATH" | null;
|
||||
|
||||
const DOTS = [
|
||||
{ size: 10, color: "#f97316", top: "6%", left: "4%", delay: "0s" },
|
||||
{ size: 7, color: "#a855f7", top: "28%", left: "2%", delay: "1.2s" },
|
||||
{ size: 9, color: "#22c55e", top: "62%", left: "3%", delay: "0.6s" },
|
||||
{ size: 12, color: "#3b82f6", top: "10%", right: "4%", delay: "1.8s" },
|
||||
{ size: 7, color: "#f43f5e", top: "48%", right: "2%", delay: "0.9s" },
|
||||
{ size: 9, color: "#eab308", top: "76%", right: "5%", 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');
|
||||
|
||||
.htm-screen {
|
||||
min-height: 100vh;
|
||||
background: #fffbf4;
|
||||
font-family: 'Nunito', sans-serif;
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.htm-blob { position:fixed;pointer-events:none;z-index:0;filter:blur(48px);opacity:0.35; }
|
||||
.htm-blob-1 { width:240px;height:240px;background:#fde68a;top:-80px;left:-80px;border-radius:60% 40% 70% 30%/50% 60% 40% 50%;animation:htmWobble1 14s ease-in-out infinite; }
|
||||
.htm-blob-2 { width:190px;height:190px;background:#a5f3c0;bottom:-50px;left:6%;border-radius:40% 60% 30% 70%/60% 40% 60% 40%;animation:htmWobble2 16s ease-in-out infinite; }
|
||||
.htm-blob-3 { width:210px;height:210px;background:#fbcfe8;top:15%;right:-60px;border-radius:70% 30% 50% 50%/40% 60% 40% 60%;animation:htmWobble1 18s ease-in-out infinite reverse; }
|
||||
.htm-blob-4 { width:150px;height:150px;background:#bfdbfe;bottom:12%;right:2%;border-radius:50% 50% 30% 70%/60% 40% 60% 40%;animation:htmWobble2 12s ease-in-out infinite; }
|
||||
|
||||
@keyframes htmWobble1 {
|
||||
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 htmWobble2 {
|
||||
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);}
|
||||
}
|
||||
|
||||
.htm-dot { position:fixed;border-radius:50%;pointer-events:none;z-index:0;opacity:0.3;animation:htmFloat 7s ease-in-out infinite; }
|
||||
@keyframes htmFloat {
|
||||
0%,100%{transform:translateY(0) rotate(0deg);}
|
||||
50%{transform:translateY(-12px) rotate(180deg);}
|
||||
}
|
||||
|
||||
.htm-inner {
|
||||
position: relative; z-index: 1;
|
||||
max-width: 560px; margin: 0 auto;
|
||||
padding: 2rem 1.25rem 8rem;
|
||||
display: flex; flex-direction: column; gap: 1.5rem;
|
||||
}
|
||||
|
||||
@keyframes htmPopIn {
|
||||
from { opacity:0; transform:scale(0.92) translateY(12px); }
|
||||
to { opacity:1; transform:scale(1) translateY(0); }
|
||||
}
|
||||
.htm-anim { animation: htmPopIn 0.4s cubic-bezier(0.34,1.56,0.64,1) both; }
|
||||
.htm-anim-1 { animation-delay: 0.05s; }
|
||||
.htm-anim-2 { animation-delay: 0.12s; }
|
||||
.htm-anim-3 { animation-delay: 0.19s; }
|
||||
|
||||
/* Header */
|
||||
.htm-eyebrow {
|
||||
font-size: 0.62rem; font-weight: 800; letter-spacing: 0.16em;
|
||||
text-transform: uppercase; color: #84cc16;
|
||||
display: flex; align-items: center; gap: 0.35rem;
|
||||
}
|
||||
.htm-title {
|
||||
font-size: 1.75rem; font-weight: 900; color: #1e1b4b;
|
||||
letter-spacing: -0.02em; line-height: 1.15;
|
||||
}
|
||||
.htm-sub {
|
||||
font-family: 'Nunito Sans', sans-serif;
|
||||
font-size: 0.82rem; font-weight: 600; color: #9ca3af; line-height: 1.5;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
/* Module cards */
|
||||
.htm-card {
|
||||
border-radius: 28px;
|
||||
position: relative; overflow: hidden;
|
||||
cursor: pointer;
|
||||
border: 3px solid transparent;
|
||||
transition: transform 0.2s cubic-bezier(0.34,1.56,0.64,1),
|
||||
box-shadow 0.2s ease,
|
||||
border-color 0.2s ease;
|
||||
min-height: 200px;
|
||||
display: flex; flex-direction: column; justify-content: flex-end;
|
||||
}
|
||||
.htm-card:hover { transform: translateY(-4px); }
|
||||
.htm-card:active { transform: translateY(2px) scale(0.98); }
|
||||
|
||||
.htm-card.ebrw {
|
||||
background: linear-gradient(145deg, #3b82f6 0%, #1d4ed8 60%, #1e40af 100%);
|
||||
box-shadow: 0 10px 0 #1e3a8a66, 0 14px 32px rgba(29,78,216,0.3);
|
||||
}
|
||||
.htm-card.ebrw.selected {
|
||||
border-color: #93c5fd;
|
||||
box-shadow: 0 10px 0 #1e3a8a88, 0 16px 40px rgba(29,78,216,0.45);
|
||||
transform: translateY(-4px) scale(1.01);
|
||||
}
|
||||
|
||||
.htm-card.math {
|
||||
background: linear-gradient(145deg, #f43f5e 0%, #e11d48 60%, #be123c 100%);
|
||||
box-shadow: 0 10px 0 #9f123666, 0 14px 32px rgba(225,29,72,0.3);
|
||||
}
|
||||
.htm-card.math.selected {
|
||||
border-color: #fda4af;
|
||||
box-shadow: 0 10px 0 #9f123688, 0 16px 40px rgba(225,29,72,0.45);
|
||||
transform: translateY(-4px) scale(1.01);
|
||||
}
|
||||
|
||||
/* Decorative icons */
|
||||
.htm-card-icons {
|
||||
position: absolute; inset: 0; pointer-events: none;
|
||||
}
|
||||
|
||||
/* Card body (text + chips) */
|
||||
.htm-card-body {
|
||||
position: relative; z-index: 2;
|
||||
padding: 1.5rem 1.5rem 1.75rem;
|
||||
display: flex; flex-direction: column; gap: 0.5rem;
|
||||
}
|
||||
|
||||
.htm-card-tag {
|
||||
font-size: 0.6rem; font-weight: 800; letter-spacing: 0.18em;
|
||||
text-transform: uppercase; color: rgba(255,255,255,0.6);
|
||||
}
|
||||
.htm-card-name {
|
||||
font-size: 1.6rem; font-weight: 900; color: white;
|
||||
letter-spacing: -0.02em; line-height: 1.1;
|
||||
}
|
||||
.htm-card-desc {
|
||||
font-family: 'Nunito Sans', sans-serif;
|
||||
font-size: 0.78rem; font-weight: 600; color: rgba(255,255,255,0.7);
|
||||
line-height: 1.4; margin-top: 0.1rem;
|
||||
}
|
||||
|
||||
/* Stat pills row */
|
||||
.htm-stat-row {
|
||||
display: flex; gap: 0.4rem; margin-top: 0.5rem; flex-wrap: wrap;
|
||||
}
|
||||
.htm-stat-pill {
|
||||
background: rgba(255,255,255,0.15);
|
||||
border: 1.5px solid rgba(255,255,255,0.25);
|
||||
border-radius: 100px; padding: 0.25rem 0.65rem;
|
||||
font-size: 0.65rem; font-weight: 800; color: white;
|
||||
display: flex; align-items: center; gap: 0.3rem;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
/* Selected check badge */
|
||||
.htm-check-badge {
|
||||
position: absolute; top: 1rem; right: 1rem; z-index: 3;
|
||||
width: 32px; height: 32px; border-radius: 50%;
|
||||
background: rgba(255,255,255,0.2);
|
||||
border: 2px solid rgba(255,255,255,0.4);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
transition: all 0.25s cubic-bezier(0.34,1.56,0.64,1);
|
||||
opacity: 0; transform: scale(0.7);
|
||||
}
|
||||
.htm-card.selected .htm-check-badge {
|
||||
opacity: 1; transform: scale(1);
|
||||
background: rgba(255,255,255,0.3);
|
||||
border-color: rgba(255,255,255,0.7);
|
||||
}
|
||||
|
||||
/* CTA bar */
|
||||
.htm-cta-bar {
|
||||
position: fixed; bottom: 96px; left: 0; right: 0; z-index: 10;
|
||||
padding: 0.85rem 1.25rem calc(0.85rem + env(safe-area-inset-bottom));
|
||||
|
||||
transition: transform 0.3s cubic-bezier(0.34,1.56,0.64,1), opacity 0.25s ease;
|
||||
}
|
||||
.htm-cta-bar.hidden {
|
||||
transform: translateY(100%); opacity: 0; pointer-events: none;
|
||||
}
|
||||
.htm-cta-inner {
|
||||
max-width: 560px; margin: 0 auto;
|
||||
}
|
||||
|
||||
.htm-start-btn {
|
||||
width: 100%; padding: 0.95rem;
|
||||
background: linear-gradient(135deg, #84cc16, #65a30d);
|
||||
color: white; border: none; border-radius: 100px; cursor: pointer;
|
||||
font-family: 'Nunito', sans-serif; font-size: 0.95rem; font-weight: 900;
|
||||
display: flex; align-items: center; justify-content: center; gap: 0.5rem;
|
||||
box-shadow: 0 6px 0 #3f6212, 0 8px 20px rgba(101,163,13,0.3);
|
||||
transition: transform 0.1s ease, box-shadow 0.1s ease;
|
||||
}
|
||||
.htm-start-btn:hover { transform:translateY(-2px); box-shadow:0 8px 0 #3f6212,0 12px 24px rgba(101,163,13,0.35); }
|
||||
.htm-start-btn:active { transform:translateY(3px); box-shadow:0 3px 0 #3f6212; }
|
||||
|
||||
.htm-start-label {
|
||||
font-size: 0.7rem; font-weight: 700; color: #9ca3af;
|
||||
text-align: center; margin-top: 0.5rem;
|
||||
font-family: 'Nunito Sans', sans-serif;
|
||||
}
|
||||
.htm-start-label span { font-weight: 800; color: #6b7280; }
|
||||
`;
|
||||
|
||||
export const HardTestModules = () => {
|
||||
const user = useAuthStore((state) => state.user);
|
||||
const navigate = useNavigate();
|
||||
const [selected, setSelected] = useState<Module>(null);
|
||||
|
||||
const { setMode, storeDuration, setSection } = useExamConfigStore();
|
||||
|
||||
function handleStartModule() {
|
||||
if (!user) return;
|
||||
|
||||
(setMode("MODULE"), storeDuration(7), setSection(selected));
|
||||
|
||||
if (!user || !selected) return;
|
||||
setMode("MODULE");
|
||||
storeDuration(7);
|
||||
setSection(selected);
|
||||
navigate(`/student/practice/${selected}/test`, { replace: true });
|
||||
}
|
||||
|
||||
const toggle = (mod: "EBRW" | "MATH") =>
|
||||
setSelected((prev) => (prev === mod ? null : mod));
|
||||
|
||||
return (
|
||||
<main className="min-h-screen max-w-7xl mx-auto px-8 sm:px-6 lg:px-8 py-8 space-y-4">
|
||||
<header className="space-y-2">
|
||||
<h1 className="font-satoshi-bold text-3xl">Hard Test Modules</h1>
|
||||
<p className="font-satoshi text-md text-gray-500">
|
||||
Tackle hard practice test modules by selecting a section.
|
||||
</p>
|
||||
</header>
|
||||
<section className="space-y-6">
|
||||
<Card
|
||||
onClick={() =>
|
||||
setSelected((prev) => (prev === "EBRW" ? null : "EBRW"))
|
||||
<div className="htm-screen">
|
||||
<style>{STYLES}</style>
|
||||
|
||||
{/* Blobs */}
|
||||
<div className="htm-blob htm-blob-1" />
|
||||
<div className="htm-blob htm-blob-2" />
|
||||
<div className="htm-blob htm-blob-3" />
|
||||
<div className="htm-blob htm-blob-4" />
|
||||
|
||||
{/* Dots */}
|
||||
{DOTS.map((d, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="htm-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
|
||||
}
|
||||
className={`relative cursor-pointer overflow-hidden transition
|
||||
${
|
||||
selected === "EBRW"
|
||||
? "ring-2 ring-blue-500 scale-[1.02]"
|
||||
: "hover:scale-[1.01]"
|
||||
}
|
||||
bg-linear-to-br from-blue-400 to-blue-600
|
||||
`}
|
||||
/>
|
||||
))}
|
||||
|
||||
<div className="htm-inner">
|
||||
{/* Header */}
|
||||
<header className="htm-anim">
|
||||
<p className="htm-eyebrow">
|
||||
<Trophy size={11} /> Hard Modules
|
||||
</p>
|
||||
<h1 className="htm-title">Pick your challenge</h1>
|
||||
<p className="htm-sub">
|
||||
Tackle the hardest SAT questions. Select a section to begin.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
{/* EBRW Card */}
|
||||
<div
|
||||
className={`htm-card ebrw htm-anim htm-anim-1${selected === "EBRW" ? " selected" : ""}`}
|
||||
onClick={() => toggle("EBRW")}
|
||||
>
|
||||
<CardContent className="z-10 flex items-center justify-center py-16 ">
|
||||
<h1 className="font-satoshi-bold text-2xl text-blue-50">
|
||||
Reading & Writing
|
||||
</h1>
|
||||
</CardContent>
|
||||
<Languages
|
||||
size={250}
|
||||
className="absolute -top-5 -right-10 -rotate-23 text-white opacity-30"
|
||||
/>
|
||||
<WholeWord
|
||||
size={150}
|
||||
className="absolute -top-10 -left-3 rotate-23 text-white opacity-30"
|
||||
/>
|
||||
<Pilcrow
|
||||
size={150}
|
||||
className="absolute -bottom-12 left-8 -rotate-23 text-white opacity-30"
|
||||
/>
|
||||
</Card>
|
||||
<Card
|
||||
onClick={() =>
|
||||
setSelected((prev) => (prev === "MATH" ? null : "MATH"))
|
||||
}
|
||||
className={`relative cursor-pointer overflow-hidden transition
|
||||
${
|
||||
selected === "MATH"
|
||||
? "ring-2 ring-rose-500 scale-[1.02]"
|
||||
: "hover:scale-[1.01]"
|
||||
}
|
||||
bg-linear-to-br from-rose-400 to-rose-600
|
||||
`}
|
||||
>
|
||||
<CardContent className="z-10 flex items-center justify-center py-16 ">
|
||||
<h1 className="font-satoshi-bold text-2xl text-blue-50">
|
||||
Mathematics
|
||||
</h1>
|
||||
</CardContent>
|
||||
<DecimalsArrowRight
|
||||
size={250}
|
||||
className="absolute -top-5 -right-10 -rotate-23 text-white opacity-30"
|
||||
/>
|
||||
<Superscript
|
||||
size={150}
|
||||
className="absolute -top-10 -left-3 rotate-23 text-white opacity-30"
|
||||
/>
|
||||
<Percent
|
||||
size={120}
|
||||
className="absolute -bottom-5 left-8 -rotate-10 text-white opacity-30"
|
||||
/>
|
||||
</Card>
|
||||
</section>
|
||||
{selected && (
|
||||
<div className=" bottom-6 left-0 right-0 flex justify-center z-50">
|
||||
<button
|
||||
onClick={() => {
|
||||
handleStartModule();
|
||||
}}
|
||||
className="rounded-2xl px-10 py-4 font-satoshi-bold text-lg
|
||||
bg-linear-to-br from-purple-500 to-purple-600 text-white
|
||||
shadow-xl animate-in slide-in-from-bottom-4"
|
||||
>
|
||||
Start Test
|
||||
</button>
|
||||
{/* Background icons */}
|
||||
<div className="htm-card-icons">
|
||||
<Languages
|
||||
size={220}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: -20,
|
||||
right: -30,
|
||||
transform: "rotate(-20deg)",
|
||||
color: "white",
|
||||
opacity: 0.12,
|
||||
}}
|
||||
/>
|
||||
<WholeWord
|
||||
size={130}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: -15,
|
||||
left: -10,
|
||||
transform: "rotate(15deg)",
|
||||
color: "white",
|
||||
opacity: 0.1,
|
||||
}}
|
||||
/>
|
||||
<Pilcrow
|
||||
size={110}
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: -20,
|
||||
left: 20,
|
||||
transform: "rotate(-18deg)",
|
||||
color: "white",
|
||||
opacity: 0.1,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Check */}
|
||||
<div className="htm-check-badge">
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
||||
<path
|
||||
d="M2.5 7L5.5 10L11.5 4"
|
||||
stroke="white"
|
||||
strokeWidth="2.2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="htm-card-body">
|
||||
<span className="htm-card-tag">Section 1</span>
|
||||
<h2 className="htm-card-name">
|
||||
Reading &
|
||||
<br />
|
||||
Writing
|
||||
</h2>
|
||||
<p className="htm-card-desc">
|
||||
Grammar, vocabulary, comprehension & evidence-based analysis
|
||||
</p>
|
||||
<div className="htm-stat-row">
|
||||
<span className="htm-stat-pill">📖 27 Questions</span>
|
||||
<span className="htm-stat-pill">⏱️ 32 min</span>
|
||||
<span className="htm-stat-pill">🔥 Hard tier</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
|
||||
{/* MATH Card */}
|
||||
<div
|
||||
className={`htm-card math htm-anim htm-anim-2${selected === "MATH" ? " selected" : ""}`}
|
||||
onClick={() => toggle("MATH")}
|
||||
>
|
||||
{/* Background icons */}
|
||||
<div className="htm-card-icons">
|
||||
<DecimalsArrowRight
|
||||
size={220}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: -20,
|
||||
right: -30,
|
||||
transform: "rotate(-20deg)",
|
||||
color: "white",
|
||||
opacity: 0.12,
|
||||
}}
|
||||
/>
|
||||
<Superscript
|
||||
size={130}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: -15,
|
||||
left: -10,
|
||||
transform: "rotate(15deg)",
|
||||
color: "white",
|
||||
opacity: 0.1,
|
||||
}}
|
||||
/>
|
||||
<Percent
|
||||
size={110}
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: -15,
|
||||
left: 20,
|
||||
transform: "rotate(-10deg)",
|
||||
color: "white",
|
||||
opacity: 0.1,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Check */}
|
||||
<div className="htm-check-badge">
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
||||
<path
|
||||
d="M2.5 7L5.5 10L11.5 4"
|
||||
stroke="white"
|
||||
strokeWidth="2.2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="htm-card-body">
|
||||
<span className="htm-card-tag">Section 2</span>
|
||||
<h2 className="htm-card-name">Mathematics</h2>
|
||||
<p className="htm-card-desc">
|
||||
Algebra, advanced math, geometry & data analysis under
|
||||
pressure
|
||||
</p>
|
||||
<div className="htm-stat-row">
|
||||
<span className="htm-stat-pill">🔢 22 Questions</span>
|
||||
<span className="htm-stat-pill">⏱️ 35 min</span>
|
||||
<span className="htm-stat-pill">🔥 Hard tier</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CTA bar */}
|
||||
<div className={`htm-cta-bar${!selected ? " hidden" : ""}`}>
|
||||
<div className="htm-cta-inner">
|
||||
<button className="htm-start-btn" onClick={handleStartModule}>
|
||||
🏆 Start{" "}
|
||||
{selected === "EBRW"
|
||||
? "Reading & Writing"
|
||||
: selected === "MATH"
|
||||
? "Mathematics"
|
||||
: ""}{" "}
|
||||
Module
|
||||
</button>
|
||||
<p className="htm-start-label">
|
||||
Tap again to <span>deselect</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user