generated from nhcarrigan/template
feat: add show/hide locked toggle to all panels
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user