fix(api): fix api integration for quest map and adjacent components

This commit is contained in:
shafin-r
2026-03-01 12:57:54 +06:00
parent c7f0183956
commit 2eaf77e13c
12 changed files with 2039 additions and 618 deletions

View File

@ -0,0 +1,217 @@
import { useState } from "react";
import {
useInventoryStore,
getLiveEffects,
formatTimeLeft,
hasActiveEffect,
} from "../stores/useInventoryStore";
import { InventoryModal } from "./InventoryModal";
// ─── Styles ───────────────────────────────────────────────────────────────────
const BTN_STYLES = `
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@800;900&family=Cinzel:wght@700&display=swap');
/* ── Inventory trigger button ── */
.inv-btn {
position: relative;
display: inline-flex; align-items: center; gap: 0.38rem;
padding: 0.48rem 0.85rem;
background: rgba(255,255,255,0.05);
border: 1.5px solid rgba(255,255,255,0.1);
border-radius: 100px;
cursor: pointer;
font-family: 'Nunito', sans-serif;
font-size: 0.72rem; font-weight: 900;
color: rgba(255,255,255,0.7);
transition: all 0.18s ease;
outline: none;
white-space: nowrap;
}
.inv-btn:hover {
background: rgba(255,255,255,0.09);
border-color: rgba(255,255,255,0.2);
color: white;
transform: translateY(-1px);
box-shadow: 0 4px 16px rgba(0,0,0,0.25);
}
.inv-btn:active { transform: translateY(0) scale(0.97); }
/* When active effects are running — gold glow */
.inv-btn.has-active {
border-color: rgba(251,191,36,0.45);
color: #fbbf24;
background: rgba(251,191,36,0.08);
animation: invBtnGlow 2.6s ease-in-out infinite;
}
@keyframes invBtnGlow {
0%,100% { box-shadow: 0 0 0 0 rgba(251,191,36,0); }
50% { box-shadow: 0 0 14px 3px rgba(251,191,36,0.2); }
}
.inv-btn.has-active:hover {
border-color: rgba(251,191,36,0.7);
background: rgba(251,191,36,0.14);
}
/* Badge dot */
.inv-btn-badge {
position: absolute; top: -4px; right: -4px;
width: 14px; height: 14px; border-radius: 50%;
background: #fbbf24;
border: 2px solid transparent; /* will be set to match parent bg via CSS var */
display: flex; align-items: center; justify-content: center;
font-family: 'Nunito', sans-serif;
font-size: 0.45rem; font-weight: 900; color: #1a0800;
animation: invBadgePop 1.8s ease-in-out infinite;
}
@keyframes invBadgePop {
0%,100%{ transform: scale(1); }
50% { transform: scale(1.15); }
}
/* ── Active Effect Banner (shown on other screens, e.g. pretest) ── */
.aeb-wrap {
display: flex; gap: 0.5rem; flex-wrap: wrap;
}
.aeb-pill {
display: inline-flex; align-items: center; gap: 0.4rem;
padding: 0.38rem 0.85rem;
border-radius: 100px;
font-family: 'Nunito', sans-serif;
font-size: 0.72rem; font-weight: 900;
animation: aebPillIn 0.35s cubic-bezier(0.34,1.56,0.64,1) both;
animation-delay: var(--aeb-delay, 0s);
}
@keyframes aebPillIn {
from { opacity:0; transform: scale(0.8) translateY(6px); }
to { opacity:1; transform: scale(1) translateY(0); }
}
/* Color variants per effect type */
.aeb-pill.xp_boost {
background: rgba(251,191,36,0.12);
border: 1.5px solid rgba(251,191,36,0.4);
color: #fbbf24;
}
.aeb-pill.streak_shield {
background: rgba(96,165,250,0.1);
border: 1.5px solid rgba(96,165,250,0.35);
color: #60a5fa;
}
.aeb-pill.coin_boost {
background: rgba(167,243,208,0.08);
border: 1.5px solid rgba(52,211,153,0.35);
color: #34d399;
}
.aeb-pill.default {
background: rgba(255,255,255,0.06);
border: 1.5px solid rgba(255,255,255,0.15);
color: rgba(255,255,255,0.7);
}
.aeb-pill-icon { font-size: 0.9rem; line-height:1; }
.aeb-pill-label { line-height:1; }
.aeb-pill-time {
font-family: 'Nunito Sans', sans-serif;
font-size: 0.58rem; font-weight: 700;
opacity: 0.55; margin-left: 0.1rem;
}
`;
const ITEM_ICON: Record<string, string> = {
xp_boost: "⚡",
streak_shield: "🛡️",
title: "🏴‍☠️",
coin_boost: "🪙",
};
function itemIcon(effectType: string): string {
return ITEM_ICON[effectType] ?? "📦";
}
// ─── InventoryButton ──────────────────────────────────────────────────────────
/**
* Drop-in trigger button. Can be placed in any nav bar, header, or screen.
* Shows a gold glow + badge count when active effects are running.
*
* Usage:
* <InventoryButton />
* <InventoryButton label="Hold" />
*/
export const InventoryButton = ({}: {}) => {
const [open, setOpen] = useState(false);
const activeEffects = useInventoryStore((s) => s.activeEffects);
const liveEffects = getLiveEffects(activeEffects);
const hasActive = liveEffects.length > 0;
return (
<>
<style>{BTN_STYLES}</style>
<button
className={`inv-btn${hasActive ? " has-active" : ""}`}
onClick={() => setOpen(true)}
aria-label="Open inventory"
>
🎒
{hasActive && (
<span className="inv-btn-badge">{liveEffects.length}</span>
)}
</button>
{open && <InventoryModal onClose={() => setOpen(false)} />}
</>
);
};
// ─── ActiveEffectBanner ───────────────────────────────────────────────────────
/**
* Shows pills for each currently-active effect.
* Place wherever you want a contextual reminder (pretest screen, dashboard, etc.)
*
* Usage:
* <ActiveEffectBanner />
* <ActiveEffectBanner filter="xp_boost" /> ← only show a specific effect
*
* Example output on Pretest screen:
* ⚡ XP Boost ×2 · 1h 42m 🛡️ Streak Shield · 23m
*/
export const ActiveEffectBanner = ({
filter,
className,
}: {
filter?: string;
className?: string;
}) => {
const activeEffects = useInventoryStore((s) => s.activeEffects);
const live = getLiveEffects(activeEffects).filter(
(e) => !filter || e.item.effect_type === filter,
);
if (live.length === 0) return null;
return (
<>
<style>{BTN_STYLES}</style>
<div className={`aeb-wrap${className ? ` ${className}` : ""}`}>
{live.map((e, i) => (
<div
key={e.id}
className={`aeb-pill ${e.item.effect_type ?? "default"}`}
style={{ "--aeb-delay": `${i * 0.07}s` } as React.CSSProperties}
>
<span className="aeb-pill-icon">
{itemIcon(e.item.effect_type)}
</span>
<span className="aeb-pill-label">
{e.item.name}
{e.item.effect_type === "xp_boost" && e.item.effect_value
? ` ×${e.item.effect_value}`
: ""}
</span>
<span className="aeb-pill-time">
{formatTimeLeft(e.expires_at)}
</span>
</div>
))}
</div>
</>
);
};