feat: add show/hide locked toggle to all panels

This commit is contained in:
2026-03-06 13:46:28 -08:00
committed by Naomi Carrigan
parent 897eba5f64
commit f734176965
7 changed files with 141 additions and 16 deletions
@@ -1,6 +1,8 @@
import type { Achievement } from "@elysium/types";
import { useState } from "react";
import { useGame } from "../../context/GameContext.js";
import { formatNumber } from "../../utils/format.js";
import { LockToggle } from "../ui/LockToggle.js";
const conditionDescription = (achievement: Achievement): string => {
const { condition } = achievement;
@@ -53,20 +55,30 @@ const AchievementCard = ({ achievement }: AchievementCardProps): React.JSX.Eleme
export const AchievementPanel = (): React.JSX.Element => {
const { state } = useGame();
const [showLocked, setShowLocked] = useState(true);
if (!state) return <section className="panel"><p>Loading...</p></section>;
const achievements = state.achievements ?? [];
const unlocked = achievements.filter((a) => a.unlockedAt !== null).length;
const unlocked = achievements.filter((a) => a.unlockedAt !== null);
const locked = achievements.filter((a) => a.unlockedAt === null);
const visible = showLocked ? achievements : unlocked;
return (
<section className="panel achievement-panel">
<h2>Achievements</h2>
<div className="panel-header">
<h2>Achievements</h2>
<LockToggle
lockedCount={locked.length}
showLocked={showLocked}
onToggle={() => { setShowLocked((v) => !v); }}
/>
</div>
<p className="achievement-progress">
{unlocked} / {achievements.length} unlocked
{unlocked.length} / {achievements.length} unlocked
</p>
<div className="achievement-list">
{achievements.map((achievement) => (
{visible.map((achievement) => (
<AchievementCard key={achievement.id} achievement={achievement} />
))}
</div>
+17 -4
View File
@@ -2,6 +2,7 @@ import type { Boss } from "@elysium/types";
import { useState } from "react";
import { useGame } from "../../context/GameContext.js";
import { formatNumber } from "../../utils/format.js";
import { LockToggle } from "../ui/LockToggle.js";
import { ZoneSelector } from "./ZoneSelector.js";
interface BossCardProps {
@@ -89,6 +90,7 @@ export const BossPanel = (): React.JSX.Element => {
const { state, challengeBoss } = useGame();
const [challengingBossId, setChallengingBossId] = useState<string | null>(null);
const [activeZoneId, setActiveZoneId] = useState("verdant_vale");
const [showLocked, setShowLocked] = useState(true);
if (!state) return <section className="panel"><p>Loading...</p></section>;
@@ -139,10 +141,21 @@ export const BossPanel = (): React.JSX.Element => {
const zones = state.zones ?? [];
const zoneBosses = state.bosses.filter((b) => b.zoneId === activeZoneId);
const lockedCount = zoneBosses.filter((b) => b.status === "locked").length;
const visibleBosses = showLocked
? zoneBosses
: zoneBosses.filter((b) => b.status !== "locked");
return (
<section className="panel boss-panel">
<h2>Boss Encounters</h2>
<div className="panel-header">
<h2>Boss Encounters</h2>
<LockToggle
lockedCount={lockedCount}
showLocked={showLocked}
onToggle={() => { setShowLocked((v) => !v); }}
/>
</div>
<ZoneSelector
activeZoneId={activeZoneId}
@@ -162,7 +175,7 @@ export const BossPanel = (): React.JSX.Element => {
</div>
<div className="boss-list">
{zoneBosses.map((boss) => (
{visibleBosses.map((boss) => (
<BossCard
key={boss.id}
boss={boss}
@@ -173,8 +186,8 @@ export const BossPanel = (): React.JSX.Element => {
}}
/>
))}
{zoneBosses.length === 0 && (
<p className="empty-zone">No bosses in this zone yet.</p>
{visibleBosses.length === 0 && (
<p className="empty-zone">No bosses to show in this zone.</p>
)}
</div>
</section>
@@ -1,5 +1,7 @@
import type { Equipment, EquipmentType } from "@elysium/types";
import { useState } from "react";
import { useGame } from "../../context/GameContext.js";
import { LockToggle } from "../ui/LockToggle.js";
const RARITY_LABEL: Record<string, string> = {
common: "Common",
@@ -72,20 +74,31 @@ const SLOT_LABEL: Record<EquipmentType, string> = {
export const EquipmentPanel = (): React.JSX.Element => {
const { state } = useGame();
const [showLocked, setShowLocked] = useState(true);
if (!state) return <section className="panel"><p>Loading...</p></section>;
const equipment = state.equipment ?? [];
const unownedCount = equipment.filter((e) => !e.owned).length;
return (
<section className="panel equipment-panel">
<h2>Equipment</h2>
<div className="panel-header">
<h2>Equipment</h2>
<LockToggle
lockedCount={unownedCount}
showLocked={showLocked}
onToggle={() => { setShowLocked((v) => !v); }}
/>
</div>
<p className="equipment-intro">
Equipment drops from bosses and grants passive bonuses. Only one item per slot can be equipped at a time.
</p>
{SLOT_ORDER.map((slotType) => {
const items = equipment.filter((e) => e.type === slotType);
const items = equipment.filter(
(e) => e.type === slotType && (showLocked || e.owned),
);
return (
<div key={slotType} className="equipment-slot-section">
<h3 className="slot-heading">{SLOT_LABEL[slotType]}</h3>
@@ -93,6 +106,9 @@ export const EquipmentPanel = (): React.JSX.Element => {
{items.map((item) => (
<EquipmentCard key={item.id} item={item} />
))}
{items.length === 0 && (
<p className="empty-zone">No items to show in this slot.</p>
)}
</div>
</div>
);
+17 -4
View File
@@ -1,6 +1,7 @@
import type { Quest } from "@elysium/types";
import { useState } from "react";
import { useGame } from "../../context/GameContext.js";
import { LockToggle } from "../ui/LockToggle.js";
import { ZoneSelector } from "./ZoneSelector.js";
const formatDuration = (seconds: number): string => {
@@ -65,15 +66,27 @@ const QuestCard = ({ quest }: QuestCardProps): React.JSX.Element => {
export const QuestPanel = (): React.JSX.Element => {
const { state } = useGame();
const [activeZoneId, setActiveZoneId] = useState("verdant_vale");
const [showLocked, setShowLocked] = useState(true);
if (!state) return <section className="panel"><p>Loading...</p></section>;
const zones = state.zones ?? [];
const zoneQuests = state.quests.filter((q) => q.zoneId === activeZoneId);
const lockedCount = zoneQuests.filter((q) => q.status === "locked").length;
const visibleQuests = showLocked
? zoneQuests
: zoneQuests.filter((q) => q.status !== "locked");
return (
<section className="panel quest-panel">
<h2>Quests</h2>
<div className="panel-header">
<h2>Quests</h2>
<LockToggle
lockedCount={lockedCount}
showLocked={showLocked}
onToggle={() => { setShowLocked((v) => !v); }}
/>
</div>
<ZoneSelector
activeZoneId={activeZoneId}
@@ -82,11 +95,11 @@ export const QuestPanel = (): React.JSX.Element => {
/>
<div className="quest-list">
{zoneQuests.map((quest) => (
{visibleQuests.map((quest) => (
<QuestCard key={quest.id} quest={quest} />
))}
{zoneQuests.length === 0 && (
<p className="empty-zone">No quests in this zone yet.</p>
{visibleQuests.length === 0 && (
<p className="empty-zone">No quests to show in this zone.</p>
)}
</div>
</section>
+12 -2
View File
@@ -1,5 +1,7 @@
import type { Upgrade } from "@elysium/types";
import { useState } from "react";
import { useGame } from "../../context/GameContext.js";
import { LockToggle } from "../ui/LockToggle.js";
interface UpgradeCardProps {
upgrade: Upgrade;
@@ -63,6 +65,7 @@ const UpgradeCard = ({ upgrade, currentGold, currentEssence }: UpgradeCardProps)
export const UpgradePanel = (): React.JSX.Element => {
const { state } = useGame();
const [showLocked, setShowLocked] = useState(true);
if (!state) return <section className="panel"><p>Loading...</p></section>;
@@ -72,7 +75,14 @@ export const UpgradePanel = (): React.JSX.Element => {
return (
<section className="panel upgrade-panel">
<h2>Upgrades</h2>
<div className="panel-header">
<h2>Upgrades</h2>
<LockToggle
lockedCount={locked.length}
showLocked={showLocked}
onToggle={() => { setShowLocked((v) => !v); }}
/>
</div>
<p className="upgrade-progress">{purchased.length} / {state.upgrades.length} purchased</p>
{state.upgrades.length === 0 ? (
<p className="empty-state">No upgrades available yet β€” keep adventuring!</p>
@@ -94,7 +104,7 @@ export const UpgradePanel = (): React.JSX.Element => {
currentEssence={state.resources.essence}
/>
))}
{locked.map((upgrade) => (
{showLocked && locked.map((upgrade) => (
<UpgradeCard
key={upgrade.id}
upgrade={upgrade}
+20
View File
@@ -0,0 +1,20 @@
interface LockToggleProps {
showLocked: boolean;
onToggle: () => void;
lockedCount: number;
}
export const LockToggle = ({
showLocked,
onToggle,
lockedCount,
}: LockToggleProps): React.JSX.Element => (
<button
className={`lock-toggle ${showLocked ? "lock-toggle-on" : "lock-toggle-off"}`}
onClick={onToggle}
title={showLocked ? "Hide locked items" : "Show locked items"}
type="button"
>
{showLocked ? "πŸ”“" : "πŸ”’"} {showLocked ? "Hide" : "Show"} locked ({lockedCount})
</button>
);
+41
View File
@@ -1107,6 +1107,47 @@ body {
font-size: 0.85rem;
}
/* ── Panel Header (title + lock toggle row) ────────────────────────────── */
.panel-header {
align-items: center;
display: flex;
gap: 1rem;
justify-content: space-between;
margin-bottom: 0.25rem;
}
.panel-header h2 {
margin: 0;
}
.lock-toggle {
align-items: center;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(147, 51, 234, 0.3);
border-radius: 1rem;
color: var(--colour-text-muted);
cursor: pointer;
display: flex;
font-family: inherit;
font-size: 0.75rem;
gap: 0.3rem;
padding: 0.3rem 0.75rem;
transition: all 0.2s;
white-space: nowrap;
}
.lock-toggle:hover {
background: rgba(147, 51, 234, 0.15);
border-color: rgba(147, 51, 234, 0.6);
color: var(--colour-text);
}
.lock-toggle-on {
border-color: rgba(147, 51, 234, 0.5);
color: var(--colour-text);
}
/* ── Zone Selector ─────────────────────────────────────────────────────── */
.zone-selector {