feat(treasure): add treasure quest, quest modal, island node, quest widget

This commit is contained in:
shafin-r
2026-02-26 01:31:48 +06:00
parent 894863c196
commit f64d2cac4a
12 changed files with 4018 additions and 19 deletions

View File

@ -1,5 +1,5 @@
import { Outlet, NavLink } from "react-router-dom";
import { Home, BookOpen, Award, User, Video } from "lucide-react";
import { Outlet, NavLink, useLocation } from "react-router-dom";
import { Home, BookOpen, Award, User, Video, Map } from "lucide-react";
import { SidebarProvider, SidebarTrigger } from "../../components/ui/sidebar";
import { AppSidebar } from "../../components/AppSidebar";
@ -18,6 +18,13 @@ const NAV_ITEMS = [
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,
@ -41,19 +48,26 @@ const NAV_ITEMS = [
},
];
const STYLES = `
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@700;800;900&display=swap');
// ── 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)" },
);
/* ── The floating island dock ── */
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;
/* Frosted pill */
background: rgba(255, 251, 244, 0.72);
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);
@ -62,11 +76,42 @@ const STYLES = `
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 ── */
@ -83,10 +128,11 @@ const STYLES = `
-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),
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 {
@ -102,11 +148,14 @@ const STYLES = `
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);
.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 (only visible when active) ── */
/* ── Label ── */
.sl-dock-label {
font-family: 'Nunito', sans-serif;
font-size: 0.8rem;
@ -124,9 +173,32 @@ const STYLES = `
max-width: 80px;
opacity: 1;
}
/* 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;
}
`;
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>
@ -136,15 +208,16 @@ export function StudentLayout() {
<div className="flex flex-col flex-1 min-w-0">
<SidebarTrigger className="hidden md:block" />
{/* Extra bottom padding so content clears the floating dock */}
<main className="flex-1 md:pb-0">
<Outlet />
</main>
</div>
{/* ── Floating island dock (mobile only) ── */}
<nav className="sl-dock-wrap md:hidden">
{NAV_ITEMS.map((item) => (
{/* ── Floating dock (mobile only) ── */}
<nav
className={`sl-dock-wrap md:hidden${isQuestPage ? " quest-mode" : ""}`}
>
{items.map((item) => (
<NavLink
key={item.to}
to={item.to}
@ -161,7 +234,13 @@ export function StudentLayout() {
<item.icon
size={18}
strokeWidth={isActive ? 2.5 : 1.75}
color={isActive ? item.color : "#94a3b8"}
color={
isActive
? item.color
: isQuestPage
? "rgba(255,255,255,0.4)"
: "#94a3b8"
}
/>
</div>
<span className="sl-dock-label" style={{ color: item.color }}>