import { useEffect, useRef, useState } from "react"; import { ChevronDown, ChevronRight, Gauge, Map } from "lucide-react"; import { useNavigate } from "react-router-dom"; import { useAuthStore } from "../stores/authStore"; import { useQuestStore, getQuestSummary, getCrewRank, getEarnedXP, } from "../stores/useQuestStore"; import type { QuestNode, QuestArc } from "../types/quest"; import { CREW_RANKS } from "../types/quest"; import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar"; import { Drawer, DrawerContent, DrawerTrigger } from "./ui/drawer"; import { PredictedScoreCard } from "./PredictedScoreCard"; import { ChestOpenModal } from "./ChestOpenModal"; // ─── Styles ─────────────────────────────────────────────────────────────────── const STYLES = ` @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@700;800;900&family=Nunito+Sans:wght@400;600;700&family=Cinzel:wght@700;900&display=swap'); /* ════ SHARED ANIMATION ════ */ @keyframes hcIn { from { opacity:0; transform:translateY(10px) scale(0.97); } to { opacity:1; transform:translateY(0) scale(1); } } /* ════ WHITE CARD (DEFAULT / LEVEL / QUEST_COMPACT) ════ */ .hc-card { background: white; border: 2.5px solid #f3f4f6; border-radius: 26px; box-shadow: 0 4px 20px rgba(0,0,0,0.06); overflow: hidden; animation: hcIn 0.4s cubic-bezier(0.34,1.56,0.64,1) both; } /* Identity */ .hc-top { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; padding: 1.1rem 1.2rem 0.9rem; } .hc-identity { display: flex; align-items: center; gap: 0.7rem; flex: 1; min-width: 0; } .hc-av-wrap { position: relative; flex-shrink: 0; } .hc-av-pip { position: absolute; bottom: -3px; right: -3px; min-width: 18px; height: 18px; border-radius: 9px; padding: 0 4px; background: linear-gradient(135deg, #a855f7, #7c3aed); border: 2px solid white; display: flex; align-items: center; justify-content: center; font-family: 'Nunito', sans-serif; font-size: 0.55rem; font-weight: 900; color: white; } .hc-nameblock { flex: 1; min-width: 0; } .hc-greeting { font-family: 'Nunito', sans-serif; font-size: 0.98rem; font-weight: 900; color: #1e1b4b; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 1.2; } .hc-greeting em { font-style: normal; color: #a855f7; } .hc-role { font-family: 'Nunito Sans', sans-serif; font-size: 0.63rem; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; color: #9ca3af; margin-top: 0.05rem; } .hc-score-btn { display: flex; align-items: center; gap: 0.3rem; background: #f7ffe4; border: 2px solid #d9f99d; border-radius: 100px; padding: 0.42rem 0.72rem; font-family: 'Nunito', sans-serif; font-size: 0.76rem; font-weight: 800; color: #65a30d; cursor: pointer; flex-shrink: 0; transition: transform 0.15s, box-shadow 0.15s; box-shadow: 0 2px 8px rgba(0,0,0,0.04); } .hc-score-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0,0,0,0.07); } .hc-sep { height: 1px; margin: 0 1.2rem; background: #f3f4f6; } /* XP bar */ .hc-xp-row { display: flex; align-items: center; gap: 0.75rem; padding: 0.85rem 1.2rem; } .hc-lvl-tag { font-family: 'Nunito', sans-serif; font-size: 0.7rem; font-weight: 900; color: #a855f7; flex-shrink: 0; background: #f3e8ff; border-radius: 8px; padding: 0.22rem 0.5rem; white-space: nowrap; } .hc-bar-wrap { flex: 1; display: flex; flex-direction: column; gap: 0.22rem; } .hc-track { height: 8px; background: #f3f4f6; border-radius: 100px; overflow: hidden; } .hc-fill { height: 100%; border-radius: 100px; background: linear-gradient(90deg, #a855f7, #f97316); transition: width 1.1s cubic-bezier(0.34,1.56,0.64,1); position: relative; overflow: hidden; } .hc-fill::after { content: ''; position: absolute; inset: 0; background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent); transform: translateX(-100%); animation: hcShimmer 2.6s ease-in-out 1s infinite; } @keyframes hcShimmer { to { transform: translateX(200%); } } .hc-xp-label { font-family: 'Nunito Sans', sans-serif; font-size: 0.6rem; font-weight: 700; color: #9ca3af; display: flex; justify-content: space-between; } .hc-xp-label span:first-child { color: #a855f7; font-weight: 900; } /* Rank row (compact) */ .hc-rank-row { display: flex; align-items: center; gap: 0.6rem; padding: 0.75rem 1.2rem; cursor: pointer; transition: background 0.15s; border-top: 1px solid #f3f4f6; } .hc-rank-row:first-child { border-top: none; } .hc-rank-row:hover { background: #fafafa; } .hc-rank-emoji { font-size: 1.15rem; flex-shrink: 0; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.1)); } .hc-rank-text { flex: 1; min-width: 0; } .hc-rank-name { font-family: 'Cinzel', serif; font-size: 0.8rem; font-weight: 700; color: #1e1b4b; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .hc-rank-progress-label { font-family: 'Nunito Sans', sans-serif; font-size: 0.58rem; font-weight: 700; color: #9ca3af; margin-top: 0.08rem; } .hc-rank-right { display: flex; align-items: center; gap: 0.4rem; flex-shrink: 0; } .hc-streak-pill { display: flex; align-items: center; gap: 0.22rem; background: #fff5f5; border: 1.5px solid #fecaca; border-radius: 100px; padding: 0.2rem 0.5rem; font-family: 'Nunito', sans-serif; font-size: 0.7rem; font-weight: 900; color: #ef4444; } .hc-chest-badge { display: flex; align-items: center; gap: 0.18rem; background: #fef3c7; border: 1.5px solid #fde68a; border-radius: 100px; padding: 0.2rem 0.5rem; font-family: 'Nunito', sans-serif; font-size: 0.7rem; font-weight: 900; color: #b45309; animation: hcPop 1.8s ease-in-out infinite; } @keyframes hcPop { 0%,100%{transform:scale(1);} 50%{transform:scale(1.07);} } .hc-chevron { color: #d1d5db; transition: transform 0.3s cubic-bezier(0.34,1.56,0.64,1), color 0.2s; } .hc-chevron.open { transform: rotate(180deg); color: #a855f7; } /* Collapsible quest panel */ .hc-quests-wrap { overflow: hidden; max-height: 0; transition: max-height 0.38s cubic-bezier(0.4,0,0.2,1); background: #fafafa; border-top: 1px solid #f3f4f6; } .hc-quests-wrap.open { max-height: 480px; } .hc-quest-list { display: flex; flex-direction: column; padding: 0.35rem 0; } .hc-quest-row { display: flex; align-items: center; gap: 0.6rem; padding: 0.65rem 1.2rem; cursor: pointer; transition: background 0.13s; position: relative; } .hc-quest-row:hover { background: #f3f4f6; } .hc-quest-row::before { content: ''; position: absolute; left: 0; top: 20%; bottom: 20%; width: 3px; border-radius: 0 3px 3px 0; background: var(--ac); } .hc-q-icon { width: 34px; height: 34px; border-radius: 10px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 1rem; background: white; border: 1.5px solid #f3f4f6; transition: transform 0.15s; } .hc-quest-row:hover .hc-q-icon { transform: scale(1.08) rotate(-4deg); } .hc-q-icon.claimable { background: #fef3c7; border-color: #fde68a; animation: hcWiggle 2s ease-in-out infinite; } @keyframes hcWiggle { 0%,100%{transform:rotate(0);} 30%{transform:rotate(-7deg) scale(1.05);} 70%{transform:rotate(7deg) scale(1.05);} } .hc-q-body { flex: 1; min-width: 0; } .hc-q-name { font-family: 'Nunito', sans-serif; font-size: 0.8rem; font-weight: 800; color: #1e1b4b; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .hc-q-sub { font-family: 'Nunito Sans', sans-serif; font-size: 0.62rem; font-weight: 600; color: #9ca3af; margin-top: 0.1rem; } .hc-q-claimable { font-family: 'Nunito Sans', sans-serif; font-size: 0.62rem; font-weight: 700; color: #d97706; margin-top: 0.1rem; } .hc-claim-btn { padding: 0.28rem 0.62rem; border: none; border-radius: 100px; cursor: pointer; background: linear-gradient(135deg, #fbbf24, #f59e0b); font-family: 'Nunito', sans-serif; font-size: 0.65rem; font-weight: 900; color: #1a0800; box-shadow: 0 2px 0 #d97706; flex-shrink: 0; transition: all 0.12s; } .hc-claim-btn:hover { transform: translateY(-1px); } .hc-claim-btn:active { transform: translateY(1px); } .hc-empty { padding: 1rem 1.2rem; text-align: center; font-family: 'Nunito', sans-serif; font-size: 0.82rem; font-weight: 700; color: #9ca3af; } .hc-map-link { display: flex; align-items: center; justify-content: center; gap: 0.3rem; padding: 0.6rem 1.2rem; border-top: 1px solid #f3f4f6; cursor: pointer; transition: background 0.13s; font-family: 'Nunito', sans-serif; font-size: 0.7rem; font-weight: 800; color: #a855f7; } .hc-map-link:hover { background: #fdf4ff; } /* ════ DARK OCEAN CARD (QUEST_EXTENDED) ════ */ .hc-ext { background: linear-gradient(160deg, #0b1a35 0%, #060e1f 55%, #0d1530 100%); border-radius: 26px; overflow: hidden; position: relative; box-shadow: 0 8px 32px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.06); animation: hcIn 0.4s cubic-bezier(0.34,1.56,0.64,1) both; margin-bottom: 12px; } /* Animated sea shimmer */ .hc-ext::before { content: ''; position: absolute; inset: 0; pointer-events: none; z-index: 0; background: repeating-linear-gradient(105deg, transparent 55%, rgba(56,189,248,0.018) 56%, transparent 57%), repeating-linear-gradient(75deg, transparent 70%, rgba(56,189,248,0.012) 71%, transparent 72%); background-size: 320% 320%, 260% 260%; animation: hcExtSea 14s ease-in-out infinite alternate; } @keyframes hcExtSea { 0% { background-position: 0% 0%, 100% 0%; } 100% { background-position: 100% 100%, 0% 100%; } } /* Gold orb */ .hc-ext::after { content: ''; position: absolute; top: -40px; right: -30px; z-index: 0; width: 180px; height: 180px; border-radius: 50%; background: radial-gradient(circle, rgba(251,191,36,0.1), transparent 70%); pointer-events: none; } /* Header */ .hc-ext-header { position: relative; z-index: 2; display: flex; align-items: center; justify-content: space-between; padding: 1rem 1.2rem 0.3rem; } .hc-ext-title { font-family: 'Cinzel', serif; font-size: 0.6rem; font-weight: 700; letter-spacing: 0.2em; text-transform: uppercase; color: rgba(251,191,36,0.65); } .hc-ext-earned { font-family: 'Nunito', sans-serif; font-size: 0.7rem; font-weight: 900; color: #fbbf24; background: rgba(251,191,36,0.1); border: 1px solid rgba(251,191,36,0.18); border-radius: 100px; padding: 0.2rem 0.6rem; } /* Scrollable track container */ .hc-ext-scroll { position: relative; z-index: 2; overflow-x: auto; overflow-y: hidden; -webkit-overflow-scrolling: touch; scrollbar-width: none; cursor: grab; padding: 1.0rem 1.0rem 0.8rem; } .hc-ext-scroll::-webkit-scrollbar { display: none; } .hc-ext-scroll:active { cursor: grabbing; } /* Track inner wrapper — the thing that actually lays out rank nodes */ .hc-ext-inner { display: flex; align-items: flex-end; position: relative; /* height: ship(28px) + gap(14px) + node(52px) + label(36px) = ~130px */ height: 110px; /* width set inline per node count */ } /* Baseline connector line — full width, dim */ .hc-ext-baseline { position: absolute; top: 56px; /* ship(28) + gap(14) + half of node(26) — sits at node centre */ left: 26px; right: 26px; height: 2px; background: rgba(255,255,255,0.07); border-radius: 2px; z-index: 0; } /* Gold progress line — width set inline */ .hc-ext-progress-line { position: absolute; top: 56px; left: 26px; height: 2px; background: linear-gradient(90deg, #fbbf24, #f59e0b); box-shadow: 0 0 10px rgba(251,191,36,0.5); border-radius: 2px; z-index: 1; transition: width 1.2s cubic-bezier(0.34,1.56,0.64,1); } /* Ship — absolutely positioned, transition on 'left' */ .hc-ext-ship-wrap { position: absolute; top: 25px; /* sits at top of inner, ship 28px + gap 14px = 42px to node top (56px centre) */ z-index: 10; pointer-events: none; display: flex; flex-direction: column; align-items: center; gap: 0px; transition: left 1.2s cubic-bezier(0.34,1.56,0.64,1); transform: translateX(-50%); } .hc-ext-ship { font-size: 1.5rem; filter: drop-shadow(0 2px 12px rgba(251,191,36,0.6)); animation: hcShipBob 2.8s ease-in-out infinite; display: block; } @keyframes hcShipBob { 0%,100% { transform: translateY(0) rotate(-3deg); } 50% { transform: translateY(-6px) rotate(3deg); } } .hc-ext-ship-tether { width: 1px; height: 14px; background: linear-gradient(to bottom, rgba(251,191,36,0.5), transparent); } /* Each rank column */ .hc-ext-col { display: flex; flex-direction: column; align-items: center; position: relative; z-index: 2; width: 88px; flex-shrink: 0; } /* Narrow first/last columns so line extends correctly */ .hc-ext-col:first-child, .hc-ext-col:last-child { width: 52px; } /* Node circle */ .hc-ext-node { width: 52px; height: 52px; border-radius: 50%; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 1.4rem; position: relative; z-index: 2; margin-top: 42px; /* push down below ship zone */ } .hc-ext-node.reached { background: linear-gradient(145deg, #1e0e4a, #3730a3); border: 2px solid rgba(251,191,36,0.45); box-shadow: 0 0 18px rgba(251,191,36,0.2), 0 4px 0 rgba(20,10,50,0.7); } .hc-ext-node.current { background: linear-gradient(145deg, #6d28d9, #a855f7); border: 2.5px solid #fbbf24; box-shadow: 0 0 0 4px rgba(251,191,36,0.12), 0 0 22px rgba(168,85,247,0.45), 0 4px 0 rgba(80,30,150,0.5); animation: hcExtNodePulse 2.2s ease-in-out infinite; } @keyframes hcExtNodePulse { 0%,100% { box-shadow: 0 0 0 4px rgba(251,191,36,0.12), 0 0 22px rgba(168,85,247,0.45), 0 4px 0 rgba(80,30,150,0.5); } 50% { box-shadow: 0 0 0 7px rgba(251,191,36,0.06), 0 0 30px rgba(168,85,247,0.6), 0 4px 0 rgba(80,30,150,0.5); } } .hc-ext-node.locked { background: rgba(0,0,0); border: 2px solid rgba(255,255,255,0.09); filter: grayscale(0.7) opacity(0.45); } /* Labels below node */ .hc-ext-label { margin-top: 7px; display: flex; flex-direction: column; align-items: center; gap: 2px; } .hc-ext-label-name { font-family: 'Cinzel', serif; font-size: 0.48rem; font-weight: 700; text-align: center; line-height: 1.3; letter-spacing: 0.03em; max-width: 70px; } .hc-ext-label-name.reached { color: #fbbf24; } .hc-ext-label-name.current { color: #c084fc; } .hc-ext-label-name.locked { color: rgba(255,255,255,0.2); } .hc-ext-label-xp { font-family: 'Nunito Sans', sans-serif; font-size: 0.42rem; font-weight: 700; text-align: center; } .hc-ext-label-xp.reached { color: rgba(251,191,36,0.4); } .hc-ext-label-xp.current { color: rgba(192,132,252,0.6); } .hc-ext-label-xp.locked { color: rgba(255,255,255,0.15); } /* Footer link */ .hc-ext-footer { position: relative; z-index: 2; display: flex; align-items: center; justify-content: center; gap: 0.3rem; padding: 0.5rem 1.2rem 0.85rem; margin-top: 0.2rem; border-top: 1px solid rgba(255,255,255,0.06); cursor: pointer; transition: opacity 0.15s; font-family: 'Nunito', sans-serif; font-size: 0.68rem; font-weight: 800; color: rgba(251,191,36,0.55); letter-spacing: 0.04em; } .hc-ext-footer:hover { opacity: 0.75; } `; // ─── Helpers ───────────────────────────────────────────────────────────────── function getActiveQuests(arcs: QuestArc[]) { const out: { node: QuestNode; arc: QuestArc }[] = []; for (const arc of arcs) for (const node of arc.nodes) if (node.status === "claimable" || node.status === "active") out.push({ node, arc }); out.sort((a, b) => a.node.status === "claimable" && b.node.status !== "claimable" ? -1 : b.node.status === "claimable" && a.node.status !== "claimable" ? 1 : 0, ); return out.slice(0, 2); } // Segment width for nodes that aren't first/last const SEG_W = 88; const EDGE_W = 52; // Centre x of node at index i (0-based, total N nodes) function nodeX(i: number, total: number): number { if (i === 0) return EDGE_W / 2; if (i === total - 1) return EDGE_W / 2 + SEG_W * (total - 2) + EDGE_W / 2; return EDGE_W + SEG_W * (i - 1) + SEG_W / 2; } // ─── QUEST_EXTENDED sub-component ──────────────────────────────────────────── const RankLadder = ({ earnedXP, onViewAll, }: { earnedXP: number; onViewAll: () => void; }) => { const scrollRef = useRef(null); const ladder = [...CREW_RANKS] as typeof CREW_RANKS; const N = ladder.length; // Which rank the user is currently on (0-based) let currentIdx = 0; for (let i = N - 1; i >= 0; i--) { if (earnedXP >= ladder[i].xpRequired) { currentIdx = i; break; } } const current = ladder[currentIdx]; const nextRank = ladder[currentIdx + 1] ?? null; const progressToNext = nextRank ? Math.min( 1, (earnedXP - current.xpRequired) / (nextRank.xpRequired - current.xpRequired), ) : 1; // Ship x position: interpolate between current node and next node const shipX = nextRank ? nodeX(currentIdx, N) + (nodeX(currentIdx + 1, N) - nodeX(currentIdx, N)) * progressToNext : nodeX(currentIdx, N); // Gold progress line width: from left edge to ship position const progressLineW = shipX; // Total scroll width const totalW = EDGE_W + SEG_W * (N - 2) + EDGE_W; // Animate ship in after mount const [animated, setAnimated] = useState(false); useEffect(() => { const id = requestAnimationFrame(() => requestAnimationFrame(() => setAnimated(true)), ); return () => cancelAnimationFrame(id); }, []); // Auto-scroll to ship position on mount useEffect(() => { if (!scrollRef.current) return; const el = scrollRef.current; const containerW = el.offsetWidth; const targetScroll = shipX - containerW / 2; el.scrollTo({ left: Math.max(0, targetScroll), behavior: "smooth" }); }, [shipX]); const rankPct = nextRank ? Math.round(progressToNext * 100) : 100; const nextLabel = nextRank ? `${rankPct}% · ${nextRank.xpRequired - earnedXP} XP to ${nextRank.label}` : "Maximum rank achieved"; return (
{/* Header */}
⚓ Crew Rank {earnedXP.toLocaleString()} XP
{/* Current rank label */}
{current.emoji} {current.label} {nextLabel}
{/* Scrollable rank track */}
{/* Baseline dim line */}
{/* Gold progress line */}
{/* Ship marker */}
{/* Rank nodes */} {ladder.map((r, i) => { const state = i < currentIdx ? "reached" : i === currentIdx ? "current" : "locked"; return (
{r.emoji}
{r.label} {r.xpRequired === 0 ? "Start" : `${r.xpRequired.toLocaleString()} XP`}
); })}
{/* Footer */} {/*
View quest map
*/}
); }; // ─── Props ──────────────────────────────────────────────────────────────────── type Mode = "DEFAULT" | "LEVEL" | "QUEST_COMPACT" | "QUEST_EXTENDED"; interface Props { onViewAll?: () => void; mode?: Mode; } // ─── Main component ─────────────────────────────────────────────────────────── export const InfoHeader = ({ onViewAll, mode = "DEFAULT" }: Props) => { const navigate = useNavigate(); const user = useAuthStore((s) => s.user); const arcs = useQuestStore((s) => s.arcs); const claimNode = useQuestStore((s) => s.claimNode); const summary = getQuestSummary(arcs); const rank = getCrewRank(arcs); const earnedXP = getEarnedXP(arcs); const activeQuests = getActiveQuests(arcs); const u = user as any; const level = u?.current_level ?? u?.level ?? 1; const totalXP = u?.total_xp ?? u?.xp ?? 0; const levelStart = u?.current_level_start ?? u?.level_min_xp ?? 0; const levelEnd = u?.next_level_threshold ?? u?.level_max_xp ?? levelStart + 1000; const streak = u?.streak ?? u?.current_streak ?? 0; const firstName = user?.name?.split(" ")[0] || "there"; const roleLabel = u?.role === "ADMIN" ? "Admin" : u?.role === "TEACHER" ? "Teacher" : "Student"; const hour = new Date().getHours(); const timeLabel = hour < 12 ? "morning" : hour < 17 ? "afternoon" : "evening"; const levelRange = Math.max(levelEnd - levelStart, 1); const xpIntoLevel = Math.max(totalXP - levelStart, 0); const rawPct = Math.min(Math.round((xpIntoLevel / levelRange) * 100), 100); const xpToGo = Math.max(levelEnd - totalXP, 0); const [barPct, setBarPct] = useState(0); useEffect(() => { const id = requestAnimationFrame(() => requestAnimationFrame(() => setBarPct(rawPct)), ); return () => cancelAnimationFrame(id); }, [rawPct]); const [open, setOpen] = useState(false); const [claimingNode, setClaimingNode] = useState<{ node: QuestNode; arcId: string; } | null>(null); const handleViewAll = () => { if (onViewAll) onViewAll(); else navigate("/student/quests"); }; const handleClaim = (node: QuestNode, arcId: string) => setClaimingNode({ node, arcId }); const handleChestClose = () => { if (!claimingNode) return; claimNode(claimingNode.arcId, claimingNode.node.id); setClaimingNode(null); }; const rankProgress = Math.round(rank.progressToNext * 100); const nextLabel = rank.next ? `${rankProgress}% to ${rank.next.label}` : "Max rank"; const showIdentity = mode === "DEFAULT"; const showLevel = mode === "DEFAULT" || mode === "LEVEL"; const showQuestCompact = mode === "DEFAULT" || mode === "QUEST_COMPACT"; const showQuestExtended = mode === "QUEST_EXTENDED"; // QUEST_EXTENDED renders its own standalone dark card — no .hc-card wrapper if (showQuestExtended) { return ( <> {claimingNode && ( )} ); } return ( <>
{/* Identity — DEFAULT only */} {showIdentity && ( <>
{user?.name?.slice(0, 1)}
{level}

Good {timeLabel}, {firstName} 👋

{roleLabel}

)} {/* XP bar — DEFAULT + LEVEL */} {showLevel && (
Lv {level}
{totalXP.toLocaleString()} XP {xpToGo.toLocaleString()} to go
)} {/* Rank + collapsible quests — DEFAULT + QUEST_COMPACT */} {showQuestCompact && ( <>
setOpen((o) => !o)}> {rank.emoji}

{rank.label}

{nextLabel}

{streak > 0 && ( 🔥 {streak} )} {summary.claimableNodes > 0 && ( 📦 {summary.claimableNodes} )}
{activeQuests.length === 0 ? (

⚓ All caught up — keep sailing!

) : ( activeQuests.map(({ node, arc }) => { const pct = Math.min( 100, Math.round( (node.progress / node.requirement.target) * 100, ), ); const isClaimable = node.status === "claimable"; return (
!isClaimable && handleViewAll()} >
{isClaimable ? "📦" : node.emoji}

{node.title}

{isClaimable ? (

✨ Ready to claim!

) : (

{node.progress}/{node.requirement.target}{" "} {node.requirement.label} · {pct}%

)}
{isClaimable ? ( ) : ( )}
); }) )}
View quest map
)}
{claimingNode && ( )} ); };