feat(ui): add new ui
This commit is contained in:
@ -1,23 +1,8 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Tabs,
|
||||
TabsTrigger,
|
||||
TabsList,
|
||||
TabsContent,
|
||||
} from "../../components/ui/tabs";
|
||||
import { useAuthStore } from "../../stores/authStore";
|
||||
import { CheckCircle, Flame, Search, Zap } from "lucide-react";
|
||||
import { CheckCircle, Flame, Gauge, Play, Search } from "lucide-react";
|
||||
import { api } from "../../utils/api";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "../../components/ui/card";
|
||||
import { Badge } from "../../components/ui/badge";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import type { PracticeSheet } from "../../types/sheet";
|
||||
import { formatStatus } from "../../lib/utils";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
@ -29,7 +14,297 @@ import {
|
||||
AvatarImage,
|
||||
} from "../../components/ui/avatar";
|
||||
import { useExamConfigStore } from "../../stores/useExamConfigStore";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
DrawerTrigger,
|
||||
} from "../../components/ui/drawer";
|
||||
|
||||
// ─── Shared blob/dot background (same as break/results screens) ────────────────
|
||||
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" },
|
||||
];
|
||||
|
||||
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');
|
||||
|
||||
.home-screen {
|
||||
min-height: 100vh;
|
||||
background: #fffbf4;
|
||||
font-family: 'Satoshi', sans-serif;
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* ── Blobs ── */
|
||||
.h-blob { position: fixed; pointer-events: none; z-index: 0; filter: blur(48px); opacity: 0.35; }
|
||||
.h-blob-1 { width:240px;height:240px;background:#fde68a;top:-80px;left:-80px;border-radius:60% 40% 70% 30%/50% 60% 40% 50%;animation:hWobble1 14s ease-in-out infinite; }
|
||||
.h-blob-2 { width:190px;height:190px;background:#a5f3c0;bottom:-50px;left:6%;border-radius:40% 60% 30% 70%/60% 40% 60% 40%;animation:hWobble2 16s ease-in-out infinite; }
|
||||
.h-blob-3 { width:210px;height:210px;background:#fbcfe8;top:15%;right:-60px;border-radius:70% 30% 50% 50%/40% 60% 40% 60%;animation:hWobble1 18s ease-in-out infinite reverse; }
|
||||
.h-blob-4 { width:150px;height:150px;background:#bfdbfe;bottom:12%;right:2%;border-radius:50% 50% 30% 70%/60% 40% 60% 40%;animation:hWobble2 12s ease-in-out infinite; }
|
||||
|
||||
@keyframes hWobble1 {
|
||||
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 hWobble2 {
|
||||
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 ── */
|
||||
.h-dot { position:fixed;border-radius:50%;pointer-events:none;z-index:0;opacity:0.3;animation:hFloat 6s ease-in-out infinite; }
|
||||
@keyframes hFloat {
|
||||
0%,100%{transform:translateY(0) rotate(0deg);}
|
||||
50%{transform:translateY(-14px) rotate(180deg);}
|
||||
}
|
||||
|
||||
/* ── Inner scroll container ── */
|
||||
.home-inner {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
max-width: 580px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem 1.25rem 4rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.75rem;
|
||||
}
|
||||
|
||||
/* ── Header ── */
|
||||
.home-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
animation: hPopIn 0.4s cubic-bezier(0.34,1.56,0.64,1) both;
|
||||
}
|
||||
.home-header-left { display:flex;align-items:center;gap:0.75rem; }
|
||||
.home-user-name {
|
||||
font-size: 1.1rem; font-weight: 900; color: #1e1b4b; line-height:1.1;
|
||||
}
|
||||
.home-user-role {
|
||||
font-size: 0.72rem; font-weight: 700; letter-spacing:0.08em;
|
||||
text-transform: uppercase; color: #a855f7;
|
||||
}
|
||||
.home-header-right { display:flex;align-items:center;gap:0.6rem; }
|
||||
|
||||
/* Header action chips */
|
||||
.h-chip {
|
||||
display: flex; align-items: center; gap: 0.4rem;
|
||||
background: white; border: 2.5px solid #f3f4f6;
|
||||
border-radius: 100px; padding: 0.5rem 0.9rem;
|
||||
box-shadow: 0 3px 10px rgba(0,0,0,0.06);
|
||||
cursor: pointer; font-size:0.85rem; font-weight:800;
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
.h-chip:hover { transform:translateY(-2px);box-shadow:0 6px 14px rgba(0,0,0,0.08); }
|
||||
.h-chip.streak { border-color:#fecaca; background:#fff5f5; color:#ef4444; }
|
||||
.h-chip.score { border-color:#d9f99d; background:#f7ffe4; color:#65a30d; }
|
||||
|
||||
/* ── Section titles ── */
|
||||
.h-section-title {
|
||||
font-size: 1.2rem; font-weight: 900; color: #1e1b4b;
|
||||
letter-spacing: -0.01em; margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
/* ── Search bar ── */
|
||||
.h-search-wrap {
|
||||
position: relative;
|
||||
animation: hPopIn 0.4s cubic-bezier(0.34,1.56,0.64,1) 0.05s both;
|
||||
}
|
||||
.h-search-input {
|
||||
width: 100%; box-sizing: border-box;
|
||||
padding: 0.85rem 1rem 0.85rem 2.8rem;
|
||||
background: white; border: 2.5px solid #f3f4f6;
|
||||
border-radius: 18px;
|
||||
font-family: 'Nunito', sans-serif;
|
||||
font-size: 0.9rem; font-weight: 700; color: #9ca3af;
|
||||
box-shadow: 0 4px 14px rgba(0,0,0,0.05);
|
||||
cursor: pointer; outline: none;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
.h-search-input:focus { border-color:#c084fc; box-shadow:0 4px 20px rgba(192,132,252,0.15); }
|
||||
.h-search-icon {
|
||||
position: absolute; left: 0.9rem; top: 50%;
|
||||
transform: translateY(-50%); pointer-events:none;
|
||||
}
|
||||
|
||||
/* ── In-progress card ── */
|
||||
.h-inprogress-card {
|
||||
background: white;
|
||||
border: 2.5px solid #c4b5fd;
|
||||
border-radius: 22px;
|
||||
padding: 1.1rem 1.25rem;
|
||||
box-shadow: 0 4px 16px rgba(167,139,250,0.12);
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
gap: 1rem; cursor: pointer;
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
.h-inprogress-card:hover { transform:translateY(-2px);box-shadow:0 8px 24px rgba(167,139,250,0.2); }
|
||||
.h-inprogress-info { display:flex;flex-direction:column;gap:0.25rem;flex:1;min-width:0; }
|
||||
.h-inprogress-title {
|
||||
font-size: 0.95rem; font-weight: 900; color: #1e1b4b;
|
||||
white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
|
||||
}
|
||||
.h-inprogress-badge {
|
||||
font-size: 0.65rem; font-weight: 800; letter-spacing:0.1em;
|
||||
text-transform:uppercase; color:#a855f7;
|
||||
background:#f3e8ff; border-radius:100px; padding:0.2rem 0.6rem;
|
||||
width: fit-content;
|
||||
}
|
||||
.h-play-btn {
|
||||
width: 44px; height: 44px; border-radius: 50%; border: none; cursor: pointer;
|
||||
background: linear-gradient(135deg, #a855f7, #7c3aed);
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
box-shadow: 0 4px 0 #5b21b6aa;
|
||||
transition: transform 0.1s ease, box-shadow 0.1s ease;
|
||||
flex-shrink:0;
|
||||
}
|
||||
.h-play-btn:hover { transform:translateY(-2px);box-shadow:0 6px 0 #5b21b6aa; }
|
||||
.h-play-btn:active { transform:translateY(2px);box-shadow:0 2px 0 #5b21b6aa; }
|
||||
|
||||
/* ── Empty state ── */
|
||||
.h-empty {
|
||||
background:white; border:2.5px dashed #e5e7eb; border-radius:22px;
|
||||
padding: 1.75rem; text-align:center;
|
||||
font-size:0.9rem; font-weight:700; color:#9ca3af;
|
||||
}
|
||||
.h-empty-emoji { font-size:2rem; display:block; margin-bottom:0.5rem; }
|
||||
|
||||
/* ── Tabs ── */
|
||||
.h-tabs-list {
|
||||
display:flex; border-bottom: 2px solid #f3f4f6;
|
||||
gap: 0; margin-bottom:1rem;
|
||||
}
|
||||
.h-tab-btn {
|
||||
flex:1; padding:0.65rem 0; text-align:center;
|
||||
font-family:'Nunito',sans-serif; font-size:0.82rem; font-weight:800;
|
||||
color:#9ca3af; background:transparent; border:none; border-bottom: 3px solid transparent;
|
||||
margin-bottom:-2px; cursor:pointer;
|
||||
transition: color 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
.h-tab-btn.active { color:#1e1b4b; border-bottom-color:#a855f7; }
|
||||
|
||||
/* ── Practice sheet card ── */
|
||||
.h-sheet-grid {
|
||||
display:grid; gap:0.85rem;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@media(min-width:520px){ .h-sheet-grid { grid-template-columns:1fr 1fr; } }
|
||||
|
||||
.h-sheet-card {
|
||||
background:white; border:2.5px solid #f3f4f6; border-radius:22px;
|
||||
padding:1.1rem; box-shadow:0 4px 14px rgba(0,0,0,0.04);
|
||||
display:flex; flex-direction:column; gap:0.6rem;
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
.h-sheet-card:hover { transform:translateY(-2px);box-shadow:0 8px 20px rgba(0,0,0,0.07); }
|
||||
|
||||
.h-sheet-title { font-size:0.95rem;font-weight:900;color:#1e1b4b;line-height:1.2; }
|
||||
.h-sheet-desc { font-size:0.78rem;font-weight:600;color:#9ca3af;font-family:'Nunito Sans',sans-serif; }
|
||||
|
||||
.h-sheet-meta {
|
||||
display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:0.4rem;
|
||||
}
|
||||
.h-status-pill {
|
||||
font-size:0.65rem;font-weight:800;letter-spacing:0.08em;text-transform:uppercase;
|
||||
border-radius:100px;padding:0.25rem 0.65rem;
|
||||
}
|
||||
.h-status-pill.inprogress { background:#f3e8ff;color:#9333ea; }
|
||||
.h-status-pill.notstarted { background:#f3f4f6;color:#6b7280; }
|
||||
.h-status-pill.completed { background:#dcfce7;color:#16a34a; }
|
||||
|
||||
.h-modules-badge {
|
||||
font-size:0.65rem;font-weight:800;
|
||||
background:#ede9fe;color:#7c3aed;border-radius:100px;padding:0.25rem 0.65rem;
|
||||
}
|
||||
.h-time-badge {
|
||||
font-size:0.72rem;font-weight:700;color:#9ca3af;display:flex;align-items:center;gap:0.3rem;
|
||||
}
|
||||
|
||||
.h-start-btn {
|
||||
width:100%;margin-top:auto;
|
||||
background:#f97316;color:white;border:none;border-radius:100px;
|
||||
padding:0.75rem;font-family:'Nunito',sans-serif;
|
||||
font-size:0.9rem;font-weight:800;cursor:pointer;
|
||||
box-shadow:0 4px 0 #c2560e,0 6px 16px rgba(249,115,22,0.2);
|
||||
transition:transform 0.1s ease,box-shadow 0.1s ease;
|
||||
}
|
||||
.h-start-btn:hover { transform:translateY(-2px);box-shadow:0 6px 0 #c2560e,0 10px 20px rgba(249,115,22,0.25); }
|
||||
.h-start-btn:active { transform:translateY(2px); box-shadow:0 2px 0 #c2560e,0 3px 8px rgba(249,115,22,0.15); }
|
||||
|
||||
/* ── Tips section ── */
|
||||
.h-tips-list { display:flex;flex-direction:column;gap:0.6rem; }
|
||||
.h-tip-row {
|
||||
display:flex;align-items:flex-start;gap:0.65rem;
|
||||
background:white;border:2.5px solid #f3f4f6;border-radius:16px;
|
||||
padding:0.75rem 1rem;box-shadow:0 2px 8px rgba(0,0,0,0.03);
|
||||
}
|
||||
.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; }
|
||||
|
||||
@keyframes hPopIn {
|
||||
from{opacity:0;transform:scale(0.92) translateY(10px);}
|
||||
to{opacity:1;transform:scale(1) translateY(0);}
|
||||
}
|
||||
.h-anim { animation: hPopIn 0.4s cubic-bezier(0.34,1.56,0.64,1) both; }
|
||||
.h-anim-1 { animation-delay:0.05s; }
|
||||
.h-anim-2 { animation-delay:0.1s; }
|
||||
.h-anim-3 { animation-delay:0.15s; }
|
||||
.h-anim-4 { animation-delay:0.2s; }
|
||||
.h-anim-5 { animation-delay:0.25s; }
|
||||
`;
|
||||
|
||||
// ─── Sheet card ───────────────────────────────────────────────────────────────
|
||||
const SheetCard = ({
|
||||
sheet,
|
||||
onStart,
|
||||
}: {
|
||||
sheet: PracticeSheet;
|
||||
onStart: (id: string) => void;
|
||||
}) => {
|
||||
const statusClass =
|
||||
sheet.user_status === "IN_PROGRESS"
|
||||
? "inprogress"
|
||||
: sheet.user_status === "COMPLETED"
|
||||
? "completed"
|
||||
: "notstarted";
|
||||
|
||||
return (
|
||||
<div className="h-sheet-card">
|
||||
<p className="h-sheet-title">{sheet.title}</p>
|
||||
{sheet.description && <p className="h-sheet-desc">{sheet.description}</p>}
|
||||
<div className="h-sheet-meta">
|
||||
<span className={`h-status-pill ${statusClass}`}>
|
||||
{formatStatus(sheet.user_status)}
|
||||
</span>
|
||||
<span className="h-modules-badge">
|
||||
📚 {sheet.modules_count} modules
|
||||
</span>
|
||||
</div>
|
||||
<p className="h-time-badge">⏱️ {sheet.time_limit} min</p>
|
||||
<button className="h-start-btn" onClick={() => onStart(sheet.id)}>
|
||||
{sheet.user_status === "COMPLETED" ? "Retry →" : "Start →"}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ─── Tips data ────────────────────────────────────────────────────────────────
|
||||
const TIPS = [
|
||||
"Practice regularly with official SAT materials",
|
||||
"Review your mistakes and learn from them",
|
||||
"Focus on your weak areas first",
|
||||
"Take full-length practice tests",
|
||||
"Get plenty of rest before test day",
|
||||
];
|
||||
|
||||
// ─── Main component ───────────────────────────────────────────────────────────
|
||||
export const Home = () => {
|
||||
const user = useAuthStore((state) => state.user);
|
||||
const navigate = useNavigate();
|
||||
@ -39,358 +314,244 @@ export const Home = () => {
|
||||
const [notStartedSheets, setNotStartedSheets] = useState<PracticeSheet[]>([]);
|
||||
const [inProgressSheets, setInProgressSheets] = useState<PracticeSheet[]>([]);
|
||||
const [completedSheets, setCompletedSheets] = useState<PracticeSheet[]>([]);
|
||||
|
||||
const [activeTab, setActiveTab] = useState<
|
||||
"all" | "NOT_STARTED" | "COMPLETED"
|
||||
>("all");
|
||||
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const sortPracticeSheets = (sheets: PracticeSheet[]) => {
|
||||
const notStarted = sheets.filter(
|
||||
(sheet) => sheet.user_status === "NOT_STARTED",
|
||||
const sort = (sheets: PracticeSheet[]) => {
|
||||
setNotStartedSheets(
|
||||
sheets.filter((s) => s.user_status === "NOT_STARTED"),
|
||||
);
|
||||
const inProgress = sheets.filter(
|
||||
(sheet) => sheet.user_status === "IN_PROGRESS",
|
||||
setInProgressSheets(
|
||||
sheets.filter((s) => s.user_status === "IN_PROGRESS"),
|
||||
);
|
||||
const completed = sheets.filter(
|
||||
(sheet) => sheet.user_status === "COMPLETED",
|
||||
);
|
||||
|
||||
setNotStartedSheets(notStarted);
|
||||
setInProgressSheets(inProgress);
|
||||
setCompletedSheets(completed);
|
||||
setCompletedSheets(sheets.filter((s) => s.user_status === "COMPLETED"));
|
||||
};
|
||||
|
||||
const fetchPracticeSheets = async () => {
|
||||
const fetch = async () => {
|
||||
if (!user) return;
|
||||
|
||||
try {
|
||||
const authStorage = localStorage.getItem("auth-storage");
|
||||
if (!authStorage) {
|
||||
console.error("authStorage not found in local storage");
|
||||
return;
|
||||
}
|
||||
if (!authStorage) return;
|
||||
const {
|
||||
state: { token },
|
||||
} = JSON.parse(authStorage);
|
||||
if (!token) {
|
||||
console.error("Token not found in authStorage");
|
||||
return;
|
||||
}
|
||||
if (!token) return;
|
||||
const sheets = await api.getPracticeSheets(token, 1, 10);
|
||||
setPracticeSheets(sheets.data);
|
||||
sortPracticeSheets(sheets.data);
|
||||
} catch (error) {
|
||||
console.error("Error fetching practice sheets:", error);
|
||||
sort(sheets.data);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPracticeSheets();
|
||||
fetch();
|
||||
}, [user]);
|
||||
|
||||
const handleStartPracticeSheet = (sheetId: string) => {
|
||||
navigate(`/student/practice/${sheetId}`);
|
||||
};
|
||||
const handleStart = (id: string) => navigate(`/student/practice/${id}`);
|
||||
|
||||
const tabSheets =
|
||||
activeTab === "all"
|
||||
? practiceSheets
|
||||
: activeTab === "NOT_STARTED"
|
||||
? notStartedSheets
|
||||
: completedSheets;
|
||||
|
||||
const greeting =
|
||||
new Date().getHours() < 12
|
||||
? "Good morning"
|
||||
: new Date().getHours() < 17
|
||||
? "Good afternoon"
|
||||
: "Good evening";
|
||||
|
||||
return (
|
||||
<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" />
|
||||
<div className="home-screen">
|
||||
<style>{STYLES}</style>
|
||||
|
||||
<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" />
|
||||
{/* Blobs */}
|
||||
<div className="h-blob h-blob-1" />
|
||||
<div className="h-blob h-blob-2" />
|
||||
<div className="h-blob h-blob-3" />
|
||||
<div className="h-blob h-blob-4" />
|
||||
|
||||
<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>
|
||||
<section className="relative w-full">
|
||||
<input
|
||||
onFocus={() => setIsSearchOpen(true)}
|
||||
placeholder="Search practice sheets..."
|
||||
readOnly
|
||||
className="font-satoshi w-full pl-10 pr-4 py-3 border border-gray-300 rounded-2xl shadow-sm cursor-pointer"
|
||||
{/* Dots */}
|
||||
{DOTS.map((d, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="h-dot"
|
||||
style={
|
||||
{
|
||||
width: d.size,
|
||||
height: d.size,
|
||||
background: d.color,
|
||||
top: d.top,
|
||||
left: d.left,
|
||||
right: d.right,
|
||||
animationDelay: d.delay,
|
||||
animationDuration: `${3.5 + i * 0.4}s`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||
<Search size={22} color="gray" />
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
|
||||
<section className="space-y-4">
|
||||
<h1 className="font-satoshi-bold text-2xl tracking-tight">
|
||||
Pick up where you left off
|
||||
</h1>
|
||||
{inProgressSheets.length > 0 ? (
|
||||
inProgressSheets.map((sheet) => (
|
||||
<Card
|
||||
key={sheet?.id}
|
||||
className="rounded-4xl border bg-indigo-50/70 border-indigo-500"
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle className="font-satoshi-medium text-xl">
|
||||
{sheet?.title}
|
||||
</CardTitle>
|
||||
<CardDescription className="font-satoshi">
|
||||
{sheet?.description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex justify-between">
|
||||
<p className="font-satoshi text-sm border px-2 rounded-full bg-indigo-500 text-white py-1">
|
||||
{formatStatus(sheet?.user_status)}
|
||||
</p>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="bg-indigo-100 text-indigo-500 font-satoshi tracking-wide"
|
||||
>
|
||||
{sheet?.modules_count} modules
|
||||
</Badge>
|
||||
</CardContent>
|
||||
<CardContent>
|
||||
<p className="font-satoshi text-gray-700">
|
||||
{sheet?.time_limit} minutes
|
||||
</p>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button
|
||||
onClick={() => handleStartPracticeSheet(sheet?.id)}
|
||||
variant="outline"
|
||||
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>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
<Card className="flex items-center justify-center py-4 rounded-4xl">
|
||||
<h2 className="text-center font-satoshi text-lg text-gray-800">
|
||||
You don't have any practice sheets in progress. Why not start one?
|
||||
</h2>
|
||||
</Card>
|
||||
)}
|
||||
</section>
|
||||
<section className="w-full">
|
||||
<Tabs defaultValue="all" className="w-full">
|
||||
<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-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-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-indigo-800 data-[state=active]:text-indigo-800"
|
||||
>
|
||||
Completed
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="all" className="pt-6">
|
||||
<div className="gap-6 flex flex-col md:grid md:grid-cols-2">
|
||||
{practiceSheets.length > 0 ? (
|
||||
practiceSheets.map((sheet) => (
|
||||
<Card key={sheet?.id} className="rounded-4xl">
|
||||
<CardHeader>
|
||||
<CardTitle className="font-satoshi-medium text-xl">
|
||||
{sheet?.title}
|
||||
</CardTitle>
|
||||
<CardDescription className="font-satoshi">
|
||||
{sheet?.description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex justify-between">
|
||||
<p className="font-satoshi text-gray-500">
|
||||
{formatStatus(sheet?.user_status)}
|
||||
</p>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="bg-indigo-100 text-indigo-500 font-satoshi tracking-wide"
|
||||
>
|
||||
{sheet?.modules_count} modules
|
||||
</Badge>
|
||||
</CardContent>
|
||||
<CardContent>
|
||||
<p className="font-satoshi text-gray-700">
|
||||
{sheet?.time_limit} minutes
|
||||
</p>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button
|
||||
onClick={() => handleStartPracticeSheet(sheet?.id)}
|
||||
variant="outline"
|
||||
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>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
<div className="flex items-center justify-center py-4 rounded-full">
|
||||
<h2 className="text-center font-satoshi text-lg text-gray-500">
|
||||
No Practice Sheets available.
|
||||
</h2>
|
||||
</div>
|
||||
)}
|
||||
<div className="home-inner">
|
||||
{/* ── Header ── */}
|
||||
<header className="home-header">
|
||||
<div className="home-header-left">
|
||||
<Avatar style={{ width: 48, height: 48 }}>
|
||||
<AvatarImage src={user?.avatar_url} />
|
||||
<AvatarFallback
|
||||
style={{
|
||||
fontWeight: 900,
|
||||
fontSize: "1.1rem",
|
||||
color: "white",
|
||||
textTransform: "uppercase",
|
||||
background: "linear-gradient(135deg,#a855f7,#7c3aed)",
|
||||
}}
|
||||
>
|
||||
{user?.name?.slice(0, 1)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="space-y-1">
|
||||
<p className="home-user-name">
|
||||
{greeting}, {user?.name?.split(" ")[0] || "Student"}
|
||||
</p>
|
||||
<p className="home-user-role">
|
||||
{user?.role === "STUDENT"
|
||||
? "Student"
|
||||
: user?.role === "ADMIN"
|
||||
? "Admin"
|
||||
: "Teacher"}
|
||||
</p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="NOT_STARTED" className="pt-6">
|
||||
<div className="gap-6 flex flex-col md:grid md:grid-cols-2">
|
||||
{notStartedSheets.map((sheet) => (
|
||||
<Card key={sheet?.id} className="rounded-4xl">
|
||||
<CardHeader>
|
||||
<CardTitle className="font-satoshi-medium text-xl">
|
||||
{sheet?.title}
|
||||
</CardTitle>
|
||||
<CardDescription className="font-satoshi">
|
||||
{sheet?.description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex justify-between">
|
||||
<p className="font-satoshi text-gray-700">Not Started</p>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="bg-indigo-100 text-indigo-500 font-satoshi tracking-wide "
|
||||
>
|
||||
{sheet?.modules_count} modules
|
||||
</Badge>
|
||||
</CardContent>
|
||||
<CardContent>
|
||||
<p className="font-satoshi text-gray-700">
|
||||
{sheet?.time_limit} minutes
|
||||
</p>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
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>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="home-header-right">
|
||||
{/* Streak chip */}
|
||||
<div className="h-chip streak">
|
||||
<Flame size={18} style={{ fill: "#fca5a5" }} />
|
||||
<span>5</span>
|
||||
</div>
|
||||
|
||||
{/* Score chip */}
|
||||
<Drawer direction="top">
|
||||
<DrawerTrigger asChild>
|
||||
<div className="h-chip score">
|
||||
<Gauge size={18} />
|
||||
</div>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent>
|
||||
<PredictedScoreCard />
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* ── Search ── */}
|
||||
<div className="h-search-wrap h-anim h-anim-1">
|
||||
<span className="h-search-icon">
|
||||
<Search size={18} color="#9ca3af" />
|
||||
</span>
|
||||
<input
|
||||
className="h-search-input"
|
||||
placeholder="Search practice sheets..."
|
||||
readOnly
|
||||
onFocus={() => setIsSearchOpen(true)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* ── In progress ── */}
|
||||
<section className="h-anim h-anim-2">
|
||||
<p className="h-section-title">📌 Pick up where you left off</p>
|
||||
{inProgressSheets.length > 0 ? (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "0.65rem",
|
||||
}}
|
||||
>
|
||||
{inProgressSheets.map((sheet) => (
|
||||
<div
|
||||
key={sheet.id}
|
||||
className="h-inprogress-card"
|
||||
onClick={() => handleStart(sheet.id)}
|
||||
>
|
||||
<div className="h-inprogress-info">
|
||||
<p className="h-inprogress-title">{sheet.title}</p>
|
||||
<span className="h-inprogress-badge">In Progress</span>
|
||||
</div>
|
||||
<button
|
||||
className="h-play-btn"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleStart(sheet.id);
|
||||
}}
|
||||
>
|
||||
<Play size={18} color="white" fill="white" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="COMPLETED" className="pt-6">
|
||||
<div className="gap-6 flex flex-col md:grid md:grid-cols-2">
|
||||
{completedSheets.length > 0 ? (
|
||||
completedSheets.map((sheet) => (
|
||||
<Card key={sheet?.id} className="rounded-4xl">
|
||||
<CardHeader>
|
||||
<CardTitle className="font-satoshi-medium text-xl">
|
||||
{sheet?.title}
|
||||
</CardTitle>
|
||||
<CardDescription className="font-satoshi">
|
||||
{sheet?.description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex justify-between">
|
||||
<p className="font-satoshi text-gray-500">
|
||||
{formatStatus(sheet?.user_status)}
|
||||
</p>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="bg-indigo-100 text-indigo-500 font-satoshi tracking-wide"
|
||||
>
|
||||
{sheet?.modules_count} modules
|
||||
</Badge>
|
||||
</CardContent>
|
||||
<CardContent>
|
||||
<p className="font-satoshi text-gray-700">
|
||||
{sheet?.time_limit} minutes
|
||||
</p>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
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>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
<div className="flex items-center justify-center py-4 rounded-full">
|
||||
<h2 className="text-center font-satoshi text-lg text-gray-500">
|
||||
You have not completed any practice sheets.
|
||||
</h2>
|
||||
</div>
|
||||
)}
|
||||
) : (
|
||||
<div className="h-empty">
|
||||
<span className="h-empty-emoji">🎯</span>
|
||||
No sheets in progress — start one below!
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</section>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="space-y-4 ">
|
||||
<h1 className="font-satoshi-bold text-2xl tracking-tight">
|
||||
SAT Preparation Tips
|
||||
</h1>
|
||||
<section className="space-y-4 ">
|
||||
<div className="flex gap-2">
|
||||
<CheckCircle size={24} color="oklch(58.5% 0.233 277.117)" />
|
||||
<p className="font-satoshi text-md">
|
||||
Practice regularly with official SAT materials
|
||||
</p>
|
||||
{/* ── All sheets with tabs ── */}
|
||||
<section className="h-anim h-anim-3">
|
||||
<p className="h-section-title">📋 Practice Sheets</p>
|
||||
|
||||
{/* Tab buttons */}
|
||||
<div className="h-tabs-list">
|
||||
{(["all", "NOT_STARTED", "COMPLETED"] as const).map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
className={`h-tab-btn${activeTab === tab ? " active" : ""}`}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
>
|
||||
{tab === "all"
|
||||
? "All"
|
||||
: tab === "NOT_STARTED"
|
||||
? "Not Started"
|
||||
: "Completed"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<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="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="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="oklch(58.5% 0.233 277.117)" />
|
||||
<p className="font-satoshi text-md">
|
||||
Get plenty of rest before the test day
|
||||
</p>
|
||||
|
||||
{tabSheets.length > 0 ? (
|
||||
<div className="h-sheet-grid">
|
||||
{tabSheets.map((sheet) => (
|
||||
<SheetCard key={sheet.id} sheet={sheet} onStart={handleStart} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-empty">
|
||||
<span className="h-empty-emoji">🔍</span>
|
||||
Nothing here yet!
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* ── Tips ── */}
|
||||
<section className="h-anim h-anim-4">
|
||||
<p className="h-section-title">💡 SAT Prep Tips</p>
|
||||
<div className="h-tips-list">
|
||||
{TIPS.map((tip, i) => (
|
||||
<div key={i} className="h-tip-row">
|
||||
<CheckCircle size={18} color="#a855f7" className="h-tip-icon" />
|
||||
<p className="h-tip-text">{tip}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{isSearchOpen && (
|
||||
<SearchOverlay
|
||||
sheets={practiceSheets}
|
||||
@ -402,6 +563,6 @@ export const Home = () => {
|
||||
setSearchQuery={setSearchQuery}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user