270 lines
7.7 KiB
TypeScript
270 lines
7.7 KiB
TypeScript
import { Outlet, NavLink, useLocation } from "react-router-dom";
|
|
import { Home, BookOpen, Award, User, Video, Map } from "lucide-react";
|
|
import { SidebarProvider } from "../../components/ui/sidebar";
|
|
import { AppSidebar } from "../../components/AppSidebar";
|
|
|
|
const NAV_ITEMS = [
|
|
{
|
|
to: "/student/home",
|
|
icon: Home,
|
|
label: "Home",
|
|
color: "#f97316",
|
|
bg: "rgba(249,115,22,0.12)",
|
|
},
|
|
{
|
|
to: "/student/practice",
|
|
icon: BookOpen,
|
|
label: "Practice",
|
|
color: "#a855f7",
|
|
bg: "rgba(168,85,247,0.12)",
|
|
},
|
|
{
|
|
to: "/student/quests",
|
|
icon: Map,
|
|
label: "Quests",
|
|
color: "#587ffc",
|
|
bg: "rgba(53,75,150,0.12)",
|
|
},
|
|
{
|
|
to: "/student/lessons",
|
|
icon: Video,
|
|
label: "Lessons",
|
|
color: "#0891b2",
|
|
bg: "rgba(8,145,178,0.12)",
|
|
},
|
|
{
|
|
to: "/student/rewards",
|
|
icon: Award,
|
|
label: "Rewards",
|
|
color: "#16a34a",
|
|
bg: "rgba(22,163,74,0.12)",
|
|
},
|
|
{
|
|
to: "/student/profile",
|
|
icon: User,
|
|
label: "Profile",
|
|
color: "#e11d48",
|
|
bg: "rgba(225,29,72,0.12)",
|
|
},
|
|
];
|
|
|
|
// ── Quest dock overrides: dark navy pirate theme ──────────────────────────────
|
|
// Active color on quests page gets the gold treatment instead of the tab color.
|
|
const QUEST_NAV_ITEMS = NAV_ITEMS.map((item) =>
|
|
item.to === "/student/quests"
|
|
? { ...item, color: "#fbbf24", bg: "rgba(251,191,36,0.15)" }
|
|
: { ...item, color: "#94a3b8", bg: "rgba(255,255,255,0.08)" },
|
|
);
|
|
|
|
const STYLES = `
|
|
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@700;800;900&family=Cinzel:wght@700&display=swap');
|
|
@import url('https://fonts.googleapis.com/css2?family=Sorts+Mill+Goudy:ital@0;1&display=swap');
|
|
|
|
/* ══ DEFAULT dock (cream frosted glass) ══ */
|
|
.sl-dock-wrap {
|
|
position: fixed;
|
|
bottom: calc(1.25rem + env(safe-area-inset-bottom));
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
z-index: 20;
|
|
background: rgba(255,251,244,0.72);
|
|
backdrop-filter: blur(24px) saturate(180%);
|
|
-webkit-backdrop-filter: blur(24px) saturate(180%);
|
|
border: 1.5px solid rgba(255,255,255,0.7);
|
|
border-radius: 100px;
|
|
box-shadow:
|
|
0 8px 32px rgba(0,0,0,0.12),
|
|
0 2px 8px rgba(0,0,0,0.06),
|
|
inset 0 1px 0 rgba(255,255,255,0.8);
|
|
padding: 0.45rem 0.5rem;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.15rem;
|
|
transition:
|
|
background 0.4s ease,
|
|
border-color 0.4s ease,
|
|
box-shadow 0.4s ease;
|
|
}
|
|
|
|
/* ══ QUEST dock (dark navy pirate) ══ */
|
|
.sl-dock-wrap.quest-mode {
|
|
background: linear-gradient(
|
|
90deg,
|
|
transparent 0%,
|
|
rgba(251,191,36,0.05) 30%,
|
|
rgba(251,191,36,0.1) 50%,
|
|
rgba(251,191,36,0.15) 70%,
|
|
transparent 100%
|
|
);
|
|
background-size: 200% 100%;
|
|
animation: slGoldSweep 3s linear infinite;
|
|
backdrop-filter: blur(28px) saturate(160%);
|
|
-webkit-backdrop-filter: blur(28px) saturate(160%);
|
|
border: 1.5px solid rgba(251,191,36,0.28);
|
|
box-shadow:
|
|
0 8px 32px rgba(0,0,0,0.55),
|
|
0 2px 8px rgba(0,0,0,0.35),
|
|
0 0 0 1px rgba(251,191,36,0.08),
|
|
inset 0 1px 0 rgba(255,255,255,0.06);
|
|
}
|
|
|
|
|
|
@keyframes slGoldSweep {
|
|
0% { background-position: 200% 0; }
|
|
100% { background-position: -200% 0; }
|
|
}
|
|
|
|
/* ── Each nav item ── */
|
|
.sl-dock-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0;
|
|
border-radius: 100px;
|
|
padding: 0.5rem 0.6rem;
|
|
text-decoration: none;
|
|
border: none;
|
|
background: transparent;
|
|
cursor: pointer;
|
|
-webkit-tap-highlight-color: transparent;
|
|
transition:
|
|
padding 0.35s cubic-bezier(0.34,1.56,0.64,1),
|
|
gap 0.35s cubic-bezier(0.34,1.56,0.64,1),
|
|
background 0.25s ease;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
position: relative;
|
|
}
|
|
.sl-dock-item:active { transform: scale(0.91); }
|
|
.sl-dock-item.active {
|
|
padding: 0.5rem 1rem 0.5rem 0.75rem;
|
|
gap: 0.45rem;
|
|
}
|
|
|
|
/* ── Icon circle ── */
|
|
.sl-dock-icon {
|
|
width: 32px; height: 32px; flex-shrink: 0;
|
|
border-radius: 50%;
|
|
display: flex; align-items: center; justify-content: center;
|
|
background: transparent;
|
|
transition: background 0.25s ease, transform 0.35s cubic-bezier(0.34,1.56,0.64,1);
|
|
}
|
|
.sl-dock-item.active .sl-dock-icon { transform: scale(1.1); }
|
|
|
|
/* In quest mode, active quest icon gets a gold glow */
|
|
.quest-mode .sl-dock-item.active .sl-dock-icon {
|
|
box-shadow: 0 0 12px rgba(251,191,36,0.35);
|
|
}
|
|
|
|
/* ── Label ── */
|
|
.sl-dock-label {
|
|
font-family: 'Nunito', sans-serif;
|
|
font-size: 0.8rem;
|
|
font-weight: 900;
|
|
letter-spacing: 0.01em;
|
|
max-width: 0;
|
|
opacity: 0;
|
|
overflow: hidden;
|
|
transition:
|
|
max-width 0.35s cubic-bezier(0.34,1.56,0.64,1),
|
|
opacity 0.25s ease 0.05s;
|
|
pointer-events: none;
|
|
}
|
|
.sl-dock-item.active .sl-dock-label {
|
|
max-width: 80px;
|
|
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 .sl-dock-item.active .sl-dock-label {
|
|
font-family: 'Sorts Mill Goudy', serif;
|
|
font-size: 0.85rem;
|
|
letter-spacing: 0.05em;
|
|
text-shadow: 0 0 12px rgba(251,191,36,0.5);
|
|
}
|
|
|
|
/* ── Quest mode: inactive icons are dimmer ── */
|
|
.quest-mode .sl-dock-item:not(.active) .sl-dock-icon {
|
|
opacity: 0.55;
|
|
transition: opacity 0.2s ease, background 0.25s ease, transform 0.35s cubic-bezier(0.34,1.56,0.64,1);
|
|
}
|
|
.quest-mode .sl-dock-item:not(.active):hover .sl-dock-icon {
|
|
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() {
|
|
const location = useLocation();
|
|
const isQuestPage = location.pathname.startsWith("/student/quests");
|
|
|
|
// Pick the right nav item config based on page
|
|
const items = isQuestPage ? QUEST_NAV_ITEMS : NAV_ITEMS;
|
|
|
|
return (
|
|
<SidebarProvider>
|
|
<style>{STYLES}</style>
|
|
<div className="flex min-h-screen w-full overflow-x-hidden">
|
|
{/* Desktop Sidebar */}
|
|
<div className="hidden md:block">
|
|
<AppSidebar />
|
|
</div>
|
|
|
|
<div className="flex flex-col flex-1 min-w-0">
|
|
{/* Extra bottom padding so content clears the floating dock */}
|
|
<main className="flex-1 pb-24 md:pb-0">
|
|
<Outlet />
|
|
</main>
|
|
</div>
|
|
|
|
{/* ── Floating dock (mobile only) ── */}
|
|
<nav
|
|
className={`sl-dock-wrap md:hidden${isQuestPage ? " quest-mode" : ""}`}
|
|
>
|
|
{items.map((item) => (
|
|
<NavLink
|
|
key={item.to}
|
|
to={item.to}
|
|
className={({ isActive }) =>
|
|
`sl-dock-item${isActive ? " active" : ""}`
|
|
}
|
|
>
|
|
{({ isActive }) => (
|
|
<>
|
|
<div
|
|
className="sl-dock-icon"
|
|
style={{ background: isActive ? item.bg : "transparent" }}
|
|
>
|
|
<item.icon
|
|
size={18}
|
|
strokeWidth={isActive ? 2.5 : 1.75}
|
|
color={
|
|
isActive
|
|
? item.color
|
|
: isQuestPage
|
|
? "rgba(255,255,255,0.4)"
|
|
: "#94a3b8"
|
|
}
|
|
/>
|
|
</div>
|
|
<span className="sl-dock-label" style={{ color: item.color }}>
|
|
{item.label}
|
|
</span>
|
|
</>
|
|
)}
|
|
</NavLink>
|
|
))}
|
|
</nav>
|
|
</div>
|
|
</SidebarProvider>
|
|
);
|
|
}
|