Compare commits
2 Commits
f154ebf033
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 79fc2eacdc | |||
| 9074b17a83 |
7170
package-lock.json
generated
7170
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -16,263 +16,201 @@ import {
|
|||||||
BookOpen,
|
BookOpen,
|
||||||
Home,
|
Home,
|
||||||
Video,
|
Video,
|
||||||
|
User,
|
||||||
Target,
|
Target,
|
||||||
Zap,
|
Zap,
|
||||||
Trophy,
|
Trophy,
|
||||||
|
LayoutGrid,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import logo from "../assets/ed_logo1.png";
|
import logo from "../assets/ed_logo1.png";
|
||||||
import { NavLink, useNavigate } from "react-router-dom";
|
import { NavLink } from "react-router-dom";
|
||||||
import { useAuthStore } from "../stores/authStore";
|
import { useAuthStore } from "../stores/authStore";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar";
|
||||||
|
|
||||||
export function AppSidebar() {
|
export function AppSidebar() {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(true);
|
||||||
const user = useAuthStore((s) => s.user);
|
const user = useAuthStore((s) => s.user);
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sidebar
|
<Sidebar className="border-r bg-black text-white">
|
||||||
variant="floating"
|
{/* HEADER */}
|
||||||
className="pointer-events-none border-0 bg-transparent px-0 py-0"
|
<SidebarHeader>
|
||||||
>
|
<div className="flex items-center justify-between px-2 py-2 rounded-lg hover:bg-white/10 cursor-pointer">
|
||||||
<div className="pointer-events-auto fixed inset-y-2 left-2 flex w-64 flex-col overflow-hidden rounded-[1.75rem] bg-[rgba(249,240,255,0.82)] shadow-[0_18px_45px_rgba(15,23,42,0.16)] transition-colors duration-300">
|
<div className="flex items-center gap-3">
|
||||||
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top,rgba(168,85,247,0.18),transparent_55%),radial-gradient(circle_at_bottom,rgba(249,115,22,0.1),transparent_55%)]" />
|
<div className="flex rounded-md w-10 h-10 border overflow-hidden">
|
||||||
|
<img
|
||||||
|
src={logo}
|
||||||
|
className="w-full h-full object-cover object-left"
|
||||||
|
alt="Logo"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="relative flex h-full flex-col">
|
<div className="flex flex-col text-sm">
|
||||||
{/* HEADER */}
|
<span className="font-satoshi-medium text-black">
|
||||||
<SidebarHeader className="px-3 pb-4 pt-1">
|
Edbridge Scholars
|
||||||
<div className="flex items-center justify-start gap-2">
|
</span>
|
||||||
<div className="flex items-center gap-3 rounded-2xl px-2 py-2">
|
<span className="text-xs text-gray-400 font-satoshi">
|
||||||
<div className="flex h-10 w-10 items-center justify-center overflow-hidden rounded-full bg-linear-to-br from-purple-400 to-purple-500 shadow-[0_6px_18px_rgba(168,85,247,0.55)]">
|
Student
|
||||||
<img
|
</span>
|
||||||
src={logo}
|
</div>
|
||||||
className="h-full w-full object-cover object-left"
|
</div>
|
||||||
alt="Logo"
|
<ChevronDown size={16} />
|
||||||
|
</div>
|
||||||
|
</SidebarHeader>
|
||||||
|
|
||||||
|
{/* CONTENT */}
|
||||||
|
<SidebarContent>
|
||||||
|
<SidebarGroup>
|
||||||
|
<SidebarGroupLabel className="text-gray-400 font-satoshi">
|
||||||
|
Platform
|
||||||
|
</SidebarGroupLabel>
|
||||||
|
|
||||||
|
<SidebarMenu>
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<SidebarMenuButton asChild>
|
||||||
|
<NavLink
|
||||||
|
to="/student/home"
|
||||||
|
className={({ isActive }) =>
|
||||||
|
isActive
|
||||||
|
? "bg-zinc-800 text-white"
|
||||||
|
: "text-zinc-400 hover:bg-zinc-800"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Home size={18} className="text-black" />
|
||||||
|
<span className="font-satoshi text-black">Home</span>
|
||||||
|
</NavLink>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<SidebarMenuButton
|
||||||
|
className="cursor-pointer"
|
||||||
|
asChild
|
||||||
|
onClick={() => setOpen(!open)}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<BookOpen size={18} className="text-black" />
|
||||||
|
<span className="font-satoshi text-black">Practice</span>
|
||||||
|
|
||||||
|
<ChevronDown
|
||||||
|
size={16}
|
||||||
|
className={`ml-auto transition-transform ${
|
||||||
|
open ? "rotate-180" : ""
|
||||||
|
}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col text-sm">
|
</SidebarMenuButton>
|
||||||
<span className="font-satoshi-medium text-slate-900">
|
{open && (
|
||||||
Edbridge Scholars
|
<SidebarMenuSub className="space-y-3 mt-2">
|
||||||
</span>
|
<NavLink
|
||||||
<span className="font-satoshi text-xs text-slate-400">
|
to="/student/practice"
|
||||||
Student
|
className="text-black text-sm flex items-center gap-3"
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SidebarHeader>
|
|
||||||
|
|
||||||
{/* CONTENT */}
|
|
||||||
<SidebarContent className="px-1">
|
|
||||||
<SidebarGroup>
|
|
||||||
<SidebarGroupLabel className="px-2 text-[0.7rem] font-satoshi tracking-[0.16em] text-slate-400">
|
|
||||||
PLATFORM
|
|
||||||
</SidebarGroupLabel>
|
|
||||||
|
|
||||||
<SidebarMenu className="mt-1 space-y-1.5">
|
|
||||||
{/* HOME */}
|
|
||||||
<SidebarMenuItem>
|
|
||||||
<SidebarMenuButton
|
|
||||||
asChild
|
|
||||||
className="group cursor-pointer rounded-2xl px-2 py-2.5 transition-colors duration-200 hover:bg-white"
|
|
||||||
>
|
>
|
||||||
<NavLink
|
<LayoutGrid size={18} className="text-black" />
|
||||||
to="/student/home"
|
<span className="font-satoshi text-black">
|
||||||
className={({ isActive }) =>
|
Practice your way
|
||||||
`flex items-center gap-2.5 text-sm font-satoshi ${
|
</span>
|
||||||
isActive
|
</NavLink>
|
||||||
? "text-slate-900"
|
<NavLink
|
||||||
: "text-slate-500 group-hover:text-slate-900"
|
to="/student/practice/targeted-practice"
|
||||||
}`
|
className="text-black text-sm flex items-center gap-3"
|
||||||
}
|
|
||||||
>
|
|
||||||
{({ isActive }) => (
|
|
||||||
<>
|
|
||||||
<Home
|
|
||||||
size={18}
|
|
||||||
className={
|
|
||||||
isActive ? "text-orange-400" : "text-slate-400"
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<span>Home</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</NavLink>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
|
|
||||||
{/* PRACTICE */}
|
|
||||||
<SidebarMenuItem
|
|
||||||
onMouseEnter={() => setOpen(true)}
|
|
||||||
onMouseLeave={() => setOpen(false)}
|
|
||||||
>
|
|
||||||
<SidebarMenuButton
|
|
||||||
className="group cursor-pointer rounded-2xl px-2 py-2.5 transition-colors duration-200 hover:bg-white"
|
|
||||||
asChild
|
|
||||||
>
|
>
|
||||||
<NavLink
|
<Target size={18} className="text-black" />
|
||||||
to="/student/practice"
|
<span className="font-satoshi text-black">
|
||||||
className={({ isActive }) =>
|
Targeted Practice
|
||||||
`flex items-center gap-2.5 text-sm font-satoshi ${
|
</span>
|
||||||
isActive
|
</NavLink>
|
||||||
? "text-slate-900"
|
|
||||||
: "text-slate-500 group-hover:text-slate-900"
|
|
||||||
}`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{({ isActive }) => (
|
|
||||||
<>
|
|
||||||
<BookOpen
|
|
||||||
size={18}
|
|
||||||
className={
|
|
||||||
isActive ? "text-purple-500" : "text-slate-400"
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<span>Practice</span>
|
|
||||||
<ChevronDown
|
|
||||||
size={16}
|
|
||||||
className={`ml-auto text-slate-400 transition-transform ${
|
|
||||||
open ? "rotate-180" : ""
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</NavLink>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
{open && (
|
|
||||||
<SidebarMenuSub className="mt-2 space-y-1.5 pl-3">
|
|
||||||
<NavLink
|
|
||||||
to="/student/practice/targeted-practice"
|
|
||||||
className={({ isActive }) =>
|
|
||||||
`flex items-center gap-2.5 rounded-2xl px-2 py-2 text-sm font-satoshi transition-colors duration-200 ${
|
|
||||||
isActive
|
|
||||||
? "bg-white text-slate-900"
|
|
||||||
: "text-slate-500 hover:bg-white hover:text-slate-900"
|
|
||||||
}`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Target size={18} className="text-slate-400" />
|
|
||||||
<span>Targeted Practice</span>
|
|
||||||
</NavLink>
|
|
||||||
<NavLink
|
|
||||||
to="/student/practice/drills"
|
|
||||||
className={({ isActive }) =>
|
|
||||||
`flex items-center gap-2.5 rounded-2xl px-2 py-2 text-sm font-satoshi transition-colors duration-200 ${
|
|
||||||
isActive
|
|
||||||
? "bg-white text-slate-900"
|
|
||||||
: "text-slate-500 hover:bg-white hover:text-slate-900"
|
|
||||||
}`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Zap size={18} className="text-slate-400" />
|
|
||||||
<span>Drills</span>
|
|
||||||
</NavLink>
|
|
||||||
<NavLink
|
|
||||||
to="/student/practice/hard-test-modules"
|
|
||||||
className={({ isActive }) =>
|
|
||||||
`flex items-center gap-2.5 rounded-2xl px-2 py-2 text-sm font-satoshi transition-colors duration-200 ${
|
|
||||||
isActive
|
|
||||||
? "bg-white text-slate-900"
|
|
||||||
: "text-slate-500 hover:bg-white hover:text-slate-900"
|
|
||||||
}`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Trophy size={18} className="text-slate-400" />
|
|
||||||
<span>Hard Test Modules</span>
|
|
||||||
</NavLink>
|
|
||||||
</SidebarMenuSub>
|
|
||||||
)}
|
|
||||||
</SidebarMenuItem>
|
|
||||||
|
|
||||||
{/* LESSONS */}
|
<NavLink
|
||||||
<SidebarMenuItem>
|
to="/student/practice/drills"
|
||||||
<SidebarMenuButton
|
className="text-black text-sm flex items-center gap-3"
|
||||||
asChild
|
|
||||||
className="group cursor-pointer rounded-2xl px-2 py-2.5 transition-colors duration-200 hover:bg-white"
|
|
||||||
>
|
>
|
||||||
<NavLink
|
<Zap size={18} className="text-black" />
|
||||||
to="/student/lessons"
|
<span className="font-satoshi text-black">Drills</span>
|
||||||
className={({ isActive }) =>
|
</NavLink>
|
||||||
`flex items-center gap-2.5 text-sm font-satoshi ${
|
<NavLink
|
||||||
isActive
|
to="/student/practice/hard-test-modules"
|
||||||
? "text-slate-900"
|
className="text-black text-sm flex items-center gap-3"
|
||||||
: "text-slate-500 group-hover:text-slate-900"
|
|
||||||
}`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{({ isActive }) => (
|
|
||||||
<>
|
|
||||||
<Video
|
|
||||||
size={18}
|
|
||||||
className={
|
|
||||||
isActive ? "text-cyan-500" : "text-slate-400"
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<span>Lessons</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</NavLink>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
|
|
||||||
{/* REWARDS */}
|
|
||||||
<SidebarMenuItem>
|
|
||||||
<SidebarMenuButton
|
|
||||||
asChild
|
|
||||||
className="group cursor-pointer rounded-2xl px-2 py-2.5 transition-colors duration-200 hover:bg-white"
|
|
||||||
>
|
>
|
||||||
<NavLink
|
<Trophy size={18} className="text-black" />
|
||||||
to="/student/rewards"
|
<span className="font-satoshi text-black">
|
||||||
className={({ isActive }) =>
|
Hard Test Modules
|
||||||
`flex items-center gap-2.5 text-sm font-satoshi ${
|
</span>
|
||||||
isActive
|
</NavLink>
|
||||||
? "text-slate-900"
|
</SidebarMenuSub>
|
||||||
: "text-slate-500 group-hover:text-slate-900"
|
)}
|
||||||
}`
|
</SidebarMenuItem>
|
||||||
}
|
|
||||||
>
|
|
||||||
{({ isActive }) => (
|
|
||||||
<>
|
|
||||||
<Trophy
|
|
||||||
size={18}
|
|
||||||
className={
|
|
||||||
isActive ? "text-emerald-500" : "text-slate-400"
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<span>Rewards</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</NavLink>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
</SidebarMenu>
|
|
||||||
</SidebarGroup>
|
|
||||||
</SidebarContent>
|
|
||||||
|
|
||||||
{/* FOOTER – links to profile */}
|
{/* DOCS */}
|
||||||
<SidebarFooter className="mt-auto px-3 pb-3 pt-4">
|
<SidebarMenuItem>
|
||||||
<button
|
<NavLink
|
||||||
type="button"
|
to={`/student/lessons`}
|
||||||
onClick={() => navigate("/student/profile")}
|
className={({ isActive }) =>
|
||||||
className="flex w-full items-center gap-3 rounded-2xl bg-white/60 px-3 py-2 text-left shadow-sm ring-1 ring-white/80 hover:bg-white"
|
isActive
|
||||||
>
|
? "bg-zinc-800 text-white"
|
||||||
<Avatar>
|
: "text-zinc-400 hover:bg-zinc-800"
|
||||||
<AvatarImage src={user?.avatar_url} />
|
}
|
||||||
<AvatarFallback className="bg-linear-to-br from-purple-400 to-purple-500 font-satoshi-bold uppercase text-white">
|
>
|
||||||
{user?.name.slice(0, 1)}
|
<SidebarMenuButton className="cursor-pointer">
|
||||||
</AvatarFallback>
|
<Video size={18} className="text-black" />
|
||||||
</Avatar>
|
<span className="text-black font-satoshi">Lessons</span>
|
||||||
<div className="flex flex-col text-sm">
|
</SidebarMenuButton>
|
||||||
<span className="font-medium text-slate-900">{user?.name}</span>
|
</NavLink>
|
||||||
<span className="text-xs text-slate-400">{user?.email}</span>
|
</SidebarMenuItem>
|
||||||
</div>
|
|
||||||
<ChevronDown size={16} className="ml-auto text-slate-400" />
|
{/* SETTINGS */}
|
||||||
</button>
|
<SidebarMenuItem>
|
||||||
</SidebarFooter>
|
<NavLink
|
||||||
|
to={`/student/rewards`}
|
||||||
|
className={({ isActive }) =>
|
||||||
|
isActive
|
||||||
|
? "bg-zinc-800 text-white"
|
||||||
|
: "text-zinc-400 hover:bg-zinc-800"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SidebarMenuButton className="cursor-pointer">
|
||||||
|
<Trophy size={18} className="text-black" />
|
||||||
|
<span className="text-black font-satoshi">Rewards</span>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</NavLink>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<NavLink
|
||||||
|
to={`/student/profile`}
|
||||||
|
className={({ isActive }) =>
|
||||||
|
isActive
|
||||||
|
? "bg-zinc-800 text-white"
|
||||||
|
: "text-zinc-400 hover:bg-zinc-800"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SidebarMenuButton className="cursor-pointer">
|
||||||
|
<User size={18} className="text-black" />
|
||||||
|
<span className="text-black font-satoshi">Profile</span>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</NavLink>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarGroup>
|
||||||
|
</SidebarContent>
|
||||||
|
|
||||||
|
{/* FOOTER */}
|
||||||
|
<SidebarFooter>
|
||||||
|
<div className="flex items-center gap-3 px-2 py-2 rounded-lg hover:bg-white/10 cursor-pointer">
|
||||||
|
<Avatar>
|
||||||
|
<AvatarImage src={user?.avatar_url} />
|
||||||
|
<AvatarFallback className="font-satoshi-bold bg-linear-to-br from-purple-400 to-purple-500 uppercase">
|
||||||
|
{user?.name.slice(0, 1)}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div className="flex flex-col text-sm">
|
||||||
|
<span className="font-medium text-black">{user?.name}</span>
|
||||||
|
<span className="text-xs text-gray-400">{user?.email}</span>
|
||||||
|
</div>
|
||||||
|
<ChevronDown size={16} className="ml-auto" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</SidebarFooter>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { useEffect, useRef, useState, useCallback } from "react";
|
import { useEffect, useRef, useState, useCallback } from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import type { InventoryItem, ActiveEffect } from "../types/quest";
|
import type { InventoryItem, ActiveEffect } from "../types/quest";
|
||||||
import {
|
import {
|
||||||
@ -43,7 +44,6 @@ const STYLES = `
|
|||||||
to { transform: translateY(0); opacity:1; }
|
to { transform: translateY(0); opacity:1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sea shimmer bg */
|
|
||||||
.inv-sheet::before {
|
.inv-sheet::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute; inset: 0; pointer-events: none; z-index: 0;
|
position: absolute; inset: 0; pointer-events: none; z-index: 0;
|
||||||
@ -58,7 +58,6 @@ const STYLES = `
|
|||||||
100% { background-position: 100% 100%, 0% 100%; }
|
100% { background-position: 100% 100%, 0% 100%; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Gold orb top-right */
|
|
||||||
.inv-sheet::after {
|
.inv-sheet::after {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute; top: -60px; right: -40px; z-index: 0;
|
position: absolute; top: -60px; right: -40px; z-index: 0;
|
||||||
@ -67,7 +66,6 @@ const STYLES = `
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Handle ── */
|
|
||||||
.inv-handle-row {
|
.inv-handle-row {
|
||||||
display: flex; justify-content: center;
|
display: flex; justify-content: center;
|
||||||
padding: 0.75rem 0 0; flex-shrink: 0; position: relative; z-index: 2;
|
padding: 0.75rem 0 0; flex-shrink: 0; position: relative; z-index: 2;
|
||||||
@ -77,7 +75,6 @@ const STYLES = `
|
|||||||
background: rgba(255,255,255,0.1);
|
background: rgba(255,255,255,0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Header ── */
|
|
||||||
.inv-header {
|
.inv-header {
|
||||||
position: relative; z-index: 2;
|
position: relative; z-index: 2;
|
||||||
display: flex; align-items: center; justify-content: space-between;
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
@ -108,7 +105,6 @@ const STYLES = `
|
|||||||
background: rgba(251,191,36,0.1);
|
background: rgba(251,191,36,0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Active effects banner ── */
|
|
||||||
.inv-active-bar {
|
.inv-active-bar {
|
||||||
position: relative; z-index: 2;
|
position: relative; z-index: 2;
|
||||||
display: flex; gap: 0.5rem; overflow-x: auto; scrollbar-width: none;
|
display: flex; gap: 0.5rem; overflow-x: auto; scrollbar-width: none;
|
||||||
@ -140,7 +136,6 @@ const STYLES = `
|
|||||||
margin-left: 0.1rem;
|
margin-left: 0.1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Divider ── */
|
|
||||||
.inv-divider {
|
.inv-divider {
|
||||||
position: relative; z-index: 2;
|
position: relative; z-index: 2;
|
||||||
height: 1px; margin: 0.85rem 1.3rem 0;
|
height: 1px; margin: 0.85rem 1.3rem 0;
|
||||||
@ -154,7 +149,6 @@ const STYLES = `
|
|||||||
text-transform: uppercase; color: rgba(255,255,255,0.25);
|
text-transform: uppercase; color: rgba(255,255,255,0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Scrollable item grid ── */
|
|
||||||
.inv-scroll {
|
.inv-scroll {
|
||||||
position: relative; z-index: 2;
|
position: relative; z-index: 2;
|
||||||
flex: 1; overflow-y: auto; scrollbar-width: none;
|
flex: 1; overflow-y: auto; scrollbar-width: none;
|
||||||
@ -162,7 +156,6 @@ const STYLES = `
|
|||||||
}
|
}
|
||||||
.inv-scroll::-webkit-scrollbar { display: none; }
|
.inv-scroll::-webkit-scrollbar { display: none; }
|
||||||
|
|
||||||
/* ── Empty state ── */
|
|
||||||
.inv-empty {
|
.inv-empty {
|
||||||
display: flex; flex-direction: column; align-items: center;
|
display: flex; flex-direction: column; align-items: center;
|
||||||
justify-content: center; gap: 0.6rem;
|
justify-content: center; gap: 0.6rem;
|
||||||
@ -173,7 +166,6 @@ const STYLES = `
|
|||||||
}
|
}
|
||||||
.inv-empty-icon { font-size: 2.5rem; opacity: 0.4; }
|
.inv-empty-icon { font-size: 2.5rem; opacity: 0.4; }
|
||||||
|
|
||||||
/* ── Loading skeleton ── */
|
|
||||||
.inv-skeleton-grid {
|
.inv-skeleton-grid {
|
||||||
display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem;
|
display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem;
|
||||||
}
|
}
|
||||||
@ -187,12 +179,10 @@ const STYLES = `
|
|||||||
50% { opacity: 1; }
|
50% { opacity: 1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Item grid ── */
|
|
||||||
.inv-grid {
|
.inv-grid {
|
||||||
display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem;
|
display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Item card ── */
|
|
||||||
.inv-card {
|
.inv-card {
|
||||||
border-radius: 20px; padding: 1rem;
|
border-radius: 20px; padding: 1rem;
|
||||||
border: 1.5px solid rgba(255,255,255,0.07);
|
border: 1.5px solid rgba(255,255,255,0.07);
|
||||||
@ -213,8 +203,6 @@ const STYLES = `
|
|||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
.inv-card:active { transform: translateY(0) scale(0.98); }
|
.inv-card:active { transform: translateY(0) scale(0.98); }
|
||||||
|
|
||||||
/* Active card styling */
|
|
||||||
.inv-card.is-active {
|
.inv-card.is-active {
|
||||||
border-color: rgba(251,191,36,0.4);
|
border-color: rgba(251,191,36,0.4);
|
||||||
background: rgba(251,191,36,0.06);
|
background: rgba(251,191,36,0.06);
|
||||||
@ -223,26 +211,17 @@ const STYLES = `
|
|||||||
border-color: rgba(251,191,36,0.6);
|
border-color: rgba(251,191,36,0.6);
|
||||||
background: rgba(251,191,36,0.09);
|
background: rgba(251,191,36,0.09);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Just-activated flash */
|
|
||||||
@keyframes invActivateFlash {
|
@keyframes invActivateFlash {
|
||||||
0% { background: rgba(251,191,36,0.25); border-color: rgba(251,191,36,0.8); }
|
0% { background: rgba(251,191,36,0.25); border-color: rgba(251,191,36,0.8); }
|
||||||
100%{ background: rgba(251,191,36,0.06); border-color: rgba(251,191,36,0.4); }
|
100%{ background: rgba(251,191,36,0.06); border-color: rgba(251,191,36,0.4); }
|
||||||
}
|
}
|
||||||
.inv-card.just-activated {
|
.inv-card.just-activated { animation: invActivateFlash 0.9s ease forwards; }
|
||||||
animation: invActivateFlash 0.9s ease forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Card shimmer overlay */
|
|
||||||
.inv-card-sheen {
|
.inv-card-sheen {
|
||||||
position: absolute; inset: 0; pointer-events: none;
|
position: absolute; inset: 0; pointer-events: none;
|
||||||
background: linear-gradient(135deg, transparent 30%, rgba(255,255,255,0.04) 50%, transparent 70%);
|
background: linear-gradient(135deg, transparent 30%, rgba(255,255,255,0.04) 50%, transparent 70%);
|
||||||
transform: translateX(-100%);
|
transform: translateX(-100%); transition: transform 0.5s ease;
|
||||||
transition: transform 0.5s ease;
|
|
||||||
}
|
}
|
||||||
.inv-card:hover .inv-card-sheen { transform: translateX(100%); }
|
.inv-card:hover .inv-card-sheen { transform: translateX(100%); }
|
||||||
|
|
||||||
/* Icon area */
|
|
||||||
.inv-card-icon-wrap {
|
.inv-card-icon-wrap {
|
||||||
width: 44px; height: 44px; border-radius: 14px;
|
width: 44px; height: 44px; border-radius: 14px;
|
||||||
display: flex; align-items: center; justify-content: center;
|
display: flex; align-items: center; justify-content: center;
|
||||||
@ -258,30 +237,23 @@ const STYLES = `
|
|||||||
.inv-card-active-dot {
|
.inv-card-active-dot {
|
||||||
position: absolute; top: -3px; right: -3px;
|
position: absolute; top: -3px; right: -3px;
|
||||||
width: 10px; height: 10px; border-radius: 50%;
|
width: 10px; height: 10px; border-radius: 50%;
|
||||||
background: #fbbf24;
|
background: #fbbf24; border: 2px solid #08111f;
|
||||||
border: 2px solid #08111f;
|
|
||||||
animation: invDotPulse 2s ease-in-out infinite;
|
animation: invDotPulse 2s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
@keyframes invDotPulse {
|
@keyframes invDotPulse {
|
||||||
0%,100% { box-shadow: 0 0 0 0 rgba(251,191,36,0.6); }
|
0%,100% { box-shadow: 0 0 0 0 rgba(251,191,36,0.6); }
|
||||||
50% { box-shadow: 0 0 0 5px rgba(251,191,36,0); }
|
50% { box-shadow: 0 0 0 5px rgba(251,191,36,0); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Card text */
|
|
||||||
.inv-card-name {
|
.inv-card-name {
|
||||||
font-family: 'Nunito', sans-serif;
|
font-family: 'Nunito', sans-serif;
|
||||||
font-size: 0.82rem; font-weight: 900; color: #fff;
|
font-size: 0.82rem; font-weight: 900; color: #fff; line-height: 1.2;
|
||||||
line-height: 1.2;
|
|
||||||
}
|
}
|
||||||
.inv-card.is-active .inv-card-name { color: #fbbf24; }
|
.inv-card.is-active .inv-card-name { color: #fbbf24; }
|
||||||
.inv-card-desc {
|
.inv-card-desc {
|
||||||
font-family: 'Nunito Sans', sans-serif;
|
font-family: 'Nunito Sans', sans-serif;
|
||||||
font-size: 0.63rem; font-weight: 600;
|
font-size: 0.63rem; font-weight: 600;
|
||||||
color: rgba(255,255,255,0.38); line-height: 1.4;
|
color: rgba(255,255,255,0.38); line-height: 1.4; flex: 1;
|
||||||
flex: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Qty + type row */
|
|
||||||
.inv-card-meta {
|
.inv-card-meta {
|
||||||
display: flex; align-items: center; justify-content: space-between;
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
gap: 0.4rem; margin-top: auto;
|
gap: 0.4rem; margin-top: auto;
|
||||||
@ -299,11 +271,8 @@ const STYLES = `
|
|||||||
letter-spacing: 0.1em; text-transform: uppercase;
|
letter-spacing: 0.1em; text-transform: uppercase;
|
||||||
color: rgba(255,255,255,0.22);
|
color: rgba(255,255,255,0.22);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Activate button */
|
|
||||||
.inv-activate-btn {
|
.inv-activate-btn {
|
||||||
width: 100%;
|
width: 100%; padding: 0.48rem;
|
||||||
padding: 0.48rem;
|
|
||||||
border-radius: 10px; border: none; cursor: pointer;
|
border-radius: 10px; border: none; cursor: pointer;
|
||||||
font-family: 'Nunito', sans-serif;
|
font-family: 'Nunito', sans-serif;
|
||||||
font-size: 0.7rem; font-weight: 900;
|
font-size: 0.7rem; font-weight: 900;
|
||||||
@ -315,10 +284,7 @@ const STYLES = `
|
|||||||
border: 1px solid rgba(255,255,255,0.1);
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
color: rgba(255,255,255,0.6);
|
color: rgba(255,255,255,0.6);
|
||||||
}
|
}
|
||||||
.inv-activate-btn.idle:hover {
|
.inv-activate-btn.idle:hover { background: rgba(255,255,255,0.12); color: white; }
|
||||||
background: rgba(255,255,255,0.12);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
.inv-activate-btn.activating {
|
.inv-activate-btn.activating {
|
||||||
background: rgba(251,191,36,0.1);
|
background: rgba(251,191,36,0.1);
|
||||||
border: 1px solid rgba(251,191,36,0.25);
|
border: 1px solid rgba(251,191,36,0.25);
|
||||||
@ -330,8 +296,7 @@ const STYLES = `
|
|||||||
.inv-activate-btn.active-state {
|
.inv-activate-btn.active-state {
|
||||||
background: rgba(251,191,36,0.12);
|
background: rgba(251,191,36,0.12);
|
||||||
border: 1px solid rgba(251,191,36,0.3);
|
border: 1px solid rgba(251,191,36,0.3);
|
||||||
color: #fbbf24;
|
color: #fbbf24; cursor: default;
|
||||||
cursor: default;
|
|
||||||
}
|
}
|
||||||
.inv-activate-btn.success-flash {
|
.inv-activate-btn.success-flash {
|
||||||
background: rgba(74,222,128,0.18);
|
background: rgba(74,222,128,0.18);
|
||||||
@ -339,24 +304,17 @@ const STYLES = `
|
|||||||
color: #4ade80;
|
color: #4ade80;
|
||||||
animation: invSuccessScale 0.35s cubic-bezier(0.34,1.56,0.64,1) both;
|
animation: invSuccessScale 0.35s cubic-bezier(0.34,1.56,0.64,1) both;
|
||||||
}
|
}
|
||||||
@keyframes invSuccessScale {
|
@keyframes invSuccessScale { from{transform:scale(0.94)} to{transform:scale(1)} }
|
||||||
from { transform: scale(0.94); }
|
|
||||||
to { transform: scale(1); }
|
|
||||||
}
|
|
||||||
.inv-activate-btn:disabled { pointer-events: none; }
|
.inv-activate-btn:disabled { pointer-events: none; }
|
||||||
|
|
||||||
/* Time remaining on active button */
|
|
||||||
.inv-active-time {
|
.inv-active-time {
|
||||||
font-family: 'Nunito Sans', sans-serif;
|
font-family: 'Nunito Sans', sans-serif;
|
||||||
font-size: 0.55rem; font-weight: 700;
|
font-size: 0.55rem; font-weight: 700; color: rgba(251,191,36,0.5);
|
||||||
color: rgba(251,191,36,0.5);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Toast ── */
|
|
||||||
.inv-toast {
|
.inv-toast {
|
||||||
position: fixed; bottom: calc(1.5rem + env(safe-area-inset-bottom));
|
position: fixed; bottom: calc(1.5rem + env(safe-area-inset-bottom));
|
||||||
left: 50%; transform: translateX(-50%);
|
left: 50%; transform: translateX(-50%);
|
||||||
z-index: 90;
|
z-index: 9999;
|
||||||
display: flex; align-items: center; gap: 0.55rem;
|
display: flex; align-items: center; gap: 0.55rem;
|
||||||
padding: 0.7rem 1.2rem;
|
padding: 0.7rem 1.2rem;
|
||||||
background: linear-gradient(135deg, #1a3a1a, #0d2010);
|
background: linear-gradient(135deg, #1a3a1a, #0d2010);
|
||||||
@ -380,13 +338,11 @@ const ITEM_ICON: Record<string, string> = {
|
|||||||
title: "🏴☠️",
|
title: "🏴☠️",
|
||||||
coin_boost: "🪙",
|
coin_boost: "🪙",
|
||||||
};
|
};
|
||||||
const ITEM_ICON_DEFAULT = "📦";
|
|
||||||
|
|
||||||
function itemIcon(effectType: string): string {
|
function itemIcon(effectType: string): string {
|
||||||
return ITEM_ICON[effectType] ?? ITEM_ICON_DEFAULT;
|
return ITEM_ICON[effectType] ?? "📦";
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Check if an item is currently active ─────────────────────────────────────
|
|
||||||
function isItemActive(
|
function isItemActive(
|
||||||
item: InventoryItem,
|
item: InventoryItem,
|
||||||
activeEffects: ActiveEffect[],
|
activeEffects: ActiveEffect[],
|
||||||
@ -438,33 +394,23 @@ const ItemCard = ({
|
|||||||
style={{ "--ci-delay": `${index * 0.045}s` } as React.CSSProperties}
|
style={{ "--ci-delay": `${index * 0.045}s` } as React.CSSProperties}
|
||||||
>
|
>
|
||||||
<div className="inv-card-sheen" />
|
<div className="inv-card-sheen" />
|
||||||
|
|
||||||
{/* Icon */}
|
|
||||||
<div className="inv-card-icon-wrap">
|
<div className="inv-card-icon-wrap">
|
||||||
{itemIcon(inv.item.effect_type)}
|
{itemIcon(inv.item.effect_type)}
|
||||||
{isActive && <div className="inv-card-active-dot" />}
|
{isActive && <div className="inv-card-active-dot" />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Name + description */}
|
|
||||||
<p className="inv-card-name">{inv.item.name}</p>
|
<p className="inv-card-name">{inv.item.name}</p>
|
||||||
<p className="inv-card-desc">{inv.item.description}</p>
|
<p className="inv-card-desc">{inv.item.description}</p>
|
||||||
|
|
||||||
{/* Qty + type */}
|
|
||||||
<div className="inv-card-meta">
|
<div className="inv-card-meta">
|
||||||
<span className="inv-card-qty">×{inv.quantity}</span>
|
<span className="inv-card-qty">×{inv.quantity}</span>
|
||||||
<span className="inv-card-type">
|
<span className="inv-card-type">
|
||||||
{inv.item.type.replace(/_/g, " ")}
|
{inv.item.type.replace(/_/g, " ")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Time remaining if active */}
|
|
||||||
{isActive && activeEffect && (
|
{isActive && activeEffect && (
|
||||||
<div className="inv-active-time">
|
<div className="inv-active-time">
|
||||||
{formatTimeLeft(activeEffect.expires_at)} remaining
|
{formatTimeLeft(activeEffect.expires_at)} remaining
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Activate button */}
|
|
||||||
<button
|
<button
|
||||||
className={`inv-activate-btn ${btnState}`}
|
className={`inv-activate-btn ${btnState}`}
|
||||||
onClick={() => !isActive && !isActivating && onActivate(inv.id)}
|
onClick={() => !isActive && !isActivating && onActivate(inv.id)}
|
||||||
@ -504,11 +450,9 @@ export const InventoryModal = ({ onClose }: Props) => {
|
|||||||
const [toastMsg, setToastMsg] = useState("");
|
const [toastMsg, setToastMsg] = useState("");
|
||||||
const toastTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const toastTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
// ── Fetch on open ──────────────────────────────────────────────────────────
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
const fetchInv = async () => {
|
const fetchInv = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
@ -520,31 +464,24 @@ export const InventoryModal = ({ onClose }: Props) => {
|
|||||||
if (!cancelled) setLoading(false);
|
if (!cancelled) setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchInv();
|
fetchInv();
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [token]);
|
}, [token]);
|
||||||
|
|
||||||
// ── Activate ──────────────────────────────────────────────────────────────
|
|
||||||
const handleActivate = useCallback(
|
const handleActivate = useCallback(
|
||||||
async (itemId: string) => {
|
async (itemId: string) => {
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
activateItemOptimistic(itemId);
|
activateItemOptimistic(itemId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const updatedInv = await api.activateItem(token, itemId);
|
const updatedInv = await api.activateItem(token, itemId);
|
||||||
activateItemSuccess(updatedInv, itemId);
|
activateItemSuccess(updatedInv, itemId);
|
||||||
|
|
||||||
// Find item name for toast
|
|
||||||
const name = items.find((i) => i.id === itemId)?.item.name ?? "Item";
|
const name = items.find((i) => i.id === itemId)?.item.name ?? "Item";
|
||||||
setToastMsg(
|
setToastMsg(
|
||||||
`${itemIcon(items.find((i) => i.id === itemId)?.item.effect_type ?? "")} ${name} activated!`,
|
`${itemIcon(items.find((i) => i.id === itemId)?.item.effect_type ?? "")} ${name} activated!`,
|
||||||
);
|
);
|
||||||
setShowToast(true);
|
setShowToast(true);
|
||||||
|
|
||||||
// Auto-clear success state + toast
|
|
||||||
if (toastTimer.current) clearTimeout(toastTimer.current);
|
if (toastTimer.current) clearTimeout(toastTimer.current);
|
||||||
toastTimer.current = setTimeout(() => {
|
toastTimer.current = setTimeout(() => {
|
||||||
setShowToast(false);
|
setShowToast(false);
|
||||||
@ -560,7 +497,6 @@ export const InventoryModal = ({ onClose }: Props) => {
|
|||||||
[token, items],
|
[token, items],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Cleanup timer on unmount
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() => () => {
|
() => () => {
|
||||||
if (toastTimer.current) clearTimeout(toastTimer.current);
|
if (toastTimer.current) clearTimeout(toastTimer.current);
|
||||||
@ -570,18 +506,19 @@ export const InventoryModal = ({ onClose }: Props) => {
|
|||||||
|
|
||||||
const liveEffects = getLiveEffects(activeEffects);
|
const liveEffects = getLiveEffects(activeEffects);
|
||||||
|
|
||||||
return (
|
// Portal the entire modal to document.body so it always
|
||||||
|
// renders at the top of the DOM tree, escaping any parent
|
||||||
|
// stacking context, overflow:hidden, or z-index constraints.
|
||||||
|
return createPortal(
|
||||||
<>
|
<>
|
||||||
<style>{STYLES}</style>
|
<style>{STYLES}</style>
|
||||||
|
|
||||||
<div className="inv-overlay" onClick={onClose}>
|
<div className="inv-overlay" onClick={onClose}>
|
||||||
<div className="inv-sheet" onClick={(e) => e.stopPropagation()}>
|
<div className="inv-sheet" onClick={(e) => e.stopPropagation()}>
|
||||||
{/* Handle */}
|
|
||||||
<div className="inv-handle-row">
|
<div className="inv-handle-row">
|
||||||
<div className="inv-handle" />
|
<div className="inv-handle" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Header */}
|
|
||||||
<div className="inv-header">
|
<div className="inv-header">
|
||||||
<div className="inv-header-left">
|
<div className="inv-header-left">
|
||||||
<span className="inv-eyebrow">⚓ Pirate's Hold</span>
|
<span className="inv-eyebrow">⚓ Pirate's Hold</span>
|
||||||
@ -592,7 +529,6 @@ export const InventoryModal = ({ onClose }: Props) => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Active effects bar */}
|
|
||||||
{liveEffects.length > 0 && (
|
{liveEffects.length > 0 && (
|
||||||
<div className="inv-active-bar">
|
<div className="inv-active-bar">
|
||||||
{liveEffects.map((e) => (
|
{liveEffects.map((e) => (
|
||||||
@ -616,7 +552,6 @@ export const InventoryModal = ({ onClose }: Props) => {
|
|||||||
: "Your hold"}
|
: "Your hold"}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Scroll area */}
|
|
||||||
<div className="inv-scroll">
|
<div className="inv-scroll">
|
||||||
{loading && items.length === 0 ? (
|
{loading && items.length === 0 ? (
|
||||||
<div className="inv-skeleton-grid">
|
<div className="inv-skeleton-grid">
|
||||||
@ -649,7 +584,6 @@ export const InventoryModal = ({ onClose }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Error inline */}
|
|
||||||
{error && (
|
{error && (
|
||||||
<p
|
<p
|
||||||
style={{
|
style={{
|
||||||
@ -668,8 +602,8 @@ export const InventoryModal = ({ onClose }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Success toast */}
|
|
||||||
{showToast && <div className="inv-toast">{toastMsg}</div>}
|
{showToast && <div className="inv-toast">{toastMsg}</div>}
|
||||||
</>
|
</>,
|
||||||
|
document.body,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from "react";
|
||||||
import { QuizData } from '../types';
|
import { type QuizData } from "../../types/lesson";
|
||||||
import { CheckCircle2, XCircle, ChevronRight } from 'lucide-react';
|
import { CheckCircle2, XCircle, ChevronRight } from "lucide-react";
|
||||||
|
|
||||||
interface QuizProps {
|
interface QuizProps {
|
||||||
data: QuizData;
|
data: QuizData;
|
||||||
@ -21,20 +21,24 @@ const Quiz: React.FC<QuizProps> = ({ data, onComplete }) => {
|
|||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
if (!selectedId) return;
|
if (!selectedId) return;
|
||||||
setIsSubmitted(true);
|
setIsSubmitted(true);
|
||||||
const selectedOption = data.options.find(opt => opt.id === selectedId);
|
const selectedOption = data.options.find((opt) => opt.id === selectedId);
|
||||||
if (selectedOption?.isCorrect && onComplete) {
|
if (selectedOption?.isCorrect && onComplete) {
|
||||||
onComplete();
|
onComplete();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectedOption = data.options.find(opt => opt.id === selectedId);
|
const selectedOption = data.options.find((opt) => opt.id === selectedId);
|
||||||
const isCorrect = selectedOption?.isCorrect;
|
const isCorrect = selectedOption?.isCorrect;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-2xl mx-auto bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden mt-6">
|
<div className="w-full max-w-2xl mx-auto bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden mt-6">
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<h4 className="text-sm font-bold text-slate-400 uppercase tracking-wider mb-2">Concept Check</h4>
|
<h4 className="text-sm font-bold text-slate-400 uppercase tracking-wider mb-2">
|
||||||
<p className="text-lg font-medium text-slate-900 mb-6 whitespace-pre-line">{data.question}</p>
|
Concept Check
|
||||||
|
</h4>
|
||||||
|
<p className="text-lg font-medium text-slate-900 mb-6 whitespace-pre-line">
|
||||||
|
{data.question}
|
||||||
|
</p>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{data.options.map((option) => {
|
{data.options.map((option) => {
|
||||||
@ -52,11 +56,11 @@ const Quiz: React.FC<QuizProps> = ({ data, onComplete }) => {
|
|||||||
icon = <XCircle className="w-5 h-5 text-red-600" />;
|
icon = <XCircle className="w-5 h-5 text-red-600" />;
|
||||||
}
|
}
|
||||||
} else if (option.isCorrect) {
|
} else if (option.isCorrect) {
|
||||||
// Highlight correct answer if wrong one was picked
|
// Highlight correct answer if wrong one was picked
|
||||||
borderClass = "border-green-200 bg-green-50/50";
|
borderClass = "border-green-200 bg-green-50/50";
|
||||||
}
|
}
|
||||||
} else if (selectedId === option.id) {
|
} else if (selectedId === option.id) {
|
||||||
borderClass = "border-indigo-600 bg-indigo-50";
|
borderClass = "border-indigo-600 bg-indigo-50";
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -67,14 +71,22 @@ const Quiz: React.FC<QuizProps> = ({ data, onComplete }) => {
|
|||||||
className={`w-full text-left p-4 rounded-lg border-2 transition-all duration-200 flex items-center justify-between group ${borderClass} ${bgClass}`}
|
className={`w-full text-left p-4 rounded-lg border-2 transition-all duration-200 flex items-center justify-between group ${borderClass} ${bgClass}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<span className={`w-6 h-6 flex items-center justify-center rounded-full text-xs font-bold mr-3 ${
|
<span
|
||||||
isSubmitted && option.isCorrect ? 'bg-green-200 text-green-800' :
|
className={`w-6 h-6 flex items-center justify-center rounded-full text-xs font-bold mr-3 ${
|
||||||
isSubmitted && option.id === selectedId ? 'bg-red-200 text-red-800' :
|
isSubmitted && option.isCorrect
|
||||||
selectedId === option.id ? 'bg-indigo-600 text-white' : 'bg-slate-100 text-slate-500'
|
? "bg-green-200 text-green-800"
|
||||||
}`}>
|
: isSubmitted && option.id === selectedId
|
||||||
|
? "bg-red-200 text-red-800"
|
||||||
|
: selectedId === option.id
|
||||||
|
? "bg-indigo-600 text-white"
|
||||||
|
: "bg-slate-100 text-slate-500"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{option.id}
|
{option.id}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-slate-700 group-hover:text-slate-900">{option.text}</span>
|
<span className="text-slate-700 group-hover:text-slate-900">
|
||||||
|
{option.text}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{icon}
|
{icon}
|
||||||
</button>
|
</button>
|
||||||
@ -85,21 +97,33 @@ const Quiz: React.FC<QuizProps> = ({ data, onComplete }) => {
|
|||||||
|
|
||||||
{/* Feedback Section */}
|
{/* Feedback Section */}
|
||||||
{isSubmitted && (
|
{isSubmitted && (
|
||||||
<div className={`p-6 border-t ${isCorrect ? 'bg-green-50 border-green-100' : 'bg-slate-50 border-slate-100'}`}>
|
<div
|
||||||
|
className={`p-6 border-t ${isCorrect ? "bg-green-50 border-green-100" : "bg-slate-50 border-slate-100"}`}
|
||||||
|
>
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<div className={`mt-1 p-1 rounded-full ${isCorrect ? 'bg-green-200' : 'bg-slate-200'}`}>
|
<div
|
||||||
{isCorrect ? <CheckCircle2 className="w-4 h-4 text-green-700" /> : <div className="w-4 h-4 text-slate-500 font-bold text-center leading-4">i</div>}
|
className={`mt-1 p-1 rounded-full ${isCorrect ? "bg-green-200" : "bg-slate-200"}`}
|
||||||
</div>
|
>
|
||||||
<div>
|
{isCorrect ? (
|
||||||
<p className={`font-bold ${isCorrect ? 'text-green-800' : 'text-slate-800'} mb-1`}>
|
<CheckCircle2 className="w-4 h-4 text-green-700" />
|
||||||
{isCorrect ? "That's right!" : "Not quite."}
|
) : (
|
||||||
</p>
|
<div className="w-4 h-4 text-slate-500 font-bold text-center leading-4">
|
||||||
<p className="text-slate-600 mb-2">{selectedOption?.feedback}</p>
|
i
|
||||||
<div className="text-sm text-slate-500 bg-white p-3 rounded border border-slate-200">
|
</div>
|
||||||
<span className="font-semibold block mb-1">Explanation:</span>
|
)}
|
||||||
{data.explanation}
|
</div>
|
||||||
</div>
|
<div>
|
||||||
</div>
|
<p
|
||||||
|
className={`font-bold ${isCorrect ? "text-green-800" : "text-slate-800"} mb-1`}
|
||||||
|
>
|
||||||
|
{isCorrect ? "That's right!" : "Not quite."}
|
||||||
|
</p>
|
||||||
|
<p className="text-slate-600 mb-2">{selectedOption?.feedback}</p>
|
||||||
|
<div className="text-sm text-slate-500 bg-white p-3 rounded border border-slate-200">
|
||||||
|
<span className="font-semibold block mb-1">Explanation:</span>
|
||||||
|
{data.explanation}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -111,8 +135,8 @@ const Quiz: React.FC<QuizProps> = ({ data, onComplete }) => {
|
|||||||
disabled={!selectedId}
|
disabled={!selectedId}
|
||||||
className={`px-6 py-2 rounded-full font-semibold transition-all flex items-center ${
|
className={`px-6 py-2 rounded-full font-semibold transition-all flex items-center ${
|
||||||
selectedId
|
selectedId
|
||||||
? 'bg-slate-900 text-white hover:bg-slate-800 shadow-md transform hover:-translate-y-0.5'
|
? "bg-slate-900 text-white hover:bg-slate-800 shadow-md transform hover:-translate-y-0.5"
|
||||||
: 'bg-slate-200 text-slate-400 cursor-not-allowed'
|
: "bg-slate-200 text-slate-400 cursor-not-allowed"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Check Answer <ChevronRight className="w-4 h-4 ml-1" />
|
Check Answer <ChevronRight className="w-4 h-4 ml-1" />
|
||||||
|
|||||||
@ -244,14 +244,7 @@ function Sidebar({
|
|||||||
<div
|
<div
|
||||||
data-sidebar="sidebar"
|
data-sidebar="sidebar"
|
||||||
data-slot="sidebar-inner"
|
data-slot="sidebar-inner"
|
||||||
className={cn(
|
className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
|
||||||
"flex h-full w-full flex-col",
|
|
||||||
// For the custom floating pill sidebar we render our own card,
|
|
||||||
// so keep this container visually transparent.
|
|
||||||
variant === "floating"
|
|
||||||
? "bg-transparent"
|
|
||||||
: "bg-sidebar group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -22,8 +22,6 @@ const DOTS = [
|
|||||||
const STYLES = `
|
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');
|
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;800;900&family=Nunito+Sans:wght@400;600;700&display=swap');
|
||||||
|
|
||||||
:root { --content-max: 1100px; }
|
|
||||||
|
|
||||||
.home-screen {
|
.home-screen {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: #fffbf4;
|
background: #fffbf4;
|
||||||
@ -151,7 +149,7 @@ const STYLES = `
|
|||||||
}
|
}
|
||||||
.h-tab-btn.active { color:#1e1b4b; border-bottom-color:#a855f7; }
|
.h-tab-btn.active { color:#1e1b4b; border-bottom-color:#a855f7; }
|
||||||
|
|
||||||
/* ── Practice sheet ── */
|
/* ── Practice sheet card ── */
|
||||||
.h-sheet-grid {
|
.h-sheet-grid {
|
||||||
display:grid; gap:0.85rem;
|
display:grid; gap:0.85rem;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
@ -243,18 +241,6 @@ const STYLES = `
|
|||||||
.h-anim-3 { animation-delay:0.15s; }
|
.h-anim-3 { animation-delay:0.15s; }
|
||||||
.h-anim-4 { animation-delay:0.2s; }
|
.h-anim-4 { animation-delay:0.2s; }
|
||||||
.h-anim-5 { animation-delay:0.25s; }
|
.h-anim-5 { animation-delay:0.25s; }
|
||||||
|
|
||||||
/* Desktop / wide tweaks */
|
|
||||||
@media (min-width: 900px) {
|
|
||||||
.home-inner { max-width: var(--content-max); padding: 3rem 1.5rem 6rem; }
|
|
||||||
.h-sheet-grid { grid-template-columns: repeat(3, 1fr); gap: 1rem; }
|
|
||||||
|
|
||||||
/* nudge blobs so they align visually with the centered container */
|
|
||||||
.h-blob-1 { left: calc((100vw - var(--content-max)) / 2 - 120px); top: -120px; width: 300px; height: 300px; }
|
|
||||||
.h-blob-2 { left: calc((100vw - var(--content-max)) / 2 + 20px); bottom: -80px; width: 220px; height: 220px; }
|
|
||||||
.h-blob-3 { right: calc((100vw - var(--content-max)) / 2 - 40px); top: 10%; width: 260px; height: 260px; }
|
|
||||||
.h-blob-4 { right: calc((100vw - var(--content-max)) / 2 + 10px); bottom: 6%; width: 180px; height: 180px; }
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// ─── Sheet card ───────────────────────────────────────────────────────────────
|
// ─── Sheet card ───────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@ -33,10 +33,6 @@ const DOTS = [
|
|||||||
const STYLES = `
|
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');
|
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;800;900&family=Nunito+Sans:wght@400;600;700&display=swap');
|
||||||
|
|
||||||
:root { --content-max: 1100px; }
|
|
||||||
|
|
||||||
:root { --content-max: 1100px; }
|
|
||||||
|
|
||||||
.ls-screen {
|
.ls-screen {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: #fffbf4;
|
background: #fffbf4;
|
||||||
|
|||||||
@ -24,8 +24,6 @@ const DOTS = [
|
|||||||
const STYLES = `
|
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');
|
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;800;900&family=Nunito+Sans:wght@400;600;700&display=swap');
|
||||||
|
|
||||||
:root { --content-max: 1100px; }
|
|
||||||
|
|
||||||
.pr-screen {
|
.pr-screen {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: #fffbf4;
|
background: #fffbf4;
|
||||||
@ -65,20 +63,6 @@ const STYLES = `
|
|||||||
display: flex; flex-direction: column; gap: 1.5rem;
|
display: flex; flex-direction: column; gap: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Desktop / wide layout */
|
|
||||||
@media (min-width: 900px) {
|
|
||||||
.pr-inner { max-width: var(--content-max); padding: 3rem 1.5rem 6rem; }
|
|
||||||
.pr-grid { grid-template-columns: repeat(3, 1fr); gap: 1rem; }
|
|
||||||
|
|
||||||
.pr-blob-1 { left: calc((100vw - var(--content-max)) / 2 - 120px); top: -120px; width: 300px; height: 300px; }
|
|
||||||
.pr-blob-2 { left: calc((100vw - var(--content-max)) / 2 + 20px); bottom: -80px; width: 220px; height: 220px; }
|
|
||||||
.pr-blob-3 { right: calc((100vw - var(--content-max)) / 2 - 40px); top: 10%; width: 260px; height: 260px; }
|
|
||||||
.pr-blob-4 { right: calc((100vw - var(--content-max)) / 2 + 10px); bottom: 6%; width: 180px; height: 180px; }
|
|
||||||
|
|
||||||
.pr-hero { padding: 2rem; }
|
|
||||||
.pr-hero-icon-bg { right: -60px; top: -40px; opacity: 0.12; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Animations ── */
|
/* ── Animations ── */
|
||||||
@keyframes prPopIn {
|
@keyframes prPopIn {
|
||||||
from { opacity:0; transform: scale(0.92) translateY(12px); }
|
from { opacity:0; transform: scale(0.92) translateY(12px); }
|
||||||
|
|||||||
@ -19,8 +19,6 @@ const DOTS = [
|
|||||||
const STYLES = `
|
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');
|
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;800;900&family=Nunito+Sans:wght@400;600;700&display=swap');
|
||||||
|
|
||||||
:root { --content-max: 1100px; }
|
|
||||||
|
|
||||||
.pf-screen {
|
.pf-screen {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: #fffbf4;
|
background: #fffbf4;
|
||||||
@ -57,34 +55,6 @@ const STYLES = `
|
|||||||
display: flex; flex-direction: column; gap: 1.5rem;
|
display: flex; flex-direction: column; gap: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Desktop / web layout: wider container and two-column grid */
|
|
||||||
@media (min-width: 900px) {
|
|
||||||
.pf-inner {
|
|
||||||
max-width: var(--content-max);
|
|
||||||
padding: 2.5rem 2.5rem 4rem;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 420px;
|
|
||||||
grid-template-rows: auto;
|
|
||||||
gap: 1.5rem 2rem;
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hero spans full width */
|
|
||||||
.pf-hero { grid-column: 1 / -1; display: flex; align-items: center; gap: 1.5rem; }
|
|
||||||
|
|
||||||
/* Keep page title centered across the full layout */
|
|
||||||
.pf-page-title { grid-column: 1 / -1; }
|
|
||||||
|
|
||||||
/* Place first section (Account) in left column */
|
|
||||||
.pf-inner > section:nth-of-type(1) { grid-column: 1 / 2; }
|
|
||||||
|
|
||||||
/* Right column wrapper (Legal + Support) */
|
|
||||||
.pf-right-col { grid-column: 2 / 3; display: flex; flex-direction: column; gap: 1.5rem; }
|
|
||||||
|
|
||||||
/* Make signout button centered and constrained width */
|
|
||||||
.pf-signout-btn { grid-column: 1 / -1; justify-self: center; width: 420px; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pfPopIn {
|
@keyframes pfPopIn {
|
||||||
from { opacity:0; transform: scale(0.92) translateY(12px); }
|
from { opacity:0; transform: scale(0.92) translateY(12px); }
|
||||||
to { opacity:1; transform: scale(1) translateY(0); }
|
to { opacity:1; transform: scale(1) translateY(0); }
|
||||||
@ -337,18 +307,17 @@ export const Profile = () => {
|
|||||||
<SettingsGroup rows={ACCOUNT_ROWS} />
|
<SettingsGroup rows={ACCOUNT_ROWS} />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Right column: Legal + Support (stacked) */}
|
{/* Legal */}
|
||||||
<div className="pf-right-col pf-anim pf-anim-3">
|
<section className="pf-section pf-anim pf-anim-3">
|
||||||
<section className="pf-section">
|
<p className="pf-section-label">Legal</p>
|
||||||
<p className="pf-section-label">Legal</p>
|
<SettingsGroup rows={LEGAL_ROWS} />
|
||||||
<SettingsGroup rows={LEGAL_ROWS} />
|
</section>
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="pf-section">
|
{/* Support */}
|
||||||
<p className="pf-section-label">Support</p>
|
<section className="pf-section pf-anim pf-anim-4">
|
||||||
<SettingsGroup rows={SUPPORT_ROWS} />
|
<p className="pf-section-label">Support</p>
|
||||||
</section>
|
<SettingsGroup rows={SUPPORT_ROWS} />
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
{/* Sign out */}
|
{/* Sign out */}
|
||||||
<button
|
<button
|
||||||
|
|||||||
@ -33,8 +33,6 @@ const DOTS = [
|
|||||||
const STYLES = `
|
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');
|
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;800;900&family=Nunito+Sans:wght@400;600;700&display=swap');
|
||||||
|
|
||||||
:root { --content-max: 1100px; }
|
|
||||||
|
|
||||||
.rw-screen {
|
.rw-screen {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
background: #fffbf4;
|
background: #fffbf4;
|
||||||
@ -68,6 +66,7 @@ const STYLES = `
|
|||||||
|
|
||||||
.rw-sticky-top {
|
.rw-sticky-top {
|
||||||
position:relative;z-index:2;
|
position:relative;z-index:2;
|
||||||
|
background:#fffbf4;
|
||||||
flex-shrink:0;
|
flex-shrink:0;
|
||||||
padding:2rem 1.25rem 0;
|
padding:2rem 1.25rem 0;
|
||||||
}
|
}
|
||||||
@ -86,26 +85,6 @@ const STYLES = `
|
|||||||
}
|
}
|
||||||
.rw-scroll-inner { max-width:580px;margin:0 auto; }
|
.rw-scroll-inner { max-width:580px;margin:0 auto; }
|
||||||
|
|
||||||
/* Desktop: wider centered layout */
|
|
||||||
@media (min-width: 900px) {
|
|
||||||
.rw-sticky-top-inner { max-width: var(--content-max); padding: 2rem 2rem 1.25rem; }
|
|
||||||
.rw-scroll-inner { max-width: var(--content-max); }
|
|
||||||
.rw-scroll-area { padding: 1.5rem 2.5rem 10rem; }
|
|
||||||
|
|
||||||
/* Make empty state sit visually centered within larger canvas */
|
|
||||||
.rw-empty { padding: 5rem 1rem; }
|
|
||||||
|
|
||||||
/* Slightly larger island pill on wide screens and rebalance blobs */
|
|
||||||
.rw-island-wrap { max-width: 420px; left:auto; right:calc((100vw - 256px - var(--content-max)) / 2); top:240px; bottom:auto; transform:none; margin-left:150px; }
|
|
||||||
.rw-island-card { gap: 0.75rem; }
|
|
||||||
|
|
||||||
/* Rebalance decorative blobs on wide screens */
|
|
||||||
.rw-blob-1 { left: -120px; top: -100px; width:300px; height:300px; }
|
|
||||||
.rw-blob-2 { left: 6%; bottom: -40px; }
|
|
||||||
.rw-blob-3 { right: -100px; top: 8%; width:260px; height:260px; }
|
|
||||||
.rw-blob-4 { right: 2%; bottom: 6%; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes rwPopIn {
|
@keyframes rwPopIn {
|
||||||
from{opacity:0;transform:scale(0.92) translateY(12px);}
|
from{opacity:0;transform:scale(0.92) translateY(12px);}
|
||||||
to{opacity:1;transform:scale(1) translateY(0);}
|
to{opacity:1;transform:scale(1) translateY(0);}
|
||||||
@ -198,9 +177,8 @@ const STYLES = `
|
|||||||
flex-direction:column;
|
flex-direction:column;
|
||||||
align-items:center;
|
align-items:center;
|
||||||
gap:0.5rem;
|
gap:0.5rem;
|
||||||
width:auto;
|
width:calc(100% - 2rem);
|
||||||
max-width:300px;
|
max-width:300px;
|
||||||
top:auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.rw-island-card {
|
.rw-island-card {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Outlet, NavLink, useLocation } from "react-router-dom";
|
import { Outlet, NavLink, useLocation } from "react-router-dom";
|
||||||
import { Home, BookOpen, Award, User, Video, Map } from "lucide-react";
|
import { Home, BookOpen, Award, User, Video, Map } from "lucide-react";
|
||||||
import { SidebarProvider } from "../../components/ui/sidebar";
|
import { SidebarProvider, SidebarTrigger } from "../../components/ui/sidebar";
|
||||||
import { AppSidebar } from "../../components/AppSidebar";
|
import { AppSidebar } from "../../components/AppSidebar";
|
||||||
|
|
||||||
const NAV_ITEMS = [
|
const NAV_ITEMS = [
|
||||||
@ -174,11 +174,6 @@ const STYLES = `
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure the dock is hidden on desktop (md and up) */
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.sl-dock-wrap { display: none !important; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Quest mode: active label uses Cinzel for the pirate feel */
|
/* Quest mode: active label uses Cinzel for the pirate feel */
|
||||||
.quest-mode .sl-dock-item.active .sl-dock-label {
|
.quest-mode .sl-dock-item.active .sl-dock-label {
|
||||||
font-family: 'Sorts Mill Goudy', serif;
|
font-family: 'Sorts Mill Goudy', serif;
|
||||||
@ -195,11 +190,6 @@ const STYLES = `
|
|||||||
.quest-mode .sl-dock-item:not(.active):hover .sl-dock-icon {
|
.quest-mode .sl-dock-item:not(.active):hover .sl-dock-icon {
|
||||||
opacity: 0.85;
|
opacity: 0.85;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure the dock is hidden on desktop (md and up) */
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.sl-dock-wrap { display: none !important; }
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export function StudentLayout() {
|
export function StudentLayout() {
|
||||||
@ -214,13 +204,11 @@ export function StudentLayout() {
|
|||||||
<style>{STYLES}</style>
|
<style>{STYLES}</style>
|
||||||
<div className="flex min-h-screen w-full overflow-x-hidden">
|
<div className="flex min-h-screen w-full overflow-x-hidden">
|
||||||
{/* Desktop Sidebar */}
|
{/* Desktop Sidebar */}
|
||||||
<div className="hidden md:block">
|
<AppSidebar />
|
||||||
<AppSidebar />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col flex-1 min-w-0">
|
<div className="flex flex-col flex-1 min-w-0">
|
||||||
{/* Extra bottom padding so content clears the floating dock */}
|
<SidebarTrigger className="hidden md:block" />
|
||||||
<main className="flex-1 pb-24 md:pb-0">
|
<main className="flex-1 md:pb-0">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -24,8 +24,6 @@ const DOTS = [
|
|||||||
const STYLES = `
|
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');
|
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;800;900&family=Nunito+Sans:wght@400;600;700&display=swap');
|
||||||
|
|
||||||
:root { --content-max: 1100px; }
|
|
||||||
|
|
||||||
.dr-screen {
|
.dr-screen {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: #fffbf4;
|
background: #fffbf4;
|
||||||
@ -215,31 +213,14 @@ const STYLES = `
|
|||||||
|
|
||||||
/* CTA bar */
|
/* CTA bar */
|
||||||
.dr-cta-bar {
|
.dr-cta-bar {
|
||||||
position: fixed;
|
position:fixed;bottom:96px;left:0;right:0;z-index:10;
|
||||||
bottom: 96px;
|
padding:0.85rem 1.25rem calc(0.85rem + env(safe-area-inset-bottom));
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 5;
|
|
||||||
padding: 0.85rem 1.25rem calc(0.85rem + env(safe-area-inset-bottom));
|
|
||||||
}
|
}
|
||||||
.dr-cta-inner {
|
.dr-cta-inner {
|
||||||
max-width: 560px;
|
max-width:560px;margin:0 auto;
|
||||||
margin: 0 auto;
|
display:flex;gap:0.75rem;align-items:center;
|
||||||
display: flex;
|
|
||||||
gap: 0.75rem;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 900px) {
|
|
||||||
.dr-inner { max-width: var(--content-max); padding: 3rem 1.5rem 10rem; }
|
|
||||||
.dr-topic-grid { grid-template-columns: repeat(3, 1fr); gap: 0.75rem; }
|
|
||||||
.dr-cta-bar { left: var(--sidebar-width); right: 0; }
|
|
||||||
|
|
||||||
/* Align decorative blobs relative to the centered content container */
|
|
||||||
.dr-blob-3 { right: calc((100vw - var(--content-max)) / 2 - 48px); }
|
|
||||||
.dr-blob-1 { left: calc((100vw - var(--content-max)) / 2 - 56px); }
|
|
||||||
.dr-blob-2 { left: calc((100vw - var(--content-max)) / 2 + 12px); }
|
|
||||||
.dr-blob-4 { right: calc((100vw - var(--content-max)) / 2 + 12px); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dr-next-btn {
|
.dr-next-btn {
|
||||||
|
|||||||
@ -26,8 +26,6 @@ const DOTS = [
|
|||||||
const STYLES = `
|
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');
|
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;800;900&family=Nunito+Sans:wght@400;600;700&display=swap');
|
||||||
|
|
||||||
:root { --content-max: 1100px; }
|
|
||||||
|
|
||||||
.htm-screen {
|
.htm-screen {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: #fffbf4;
|
background: #fffbf4;
|
||||||
@ -181,35 +179,16 @@ const STYLES = `
|
|||||||
|
|
||||||
/* CTA bar */
|
/* CTA bar */
|
||||||
.htm-cta-bar {
|
.htm-cta-bar {
|
||||||
position: fixed;
|
position: fixed; bottom: 96px; left: 0; right: 0; z-index: 10;
|
||||||
bottom: 96px;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 10;
|
|
||||||
padding: 0.85rem 1.25rem calc(0.85rem + env(safe-area-inset-bottom));
|
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;
|
transition: transform 0.3s cubic-bezier(0.34,1.56,0.64,1), opacity 0.25s ease;
|
||||||
}
|
}
|
||||||
.htm-cta-bar.hidden {
|
.htm-cta-bar.hidden {
|
||||||
transform: translateY(100%); opacity: 0; pointer-events: none;
|
transform: translateY(100%); opacity: 0; pointer-events: none;
|
||||||
}
|
}
|
||||||
.htm-cta-inner {
|
.htm-cta-inner {
|
||||||
max-width: 560px;
|
max-width: 560px; margin: 0 auto;
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 900px) {
|
|
||||||
.htm-inner { max-width: var(--content-max); padding: 3rem 1.5rem 10rem; }
|
|
||||||
.htm-cta-bar { left: var(--sidebar-width); right: 0; }
|
|
||||||
.htm-cta-inner { max-width: var(--content-max); margin: 0 auto; }
|
|
||||||
|
|
||||||
/* align blobs to centered content */
|
|
||||||
.htm-blob-3 { right: calc((100vw - var(--content-max)) / 2 - 48px); }
|
|
||||||
.htm-blob-1 { left: calc((100vw - var(--content-max)) / 2 - 56px); }
|
|
||||||
.htm-blob-2 { left: calc((100vw - var(--content-max)) / 2 + 12px); }
|
|
||||||
.htm-blob-4 { right: calc((100vw - var(--content-max)) / 2 + 12px); }
|
|
||||||
|
|
||||||
/* make module cards slightly wider on desktop */
|
|
||||||
.htm-card { min-height: 220px; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.htm-start-btn {
|
.htm-start-btn {
|
||||||
|
|||||||
@ -25,8 +25,6 @@ const DOTS = [
|
|||||||
const STYLES = `
|
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');
|
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;800;900&family=Nunito+Sans:wght@400;600;700&display=swap');
|
||||||
|
|
||||||
:root { --content-max: 1100px; }
|
|
||||||
|
|
||||||
.pt-screen {
|
.pt-screen {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: #fffbf4;
|
background: #fffbf4;
|
||||||
@ -66,17 +64,6 @@ const STYLES = `
|
|||||||
display: flex; flex-direction: column; gap: 1.25rem;
|
display: flex; flex-direction: column; gap: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Desktop / wide layout */
|
|
||||||
@media (min-width: 900px) {
|
|
||||||
.pt-inner { max-width: var(--content-max); padding: 3rem 1.5rem 6rem; }
|
|
||||||
.pt-stats-row { grid-template-columns: repeat(3, 1fr); }
|
|
||||||
|
|
||||||
.pt-blob-1 { left: calc((100vw - var(--content-max)) / 2 - 120px); top: -120px; width: 300px; height: 300px; }
|
|
||||||
.pt-blob-2 { left: calc((100vw - var(--content-max)) / 2 + 20px); bottom: -80px; width: 220px; height: 220px; }
|
|
||||||
.pt-blob-3 { right: calc((100vw - var(--content-max)) / 2 - 40px); top: 10%; width: 260px; height: 260px; }
|
|
||||||
.pt-blob-4 { right: calc((100vw - var(--content-max)) / 2 + 10px); bottom: 6%; width: 180px; height: 180px; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Pop-in animation ── */
|
/* ── Pop-in animation ── */
|
||||||
@keyframes ptPopIn {
|
@keyframes ptPopIn {
|
||||||
from { opacity:0; transform: scale(0.92) translateY(12px); }
|
from { opacity:0; transform: scale(0.92) translateY(12px); }
|
||||||
|
|||||||
@ -9,8 +9,6 @@ import { useExamConfigStore } from "../../../stores/useExamConfigStore";
|
|||||||
const STYLES = `
|
const STYLES = `
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;800;900&family=Nunito+Sans:wght@400;600&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;800;900&family=Nunito+Sans:wght@400;600&display=swap');
|
||||||
|
|
||||||
:root { --content-max: 1100px; }
|
|
||||||
|
|
||||||
.results-screen {
|
.results-screen {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: #fffbf4;
|
background: #fffbf4;
|
||||||
@ -74,17 +72,6 @@ const STYLES = `
|
|||||||
display: flex; flex-direction: column; gap: 1rem;
|
display: flex; flex-direction: column; gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Desktop / wide layout */
|
|
||||||
@media (min-width: 900px) {
|
|
||||||
.results-inner { max-width: var(--content-max); padding: 3rem 1.5rem 4rem; }
|
|
||||||
.stats-grid { grid-template-columns: repeat(4, 1fr); }
|
|
||||||
|
|
||||||
.r-blob-1 { left: calc((100vw - var(--content-max)) / 2 - 120px); top: -120px; width: 300px; height: 300px; }
|
|
||||||
.r-blob-2 { left: calc((100vw - var(--content-max)) / 2 + 20px); bottom: -80px; width: 220px; height: 220px; }
|
|
||||||
.r-blob-3 { right: calc((100vw - var(--content-max)) / 2 - 40px); top: 10%; width: 260px; height: 260px; }
|
|
||||||
.r-blob-4 { right: calc((100vw - var(--content-max)) / 2 + 10px); bottom: 6%; width: 180px; height: 180px; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Header ── */
|
/* ── Header ── */
|
||||||
.results-header {
|
.results-header {
|
||||||
display: flex; align-items: center; gap: 1rem;
|
display: flex; align-items: center; gap: 1rem;
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { useEffect, useState, useRef } from "react";
|
import { useEffect, useState, useRef } from "react";
|
||||||
import { Navigate, useNavigate } from "react-router-dom";
|
import { Navigate, useNavigate } from "react-router-dom";
|
||||||
import { BlockMath, InlineMath } from "react-katex";
|
|
||||||
import {
|
import {
|
||||||
Binary,
|
Binary,
|
||||||
Calculator,
|
Calculator,
|
||||||
@ -17,9 +16,6 @@ import {
|
|||||||
BookOpen,
|
BookOpen,
|
||||||
ZoomIn,
|
ZoomIn,
|
||||||
ZoomOut,
|
ZoomOut,
|
||||||
Eye,
|
|
||||||
EyeOff,
|
|
||||||
Highlighter,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
import { api } from "../../../utils/api";
|
import { api } from "../../../utils/api";
|
||||||
@ -89,8 +85,6 @@ const DOTS = [
|
|||||||
const 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');
|
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;800;900&family=Nunito+Sans:wght@400;600;700&display=swap');
|
||||||
|
|
||||||
:root { --content-max: 1100px; }
|
|
||||||
|
|
||||||
.test-screen {
|
.test-screen {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: ${COLORS.bg};
|
background: ${COLORS.bg};
|
||||||
@ -251,115 +245,6 @@ const GLOBAL_STYLES = `
|
|||||||
/* Incorrect flash */
|
/* Incorrect flash */
|
||||||
@keyframes tFlashRed { 0%,100%{background:transparent;}50%{background:rgba(239,68,68,0.15);} }
|
@keyframes tFlashRed { 0%,100%{background:transparent;}50%{background:rgba(239,68,68,0.15);} }
|
||||||
.t-flash-red { animation:tFlashRed 0.6s ease; }
|
.t-flash-red { animation:tFlashRed 0.6s ease; }
|
||||||
|
|
||||||
/* Highlighting */
|
|
||||||
.t-highlight {
|
|
||||||
background: rgba(251, 191, 36, 0.42);
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 0 2px;
|
|
||||||
box-shadow: inset 0 -1px 0 rgba(217, 119, 6, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Desktop / wide tweaks */
|
|
||||||
@media (min-width: 900px) {
|
|
||||||
.t-blob-1 { left: calc((100vw - var(--content-max)) / 2 - 120px); top: -120px; width: 300px; height: 300px; }
|
|
||||||
.t-blob-2 { left: calc((100vw - var(--content-max)) / 2 + 20px); bottom: -80px; width: 220px; height: 220px; }
|
|
||||||
.t-blob-3 { right: calc((100vw - var(--content-max)) / 2 - 40px); top: 10%; width: 260px; height: 260px; }
|
|
||||||
.t-blob-4 { right: calc((100vw - var(--content-max)) / 2 + 10px); bottom: 6%; width: 180px; height: 180px; }
|
|
||||||
|
|
||||||
/* Desktop split layout */
|
|
||||||
.t-desktop-shell {
|
|
||||||
max-width: var(--content-max);
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
.t-split-layout {
|
|
||||||
display: flex;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
.t-split-left {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1.5rem;
|
|
||||||
}
|
|
||||||
.t-split-right {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1.5rem;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
.t-split-divider {
|
|
||||||
position: relative;
|
|
||||||
width: 10px;
|
|
||||||
cursor: col-resize;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.t-split-divider-bar {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 50%;
|
|
||||||
width: 3px;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
border-radius: 999px;
|
|
||||||
background: #e5e7eb;
|
|
||||||
}
|
|
||||||
.t-split-divider-handle {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
width: 26px;
|
|
||||||
height: 26px;
|
|
||||||
border-radius: 999px;
|
|
||||||
border: 2px solid #e5e7eb;
|
|
||||||
background: white;
|
|
||||||
box-shadow: 0 4px 12px rgba(15,23,42,0.12);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.t-split-divider-dots {
|
|
||||||
width: 3px;
|
|
||||||
height: 14px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: linear-gradient(to bottom, #c4b5fd, #a855f7);
|
|
||||||
}
|
|
||||||
.t-split-divider:hover .t-split-divider-bar {
|
|
||||||
background: #c4b5fd;
|
|
||||||
}
|
|
||||||
.t-split-divider:hover .t-split-divider-handle {
|
|
||||||
border-color: #c4b5fd;
|
|
||||||
box-shadow: 0 6px 18px rgba(129,140,248,0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Directions button (desktop only) */
|
|
||||||
.t-directions-btn {
|
|
||||||
font-weight: 900;
|
|
||||||
font-size: 0.78rem;
|
|
||||||
padding: 0.35rem 0.9rem;
|
|
||||||
border-radius: 999px;
|
|
||||||
border: 2.5px solid ${COLORS.border};
|
|
||||||
background: white;
|
|
||||||
color: ${COLORS.text};
|
|
||||||
box-shadow: 0 3px 10px rgba(0,0,0,0.06);
|
|
||||||
}
|
|
||||||
.t-directions-btn:hover {
|
|
||||||
border-color: ${COLORS.borderPurple};
|
|
||||||
background: #fdf4ff;
|
|
||||||
box-shadow: 0 6px 14px rgba(0,0,0,0.08);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
.t-directions-btn.active {
|
|
||||||
background: linear-gradient(135deg, ${COLORS.primary}, ${COLORS.primaryDark});
|
|
||||||
border-color: ${COLORS.primaryDark};
|
|
||||||
color: white;
|
|
||||||
box-shadow: 0 4px 0 ${COLORS.primaryDark}, 0 8px 18px rgba(168,85,247,0.28);
|
|
||||||
}
|
|
||||||
.t-directions-btn.active:hover {
|
|
||||||
box-shadow: 0 6px 0 ${COLORS.primaryDark}, 0 12px 22px rgba(168,85,247,0.32);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// ─── Confetti ─────────────────────────────────────────────────────────────────
|
// ─── Confetti ─────────────────────────────────────────────────────────────────
|
||||||
@ -384,9 +269,6 @@ interface IncorrectEntry {
|
|||||||
originalIndex: number;
|
originalIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
type HighlightRange = { start: number; end: number };
|
|
||||||
type HighlightsByField = Record<string, HighlightRange[]>;
|
|
||||||
|
|
||||||
const Confetti = ({ active }: { active: boolean }) => {
|
const Confetti = ({ active }: { active: boolean }) => {
|
||||||
const [particles, setParticles] = useState<ConfettiParticle[]>([]);
|
const [particles, setParticles] = useState<ConfettiParticle[]>([]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -745,19 +627,6 @@ export const Test = () => {
|
|||||||
const [menuOpen, setMenuOpen] = useState(false);
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
const [feedback, setFeedback] = useState<FeedbackState>(null);
|
const [feedback, setFeedback] = useState<FeedbackState>(null);
|
||||||
const [showConfetti, setShowConfetti] = useState(false);
|
const [showConfetti, setShowConfetti] = useState(false);
|
||||||
const [showDirections, setShowDirections] = useState(false);
|
|
||||||
const [timerVisible, setTimerVisible] = useState(true);
|
|
||||||
const [leftColumnWidth, setLeftColumnWidth] = useState(350); // desktop only (re-centered on mount)
|
|
||||||
const dragRef = useRef<{ startX: number; startWidth: number } | null>(null);
|
|
||||||
const [isDraggingDivider, setIsDraggingDivider] = useState(false);
|
|
||||||
const userAdjustedSplitRef = useRef(false);
|
|
||||||
const desktopShellRef = useRef<HTMLDivElement | null>(null);
|
|
||||||
const [isMdUp, setIsMdUp] = useState(false);
|
|
||||||
const HIGHLIGHTS_STORAGE_KEY = "edbridge_highlights_v1";
|
|
||||||
const [highlightMode, setHighlightMode] = useState(false);
|
|
||||||
const [highlightsByField, setHighlightsByField] = useState<HighlightsByField>(
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
const [showIncorrectFlash, setShowIncorrectFlash] = useState(false);
|
const [showIncorrectFlash, setShowIncorrectFlash] = useState(false);
|
||||||
|
|
||||||
// ── Retry / targeted state ──────────────────────────────────────────────────
|
// ── Retry / targeted state ──────────────────────────────────────────────────
|
||||||
@ -786,111 +655,6 @@ export const Test = () => {
|
|||||||
? currentModule?.questions[retryQueue[retryIndex]?.originalIndex]
|
? currentModule?.questions[retryQueue[retryIndex]?.originalIndex]
|
||||||
: currentModule?.questions[questionIndex];
|
: currentModule?.questions[questionIndex];
|
||||||
|
|
||||||
// close directions when question changes
|
|
||||||
useEffect(() => {
|
|
||||||
setShowDirections(false);
|
|
||||||
}, [currentQuestion?.id]);
|
|
||||||
|
|
||||||
// Handle separator drag (desktop only)
|
|
||||||
const handleSeparatorMouseDown = (e: React.MouseEvent) => {
|
|
||||||
if (e.button !== 0) return;
|
|
||||||
e.preventDefault(); // prevent text selection kick-off
|
|
||||||
userAdjustedSplitRef.current = true;
|
|
||||||
dragRef.current = {
|
|
||||||
startX: e.clientX,
|
|
||||||
startWidth: leftColumnWidth,
|
|
||||||
};
|
|
||||||
setIsDraggingDivider(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Center the splitter on initial desktop load (and on resize until user drags)
|
|
||||||
useEffect(() => {
|
|
||||||
const centerSplit = () => {
|
|
||||||
if (userAdjustedSplitRef.current) return;
|
|
||||||
if (typeof window === "undefined") return;
|
|
||||||
if (window.innerWidth < 768) return; // md breakpoint
|
|
||||||
const shell = desktopShellRef.current;
|
|
||||||
if (!shell) return;
|
|
||||||
const w = shell.getBoundingClientRect().width;
|
|
||||||
// Account for divider+margin so the handle lands near visual center.
|
|
||||||
const target = Math.round((w - 40) / 2);
|
|
||||||
setLeftColumnWidth(Math.max(300, Math.min(600, target)));
|
|
||||||
};
|
|
||||||
|
|
||||||
centerSplit();
|
|
||||||
window.addEventListener("resize", centerSplit);
|
|
||||||
return () => window.removeEventListener("resize", centerSplit);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// True responsive check (portals ignore parent display:none)
|
|
||||||
useEffect(() => {
|
|
||||||
if (typeof window === "undefined") return;
|
|
||||||
const mq = window.matchMedia("(min-width: 768px)");
|
|
||||||
const update = () => setIsMdUp(mq.matches);
|
|
||||||
update();
|
|
||||||
mq.addEventListener("change", update);
|
|
||||||
return () => mq.removeEventListener("change", update);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Highlights persistence (desktop/web feature; safe on mobile too)
|
|
||||||
useEffect(() => {
|
|
||||||
try {
|
|
||||||
const raw = localStorage.getItem(HIGHLIGHTS_STORAGE_KEY);
|
|
||||||
if (!raw) return;
|
|
||||||
const parsed = JSON.parse(raw) as HighlightsByField;
|
|
||||||
if (parsed && typeof parsed === "object") setHighlightsByField(parsed);
|
|
||||||
} catch {
|
|
||||||
// ignore corrupted storage
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
try {
|
|
||||||
localStorage.setItem(
|
|
||||||
HIGHLIGHTS_STORAGE_KEY,
|
|
||||||
JSON.stringify(highlightsByField),
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
// ignore quota / private mode issues
|
|
||||||
}
|
|
||||||
}, [HIGHLIGHTS_STORAGE_KEY, highlightsByField]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleMouseMove = (e: MouseEvent) => {
|
|
||||||
if (!dragRef.current) return;
|
|
||||||
const delta = e.clientX - dragRef.current.startX;
|
|
||||||
const newWidth = Math.max(
|
|
||||||
300,
|
|
||||||
Math.min(600, dragRef.current.startWidth + delta),
|
|
||||||
);
|
|
||||||
setLeftColumnWidth(newWidth);
|
|
||||||
};
|
|
||||||
const handleMouseUp = () => {
|
|
||||||
dragRef.current = null;
|
|
||||||
setIsDraggingDivider(false);
|
|
||||||
};
|
|
||||||
window.addEventListener("mousemove", handleMouseMove);
|
|
||||||
window.addEventListener("mouseup", handleMouseUp);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("mousemove", handleMouseMove);
|
|
||||||
window.removeEventListener("mouseup", handleMouseUp);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Prevent text selection while dragging divider
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isDraggingDivider) return;
|
|
||||||
const prevUserSelect = document.body.style.userSelect;
|
|
||||||
const prevCursor = document.body.style.cursor;
|
|
||||||
document.body.style.userSelect = "none";
|
|
||||||
document.body.style.cursor = "col-resize";
|
|
||||||
return () => {
|
|
||||||
document.body.style.userSelect = prevUserSelect;
|
|
||||||
document.body.style.cursor = prevCursor;
|
|
||||||
};
|
|
||||||
}, [isDraggingDivider]);
|
|
||||||
|
|
||||||
const currentAnswer = currentQuestion
|
const currentAnswer = currentQuestion
|
||||||
? (answers[currentQuestion.id] ?? "")
|
? (answers[currentQuestion.id] ?? "")
|
||||||
: "";
|
: "";
|
||||||
@ -1177,163 +941,6 @@ export const Test = () => {
|
|||||||
const formatTime = (s: number) =>
|
const formatTime = (s: number) =>
|
||||||
`${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;
|
`${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;
|
||||||
|
|
||||||
const mergeHighlightRanges = (ranges: HighlightRange[]) => {
|
|
||||||
const sorted = ranges
|
|
||||||
.filter((r) => Number.isFinite(r.start) && Number.isFinite(r.end))
|
|
||||||
.map((r) => ({ start: Math.max(0, r.start), end: Math.max(0, r.end) }))
|
|
||||||
.filter((r) => r.end > r.start)
|
|
||||||
.sort((a, b) => a.start - b.start);
|
|
||||||
|
|
||||||
const merged: HighlightRange[] = [];
|
|
||||||
for (const r of sorted) {
|
|
||||||
const last = merged[merged.length - 1];
|
|
||||||
if (!last || r.start > last.end) merged.push(r);
|
|
||||||
else last.end = Math.max(last.end, r.end);
|
|
||||||
}
|
|
||||||
return merged;
|
|
||||||
};
|
|
||||||
|
|
||||||
const addHighlight = (fieldKey: string, start: number, end: number) => {
|
|
||||||
const s = Math.min(start, end);
|
|
||||||
const e = Math.max(start, end);
|
|
||||||
if (!fieldKey || s === e) return;
|
|
||||||
|
|
||||||
setHighlightsByField((prev) => {
|
|
||||||
const cur = prev[fieldKey] ?? [];
|
|
||||||
const next = mergeHighlightRanges([...cur, { start: s, end: e }]);
|
|
||||||
return { ...prev, [fieldKey]: next };
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderQuestionTextWithHighlights = (
|
|
||||||
text: string,
|
|
||||||
highlights: HighlightRange[],
|
|
||||||
) => {
|
|
||||||
const merged = mergeHighlightRanges(highlights);
|
|
||||||
const parts = text.split(/(\$\$.*?\$\$|\$.*?\$)/g);
|
|
||||||
let pos = 0;
|
|
||||||
let pieceIdx = 0;
|
|
||||||
|
|
||||||
const renderPlain = (plain: string, basePos: number) => {
|
|
||||||
const segStart = basePos;
|
|
||||||
const segEnd = basePos + plain.length;
|
|
||||||
const overlapping = merged.filter(
|
|
||||||
(h) => h.end > segStart && h.start < segEnd,
|
|
||||||
);
|
|
||||||
if (!overlapping.length) {
|
|
||||||
const key = `p-${pieceIdx++}`;
|
|
||||||
return <span key={key}>{plain}</span>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nodes: React.ReactNode[] = [];
|
|
||||||
let cursor = 0;
|
|
||||||
for (const h of overlapping) {
|
|
||||||
const localStart = Math.max(0, h.start - segStart);
|
|
||||||
const localEnd = Math.min(plain.length, h.end - segStart);
|
|
||||||
if (localStart > cursor) {
|
|
||||||
nodes.push(
|
|
||||||
<span key={`t-${pieceIdx++}`}>
|
|
||||||
{plain.slice(cursor, localStart)}
|
|
||||||
</span>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
nodes.push(
|
|
||||||
<mark key={`m-${pieceIdx++}`} className="t-highlight">
|
|
||||||
{plain.slice(localStart, localEnd)}
|
|
||||||
</mark>,
|
|
||||||
);
|
|
||||||
cursor = Math.max(cursor, localEnd);
|
|
||||||
}
|
|
||||||
if (cursor < plain.length) {
|
|
||||||
nodes.push(<span key={`t-${pieceIdx++}`}>{plain.slice(cursor)}</span>);
|
|
||||||
}
|
|
||||||
return <span key={`w-${pieceIdx++}`}>{nodes}</span>;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{parts.map((part, index) => {
|
|
||||||
if (part.startsWith("$$")) {
|
|
||||||
const inner = part.slice(2, -2);
|
|
||||||
const len = inner.length;
|
|
||||||
const hasOverlap = merged.some(
|
|
||||||
(h) => h.end > pos && h.start < pos + len,
|
|
||||||
);
|
|
||||||
const node = <BlockMath key={index}>{inner}</BlockMath>;
|
|
||||||
pos += len;
|
|
||||||
if (!hasOverlap) return node;
|
|
||||||
return (
|
|
||||||
<span key={index} className="t-highlight">
|
|
||||||
{node}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (part.startsWith("$")) {
|
|
||||||
const inner = part.slice(1, -1);
|
|
||||||
const len = inner.length;
|
|
||||||
const hasOverlap = merged.some(
|
|
||||||
(h) => h.end > pos && h.start < pos + len,
|
|
||||||
);
|
|
||||||
const node = <InlineMath key={index}>{inner}</InlineMath>;
|
|
||||||
pos += len;
|
|
||||||
if (!hasOverlap) return node;
|
|
||||||
return (
|
|
||||||
<span key={index} className="t-highlight">
|
|
||||||
{node}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const base = pos;
|
|
||||||
pos += part.length;
|
|
||||||
return <span key={index}>{renderPlain(part, base)}</span>;
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const HighlightableRichText = ({
|
|
||||||
fieldKey,
|
|
||||||
text,
|
|
||||||
className,
|
|
||||||
}: {
|
|
||||||
fieldKey: string;
|
|
||||||
text: string;
|
|
||||||
className?: string;
|
|
||||||
}) => {
|
|
||||||
const rootRef = useRef<HTMLDivElement | null>(null);
|
|
||||||
const highlights = highlightsByField[fieldKey] ?? [];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={rootRef}
|
|
||||||
className={className}
|
|
||||||
onMouseUp={() => {
|
|
||||||
if (!highlightMode) return;
|
|
||||||
const root = rootRef.current;
|
|
||||||
if (!root) return;
|
|
||||||
const sel = window.getSelection();
|
|
||||||
if (!sel || sel.rangeCount === 0 || sel.isCollapsed) return;
|
|
||||||
const range = sel.getRangeAt(0);
|
|
||||||
if (
|
|
||||||
!root.contains(range.startContainer) ||
|
|
||||||
!root.contains(range.endContainer)
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
const selected = range.toString();
|
|
||||||
if (!selected.trim()) return;
|
|
||||||
|
|
||||||
const pre = document.createRange();
|
|
||||||
pre.selectNodeContents(root);
|
|
||||||
pre.setEnd(range.startContainer, range.startOffset);
|
|
||||||
const start = pre.toString().length;
|
|
||||||
addHighlight(fieldKey, start, start + selected.length);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{renderQuestionTextWithHighlights(text, highlights)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// ── Render helpers ───────────────────────────────────────────────────────────
|
// ── Render helpers ───────────────────────────────────────────────────────────
|
||||||
const renderOptions = (question?: Question) => {
|
const renderOptions = (question?: Question) => {
|
||||||
if (!question?.options?.length) return null;
|
if (!question?.options?.length) return null;
|
||||||
@ -1491,14 +1098,14 @@ export const Test = () => {
|
|||||||
key={i}
|
key={i}
|
||||||
className="flex items-center gap-4 p-4 bg-gray-50 rounded-2xl border-2 border-gray-100"
|
className="flex items-center gap-4 p-4 bg-gray-50 rounded-2xl border-2 border-gray-100"
|
||||||
>
|
>
|
||||||
<div className="w-10 h-10 rounded-full bg-purple-100 flex items-center justify-center shrink-0">
|
<div className="w-10 h-10 rounded-full bg-purple-100 flex items-center justify-center flex-shrink-0">
|
||||||
<Check size={20} className="text-purple-600" />
|
<Check size={20} className="text-purple-600" />
|
||||||
</div>
|
</div>
|
||||||
<span className="font-bold text-gray-700">{text}</span>
|
<span className="font-bold text-gray-700">{text}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{isTargeted && (
|
{isTargeted && (
|
||||||
<div className="flex items-center gap-4 p-4 bg-linear-to-r from-purple-50 to-orange-50 rounded-2xl border-2 border-purple-200">
|
<div className="flex items-center gap-4 p-4 bg-gradient-to-r from-purple-50 to-orange-50 rounded-2xl border-2 border-purple-200">
|
||||||
<span className="text-3xl">🎯</span>
|
<span className="text-3xl">🎯</span>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-bold text-purple-800">Targeted Mode</p>
|
<p className="font-bold text-purple-800">Targeted Mode</p>
|
||||||
@ -1554,104 +1161,26 @@ export const Test = () => {
|
|||||||
<header
|
<header
|
||||||
className={`fixed left-0 right-0 bg-white/95 backdrop-blur-sm border-b-2 border-purple-100 px-4 pt-6 pb-4 z-20 ${retryMode ? "top-10" : "top-0"}`}
|
className={`fixed left-0 right-0 bg-white/95 backdrop-blur-sm border-b-2 border-purple-100 px-4 pt-6 pb-4 z-20 ${retryMode ? "top-10" : "top-0"}`}
|
||||||
>
|
>
|
||||||
<div className="mx-auto max-w-2xl md:max-w-(--content-max)">
|
<div className="max-w-2xl mx-auto">
|
||||||
{/* Mobile header (unchanged) */}
|
<div className="flex items-center justify-between mb-3">
|
||||||
<div className="md:hidden">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<span
|
||||||
<div className="flex items-center gap-3">
|
className={`t-q-badge ${isCurrentMarked ? "reviewing" : ""}`}
|
||||||
<span
|
|
||||||
className={`t-q-badge ${isCurrentMarked ? "reviewing" : ""}`}
|
|
||||||
>
|
|
||||||
{isCurrentMarked ? "🔖 " : ""}Q
|
|
||||||
{retryMode ? retryIndex + 1 : questionIndex + 1}
|
|
||||||
</span>
|
|
||||||
<span className="text-sm font-bold text-gray-400">
|
|
||||||
of {currentModule?.questions.length}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col items-center">
|
|
||||||
<div className="t-timer">
|
|
||||||
{timerVisible
|
|
||||||
? `${Math.floor(time / 60)}:${String(time % 60).padStart(2, "0")}`
|
|
||||||
: "--:--"}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
className="mt-1 text-xs text-gray-500 hover:text-gray-700"
|
|
||||||
onClick={() => setTimerVisible((v) => !v)}
|
|
||||||
>
|
|
||||||
{timerVisible ? <EyeOff size={16} /> : <Eye size={16} />}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h1 className="text-center font-bold">
|
|
||||||
{currentModule?.module_title}
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Desktop header (web only) */}
|
|
||||||
<div className="hidden md:flex items-start justify-between gap-6 relative">
|
|
||||||
{/* Left: section/module + directions */}
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<div className="text-sm font-semibold text-gray-700">
|
|
||||||
{currentQuestion?.section || ""}
|
|
||||||
</div>
|
|
||||||
<div className="text-base font-bold text-gray-700">
|
|
||||||
{currentModule?.module_title}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
className={`mt-2 t-btn-3d t-directions-btn ${showDirections ? "active" : ""}`}
|
|
||||||
onClick={() => setShowDirections((v) => !v)}
|
|
||||||
>
|
>
|
||||||
Directions
|
{isCurrentMarked ? "🔖 " : ""}Q
|
||||||
</button>
|
{retryMode ? retryIndex + 1 : questionIndex + 1}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-bold text-gray-400">
|
||||||
|
of {currentModule?.questions.length}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="t-timer">
|
||||||
{/* Middle: time + hide + question number (perfectly centered) */}
|
{Math.floor(time / 60)}:{String(time % 60).padStart(2, "0")}
|
||||||
<div className="flex flex-col items-center absolute left-1/2 -translate-x-1/2 top-0">
|
|
||||||
<div className="flex flex-col items-center">
|
|
||||||
<div className="t-timer">
|
|
||||||
{timerVisible
|
|
||||||
? `${Math.floor(time / 60)}:${String(time % 60).padStart(2, "0")}`
|
|
||||||
: "--:--"}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
className="mt-1 text-xs text-gray-500 hover:text-gray-700"
|
|
||||||
onClick={() => setTimerVisible((v) => !v)}
|
|
||||||
>
|
|
||||||
{timerVisible ? <EyeOff size={16} /> : <Eye size={16} />}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="mt-2 flex items-center gap-2 cursor-pointer"
|
|
||||||
onClick={() => setShowNavigator(true)}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={`t-q-badge ${isCurrentMarked ? "reviewing" : ""}`}
|
|
||||||
>
|
|
||||||
{isCurrentMarked ? "🔖 " : ""}Q
|
|
||||||
{retryMode ? retryIndex + 1 : questionIndex + 1}
|
|
||||||
</span>
|
|
||||||
<span className="text-sm font-bold text-gray-400">
|
|
||||||
of {currentModule?.questions.length}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right: highlights */}
|
|
||||||
<div className="flex items-start">
|
|
||||||
<button
|
|
||||||
onClick={() => setHighlightMode((v) => !v)}
|
|
||||||
className={`t-btn-3d px-5 py-3 flex items-center gap-2 ${
|
|
||||||
highlightMode ? "t-btn-primary" : "t-btn-outline"
|
|
||||||
}`}
|
|
||||||
title="Highlights & Notes"
|
|
||||||
>
|
|
||||||
<Highlighter size={18} />
|
|
||||||
<span className="font-black">Highlights & Notes</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<h1 className="text-center font-bold text-gray-700">
|
||||||
|
{currentModule?.module_title}
|
||||||
|
</h1>
|
||||||
{isTargeted && retryMode && (
|
{isTargeted && retryMode && (
|
||||||
<p className="text-center text-orange-500 font-bold text-sm mt-2">
|
<p className="text-center text-orange-500 font-bold text-sm mt-2">
|
||||||
🔥 Reviewing incorrect answers — get them right to finish!
|
🔥 Reviewing incorrect answers — get them right to finish!
|
||||||
@ -1662,108 +1191,23 @@ export const Test = () => {
|
|||||||
|
|
||||||
{/* Question content */}
|
{/* Question content */}
|
||||||
<section
|
<section
|
||||||
className={`flex-1 px-4 ${isMCQ ? "pb-110" : "pb-32"} ${retryMode ? "pt-36 md:pt-52" : "pt-28 md:pt-44"}`}
|
className={`flex-1 px-4 ${isMCQ ? "pb-110" : "pb-32"} ${retryMode ? "pt-36" : "pt-28"}`}
|
||||||
>
|
>
|
||||||
{/* Mobile layout (unchanged) */}
|
<div className="max-w-2xl mx-auto space-y-6 pt-4">
|
||||||
<div className="block md:hidden">
|
{currentQuestion?.context && (
|
||||||
<div className="max-w-2xl mx-auto pt-4">
|
<div className="t-card p-6">
|
||||||
<div className="space-y-6">
|
<p className="font-semibold text-gray-700 leading-relaxed">
|
||||||
{showDirections ? (
|
{renderQuestionText(currentQuestion.context)}
|
||||||
<div className="t-card p-6 flex-1">
|
</p>
|
||||||
<p className="font-semibold text-gray-700 leading-relaxed">
|
|
||||||
{renderQuestionText(currentQuestion?.explanation || "")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{currentQuestion?.context && (
|
|
||||||
<div className="t-card p-6">
|
|
||||||
<p className="font-semibold text-gray-700 leading-relaxed">
|
|
||||||
{renderQuestionText(currentQuestion.context)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="t-card t-card-purple p-6">
|
|
||||||
<p className="font-bold text-lg text-[#1e1b4b] leading-relaxed">
|
|
||||||
{currentQuestion?.text &&
|
|
||||||
renderQuestionText(currentQuestion.text)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{!isMCQ && renderShortAnswer(currentQuestion)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Desktop layout with draggable separator */}
|
|
||||||
<div className="hidden md:block">
|
|
||||||
<div
|
|
||||||
ref={desktopShellRef}
|
|
||||||
className="t-desktop-shell pt-8"
|
|
||||||
style={{ paddingBottom: 160 }}
|
|
||||||
>
|
|
||||||
<div className="t-split-layout">
|
|
||||||
{/* Left column: passage / directions */}
|
|
||||||
<div
|
|
||||||
className="t-split-left"
|
|
||||||
style={{
|
|
||||||
width: leftColumnWidth,
|
|
||||||
minWidth: 300,
|
|
||||||
maxWidth: 600,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{showDirections ? (
|
|
||||||
<div className="t-card p-6 flex-1">
|
|
||||||
<HighlightableRichText
|
|
||||||
fieldKey={`${currentQuestion?.id ?? "unknown"}:explanation`}
|
|
||||||
text={currentQuestion?.explanation || ""}
|
|
||||||
className="font-semibold text-gray-700 leading-relaxed"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{currentQuestion?.context && (
|
|
||||||
<div className="t-card p-6">
|
|
||||||
<HighlightableRichText
|
|
||||||
fieldKey={`${currentQuestion?.id ?? "unknown"}:context`}
|
|
||||||
text={currentQuestion.context}
|
|
||||||
className="font-semibold text-gray-700 leading-relaxed"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Draggable divider */}
|
|
||||||
<div
|
|
||||||
className="t-split-divider mx-3"
|
|
||||||
onMouseDown={handleSeparatorMouseDown}
|
|
||||||
onDragStart={(e) => e.preventDefault()}
|
|
||||||
>
|
|
||||||
<div className="t-split-divider-bar" />
|
|
||||||
<div className="t-split-divider-handle">
|
|
||||||
<div className="t-split-divider-dots" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right column: question + options/answer */}
|
|
||||||
<div className="t-split-right flex-1">
|
|
||||||
<div className="t-card t-card-purple p-6 mt-4">
|
|
||||||
{currentQuestion?.text && (
|
|
||||||
<HighlightableRichText
|
|
||||||
fieldKey={`${currentQuestion.id}:text`}
|
|
||||||
text={currentQuestion.text}
|
|
||||||
className="font-bold text-lg text-[#1e1b4b] leading-relaxed"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{!isMCQ && renderShortAnswer(currentQuestion)}
|
|
||||||
{isMCQ && renderOptions(currentQuestion)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="t-card t-card-purple p-6">
|
||||||
|
<p className="font-bold text-lg text-[#1e1b4b] leading-relaxed">
|
||||||
|
{currentQuestion?.text &&
|
||||||
|
renderQuestionText(currentQuestion.text)}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
{!isMCQ && renderShortAnswer(currentQuestion)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
@ -1773,25 +1217,23 @@ export const Test = () => {
|
|||||||
className="fixed bottom-0 left-0 right-0 bg-white border-t-2 border-gray-100 py-4 px-4"
|
className="fixed bottom-0 left-0 right-0 bg-white border-t-2 border-gray-100 py-4 px-4"
|
||||||
style={{ zIndex: 20 }}
|
style={{ zIndex: 20 }}
|
||||||
>
|
>
|
||||||
<div className="mx-auto max-w-2xl md:max-w-(--content-max) flex items-center justify-between gap-3">
|
<div className="max-w-2xl mx-auto flex items-center justify-between gap-3">
|
||||||
{/* Back button (non-targeted) */}
|
{/* Back button (non-targeted) */}
|
||||||
{!isTargeted && (
|
{!isTargeted && (
|
||||||
<button
|
<button
|
||||||
disabled={isFirstQuestion}
|
disabled={isFirstQuestion}
|
||||||
onClick={prevQuestion}
|
onClick={prevQuestion}
|
||||||
className="t-btn-3d t-btn-outline px-5 py-3 disabled:opacity-40 flex items-center gap-2"
|
className="t-btn-3d t-btn-outline px-5 py-3 disabled:opacity-40"
|
||||||
>
|
>
|
||||||
<ChevronLeft size={18} />
|
<ChevronLeft size={18} />
|
||||||
<span className="hidden md:inline font-black">Previous</span>
|
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Menu */}
|
{/* Menu */}
|
||||||
<DropdownMenu open={menuOpen} onOpenChange={setMenuOpen}>
|
<DropdownMenu open={menuOpen} onOpenChange={setMenuOpen}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<button className="t-btn-3d t-btn-outline px-5 py-3 flex items-center gap-2">
|
<button className="t-btn-3d t-btn-outline px-5 py-3">
|
||||||
<Menu size={18} />
|
<Menu size={18} />
|
||||||
<span className="hidden md:inline font-black">Menu</span>
|
|
||||||
</button>
|
</button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="rounded-2xl border-2 border-gray-100 p-2">
|
<DropdownMenuContent className="rounded-2xl border-2 border-gray-100 p-2">
|
||||||
@ -1872,46 +1314,19 @@ export const Test = () => {
|
|||||||
|
|
||||||
{/* ── Mark for Review button (non-targeted) ── */}
|
{/* ── Mark for Review button (non-targeted) ── */}
|
||||||
{!isTargeted && (
|
{!isTargeted && (
|
||||||
<>
|
<button
|
||||||
{/* Mobile: keep existing icon-only button */}
|
className={`t-bookmark-btn ${isCurrentMarked ? "marked" : ""}`}
|
||||||
{!isMdUp ? (
|
onClick={toggleMark}
|
||||||
<button
|
title={
|
||||||
className={`t-bookmark-btn ${isCurrentMarked ? "marked" : ""}`}
|
isCurrentMarked ? "Remove review mark" : "Mark for review"
|
||||||
onClick={toggleMark}
|
}
|
||||||
title={
|
>
|
||||||
isCurrentMarked ? "Remove review mark" : "Mark for review"
|
{isCurrentMarked ? (
|
||||||
}
|
<BookMarked size={18} color="white" />
|
||||||
>
|
|
||||||
{isCurrentMarked ? (
|
|
||||||
<BookMarked size={18} color="white" />
|
|
||||||
) : (
|
|
||||||
<Bookmark size={18} color="#9ca3af" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
) : (
|
) : (
|
||||||
/* Desktop/web: icon + text */
|
<Bookmark size={18} color="#9ca3af" />
|
||||||
<button
|
|
||||||
onClick={toggleMark}
|
|
||||||
className={`t-btn-3d px-5 py-3 flex items-center gap-2 ${
|
|
||||||
isCurrentMarked ? "t-btn-primary" : "t-btn-outline"
|
|
||||||
}`}
|
|
||||||
title={
|
|
||||||
isCurrentMarked ? "Remove review mark" : "Mark for review"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{isCurrentMarked ? (
|
|
||||||
<BookMarked size={18} color="white" />
|
|
||||||
) : (
|
|
||||||
<Bookmark size={18} color="#9ca3af" />
|
|
||||||
)}
|
|
||||||
<span className="font-black">
|
|
||||||
{isCurrentMarked
|
|
||||||
? "Marked for review"
|
|
||||||
: "Mark for review"}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Question navigator dialog */}
|
{/* Question navigator dialog */}
|
||||||
@ -2069,8 +1484,8 @@ export const Test = () => {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* MCQ Options Drawer (mobile only) */}
|
{/* MCQ Options Drawer */}
|
||||||
{isMCQ && !isMdUp && (
|
{isMCQ && (
|
||||||
<Drawer.Root
|
<Drawer.Root
|
||||||
modal={false}
|
modal={false}
|
||||||
snapPoints={[0.35, 0.6, 0.95]}
|
snapPoints={[0.35, 0.6, 0.95]}
|
||||||
|
|||||||
@ -68,8 +68,6 @@ const getSectionMeta = (section?: string) =>
|
|||||||
const STYLES = `
|
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');
|
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;800;900&family=Nunito+Sans:wght@400;600;700&display=swap');
|
||||||
|
|
||||||
:root { --content-max: 1100px; }
|
|
||||||
|
|
||||||
.tp-screen {
|
.tp-screen {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: #fffbf4;
|
background: #fffbf4;
|
||||||
@ -353,35 +351,16 @@ const STYLES = `
|
|||||||
|
|
||||||
/* ── Bottom CTA bar ── */
|
/* ── Bottom CTA bar ── */
|
||||||
.tp-cta-bar {
|
.tp-cta-bar {
|
||||||
position: fixed;
|
position: fixed; bottom: 96px; left: 0; right: 0; z-index: 10;
|
||||||
bottom: 96px;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 5;
|
|
||||||
padding: 0.85rem 1.25rem calc(0.85rem + env(safe-area-inset-bottom));
|
padding: 0.85rem 1.25rem calc(0.85rem + env(safe-area-inset-bottom));
|
||||||
background: rgba(255, 251, 244, 0.9);
|
background: rgba(255,251,244,0.9);
|
||||||
backdrop-filter: blur(16px);
|
backdrop-filter: blur(16px);
|
||||||
-webkit-backdrop-filter: blur(16px);
|
-webkit-backdrop-filter: blur(16px);
|
||||||
border-top: 2px solid #f3f4f6;
|
border-top: 2px solid #f3f4f6;
|
||||||
}
|
}
|
||||||
.tp-cta-inner {
|
.tp-cta-inner {
|
||||||
max-width: 560px;
|
max-width: 560px; margin: 0 auto;
|
||||||
margin: 0 auto;
|
display: flex; gap: 0.75rem; align-items: center;
|
||||||
display: flex;
|
|
||||||
gap: 0.75rem;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 900px) {
|
|
||||||
.tp-inner { max-width: var(--content-max); padding: 3rem 1.5rem 10rem; }
|
|
||||||
.tp-topic-grid { grid-template-columns: repeat(3, 1fr); gap: 0.75rem; }
|
|
||||||
.tp-cta-bar { left: var(--sidebar-width); right: 0; }
|
|
||||||
|
|
||||||
/* Align decorative blobs relative to the centered content container */
|
|
||||||
.tp-blob-3 { right: calc((100vw - var(--content-max)) / 2 - 48px); }
|
|
||||||
.tp-blob-1 { left: calc((100vw - var(--content-max)) / 2 - 56px); }
|
|
||||||
.tp-blob-2 { left: calc((100vw - var(--content-max)) / 2 + 12px); }
|
|
||||||
.tp-blob-4 { right: calc((100vw - var(--content-max)) / 2 + 12px); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Next / Start button */
|
/* Next / Start button */
|
||||||
|
|||||||
Reference in New Issue
Block a user