feat(ui): add infoheader component, improve quest map visuals

This commit is contained in:
shafin-r
2026-02-27 02:18:47 +06:00
parent f64d2cac4a
commit c7f0183956
6 changed files with 936 additions and 122 deletions

View File

@ -1,26 +1,12 @@
import { useEffect, useState } from "react";
import { useAuthStore } from "../../stores/authStore";
import { CheckCircle, Flame, Gauge, Play, Search } from "lucide-react";
import { CheckCircle, Play, Search } from "lucide-react";
import { api } from "../../utils/api";
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 {
Drawer,
DrawerContent,
DrawerTrigger,
} from "../../components/ui/drawer";
import { useExamConfigStore } from "../../stores/useExamConfigStore";
import { QuestProgressCard } from "../../components/QuestProgressCard";
// somewhere in the Home JSX, above the sheets tabs:
import { InfoHeader } from "../../components/InfoHeader";
// ─── Shared blob/dot background (same as break/results screens) ────────────────
const DOTS = [
@ -78,36 +64,6 @@ const STYLES = `
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;
@ -331,12 +287,11 @@ const TIPS = [
];
// ─── Main component ───────────────────────────────────────────────────────────
const PAGE_SIZE = 2;
const PAGE_SIZE = 6;
export const Home = () => {
const user = useAuthStore((state) => state.user);
const navigate = useNavigate();
const { userMetrics } = useExamConfigStore();
const [practiceSheets, setPracticeSheets] = useState<PracticeSheet[]>([]);
const [notStartedSheets, setNotStartedSheets] = useState<PracticeSheet[]>([]);
@ -397,15 +352,8 @@ export const Home = () => {
setVisibleCount(PAGE_SIZE);
};
const greeting =
new Date().getHours() < 12
? "Good morning"
: new Date().getHours() < 17
? "Good afternoon"
: "Good evening";
return (
<div className="home-screen pb-12">
<div className="home-screen">
<style>{STYLES}</style>
{/* Blobs */}
@ -436,56 +384,10 @@ export const Home = () => {
<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>
</div>
<div className="home-header-right">
{/* Streak chip */}
<div className="h-chip streak">
<Flame size={18} style={{ fill: "#fca5a5" }} />
<span>{userMetrics.streak}</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>
<InfoHeader
mode="DEFAULT"
onViewAll={() => navigate("/student/quests")}
/>
{/* ── Search ── */}
<div className="h-search-wrap h-anim h-anim-1">
@ -499,7 +401,7 @@ export const Home = () => {
onFocus={() => setIsSearchOpen(true)}
/>
</div>
<QuestProgressCard onViewAll={() => navigate("/student/quests")} />
{/* ── In progress ── */}
<section className="h-anim h-anim-2">
<p className="h-section-title">📌 Pick up where you left off</p>

View File

@ -9,6 +9,8 @@ import {
} from "lucide-react";
import { useNavigate } from "react-router-dom";
import { useExamConfigStore } from "../../stores/useExamConfigStore";
import { LevelBar } from "../../components/LevelBar";
import { InfoHeader } from "../../components/InfoHeader";
const DOTS = [
{ size: 10, color: "#f97316", top: "8%", left: "5%", delay: "0s" },
@ -240,10 +242,9 @@ const MODE_CARDS = [
export const Practice = () => {
const navigate = useNavigate();
const userXp = useExamConfigStore.getState().userXp;
return (
<div className="pr-screen pb-12">
<div className="pr-screen">
<style>{STYLES}</style>
{/* Blobs */}
@ -274,15 +275,7 @@ export const Practice = () => {
<div className="pr-inner">
{/* ── Header ── */}
<header className="pr-header">
<div className="pr-logo-btn">
<BookOpen size={20} color="white" />
</div>
<div className="pr-xp-chip">
<span> {userXp} XP</span>
</div>
</header>
<InfoHeader mode="LEVEL" />
{/* ── Hero banner ── */}
<div className="pr-hero pr-anim pr-anim-1">
<div className="pr-hero-icon-bg">

View File

@ -4,6 +4,7 @@ import type { QuestArc, QuestNode, NodeStatus } from "../../types/quest";
import { useQuestStore, getQuestSummary } from "../../stores/useQuestStore";
import { QuestNodeModal } from "../../components/QuestNodeModal";
import { ChestOpenModal } from "../../components/ChestOpenModal";
import { InfoHeader } from "../../components/InfoHeader";
// ─── Map geometry (all in SVG user-units, viewBox width = 390) ───────────────
const VW = 390; // viewBox width — matches typical phone width
@ -784,9 +785,9 @@ export const QuestMap = () => {
{/* Header */}
<div className="qm-header">
<p className="qm-page-title">🏴 Treasure Quests</p>
<p className="qm-page-sub">Chart your course across the Grand Line</p>
<div className="qm-stats-strip">
{/* <p className="qm-page-title">🏴‍☠️ Treasure Quests</p>
<p className="qm-page-sub">Chart your course across the Grand Line</p> */}
{/* <div className="qm-stats-strip">
{[
{
e: "⚓",
@ -807,7 +808,8 @@ export const QuestMap = () => {
<span className="qm-stat-label">{s.l}</span>
</div>
))}
</div>
</div> */}
<InfoHeader mode="QUEST_EXTENDED" />
<div className="qm-arc-tabs">
{arcs.map((a) => (
<button