diff --git a/src/components/ChoiceCard.tsx b/src/components/ChoiceCard.tsx index 651d542..4df3019 100644 --- a/src/components/ChoiceCard.tsx +++ b/src/components/ChoiceCard.tsx @@ -1,4 +1,126 @@ -import { Badge } from "./ui/badge"; +const STYLES = ` + @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@700;800;900&family=Nunito+Sans:wght@600;700&display=swap'); + + .cc-btn { + width: 100%; + background: white; + border: 2.5px solid #f3f4f6; + border-radius: 18px; + padding: 0.85rem 1rem; + text-align: left; + cursor: pointer; + display: flex; + flex-direction: column; + gap: 0.2rem; + box-shadow: 0 3px 10px rgba(0,0,0,0.04); + transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease, background 0.15s ease; + font-family: 'Nunito', sans-serif; + position: relative; + overflow: hidden; + -webkit-tap-highlight-color: transparent; + } + + .cc-btn:hover:not(.cc-selected) { + transform: translateY(-2px); + box-shadow: 0 8px 20px rgba(0,0,0,0.07); + border-color: #e5e7eb; + } + + .cc-btn:active { + transform: translateY(1px); + box-shadow: 0 2px 6px rgba(0,0,0,0.05); + } + + /* Selected state */ + .cc-btn.cc-selected { + border-color: #c4b5fd; + background: #fdf4ff; + box-shadow: 0 6px 0 #e9d5ff, 0 8px 20px rgba(168,85,247,0.1); + } + + /* Selected shimmer bar on left edge */ + .cc-btn.cc-selected::before { + content: ''; + position: absolute; + left: 0; top: 0; bottom: 0; + width: 4px; + background: linear-gradient(180deg, #a855f7, #7c3aed); + border-radius: 0 2px 2px 0; + } + + /* Top row */ + .cc-top { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + } + + .cc-label { + font-size: 0.9rem; + font-weight: 900; + color: #1e1b4b; + line-height: 1.2; + flex: 1; + transition: color 0.15s ease; + } + .cc-btn.cc-selected .cc-label { color: #7c3aed; } + + /* Section badge */ + .cc-section-badge { + font-size: 0.6rem; + font-weight: 800; + letter-spacing: 0.1em; + text-transform: uppercase; + border-radius: 100px; + padding: 0.2rem 0.6rem; + flex-shrink: 0; + border: 2px solid transparent; + } + .cc-section-badge.ebrw { + background: #eff6ff; + border-color: #bfdbfe; + color: #2563eb; + } + .cc-section-badge.math { + background: #fff1f2; + border-color: #fecdd3; + color: #e11d48; + } + + /* Sub label */ + .cc-sublabel { + font-family: 'Nunito Sans', sans-serif; + font-size: 0.75rem; + font-weight: 600; + color: #9ca3af; + line-height: 1.3; + padding-left: 0.05rem; + transition: color 0.15s ease; + } + .cc-btn.cc-selected .cc-sublabel { color: #a855f7; } + + /* Checkmark */ + .cc-check { + position: absolute; + top: 0.65rem; + right: 0.75rem; + width: 20px; height: 20px; + border-radius: 50%; + border: 2px solid #e5e7eb; + display: flex; align-items: center; justify-content: center; + flex-shrink: 0; + transition: all 0.2s cubic-bezier(0.34,1.56,0.64,1); + background: white; + } + .cc-btn.cc-selected .cc-check { + background: #a855f7; + border-color: #a855f7; + transform: scale(1.1); + } +`; + +let stylesInjected = false; export const ChoiceCard = ({ label, @@ -12,23 +134,51 @@ export const ChoiceCard = ({ subLabel?: string; section?: string; onClick: () => void; -}) => ( - -); +}) => { + if (!stylesInjected) { + const tag = document.createElement("style"); + tag.textContent = STYLES; + document.head.appendChild(tag); + stylesInjected = true; + } + + const sectionClass = + section === "EBRW" + ? "ebrw" + : section === "Math" || section === "MATH" + ? "math" + : ""; + + return ( + + ); +}; diff --git a/src/components/PredictedScoreCard.tsx b/src/components/PredictedScoreCard.tsx index 0fec882..842c001 100644 --- a/src/components/PredictedScoreCard.tsx +++ b/src/components/PredictedScoreCard.tsx @@ -1,11 +1,4 @@ import { useEffect, useState } from "react"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "../components/ui/card"; import { api } from "../utils/api"; import { useAuthToken } from "../hooks/useAuthToken"; import { @@ -36,34 +29,38 @@ interface PredictedScoreResponse { const confidenceConfig: Record< string, - { label: string; color: string; bg: string; dot: string } + { label: string; color: string; bg: string; border: string; dot: string } > = { high: { label: "High confidence", - color: "text-emerald-700", - bg: "bg-emerald-50 border-emerald-200", - dot: "bg-emerald-500", + color: "#16a34a", + bg: "#f0fdf4", + border: "#bbf7d0", + dot: "#22c55e", }, medium: { label: "Medium confidence", - color: "text-amber-700", - bg: "bg-amber-50 border-amber-200", - dot: "bg-amber-400", + color: "#d97706", + bg: "#fffbeb", + border: "#fde68a", + dot: "#f59e0b", }, low: { label: "Low confidence", - color: "text-rose-700", - bg: "bg-rose-50 border-rose-200", - dot: "bg-rose-400", + color: "#e11d48", + bg: "#fff1f2", + border: "#fecdd3", + dot: "#f43f5e", }, }; const getConfidenceStyle = (confidence: string) => confidenceConfig[confidence.toLowerCase()] ?? { label: confidence, - color: "text-gray-600", - bg: "bg-gray-50 border-gray-200", - dot: "bg-gray-400", + color: "#6b7280", + bg: "#f9fafb", + border: "#f3f4f6", + dot: "#9ca3af", }; const useCountUp = (target: number, duration = 900) => { @@ -83,70 +80,256 @@ const useCountUp = (target: number, duration = 900) => { return value; }; -// ─── Expanded section detail ────────────────────────────────────────────────── +// ─── Styles ─────────────────────────────────────────────────────────────────── + +const STYLES = ` + @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@700;800;900&family=Nunito+Sans:wght@400;600;700&display=swap'); + + .psc-card { + background: white; + border: 2.5px solid #f3f4f6; + border-radius: 24px; + overflow: hidden; + box-shadow: 0 4px 20px rgba(0,0,0,0.05); + font-family: 'Nunito', sans-serif; + width: 100%; + } + + /* Header */ + .psc-header { + padding: 1.1rem 1.25rem 0.75rem; + display: flex; align-items: center; justify-content: space-between; + border-bottom: 2px solid #f9fafb; + } + .psc-header-left { display:flex;flex-direction:column;gap:0.15rem; } + .psc-header-title { + font-size: 0.88rem; font-weight: 900; color: #1e1b4b; + letter-spacing: -0.01em; + } + .psc-header-sub { + font-family: 'Nunito Sans', sans-serif; + font-size: 0.7rem; font-weight: 600; color: #9ca3af; + } + .psc-header-icon { + width: 36px; height: 36px; border-radius: 12px; + background: linear-gradient(135deg, #a855f7, #7c3aed); + display: flex; align-items: center; justify-content: center; + box-shadow: 0 4px 0 #5b21b644; + flex-shrink: 0; + } + + /* Body */ + .psc-body { padding: 1.1rem 1.25rem; display:flex;flex-direction:column;gap:0.85rem; } + + /* Scores row */ + .psc-scores-row { + display: flex; align-items: stretch; gap: 0; + background: #fafaf9; border: 2px solid #f3f4f6; + border-radius: 18px; overflow: hidden; + } + + .psc-score-cell { + flex: 1; display:flex;flex-direction:column;align-items:center; + padding: 1rem 0.5rem; + position: relative; + } + .psc-score-cell + .psc-score-cell::before { + content:''; position:absolute; left:0; top:20%; bottom:20%; + width:2px; background:#f3f4f6; border-radius:2px; + } + + /* Total cell — slightly different bg */ + .psc-score-cell.total { + background: white; + border-right: 2px solid #f3f4f6; + flex: 1.2; + } + + .psc-cell-label { + display: flex; align-items: center; gap: 0.3rem; + font-size: 0.58rem; font-weight: 800; letter-spacing: 0.12em; + text-transform: uppercase; color: #9ca3af; margin-bottom: 0.3rem; + } + .psc-cell-score { + font-weight: 900; color: #1e1b4b; line-height: 1; + } + .psc-cell-score.large { font-size: 2.8rem; } + .psc-cell-score.medium { font-size: 1.7rem; } + .psc-cell-out { + font-family: 'Nunito Sans', sans-serif; + font-size: 0.62rem; font-weight: 600; color: #d1d5db; + margin-top: 0.2rem; + } + + /* Toggle button */ + .psc-toggle-btn { + width: 100%; display:flex;align-items:center;justify-content:center;gap:0.4rem; + padding: 0.55rem; border-radius: 12px; border: 2px solid #f3f4f6; + background: white; cursor: pointer; + font-family: 'Nunito', sans-serif; + font-size: 0.72rem; font-weight: 800; color: #9ca3af; + transition: all 0.15s ease; + } + .psc-toggle-btn:hover { border-color: #e9d5ff; color: #a855f7; background: #fdf4ff; } + + /* Section detail cards */ + .psc-detail-card { + background: #fafaf9; border: 2.5px solid #f3f4f6; border-radius: 18px; + padding: 0.9rem 1rem; + display: flex; flex-direction: column; gap: 0.65rem; + } + + .psc-detail-top { + display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; + } + .psc-detail-icon-wrap { + width: 30px; height: 30px; border-radius: 10px; flex-shrink: 0; + display: flex; align-items: center; justify-content: center; + } + .psc-detail-label { + font-size: 0.8rem; font-weight: 900; color: #1e1b4b; flex: 1; + } + .psc-conf-badge { + display: flex; align-items: center; gap: 0.3rem; + padding: 0.2rem 0.6rem; border-radius: 100px; border: 2px solid; + font-family: 'Nunito Sans', sans-serif; + font-size: 0.6rem; font-weight: 700; flex-shrink: 0; + } + .psc-conf-dot { width:6px;height:6px;border-radius:50%;flex-shrink:0; } + + .psc-score-range-row { + display: flex; align-items: flex-end; justify-content: space-between; + } + .psc-detail-score { + font-size: 1.6rem; font-weight: 900; color: #1e1b4b; line-height: 1; + } + .psc-range-text { + font-family: 'Nunito Sans', sans-serif; + font-size: 0.68rem; font-weight: 600; color: #9ca3af; + text-align: right; line-height: 1.4; + } + .psc-range-text span { font-weight: 800; color: #6b7280; } + + /* Range bar */ + .psc-bar-wrap { + height: 8px; border-radius: 100px; background: #f3f4f6; + position: relative; overflow: visible; + } + .psc-bar-fill { + position: absolute; height: 100%; border-radius: 100px; opacity: 0.4; + } + .psc-bar-dot { + position: absolute; width: 14px; height: 14px; + border-radius: 50%; border: 2.5px solid white; + top: 50%; transform: translate(-50%, -50%); + box-shadow: 0 2px 6px rgba(0,0,0,0.12); + } + .psc-bar-labels { + display: flex; justify-content: space-between; + font-family: 'Nunito Sans', sans-serif; + font-size: 0.58rem; font-weight: 600; color: #d1d5db; + margin-top: 0.3rem; + } + + /* Expanded animation */ + .psc-expanded-wrap { + display: flex; flex-direction: column; gap: 0.6rem; + animation: pscFadeIn 0.3s cubic-bezier(0.34,1.56,0.64,1) both; + } + @keyframes pscFadeIn { + from { opacity:0; transform:translateY(-8px); } + to { opacity:1; transform:translateY(0); } + } + + /* Loading */ + .psc-loading { + display: flex; align-items: center; justify-content: center; + gap: 0.5rem; padding: 2rem; + font-family: 'Nunito Sans', sans-serif; + font-size: 0.82rem; font-weight: 600; color: #9ca3af; + } + .psc-spinner { animation: pscSpin 0.8s linear infinite; } + @keyframes pscSpin { to { transform: rotate(360deg); } } + + .psc-error { + font-family: 'Nunito Sans', sans-serif; + font-size: 0.82rem; font-weight: 700; color: #e11d48; + text-align: center; padding: 1.5rem; + background: #fff1f2; border-radius: 14px; border: 2px solid #fecdd3; + } +`; + +// ─── Section detail ─────────────────────────────────────────────────────────── const SectionDetail = ({ label, icon: Icon, prediction, - accentClass, + iconBg, + barColor, }: { label: string; icon: React.ElementType; prediction: SectionPrediction; - accentClass: string; + iconBg: string; + barColor: string; }) => { const conf = getConfidenceStyle(prediction.confidence); + const pct = (v: number) => ((v - 200) / (800 - 200)) * 100; + return ( -
-
-
-
- -
- - {label} - +
+
+
+
- {label} +
- +
{conf.label} - +
-
- - {prediction.score} - - - Range:{" "} - +
+ {prediction.score} +
+ Range +
+ {prediction.range_min}–{prediction.range_max} - +
- {/* Range bar */} -
-
-
-
-
- 200 - 800 +
+
+
+
+
+
+ 200 + 800 +
); @@ -154,6 +337,8 @@ const SectionDetail = ({ // ─── Main component ─────────────────────────────────────────────────────────── +let stylesInjected = false; + export const PredictedScoreCard = () => { const token = useAuthToken(); const [data, setData] = useState(null); @@ -177,129 +362,113 @@ export const PredictedScoreCard = () => { })(); }, [token]); + if (!stylesInjected) { + const tag = document.createElement("style"); + tag.textContent = STYLES; + document.head.appendChild(tag); + stylesInjected = true; + } + const animatedTotal = useCountUp(data?.total_score ?? 0, 1000); return ( - - -
-
- - Predicted SAT Score - - - Based on your practice performance - -
-
- -
+
+ {/* Header */} +
+
+

Predicted SAT Score

+

Based on your practice performance

- +
+ +
+
- + {/* Body */} +
{loading && ( -
- +
+ + Calculating your score...
)} - {error && !loading && ( -

- {error} -

- )} + {error && !loading &&
⚠️ {error}
} {data && !loading && ( <> - {/* ── Collapsed view: big numbers only ── */} -
+ {/* Score cells */} +
{/* Total */} -
- - Total - - - {animatedTotal} - - - out of 1600 - +
+
+ Total +
+ {animatedTotal} + / 1600
-
- {/* Math */} -
-
- - - Math - +
+
+ Math
- + {data.math_prediction.score} - - out of 800 - + / 800
-
- {/* R&W */} -
-
- - - R&W - +
+
+ R&W
- + {data.rw_prediction.score} - - out of 800 - + / 800
- {/* ── Expand toggle ── */} + {/* Toggle */} - {/* ── Expanded: range bars + confidence ── */} + {/* Expanded */} {expanded && ( -
+
)} )} - - +
+
); }; diff --git a/src/hooks/usePageTitle.ts b/src/hooks/usePageTitle.ts new file mode 100644 index 0000000..6cee3c7 --- /dev/null +++ b/src/hooks/usePageTitle.ts @@ -0,0 +1,7 @@ +import { useEffect } from "react"; + +export function usePageTitle(title: string) { + useEffect(() => { + document.title = title; + }, [title]); +} diff --git a/src/pages/auth/Login.tsx b/src/pages/auth/Login.tsx index ac9c681..85622a8 100644 --- a/src/pages/auth/Login.tsx +++ b/src/pages/auth/Login.tsx @@ -2,16 +2,185 @@ import { useState, useEffect } from "react"; import type { FormEvent } from "react"; import { useNavigate, useLocation } from "react-router-dom"; import { useAuthStore } from "../../stores/authStore"; +import { Loader2, Mail, Lock } from "lucide-react"; +import { usePageTitle } from "../../hooks/usePageTitle"; interface LocationState { - from?: { - pathname: string; - }; + from?: { pathname: string }; } +const DOTS = [ + { size: 12, color: "#f97316", top: "8%", left: "6%", delay: "0s" }, + { size: 7, color: "#a855f7", top: "22%", left: "3%", delay: "1.2s" }, + { size: 9, color: "#22c55e", top: "65%", left: "5%", delay: "0.6s" }, + { size: 8, color: "#f43f5e", top: "80%", left: "8%", delay: "2.1s" }, + { size: 12, color: "#3b82f6", top: "10%", right: "6%", delay: "1.8s" }, + { size: 7, color: "#eab308", top: "40%", right: "3%", delay: "0.9s" }, + { size: 10, color: "#a855f7", top: "72%", right: "5%", delay: "0.4s" }, + { size: 8, color: "#f97316", top: "55%", right: "8%", delay: "1.5s" }, +]; + +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'); + + .lg-screen { + min-height: 100vh; + background: #fffbf4; + font-family: 'Nunito', sans-serif; + position: relative; + overflow: hidden; + display: flex; align-items: center; justify-content: center; + padding: 2rem 1.25rem; + } + + /* Blobs */ + .lg-blob { position:fixed;pointer-events:none;z-index:0;filter:blur(52px);opacity:0.38; } + .lg-blob-1 { width:280px;height:280px;background:#fde68a;top:-100px;left:-100px;border-radius:60% 40% 70% 30%/50% 60% 40% 50%;animation:lgWobble1 14s ease-in-out infinite; } + .lg-blob-2 { width:220px;height:220px;background:#a5f3c0;bottom:-60px;left:4%;border-radius:40% 60% 30% 70%/60% 40% 60% 40%;animation:lgWobble2 16s ease-in-out infinite; } + .lg-blob-3 { width:250px;height:250px;background:#fbcfe8;top:10%;right:-70px;border-radius:70% 30% 50% 50%/40% 60% 40% 60%;animation:lgWobble1 18s ease-in-out infinite reverse; } + .lg-blob-4 { width:180px;height:180px;background:#bfdbfe;bottom:8%;right:0;border-radius:50% 50% 30% 70%/60% 40% 60% 40%;animation:lgWobble2 12s ease-in-out infinite; } + + @keyframes lgWobble1 { + 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(14px,18px) rotate(8deg);} + } + @keyframes lgWobble2 { + 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(-12px,14px) rotate(-6deg);} + } + + .lg-dot { position:fixed;border-radius:50%;pointer-events:none;z-index:0;opacity:0.28;animation:lgFloat 7s ease-in-out infinite; } + @keyframes lgFloat { + 0%,100%{transform:translateY(0) rotate(0deg);} + 50%{transform:translateY(-14px) rotate(180deg);} + } + + /* Card */ + .lg-card { + position: relative; z-index: 1; + width: 100%; max-width: 400px; + background: white; border: 2.5px solid #f3f4f6; + border-radius: 28px; + box-shadow: 0 12px 40px rgba(0,0,0,0.08), 0 4px 12px rgba(0,0,0,0.04); + padding: 2.25rem 2rem 2rem; + display: flex; flex-direction: column; gap: 1.75rem; + animation: lgPopIn 0.5s cubic-bezier(0.34,1.56,0.64,1) both; + } + @keyframes lgPopIn { + from { opacity:0; transform:scale(0.9) translateY(20px); } + to { opacity:1; transform:scale(1) translateY(0); } + } + + /* Logo area */ + .lg-logo-wrap { + display: flex; flex-direction: column; align-items: center; gap: 0.85rem; + } + .lg-logo-badge { + width: 64px; height: 64px; border-radius: 20px; + background: linear-gradient(135deg, #a855f7, #7c3aed); + display: flex; align-items: center; justify-content: center; + box-shadow: 0 6px 0 #5b21b655, 0 10px 24px rgba(124,58,237,0.25); + font-size: 1.75rem; + animation: lgPopIn 0.5s cubic-bezier(0.34,1.56,0.64,1) 0.1s both; + } + .lg-title { + font-size: 1.5rem; font-weight: 900; color: #1e1b4b; + letter-spacing: -0.02em; text-align: center; + } + .lg-sub { + font-family: 'Nunito Sans', sans-serif; + font-size: 0.82rem; font-weight: 600; color: #9ca3af; + text-align: center; margin-top: -0.25rem; + } + + /* Form fields */ + .lg-fields { display: flex; flex-direction: column; gap: 1rem; } + + .lg-field { display: flex; flex-direction: column; gap: 0.4rem; } + .lg-label { + font-size: 0.72rem; font-weight: 800; letter-spacing: 0.1em; + text-transform: uppercase; color: #6b7280; + padding-left: 0.25rem; + } + .lg-input-wrap { position: relative; } + .lg-input-icon { + position: absolute; left: 0.85rem; top: 50%; + transform: translateY(-50%); pointer-events: none; color: #9ca3af; + transition: color 0.2s ease; + } + .lg-input { + width: 100%; padding: 0.8rem 1rem 0.8rem 2.6rem; + background: #f9fafb; border: 2.5px solid #f3f4f6; + border-radius: 14px; + font-family: 'Nunito Sans', sans-serif; + font-size: 0.88rem; font-weight: 600; color: #1e1b4b; + outline: none; transition: all 0.2s ease; + box-sizing: border-box; + } + .lg-input:focus { + background: white; border-color: #c4b5fd; + box-shadow: 0 0 0 3px rgba(168,85,247,0.1); + } + .lg-input:focus ~ .lg-input-icon { color: #a855f7; } + .lg-input:disabled { opacity: 0.5; cursor: not-allowed; } + .lg-input::placeholder { color: #d1d5db; } + + /* Remember me */ + .lg-remember { + display: flex; align-items: center; gap: 0.5rem; + padding: 0 0.1rem; + } + .lg-checkbox { + width: 18px; height: 18px; border-radius: 6px; + accent-color: #a855f7; cursor: pointer; flex-shrink: 0; + } + .lg-remember-label { + font-family: 'Nunito Sans', sans-serif; + font-size: 0.8rem; font-weight: 600; color: #6b7280; + cursor: pointer; + } + + /* Error */ + .lg-error { + background: #fff1f2; border: 2px solid #fecdd3; + border-radius: 14px; padding: 0.75rem 1rem; + font-family: 'Nunito Sans', sans-serif; + font-size: 0.82rem; font-weight: 700; color: #e11d48; + display: flex; align-items: center; gap: 0.5rem; + } + + /* Submit button */ + .lg-btn { + width: 100%; padding: 0.95rem; + background: #f97316; 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 #c2560e, 0 8px 20px rgba(249,115,22,0.25); + transition: transform 0.1s ease, box-shadow 0.1s ease; + } + .lg-btn:hover { transform:translateY(-2px); box-shadow:0 8px 0 #c2560e,0 12px 24px rgba(249,115,22,0.3); } + .lg-btn:active { transform:translateY(3px); box-shadow:0 3px 0 #c2560e; } + .lg-btn:disabled { + background: #e5e7eb; color: #9ca3af; + cursor: not-allowed; box-shadow: 0 4px 0 #d1d5db; + } + .lg-btn:disabled:hover { transform: none; box-shadow: 0 4px 0 #d1d5db; } + + .lg-spinner { animation: lgSpin 0.8s linear infinite; } + @keyframes lgSpin { to { transform: rotate(360deg); } } + + /* Footer hint */ + .lg-footer { + text-align: center; + font-family: 'Nunito Sans', sans-serif; + font-size: 0.75rem; font-weight: 600; color: #9ca3af; + } +`; + export const Login = () => { - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); const navigate = useNavigate(); const location = useLocation(); @@ -20,14 +189,10 @@ export const Login = () => { const from = (location.state as LocationState)?.from?.pathname || "/student"; - // Redirect if already authenticated useEffect(() => { - if (isAuthenticated) { - navigate("/student/home", { replace: true }); - } + if (isAuthenticated) navigate("/student/home", { replace: true }); }, [isAuthenticated, navigate]); - // Clear error when component unmounts or inputs change useEffect(() => { return () => clearError(); }, [clearError]); @@ -35,122 +200,140 @@ export const Login = () => { const handleSubmit = async (e: FormEvent) => { e.preventDefault(); clearError(); - const success = await login({ email, password }); - if (success) { - navigate(from, { replace: true }); - } + if (success) navigate(from, { replace: true }); }; - // Don't render login form if already authenticated - if (isAuthenticated) { - return null; - } + if (isAuthenticated) return null; return ( -
-
-
- EdBridge logo -
-

- Welcome Back -

-
-
- - setEmail(e.target.value)} - disabled={isLoading} - className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none transition disabled:bg-gray-100 disabled:cursor-not-allowed" - placeholder="Enter your email" +
+ + + {/* Blobs */} +
+
+
+
+ + {/* Dots */} + {DOTS.map((d, i) => ( +
+ ))} + +
+ {/* Logo + heading */} +
+
+ EdBridge { + (e.target as HTMLImageElement).style.display = "none"; + }} />
-
-
+
+ + {/* Fields */} +
+ {/* Email */} +
+ - setPassword(e.target.value)} - disabled={isLoading} - className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none transition disabled:bg-gray-100 disabled:cursor-not-allowed" - placeholder="Enter your password" - /> -
+
+ setEmail(e.target.value)} + disabled={isLoading} /> -
+ {/* Password */} +
+ +
+ + setPassword(e.target.value)} + disabled={isLoading} + /> +
+
+ + {/* Remember me */} +
+ + +
+ + {/* Error */} {error && ( -
- {error} +
+ ⚠️ {error}
)} + {/* Submit */}
+ +

+ By signing in you agree to Edbridge's Terms & Privacy Policy. +

); diff --git a/src/pages/student/Home.tsx b/src/pages/student/Home.tsx index 34b0247..6f77b40 100644 --- a/src/pages/student/Home.tsx +++ b/src/pages/student/Home.tsx @@ -36,7 +36,7 @@ const STYLES = ` .home-screen { min-height: 100vh; background: #fffbf4; - font-family: 'Satoshi', sans-serif; + font-family: 'Nunito', sans-serif; position: relative; overflow-x: hidden; } @@ -248,6 +248,30 @@ const STYLES = ` .h-tip-icon { flex-shrink:0;margin-top:1px; } .h-tip-text { font-size:0.85rem;font-weight:700;color:#374151;line-height:1.4; } + + /* ── Load more ── */ + .h-load-more-btn { + width: 100%; margin-top: 0.25rem; + padding: 0.75rem; + background: white; border: 2.5px solid #f3f4f6; + border-radius: 100px; cursor: pointer; + font-family: 'Nunito', sans-serif; + font-size: 0.82rem; font-weight: 800; color: #9ca3af; + display: flex; align-items: center; justify-content: center; gap: 0.4rem; + box-shadow: 0 3px 10px rgba(0,0,0,0.04); + transition: all 0.2s ease; + } + .h-load-more-btn:hover { border-color: #c4b5fd; color: #a855f7; background: #fdf4ff; transform: translateY(-1px); box-shadow: 0 6px 14px rgba(0,0,0,0.06); } + .h-load-more-btn:active { transform: translateY(1px); } + + .h-sheet-count { + text-align: center; + font-family: 'Nunito Sans', sans-serif; + font-size: 0.72rem; font-weight: 600; color: #d1d5db; + margin-top: 0.5rem; + } + .h-sheet-count span { font-weight: 800; color: #9ca3af; } + @keyframes hPopIn { from{opacity:0;transform:scale(0.92) translateY(10px);} to{opacity:1;transform:scale(1) translateY(0);} @@ -305,6 +329,8 @@ const TIPS = [ ]; // ─── Main component ─────────────────────────────────────────────────────────── +const PAGE_SIZE = 2; + export const Home = () => { const user = useAuthStore((state) => state.user); const navigate = useNavigate(); @@ -319,6 +345,7 @@ export const Home = () => { >("all"); const [isSearchOpen, setIsSearchOpen] = useState(false); const [searchQuery, setSearchQuery] = useState(""); + const [visibleCount, setVisibleCount] = useState(PAGE_SIZE); useEffect(() => { const sort = (sheets: PracticeSheet[]) => { @@ -352,13 +379,22 @@ export const Home = () => { const handleStart = (id: string) => navigate(`/student/practice/${id}`); - const tabSheets = + const allTabSheets = activeTab === "all" ? practiceSheets : activeTab === "NOT_STARTED" ? notStartedSheets : completedSheets; + const tabSheets = allTabSheets.slice(0, visibleCount); + const hasMore = visibleCount < allTabSheets.length; + const remaining = allTabSheets.length - visibleCount; + + const handleTabChange = (tab: "all" | "NOT_STARTED" | "COMPLETED") => { + setActiveTab(tab); + setVisibleCount(PAGE_SIZE); + }; + const greeting = new Date().getHours() < 12 ? "Good morning" @@ -414,9 +450,9 @@ export const Home = () => { {user?.name?.slice(0, 1)} -
+

- {greeting}, {user?.name?.split(" ")[0] || "Student"} + {greeting}, {user?.name?.split(" ")[0] || "Student"} 👋

{user?.role === "STUDENT" @@ -513,7 +549,7 @@ export const Home = () => {

- {tabSheets.length > 0 ? ( -
- {tabSheets.map((sheet) => ( - - ))} -
+ {allTabSheets.length > 0 ? ( + <> +
+ {tabSheets.map((sheet) => ( + + ))} +
+ {hasMore ? ( + + ) : allTabSheets.length > PAGE_SIZE ? ( +

+ Showing all {allTabSheets.length} sheets +

+ ) : null} + ) : (
🔍 diff --git a/src/pages/student/Practice.tsx b/src/pages/student/Practice.tsx index a06859b..3df657a 100644 --- a/src/pages/student/Practice.tsx +++ b/src/pages/student/Practice.tsx @@ -279,7 +279,6 @@ export const Practice = () => {
-
⚡ {userXp} XP
diff --git a/src/pages/student/drills/page.tsx b/src/pages/student/drills/page.tsx index 803709c..620870d 100644 --- a/src/pages/student/drills/page.tsx +++ b/src/pages/student/drills/page.tsx @@ -5,119 +5,436 @@ import { api } from "../../../utils/api"; import { ChoiceCard } from "../../../components/ChoiceCard"; import { AnimatePresence, motion } from "framer-motion"; import { slideVariants } from "../../../lib/utils"; -import { Loader2 } from "lucide-react"; +import { ArrowLeft, Loader2, Search, Zap } from "lucide-react"; import { useExamConfigStore } from "../../../stores/useExamConfigStore"; import { useNavigate } from "react-router-dom"; type Step = "topic" | "review"; +const STEPS: Step[] = ["topic", "review"]; + +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'); + + .dr-screen { + min-height: 100vh; + background: #fffbf4; + font-family: 'Nunito', sans-serif; + position: relative; + overflow-x: hidden; + } + + .dr-blob { position:fixed;pointer-events:none;z-index:0;filter:blur(48px);opacity:0.35; } + .dr-blob-1 { width:240px;height:240px;background:#fde68a;top:-80px;left:-80px;border-radius:60% 40% 70% 30%/50% 60% 40% 50%;animation:drWobble1 14s ease-in-out infinite; } + .dr-blob-2 { width:190px;height:190px;background:#a5f3fc;bottom:-50px;left:6%;border-radius:40% 60% 30% 70%/60% 40% 60% 40%;animation:drWobble2 16s ease-in-out infinite; } + .dr-blob-3 { width:210px;height:210px;background:#fbcfe8;top:15%;right:-60px;border-radius:70% 30% 50% 50%/40% 60% 40% 60%;animation:drWobble1 18s ease-in-out infinite reverse; } + .dr-blob-4 { width:150px;height:150px;background:#bfdbfe;bottom:12%;right:2%;border-radius:50% 50% 30% 70%/60% 40% 60% 40%;animation:drWobble2 12s ease-in-out infinite; } + + @keyframes drWobble1 { + 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 drWobble2 { + 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);} + } + + .dr-dot { position:fixed;border-radius:50%;pointer-events:none;z-index:0;opacity:0.3;animation:drFloat 7s ease-in-out infinite; } + @keyframes drFloat { + 0%,100%{transform:translateY(0) rotate(0deg);} + 50%{transform:translateY(-12px) rotate(180deg);} + } + + .dr-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 drPopIn { + from { opacity:0; transform:scale(0.92) translateY(12px); } + to { opacity:1; transform:scale(1) translateY(0); } + } + .dr-anim { animation: drPopIn 0.4s cubic-bezier(0.34,1.56,0.64,1) both; } + .dr-anim-1 { animation-delay:0.05s; } + .dr-anim-2 { animation-delay:0.1s; } + + /* Header */ + .dr-header-row { display:flex;align-items:center;gap:0.75rem; } + .dr-back-btn { + width:40px;height:40px;border-radius:50%; + background:white;border:2.5px solid #f3f4f6; + display:flex;align-items:center;justify-content:center; + cursor:pointer;box-shadow:0 3px 10px rgba(0,0,0,0.05); + transition:all 0.15s ease;flex-shrink:0; + } + .dr-back-btn:hover { border-color:#a5f3fc;background:#ecfeff; } + .dr-back-btn:active { transform:scale(0.9); } + .dr-back-btn.hidden { opacity:0;pointer-events:none; } + + .dr-eyebrow { + font-size:0.62rem;font-weight:800;letter-spacing:0.16em; + text-transform:uppercase;color:#0891b2; + display:flex;align-items:center;gap:0.35rem; + } + .dr-title { + font-size:1.75rem;font-weight:900;color:#1e1b4b; + letter-spacing:-0.02em;line-height:1.15; + } + .dr-sub { + font-family:'Nunito Sans',sans-serif; + font-size:0.82rem;font-weight:600;color:#9ca3af; + margin-top:0.2rem;line-height:1.5; + } + + /* Progress */ + .dr-progress-wrap { + background:white;border:2.5px solid #f3f4f6; + border-radius:100px;overflow:hidden;height:8px; + box-shadow:0 2px 8px rgba(0,0,0,0.04); + } + .dr-progress-fill { + height:100%; + background:linear-gradient(90deg,#22d3ee,#0891b2); + border-radius:100px; + transition:width 0.5s cubic-bezier(0.34,1.56,0.64,1); + } + .dr-progress-labels { + display:flex;justify-content:space-between; + font-size:0.6rem;font-weight:800;letter-spacing:0.1em; + text-transform:uppercase;color:#d1d5db; + margin-top:0.35rem;padding:0 0.1rem; + } + .dr-progress-labels span.done { color:#0891b2; } + + /* Step card */ + .dr-step-card { + background:white;border:2.5px solid #f3f4f6; + border-radius:24px;padding:1.25rem; + box-shadow:0 4px 20px rgba(0,0,0,0.05); + display:flex;flex-direction:column;gap:1rem; + } + .dr-step-title { + font-size:1rem;font-weight:900;color:#1e1b4b; + display:flex;align-items:center;gap:0.5rem; + } + .dr-step-badge { + font-size:0.58rem;font-weight:800;letter-spacing:0.1em; + text-transform:uppercase;padding:0.2rem 0.55rem; + border-radius:100px;background:#ecfeff; + border:2px solid #a5f3fc;color:#0891b2; + } + + /* Search */ + .dr-search-wrap { position:relative; } + .dr-search-icon { + position:absolute;left:0.85rem;top:50%; + transform:translateY(-50%);pointer-events:none;color:#9ca3af; + } + .dr-search-input { + width:100%;padding:0.7rem 1rem 0.7rem 2.5rem; + background:#f9fafb;border:2.5px solid #f3f4f6; + border-radius:14px; + font-family:'Nunito Sans',sans-serif; + font-size:0.85rem;font-weight:600;color:#1e1b4b; + outline:none;transition:all 0.2s ease; + box-sizing:border-box; + } + .dr-search-input:focus { + border-color:#67e8f9;background:white; + box-shadow:0 0 0 3px rgba(8,145,178,0.1); + } + .dr-search-input::placeholder { color:#9ca3af; } + + /* Topic grid */ + .dr-topic-grid { + display:grid;grid-template-columns:1fr; + gap:0.6rem;max-height:380px;overflow-y:auto; + padding-right:0.25rem; + } + @media(min-width:480px){ .dr-topic-grid { grid-template-columns:1fr 1fr; } } + + /* Loading / empty */ + .dr-loading { + display:flex;align-items:center;justify-content:center; + gap:0.6rem;padding:2rem; + font-size:0.85rem;font-weight:700;color:#9ca3af; + } + .dr-spinner { animation:drSpin 0.8s linear infinite; } + @keyframes drSpin { to { transform:rotate(360deg); } } + .dr-empty { text-align:center;padding:2rem;color:#9ca3af;font-size:0.85rem;font-weight:700; } + + /* Review rows */ + .dr-review-row { + display:flex;align-items:flex-start;gap:0.75rem; + padding:0.85rem 0;border-bottom:2px solid #f9fafb; + } + .dr-review-row:last-child { border-bottom:none;padding-bottom:0; } + .dr-review-icon { + width:34px;height:34px;border-radius:10px;flex-shrink:0; + display:flex;align-items:center;justify-content:center;font-size:0.95rem; + } + .dr-review-label { + font-size:0.62rem;font-weight:800;letter-spacing:0.12em; + text-transform:uppercase;color:#9ca3af; + } + .dr-review-value { + font-size:0.9rem;font-weight:800;color:#1e1b4b; + margin-top:0.1rem;line-height:1.4; + } + .dr-chip-wrap { display:flex;flex-wrap:wrap;gap:0.35rem;margin-top:0.35rem; } + .dr-chip { + background:#ecfeff;border:2px solid #a5f3fc; + border-radius:100px;padding:0.2rem 0.65rem; + font-size:0.72rem;font-weight:800;color:#0891b2; + } + + /* Stat chips in review */ + .dr-stat-row { display:flex;gap:0.6rem;margin-top:0.25rem; } + .dr-stat { + display:flex;flex-direction:column;align-items:center; + background:#f0fdff;border:2px solid #a5f3fc; + border-radius:14px;padding:0.5rem 0.85rem;flex:1; + } + .dr-stat-val { font-size:1rem;font-weight:900;color:#0891b2; } + .dr-stat-label { + font-size:0.58rem;font-weight:800;letter-spacing:0.1em; + text-transform:uppercase;color:#67e8f9;margin-top:0.1rem; + } + + /* CTA bar */ + .dr-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)); + + + } + .dr-cta-inner { + max-width:560px;margin:0 auto; + display:flex;gap:0.75rem;align-items:center; + } + + .dr-next-btn { + flex:1;padding:0.9rem 1.5rem; + background:#0891b2;color:white;border:none; + border-radius:100px;cursor:pointer; + font-family:'Nunito',sans-serif;font-size:0.92rem;font-weight:900; + display:flex;align-items:center;justify-content:center;gap:0.4rem; + box-shadow:0 6px 0 #0e7490,0 8px 20px rgba(8,145,178,0.28); + transition:transform 0.1s ease,box-shadow 0.1s ease; + } + .dr-next-btn:hover { transform:translateY(-2px);box-shadow:0 8px 0 #0e7490,0 12px 24px rgba(8,145,178,0.32); } + .dr-next-btn:active { transform:translateY(3px); box-shadow:0 3px 0 #0e7490; } + .dr-next-btn:disabled { + background:#e5e7eb;color:#9ca3af;cursor:not-allowed; + box-shadow:0 4px 0 #d1d5db; + } + .dr-next-btn:disabled:hover { transform:none;box-shadow:0 4px 0 #d1d5db; } + + .dr-start-btn { + flex:1;padding:0.9rem 1.5rem; + background:linear-gradient(135deg,#22d3ee,#0891b2);color:white;border:none; + border-radius:100px;cursor:pointer; + font-family:'Nunito',sans-serif;font-size:0.92rem;font-weight:900; + display:flex;align-items:center;justify-content:center;gap:0.4rem; + box-shadow:0 6px 0 #0e7490,0 8px 20px rgba(8,145,178,0.3); + transition:transform 0.1s ease,box-shadow 0.1s ease; + } + .dr-start-btn:hover { transform:translateY(-2px);box-shadow:0 8px 0 #0e7490,0 12px 24px rgba(8,145,178,0.35); } + .dr-start-btn:active { transform:translateY(3px); box-shadow:0 3px 0 #0e7490; } +`; export const Drills = () => { const user = useAuthStore((state) => state.user); const navigate = useNavigate(); const [direction, setDirection] = useState<1 | -1>(1); - - const [topics, setTopics] = useState([]); - const [loading, setLoading] = useState(false); - const [selectedTopics, setSelectedTopics] = useState([]); - - const [search, setSearch] = useState(""); const [step, setStep] = useState("topic"); + const [topics, setTopics] = useState([]); + const [loading, setLoading] = useState(false); + const [selectedTopics, setSelectedTopics] = useState([]); + const [search, setSearch] = useState(""); const { storeTopics, setMode, setQuestionCount } = useExamConfigStore(); + const stepIndex = STEPS.indexOf(step); + const progressPct = ((stepIndex + 1) / STEPS.length) * 100; + const toggleTopic = (topic: Topic) => { - setSelectedTopics((prev) => { - const exists = prev.some((t) => t.id === topic.id); + setSelectedTopics((prev) => + prev.some((t) => t.id === topic.id) + ? prev.filter((t) => t.id !== topic.id) + : [...prev, topic], + ); + }; - if (exists) { - return prev.filter((t) => t.id !== topic.id); - } - - return [...prev, topic]; - }); + const goNext = () => { + setDirection(1); + setStep("review"); + }; + const goBack = () => { + setDirection(-1); + setStep("topic"); }; function handleStartDrill() { if (!user || !topics) return; - navigate(`/student/practice/${topics[0].id}/test`, { replace: true }); } useEffect(() => { const fetchAllTopics = async () => { if (!user) return; - try { setLoading(true); const authStorage = localStorage.getItem("auth-storage"); if (!authStorage) return; - - const parsed = JSON.parse(authStorage) as { - state?: { token?: string }; - }; - - const token = parsed.state?.token; + const { + state: { token }, + } = JSON.parse(authStorage) as { state?: { token?: string } }; if (!token) return; - const response = await api.fetchAllTopics(token); setTopics(response); + } catch (e) { + console.error("Failed to load topics:", e); + } finally { setLoading(false); - } catch (error) { - console.error("Failed to load topics. Reason: " + error); } }; - fetchAllTopics(); }, [user]); + const filteredTopics = topics.filter((t) => + t.name.toLowerCase().includes(search.toLowerCase()), + ); + return ( -
-
-

Drills

-

- Train your speed and accuracy with our drill-based testing system. -

-
-
-
- +
+ + + {/* Blobs */} +
+
+
+
+ + {/* Dots */} + {DOTS.map((d, i) => ( +
+ ))} + +
+ {/* Header */} +
+ +
+

+ Drills +

+

+ {step === "topic" ? "Pick your topics" : "Review & launch"} +

+

+ {step === "topic" + ? "Choose what you want to drill. Speed and accuracy await." + : "Everything look good? Time to drill."} +

+
+
+ + {/* Progress */} +
+
+
+
+
+ {STEPS.map((s, i) => ( + + {s === "topic" ? "Topics" : "Review"} + + ))} +
+
+ + {/* Step content */} +
+ + {/* Step 1 — Topic */} {step === "topic" && ( -

Choose a topic

+
+
+ Choose topics + {selectedTopics.length > 0 && ( + + {selectedTopics.length} selected + + )} +
- setSearch(e.target.value)} - className="w-full rounded-xl border px-4 py-2" - /> +
+ + setSearch(e.target.value)} + /> +
-
{loading ? ( - <> -
- -
- +
+ + Loading topics... +
+ ) : filteredTopics.length === 0 ? ( +

No topics match "{search}"

) : ( - topics - .filter((t) => - t.name.toLowerCase().includes(search.toLowerCase()), - ) - .map((t) => ( +
+ {filteredTopics.map((t) => ( { selected={selectedTopics.some((st) => st.id === t.id)} onClick={() => toggleTopic(t)} /> - )) + ))} +
)}
- )} + {/* Step 2 — Review */} {step === "review" && ( -

- Review your choices -

+
+

Your drill setup

-
-

- Topics:{" "} - {selectedTopics.map((t) => t.name).join(", ")} -

+ {/* Topics */} +
+
+ 📚 +
+
+

Topics

+
+ {selectedTopics.map((t) => ( + + {t.name} + + ))} +
+
+
+ + {/* Stats */} +
+
+ ⚡ +
+
+

Session

+
+
+ 7 + Questions +
+
+ ~5 + Minutes +
+
+ ⏱️ + Timed +
+
+
+
)}
- +
- -
-
+ {/* CTA bar */} +
+
+ {step === "topic" && ( + + )} + {step === "review" && ( + + )} +
+
+
); }; diff --git a/src/pages/student/hard-test-modules/page.tsx b/src/pages/student/hard-test-modules/page.tsx index 3199623..76f779b 100644 --- a/src/pages/student/hard-test-modules/page.tsx +++ b/src/pages/student/hard-test-modules/page.tsx @@ -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(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 ( -
-
-

Hard Test Modules

-

- Tackle hard practice test modules by selecting a section. -

-
-
- - setSelected((prev) => (prev === "EBRW" ? null : "EBRW")) +
+ + + {/* Blobs */} +
+
+
+
+ + {/* Dots */} + {DOTS.map((d, i) => ( +
+ ))} + +
+ {/* Header */} +
+

+ Hard Modules +

+

Pick your challenge

+

+ Tackle the hardest SAT questions. Select a section to begin. +

+
+ + {/* EBRW Card */} +
toggle("EBRW")} > - -

- Reading & Writing -

-
- - - - - - 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 - `} - > - -

- Mathematics -

-
- - - -
-
- {selected && ( -
- + {/* Background icons */} +
+ + + +
+ + {/* Check */} +
+ + + +
+ + {/* Body */} +
+ Section 1 +

+ Reading & +
+ Writing +

+

+ Grammar, vocabulary, comprehension & evidence-based analysis +

+
+ 📖 27 Questions + ⏱️ 32 min + 🔥 Hard tier +
+
- )} -
+ + {/* MATH Card */} +
toggle("MATH")} + > + {/* Background icons */} +
+ + + +
+ + {/* Check */} +
+ + + +
+ + {/* Body */} +
+ Section 2 +

Mathematics

+

+ Algebra, advanced math, geometry & data analysis under + pressure +

+
+ 🔢 22 Questions + ⏱️ 35 min + 🔥 Hard tier +
+
+
+
+ + {/* CTA bar */} +
+
+ +

+ Tap again to deselect +

+
+
+
); }; diff --git a/src/pages/student/practice/Test.tsx b/src/pages/student/practice/Test.tsx index 9e58923..4155194 100644 --- a/src/pages/student/practice/Test.tsx +++ b/src/pages/student/practice/Test.tsx @@ -1,11 +1,5 @@ import { useEffect, useState, useRef } from "react"; import { Navigate, useNavigate } from "react-router-dom"; -import { - Card, - CardContent, - CardHeader, - CardTitle, -} from "../../../components/ui/card"; import { Binary, Calculator, @@ -13,12 +7,15 @@ import { Loader2, LogOut, Unplug, + Play, + ChevronLeft, + Menu, + X, } from "lucide-react"; import { api } from "../../../utils/api"; import { useAuthStore } from "../../../stores/authStore"; import type { Question } from "../../../types/sheet"; -import { Button } from "../../../components/ui/button"; import { useSatExam } from "../../../stores/useSatExam"; import { useSatTimer } from "../../../hooks/useSatTimer"; import type { @@ -50,6 +47,405 @@ import { useExamConfigStore } from "../../../stores/useExamConfigStore"; import { useResults } from "../../../stores/useResults"; import { GraphCalculatorModal } from "../../../components/Calculator"; +// ─── Shared Style Constants ─────────────────────────────────────────────────── +const COLORS = { + bg: "#fffbf4", + primary: "#a855f7", + primaryDark: "#7c3aed", + accent: "#f97316", + accentDark: "#c2560e", + text: "#1e1b4b", + textMuted: "#6b7280", + textLight: "#9ca3af", + border: "#f3f4f6", + borderPurple: "#c4b5fd", + success: "#22c55e", + error: "#ef4444", + blob1: "#fde68a", + blob2: "#a5f3c0", + blob3: "#fbcfe8", + blob4: "#bfdbfe", +}; + +const DOTS = [ + { size: 12, color: "#f97316", top: "8%", left: "6%", delay: "0s" }, + { size: 8, color: "#a855f7", top: "22%", left: "2%", delay: "1s" }, + { size: 10, color: "#22c55e", top: "55%", left: "4%", delay: "0.5s" }, + { size: 14, color: "#3b82f6", top: "10%", right: "5%", delay: "1.5s" }, + { size: 8, color: "#f43f5e", top: "40%", right: "3%", delay: "0.8s" }, + { size: 10, color: "#eab308", top: "70%", right: "7%", delay: "0.3s" }, +]; + +// ─── Global Styles ──────────────────────────────────────────────────────────── +const GLOBAL_STYLES = ` + @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;800;900&family=Nunito+Sans:wght@400;600;700&display=swap'); + + .test-screen { + min-height: 100vh; + background: ${COLORS.bg}; + font-family: 'Nunito', sans-serif; + position: relative; + overflow-x: hidden; + } + + /* ── Blobs ── */ + .t-blob { + position: fixed; + pointer-events: none; + z-index: 0; + filter: blur(48px); + opacity: 0.35; + } + .t-blob-1 { + width: 240px; height: 240px; + background: ${COLORS.blob1}; + top: -80px; left: -80px; + border-radius: 60% 40% 70% 30% / 50% 60% 40% 50%; + animation: tWobble1 14s ease-in-out infinite; + } + .t-blob-2 { + width: 190px; height: 190px; + background: ${COLORS.blob2}; + bottom: -50px; left: 6%; + border-radius: 40% 60% 30% 70% / 60% 40% 60% 40%; + animation: tWobble2 16s ease-in-out infinite; + } + .t-blob-3 { + width: 210px; height: 210px; + background: ${COLORS.blob3}; + top: 15%; right: -60px; + border-radius: 70% 30% 50% 50% / 40% 60% 40% 60%; + animation: tWobble1 18s ease-in-out infinite reverse; + } + .t-blob-4 { + width: 150px; height: 150px; + background: ${COLORS.blob4}; + bottom: 12%; right: 2%; + border-radius: 50% 50% 30% 70% / 60% 40% 60% 40%; + animation: tWobble2 12s ease-in-out infinite; + } + + @keyframes tWobble1 { + 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 tWobble2 { + 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 ── */ + .t-dot { + position: fixed; + border-radius: 50%; + pointer-events: none; + z-index: 0; + opacity: 0.3; + animation: tFloat 6s ease-in-out infinite; + } + @keyframes tFloat { + 0%, 100% { transform: translateY(0) rotate(0deg); } + 50% { transform: translateY(-14px) rotate(180deg); } + } + + /* ── Animations ── */ + @keyframes tPopIn { + from { opacity: 0; transform: scale(0.92) translateY(10px); } + to { opacity: 1; transform: scale(1) translateY(0); } + } + .t-anim { animation: tPopIn 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) both; } + .t-anim-1 { animation-delay: 0.05s; } + .t-anim-2 { animation-delay: 0.1s; } + .t-anim-3 { animation-delay: 0.15s; } + .t-anim-4 { animation-delay: 0.2s; } + .t-anim-5 { animation-delay: 0.25s; } + + /* ── 3D Button Styles ── */ + .t-btn-3d { + font-family: 'Nunito', sans-serif; + font-weight: 800; + border: none; + border-radius: 100px; + cursor: pointer; + transition: transform 0.1s ease, box-shadow 0.1s ease; + } + .t-btn-3d:hover { transform: translateY(-2px); } + .t-btn-3d:active { transform: translateY(2px); } + + .t-btn-primary { + background: ${COLORS.primary}; + color: white; + box-shadow: 0 4px 0 ${COLORS.primaryDark}, 0 6px 16px rgba(168, 85, 247, 0.3); + } + .t-btn-primary:hover { + box-shadow: 0 6px 0 ${COLORS.primaryDark}, 0 10px 20px rgba(168, 85, 247, 0.35); + } + .t-btn-primary:active { + box-shadow: 0 2px 0 ${COLORS.primaryDark}, 0 3px 8px rgba(168, 85, 247, 0.2); + } + + .t-btn-accent { + background: ${COLORS.accent}; + color: white; + box-shadow: 0 4px 0 ${COLORS.accentDark}, 0 6px 16px rgba(249, 115, 22, 0.3); + } + .t-btn-accent:hover { + box-shadow: 0 6px 0 ${COLORS.accentDark}, 0 10px 20px rgba(249, 115, 22, 0.35); + } + .t-btn-accent:active { + box-shadow: 0 2px 0 ${COLORS.accentDark}, 0 3px 8px rgba(249, 115, 22, 0.2); + } + + .t-btn-outline { + background: white; + color: ${COLORS.text}; + border: 2.5px solid ${COLORS.border}; + box-shadow: 0 3px 10px rgba(0,0,0,0.06); + } + .t-btn-outline:hover { + border-color: ${COLORS.borderPurple}; + box-shadow: 0 6px 14px rgba(0,0,0,0.08); + } + + /* ── Cards ── */ + .t-card { + background: white; + border: 2.5px solid ${COLORS.border}; + border-radius: 22px; + box-shadow: 0 4px 14px rgba(0,0,0,0.05); + } + + .t-card-purple { + border-color: ${COLORS.borderPurple}; + box-shadow: 0 4px 16px rgba(167, 139, 250, 0.12); + } + + /* ── Section Title ── */ + .t-section-title { + font-size: 1.2rem; + font-weight: 900; + color: ${COLORS.text}; + letter-spacing: -0.01em; + } + + /* ── Timer Display ── */ + .t-timer { + font-size: 2.5rem; + font-weight: 900; + color: ${COLORS.text}; + letter-spacing: -0.03em; + line-height: 1; + } + + /* ── Question Number Badge ── */ + .t-q-badge { + background: linear-gradient(135deg, ${COLORS.primary}, ${COLORS.primaryDark}); + color: white; + font-weight: 800; + font-size: 0.75rem; + padding: 0.4rem 0.9rem; + border-radius: 100px; + } + + /* ── Option Buttons ── */ + .t-option { + width: 100%; + text-align: start; + font-family: 'Nunito', sans-serif; + font-weight: 700; + font-size: 1rem; + padding: 1rem 1.25rem; + background: white; + border: 2.5px solid ${COLORS.border}; + border-radius: 18px; + cursor: pointer; + transition: all 0.15s ease; + display: flex; + align-items: center; + gap: 0.75rem; + } + .t-option:hover:not(:disabled) { + border-color: ${COLORS.borderPurple}; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0,0,0,0.06); + } + .t-option.selected { + background: linear-gradient(135deg, ${COLORS.primary}, ${COLORS.primaryDark}); + border-color: ${COLORS.primaryDark}; + color: white; + } + .t-option.correct { + background: linear-gradient(135deg, #22c55e, #16a34a); + border-color: #16a34a; + color: white; + } + .t-option.incorrect { + background: linear-gradient(135deg, #ef4444, #dc2626); + border-color: #dc2626; + color: white; + } + .t-option.eliminated { + opacity: 0.5; + text-decoration: line-through; + } + + .t-option-letter { + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + font-weight: 800; + font-size: 0.85rem; + flex-shrink: 0; + } + .t-option:not(.selected) .t-option-letter { + background: ${COLORS.primary}; + color: white; + } + .t-option.selected .t-option-letter { + background: white; + color: ${COLORS.primary}; + } + + /* ── Eliminate Button ── */ + .t-eliminate-btn { + width: 32px; + height: 32px; + border-radius: 50%; + border: 2.5px solid ${COLORS.border}; + background: white; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.15s ease; + flex-shrink: 0; + } + .t-eliminate-btn:hover { + border-color: #ef4444; + background: #fef2f2; + } + .t-eliminate-btn.active { + background: #ef4444; + border-color: #ef4444; + color: white; + } + + /* ── Textarea ── */ + .t-textarea { + width: 100%; + min-height: 120px; + padding: 1rem 1.25rem; + background: white; + border: 2.5px solid ${COLORS.border}; + border-radius: 18px; + font-family: 'Nunito', sans-serif; + font-size: 1rem; + font-weight: 600; + color: ${COLORS.text}; + resize: vertical; + transition: border-color 0.2s ease, box-shadow 0.2s ease; + } + .t-textarea:focus { + outline: none; + border-color: ${COLORS.borderPurple}; + box-shadow: 0 4px 16px rgba(167, 139, 250, 0.15); + } + .t-textarea::placeholder { + color: ${COLORS.textLight}; + } + + /* ── Retry Banner ── */ + .t-retry-banner { + background: linear-gradient(90deg, ${COLORS.accent}, #ea580c); + color: white; + padding: 0.75rem 1rem; + font-weight: 700; + font-size: 0.9rem; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + } + + /* ── XP Badge ── */ + .t-xp-badge { + position: absolute; + top: -8px; + right: -8px; + background: linear-gradient(135deg, #fbbf24, #f59e0b); + color: white; + font-size: 0.7rem; + font-weight: 800; + padding: 0.25rem 0.6rem; + border-radius: 100px; + box-shadow: 0 2px 8px rgba(251, 191, 36, 0.4); + animation: tBounce 0.5s ease; + } + @keyframes tBounce { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.2); } + } + + /* ── Drawer Custom Styles ── */ + [data-vaul-drawer] { + background: white !important; + border-top: 2.5px solid ${COLORS.border} !important; + border-radius: 24px 24px 0 0 !important; + } + [data-vaul-drawer-handle] { + background: ${COLORS.border} !important; + width: 48px !important; + height: 5px !important; + border-radius: 3px !important; + } + + /* ── Navigator Grid ── */ + .t-nav-grid { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 0.5rem; + } + .t-nav-item { + aspect-ratio: 1; + display: flex; + align-items: center; + justify-content: center; + border-radius: 12px; + font-weight: 800; + font-size: 0.9rem; + border: 2.5px solid ${COLORS.border}; + background: white; + cursor: pointer; + transition: all 0.15s ease; + } + .t-nav-item:hover { + border-color: ${COLORS.borderPurple}; + transform: scale(1.05); + } + .t-nav-item.current { + background: linear-gradient(135deg, ${COLORS.primary}, ${COLORS.primaryDark}); + border-color: ${COLORS.primaryDark}; + color: white; + } + .t-nav-item.answered { + background: #dcfce7; + border-color: #22c55e; + color: #16a34a; + } + + /* ── Incorrect Flash ── */ + @keyframes tFlashRed { + 0%, 100% { background: transparent; } + 50% { background: rgba(239, 68, 68, 0.15); } + } + .t-flash-red { + animation: tFlashRed 0.6s ease; + } +`; + // ─── Confetti particle type ─────────────────────────────────────────────────── interface ConfettiParticle { id: number; @@ -151,6 +547,7 @@ const snapPoints = [ "600px", "700px", ]; + // ─── XP Popup Component ─────────────────────────────────────────────────────── const XPPopup = ({ xp, show }: { xp: number; show: boolean }) => { if (!show) return null; @@ -240,10 +637,16 @@ export const Test = () => { const [snap, setSnap] = useState(snapPoints[0]); const startExam = async () => { - if (!user || !sheetId) return; + console.log("startExam called", { user, sheetId }); + if (!user) { + console.warn("Missing user or sheetId"); + return; + } const payload = useExamConfigStore.getState().payload; + console.log("payload", payload); try { const response = await api.startSession(token as string, payload); + console.log("session started", response); setSessionId(response.id); await loadSessionQuestions(response.id); useSatExam.getState().startExam(); @@ -479,27 +882,12 @@ export const Test = () => { const isFirstQuestion = retryMode ? true : questionIndex === 0; - const getOptionStyle = (optionId: string, questionId: string) => { - const base = - "w-full text-start font-satoshi-medium text-lg space-x-2 px-4 py-4 border rounded-4xl transition duration-200"; - const eliminatedSet = eliminated[questionId] ?? new Set(); - const isSelected = currentAnswer === optionId; - const isEliminated = eliminatedSet.has(optionId); - if (!isTargeted || !feedback?.show) { - if (isSelected) - return `${base} bg-linear-to-br from-indigo-400 to-indigo-500 text-white`; - if (isEliminated) return `${base} line-through opacity-70`; - return base; - } - if (feedback.correct) { - if (isSelected) - return `${base} bg-gradient-to-br from-green-400 to-green-500 text-white ring-2 ring-green-300`; - return `${base} opacity-40`; - } else { - if (isSelected) - return `${base} bg-gradient-to-br from-red-400 to-red-500 text-white ring-2 ring-red-300`; - return `${base} opacity-40`; - } + const toggleEliminate = (questionId: string, optionId: string) => { + setEliminated((prev) => { + const current = new Set(prev[questionId] ?? []); + current.has(optionId) ? current.delete(optionId) : current.add(optionId); + return { ...prev, [questionId]: current }; + }); }; // ─── Render MCQ options for the drawer ──────────────────────────────────── @@ -512,27 +900,29 @@ export const Test = () => { const isSelected = currentAnswer === option.id; const isEliminated = eliminatedSet.has(option.id); const feedbackLocked = isTargeted && !!feedback?.show; + + let optionClass = "t-option"; + if (isSelected) optionClass += " selected"; + if (isEliminated && !feedbackLocked) optionClass += " eliminated"; + if (feedbackLocked && isSelected) { + optionClass += feedback.correct ? " correct" : " incorrect"; + } + return ( -
- {!feedbackLocked && ( +
+ +
- )} -
- {isTargeted && feedback?.show && feedback.correct && feedback.xpGained && isSelected && ( -
- +{feedback.xpGained} XP ⭐ -
+
+{feedback.xpGained} XP ⭐
)}
@@ -570,7 +956,7 @@ export const Test = () => { const renderShortAnswer = (question?: Question) => { if (!question || question.options?.length) return null; return ( -
+