fix(ui): change ui theme color

feat(calc): add geogebra based graph calculator for tests
This commit is contained in:
shafin-r
2026-02-20 00:03:23 +06:00
parent 626616c8b5
commit 3c8f945539
18 changed files with 2063 additions and 259 deletions

View File

@ -6,7 +6,7 @@ import {
TabsContent,
} from "../../components/ui/tabs";
import { useAuthStore } from "../../stores/authStore";
import { CheckCircle, Search } from "lucide-react";
import { CheckCircle, Flame, Search, Zap } from "lucide-react";
import { api } from "../../utils/api";
import {
Card,
@ -22,10 +22,18 @@ import type { PracticeSheet } from "../../types/sheet";
import { formatStatus } from "../../lib/utils";
import { useNavigate } from "react-router-dom";
import { SearchOverlay } from "../../components/SearchOverlay";
import { PredictedScoreCard } from "../../components/PredictedScoreCard";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "../../components/ui/avatar";
import { useExamConfigStore } from "../../stores/useExamConfigStore";
export const Home = () => {
const user = useAuthStore((state) => state.user);
const navigate = useNavigate();
const userXp = useExamConfigStore.getState().userXp;
const [practiceSheets, setPracticeSheets] = useState<PracticeSheet[]>([]);
const [notStartedSheets, setNotStartedSheets] = useState<PracticeSheet[]>([]);
@ -84,10 +92,42 @@ export const Home = () => {
};
return (
<main className="min-h-screen bg-gray-50 space-y-6 mx-auto px-8 sm:px-6 lg:px-90 py-12">
<h1 className="text-4xl font-satoshi-bold tracking-tight text-gray-800 text-center">
Welcome, {user?.name || "Student"}
</h1>
<main className="min-h-screen space-y-6 mx-auto px-8 sm:px-6 lg:px-90 py-12">
<header className="flex items-center gap-3 justify-between">
<div className="flex gap-3">
<Avatar className="w-12 h-12">
<AvatarImage src={user?.avatar_url} />
<AvatarFallback className="font-satoshi-bold bg-linear-to-br from-indigo-400 to-indigo-500 uppercase text-lg text-white">
{user?.name.slice(0, 1)}
</AvatarFallback>
</Avatar>
<div className="flex flex-col">
<h1 className="text-xl font-satoshi-bold tracking-tight text-gray-800 text-center">
Welcome, {user?.name || "Student"}
</h1>
<h4 className="text-sm font-satoshi-bold text-indigo-500 ">
{user?.role === "STUDENT"
? "Student"
: user?.role === "ADMIN"
? "Admin"
: "Taecher"}
</h4>
</div>
</div>
<div className="flex gap-3">
<div className="rounded-full w-fit flex items-center gap-2">
<Flame size={20} className="text-red-500 fill-amber-200" />
<span className="font-satoshi-bold text-md">5</span>
</div>
<div className="rounded-full w-fit flex items-center gap-2">
<Zap size={20} className="text-lime-500 fill-lime-200" />
<span className="font-satoshi-bold text-md">{userXp}</span>
</div>
</div>
</header>
<PredictedScoreCard />
<h1 className="font-satoshi-bold text-2xl tracking-tight">
What are you looking for?
</h1>
@ -111,7 +151,7 @@ export const Home = () => {
inProgressSheets.map((sheet) => (
<Card
key={sheet?.id}
className="rounded-4xl border bg-purple-50/70 border-purple-500"
className="rounded-4xl border bg-indigo-50/70 border-indigo-500"
>
<CardHeader>
<CardTitle className="font-satoshi-medium text-xl">
@ -122,7 +162,7 @@ export const Home = () => {
</CardDescription>
</CardHeader>
<CardContent className="flex justify-between">
<p className="font-satoshi text-sm border px-2 rounded-full bg-purple-500 text-white py-1">
<p className="font-satoshi text-sm border px-2 rounded-full bg-indigo-500 text-white py-1">
{formatStatus(sheet?.user_status)}
</p>
<Badge
@ -141,7 +181,7 @@ export const Home = () => {
<Button
onClick={() => handleStartPracticeSheet(sheet?.id)}
variant="outline"
className="font-satoshi rounded-3xl w-full text-lg py-6 bg-linear-to-br from-purple-500 to-purple-600 text-white"
className="font-satoshi rounded-3xl w-full text-lg py-6 bg-linear-to-br from-indigo-500 to-indigo-600 text-white"
>
Resume
</Button>
@ -161,19 +201,19 @@ export const Home = () => {
<TabsList className="bg-transparent p-0 w-full">
<TabsTrigger
value="all"
className="font-satoshi-regular tracking-wide text-md rounded-none border-b-3 data-[state=active]:font-satoshi-medium data-[state=active]:border-b-purple-800 data-[state=active]:text-purple-800"
className="font-satoshi-regular tracking-wide text-md rounded-none border-b-3 data-[state=active]:font-satoshi-medium data-[state=active]:border-b-indigo-800 data-[state=active]:text-indigo-800"
>
All
</TabsTrigger>
<TabsTrigger
value="NOT_STARTED"
className="font-satoshi-regular tracking-wide text-md rounded-none border-b-3 data-[state=active]:border-b-purple-800 data-[state=active]:text-purple-800"
className="font-satoshi-regular tracking-wide text-md rounded-none border-b-3 data-[state=active]:border-b-indigo-800 data-[state=active]:text-indigo-800"
>
Not Started
</TabsTrigger>
<TabsTrigger
value="COMPLETED"
className="font-satoshi-regular tracking-wide text-md rounded-none border-b-3 data-[state=active]:border-b-purple-800 data-[state=active]:text-purple-800"
className="font-satoshi-regular tracking-wide text-md rounded-none border-b-3 data-[state=active]:border-b-indigo-800 data-[state=active]:text-indigo-800"
>
Completed
</TabsTrigger>
@ -211,7 +251,7 @@ export const Home = () => {
<Button
onClick={() => handleStartPracticeSheet(sheet?.id)}
variant="outline"
className="font-satoshi rounded-3xl w-full text-lg py-6 bg-linear-to-br from-purple-500 to-purple-600 text-white"
className="font-satoshi rounded-3xl w-full text-lg py-6 bg-linear-to-br from-indigo-500 to-indigo-600 text-white"
>
Start
</Button>
@ -256,7 +296,7 @@ export const Home = () => {
<CardFooter>
<Button
variant="outline"
className="font-satoshi w-full text-lg py-6 bg-linear-to-br from-purple-500 to-purple-600 rounded-3xl text-white"
className="font-satoshi w-full text-lg py-6 bg-linear-to-br from-indigo-500 to-indigo-600 rounded-3xl text-white"
>
Start
</Button>
@ -297,7 +337,7 @@ export const Home = () => {
<CardFooter>
<Button
variant="outline"
className="font-satoshi w-full text-lg py-6 bg-linear-to-br from-purple-500 to-purple-600 rounded-3xl text-white"
className="font-satoshi w-full text-lg py-6 bg-linear-to-br from-indigo-500 to-indigo-600 rounded-3xl text-white"
>
Start
</Button>
@ -322,29 +362,29 @@ export const Home = () => {
</h1>
<section className="space-y-4 ">
<div className="flex gap-2">
<CheckCircle size={24} color="#AD45FF" />
<CheckCircle size={24} color="oklch(58.5% 0.233 277.117)" />
<p className="font-satoshi text-md">
Practice regularly with official SAT materials
</p>
</div>
<div className="flex items-center gap-2">
<CheckCircle size={24} color="#AD45FF" />
<CheckCircle size={24} color="oklch(58.5% 0.233 277.117)" />
<p className="font-satoshi text-md">
Review your mistakes and learn from them
</p>
</div>
<div className="flex items-center gap-2">
<CheckCircle size={24} color="#AD45FF" />
<CheckCircle size={24} color="oklch(58.5% 0.233 277.117)" />
<p className="font-satoshi text-md">Focus on your weak areas</p>
</div>
<div className="flex items-center gap-2">
<CheckCircle size={24} color="#AD45FF" />
<CheckCircle size={24} color="oklch(58.5% 0.233 277.117)" />
<p className="font-satoshi text-md">
Take full-length practice tests
</p>
</div>
<div className="flex items-center gap-2">
<CheckCircle size={24} color="#AD45FF" />
<CheckCircle size={24} color="oklch(58.5% 0.233 277.117)" />
<p className="font-satoshi text-md">
Get plenty of rest before the test day
</p>

View File

@ -27,17 +27,17 @@ export const Practice = () => {
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-purple-500 to-purple-600 p-3 rounded-2xl">
<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-purple-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-purple-400 to-purple-500 rounded-full"></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-purple-500 to-purple-600 rounded-4xl
className="relative bg-linear-to-br from-indigo-500 to-indigo-600 rounded-4xl
flex-row"
>
<div className="space-y-4">

View File

@ -63,7 +63,7 @@ export const Profile = () => {
</section>
<button
onClick={handleLogout}
className="w-full border rounded-4xl bg-purple-500 py-4 px-4 flex justify-center items-center active:bg-purple-600 font-satoshi-medium text-white"
className="w-full border rounded-4xl bg-linear-to-br from-indigo-400 to-indigo-600 py-4 px-4 flex justify-center items-center active:bg-purple-600 font-satoshi-medium text-white"
>
Sign Out
</button>

View File

@ -32,7 +32,7 @@ import {
AvatarFallback,
AvatarImage,
} from "../../components/ui/avatar";
import { Zap } from "lucide-react";
import { Flame, LucideBadgeQuestionMark, Zap } from "lucide-react";
import type { Leaderboard } from "../../types/leaderboard";
import { api } from "../../utils/api";
import { Card, CardContent } from "../../components/ui/card";
@ -42,6 +42,7 @@ import { useExamConfigStore } from "../../stores/useExamConfigStore";
export const Rewards = () => {
const user = useAuthStore((state) => state.user);
const [time, setTime] = useState("bottom");
const [activeTab, setActiveTab] = useState("xp");
const [leaderboard, setLeaderboard] = useState<Leaderboard>();
const [loading, setLoading] = useState<boolean>(false);
@ -94,7 +95,7 @@ export const Rewards = () => {
) : (
<p className="font-satoshi-medium text-md text-gray-500">
Don't stop now! You're{" "}
<span className="text-purple-400">
<span className="text-indigo-400">
#{leaderboard?.user_rank.rank}
</span>{" "}
in XP.
@ -103,6 +104,8 @@ export const Rewards = () => {
</header>
<section className="w-full px-7">
<Tabs
value={activeTab}
onValueChange={setActiveTab}
defaultValue="xp"
className="space-y-6 h-[calc(100vh-250px)] flex flex-col"
>
@ -198,7 +201,7 @@ export const Rewards = () => {
<div className="flex items-center gap-1">
<p className="font-satoshi-medium">{user.total_xp}</p>
<Zap size={20} color="darkgreen" />
<Zap size={20} className="text-lime-500 fill-lime-200" />
</div>
</div>
);
@ -295,7 +298,7 @@ export const Rewards = () => {
</TabsContent>
</Tabs>
</section>
<Card className="absolute mx-auto w-full left-0 md:-bottom-12 bottom-0 bg-linear-to-br from-purple-500 to-purple-600 rounded-full py-4">
<Card className="absolute mx-auto w-full left-0 md:-bottom-12 bottom-0 bg-linear-to-br from-indigo-500 to-indigo-600 rounded-full py-4">
<CardContent className="flex justify-between items-center">
{loading ? (
<div className="flex justify-between items-center animate-pulse w-full">
@ -332,9 +335,9 @@ export const Rewards = () => {
{(leaderboard?.user_rank?.rank ?? Infinity) - 1}
</span>
)}
<Avatar className={`p-6 ${getRandomColor()}`}>
<Avatar className={`p-6 bg-white`}>
<AvatarImage src={leaderboard?.user_rank.avatar_url} />
<AvatarFallback className="text-white font-satoshi-bold">
<AvatarFallback className=" font-satoshi-bold">
{leaderboard?.user_rank.name.slice(0, 1).toUpperCase()}
</AvatarFallback>
</Avatar>
@ -345,9 +348,20 @@ export const Rewards = () => {
<div className="flex items-center gap-1">
<p className="font-satoshi-medium text-white">
{leaderboard?.user_rank.total_xp}
{activeTab === "xp"
? leaderboard?.user_rank.total_xp
: activeTab === "questions"
? "23"
: "5"}
</p>
<Zap size={20} color="white" />
{activeTab === "xp" ? (
<Zap size={20} color="white" />
) : activeTab === "questions" ? (
<LucideBadgeQuestionMark size={20} color="white" />
) : (
<Flame size={20} color="white" />
)}
</div>
</>
)}

View File

@ -14,14 +14,18 @@ export function StudentLayout() {
return (
<SidebarProvider>
<div className="min-h-screen flex w-full overflow-x-hidden">
<div className="flex min-h-screen w-full overflow-x-hidden">
{/* Desktop Sidebar */}
<AppSidebar />
<main className="flex-1 pb-20 ">
<SidebarTrigger className="hidden md:block" />
<Outlet />
</main>
<div className="flex flex-col flex-1 min-w-0">
<SidebarTrigger className="hidden md:block" />
<main className="flex-1 pb-24 md:pb-0">
<Outlet />
</main>
</div>
{/* Mobile bottom nav */}
<nav className="fixed bottom-0 left-0 right-0 rounded-t-4xl pt-2 bg-white border-t border-gray-200 shadow-4xl z-20 md:hidden">
<div className="max-w-7xl mx-auto px-2">
<div className="flex justify-around items-center">
@ -32,7 +36,7 @@ export function StudentLayout() {
className={({ isActive }) =>
`flex flex-col items-center justify-center py-3 px-4 flex-1 transition-all duration-200 font-satoshi tracking-wide ${
isActive
? "text-purple-600"
? "text-indigo-600"
: "text-gray-500 hover:text-gray-700"
}`
}

View File

@ -201,7 +201,7 @@ export const Pretest = () => {
<div
key={index}
className={`w-2 h-2 mx-1 rounded-full ${
index + 1 === current ? "bg-purple-500" : "bg-gray-300"
index + 1 === current ? "bg-indigo-500" : "bg-gray-300"
}`}
></div>
))}
@ -216,7 +216,7 @@ export const Pretest = () => {
<Button
onClick={() => handleStartTest(practiceSheet?.id!)}
variant="outline"
className="font-satoshi rounded-3xl w-full text-lg py-8 bg-linear-to-br from-purple-500 to-purple-600 text-white active:bg-linear-to-br active:from-purple-600 active:to-purple-700 active:translate-y-1"
className="font-satoshi rounded-3xl w-full text-lg py-8 bg-linear-to-br from-indigo-500 to-indigo-600 text-white active:bg-linear-to-br active:from-indigo-600 active:to-indigo-700 active:translate-y-1"
disabled={!practiceSheet}
>
{practiceSheet ? (

View File

@ -1,7 +1,5 @@
import { useNavigate } from "react-router-dom";
import { Button } from "../../../components/ui/button";
import { useResults } from "../../../stores/useResults";
import { useSatExam } from "../../../stores/useSatExam";
import { LucideArrowLeft } from "lucide-react";
import {
Card,
@ -11,7 +9,6 @@ import {
CardHeader,
CardTitle,
} from "../../../components/ui/card";
import { Progress } from "../../../components/ui/progress";
import { CircularLevelProgress } from "../../../components/CircularLevelProgress";
import { useEffect, useState } from "react";
import { useExamConfigStore } from "../../../stores/useExamConfigStore";
@ -33,7 +30,7 @@ const XPGainedCard = ({
if (!results?.xp_gained) return;
let startTime: number | null = null;
const duration = 800; // ms
const duration = 800;
const animate = (time: number) => {
if (!startTime) startTime = time;
@ -58,39 +55,139 @@ const XPGainedCard = ({
);
};
// ─── Targeted static results ──────────────────────────────────────────────────
const TARGETED_XP = 15;
const TARGETED_SCORE = 15;
const TargetedResults = ({ onFinish }: { onFinish: () => void }) => {
const { userXp, setUserXp } = useExamConfigStore();
// previousXP is whatever the user had before; we add 15 on top
const previousXP = userXp ?? 0;
const gainedXP = TARGETED_XP;
const totalXP = previousXP;
// Sync updated XP back into the store
useEffect(() => {
setUserXp(totalXP);
}, []);
// Simple level bounds — 0100 per level so progress is visible
// Adjust these to match your real level thresholds if needed
const levelMinXP = Math.floor(previousXP / 100) * 100;
const levelMaxXP = levelMinXP + 100;
const currentLevel = Math.floor(previousXP / 100) + 1;
const [displayXP, setDisplayXP] = useState(0);
useEffect(() => {
let startTime: number | null = null;
const duration = 800;
const animate = (time: number) => {
if (!startTime) startTime = time;
const t = Math.min((time - startTime) / duration, 1);
setDisplayXP(Math.floor(t * gainedXP));
if (t < 1) requestAnimationFrame(animate);
};
requestAnimationFrame(animate);
}, []);
return (
<main className="min-h-screen bg-gray-50 space-y-6 mx-auto px-8 sm:px-6 lg:px-90 py-10">
<header className="flex gap-4">
<button
onClick={onFinish}
className="p-2 rounded-full border border-purple-400 bg-linear-to-br from-purple-400 to-purple-500"
>
<LucideArrowLeft size={20} color="white" />
</button>
<h1 className="text-3xl font-satoshi-bold">Results</h1>
</header>
{/* Targeted mode badge */}
<div className="flex items-center gap-2 bg-purple-50 border border-purple-200 rounded-2xl px-4 py-3">
<span className="text-xl">🎯</span>
<p className="font-satoshi text-purple-700 text-sm">
<strong>Targeted Mode Complete!</strong> You answered all questions
correctly.
</p>
</div>
<section className="w-full flex items-center justify-center">
<CircularLevelProgress
previousXP={previousXP}
gainedXP={gainedXP}
levelMinXP={levelMinXP}
levelMaxXP={levelMaxXP}
level={currentLevel}
/>
</section>
<Card>
<CardHeader>
<CardTitle>XP</CardTitle>
<CardDescription>How much did you improve?</CardDescription>
<CardAction>
<p className="font-satoshi-medium">+{displayXP} XP</p>
</CardAction>
</CardHeader>
</Card>
<Card>
<CardHeader>
<CardTitle>Score</CardTitle>
<CardDescription>Total score you achieved.</CardDescription>
<CardAction>
<p className="font-satoshi-medium">{TARGETED_SCORE}</p>
</CardAction>
</CardHeader>
</Card>
<Card>
<CardHeader>
<CardTitle>Keep it up! 🚀</CardTitle>
<CardDescription>
Great work getting every question right. Keep practicing to level up
faster!
</CardDescription>
</CardHeader>
</Card>
<button
onClick={onFinish}
className="w-full font-satoshi rounded-3xl text-lg py-4 bg-linear-to-br from-purple-500 to-purple-600 text-white"
>
Done
</button>
</main>
);
};
// ─── Main Results ─────────────────────────────────────────────────────────────
export const Results = () => {
const navigate = useNavigate();
const results = useResults((s) => s.results);
const clearResults = useResults((s) => s.clearResults);
const { setUserXp } = useExamConfigStore();
const { setUserXp, payload } = useExamConfigStore();
const isTargeted = payload?.mode === "TARGETED";
useEffect(() => {
if (results) setUserXp(results?.total_xp);
}, [results]);
function handleFinishExam() {
useExamConfigStore.getState().clearPayload();
clearResults();
navigate(`/student/home`);
}
// const [displayXP, setDisplayXP] = useState(0);
// useEffect(() => {
// if (!results?.score) return;
// let start = 0;
// const duration = 600;
// const startTime = performance.now();
// const animate = (time: number) => {
// const t = Math.min((time - startTime) / duration, 1);
// setDisplayXP(Math.floor(t * results.score));
// if (t < 1) requestAnimationFrame(animate);
// };
// requestAnimationFrame(animate);
// }, [results?.score]);
// ── Targeted mode: show static screen ──────────────────────────────────────
if (isTargeted) {
return <TargetedResults onFinish={handleFinishExam} />;
}
// ── Standard mode ──────────────────────────────────────────────────────────
const previousXP = results ? results.total_xp - results.xp_gained : 0;
return (
@ -107,11 +204,6 @@ export const Results = () => {
<section className="w-full flex items-center justify-center">
{results && (
<CircularLevelProgress
// previousXP={505}
// gainedXP={605}
// levelMinXP={500}
// levelMaxXP={1000}
// level={3}
previousXP={previousXP}
gainedXP={results.xp_gained}
levelMinXP={results.current_level_start}

File diff suppressed because it is too large Load Diff

View File

@ -126,7 +126,7 @@ export const TargetedPractice = () => {
<div>
<Loader2
size={30}
color="purple"
color="indigo"
className="animate-spin"
/>
</div>
@ -162,7 +162,7 @@ export const TargetedPractice = () => {
${
selectedTopics.length === 0
? "bg-gray-300 text-gray-500 cursor-not-allowed"
: "bg-linear-to-br from-purple-500 to-purple-600 text-white"
: "bg-linear-to-br from-indigo-500 to-indigo-600 text-white"
}`}
>
Next
@ -249,7 +249,7 @@ export const TargetedPractice = () => {
${
step !== "review"
? "opacity-0 pointer-events-none"
: "bg-linear-to-br from-purple-500 to-purple-600 text-white"
: "bg-linear-to-br from-indigo-500 to-indigo-600 text-white"
}`}
onClick={() => {
handleStartTargetedPractice();