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 type { Achievement } from "@elysium/types";
|
||||||
|
import { useState } from "react";
|
||||||
import { useGame } from "../../context/GameContext.js";
|
import { useGame } from "../../context/GameContext.js";
|
||||||
import { formatNumber } from "../../utils/format.js";
|
import { formatNumber } from "../../utils/format.js";
|
||||||
|
import { LockToggle } from "../ui/LockToggle.js";
|
||||||
|
|
||||||
const conditionDescription = (achievement: Achievement): string => {
|
const conditionDescription = (achievement: Achievement): string => {
|
||||||
const { condition } = achievement;
|
const { condition } = achievement;
|
||||||
@@ -53,20 +55,30 @@ const AchievementCard = ({ achievement }: AchievementCardProps): React.JSX.Eleme
|
|||||||
|
|
||||||
export const AchievementPanel = (): React.JSX.Element => {
|
export const AchievementPanel = (): React.JSX.Element => {
|
||||||
const { state } = useGame();
|
const { state } = useGame();
|
||||||
|
const [showLocked, setShowLocked] = useState(true);
|
||||||
|
|
||||||
if (!state) return <section className="panel"><p>Loading...</p></section>;
|
if (!state) return <section className="panel"><p>Loading...</p></section>;
|
||||||
|
|
||||||
const achievements = state.achievements ?? [];
|
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 (
|
return (
|
||||||
<section className="panel achievement-panel">
|
<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">
|
<p className="achievement-progress">
|
||||||
{unlocked} / {achievements.length} unlocked
|
{unlocked.length} / {achievements.length} unlocked
|
||||||
</p>
|
</p>
|
||||||
<div className="achievement-list">
|
<div className="achievement-list">
|
||||||
{achievements.map((achievement) => (
|
{visible.map((achievement) => (
|
||||||
<AchievementCard key={achievement.id} achievement={achievement} />
|
<AchievementCard key={achievement.id} achievement={achievement} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { Boss } from "@elysium/types";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useGame } from "../../context/GameContext.js";
|
import { useGame } from "../../context/GameContext.js";
|
||||||
import { formatNumber } from "../../utils/format.js";
|
import { formatNumber } from "../../utils/format.js";
|
||||||
|
import { LockToggle } from "../ui/LockToggle.js";
|
||||||
import { ZoneSelector } from "./ZoneSelector.js";
|
import { ZoneSelector } from "./ZoneSelector.js";
|
||||||
|
|
||||||
interface BossCardProps {
|
interface BossCardProps {
|
||||||
@@ -89,6 +90,7 @@ export const BossPanel = (): React.JSX.Element => {
|
|||||||
const { state, challengeBoss } = useGame();
|
const { state, challengeBoss } = useGame();
|
||||||
const [challengingBossId, setChallengingBossId] = useState<string | null>(null);
|
const [challengingBossId, setChallengingBossId] = useState<string | null>(null);
|
||||||
const [activeZoneId, setActiveZoneId] = useState("verdant_vale");
|
const [activeZoneId, setActiveZoneId] = useState("verdant_vale");
|
||||||
|
const [showLocked, setShowLocked] = useState(true);
|
||||||
|
|
||||||
if (!state) return <section className="panel"><p>Loading...</p></section>;
|
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 zones = state.zones ?? [];
|
||||||
const zoneBosses = state.bosses.filter((b) => b.zoneId === activeZoneId);
|
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 (
|
return (
|
||||||
<section className="panel boss-panel">
|
<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
|
<ZoneSelector
|
||||||
activeZoneId={activeZoneId}
|
activeZoneId={activeZoneId}
|
||||||
@@ -162,7 +175,7 @@ export const BossPanel = (): React.JSX.Element => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="boss-list">
|
<div className="boss-list">
|
||||||
{zoneBosses.map((boss) => (
|
{visibleBosses.map((boss) => (
|
||||||
<BossCard
|
<BossCard
|
||||||
key={boss.id}
|
key={boss.id}
|
||||||
boss={boss}
|
boss={boss}
|
||||||
@@ -173,8 +186,8 @@ export const BossPanel = (): React.JSX.Element => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{zoneBosses.length === 0 && (
|
{visibleBosses.length === 0 && (
|
||||||
<p className="empty-zone">No bosses in this zone yet.</p>
|
<p className="empty-zone">No bosses to show in this zone.</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { Equipment, EquipmentType } from "@elysium/types";
|
import type { Equipment, EquipmentType } from "@elysium/types";
|
||||||
|
import { useState } from "react";
|
||||||
import { useGame } from "../../context/GameContext.js";
|
import { useGame } from "../../context/GameContext.js";
|
||||||
|
import { LockToggle } from "../ui/LockToggle.js";
|
||||||
|
|
||||||
const RARITY_LABEL: Record<string, string> = {
|
const RARITY_LABEL: Record<string, string> = {
|
||||||
common: "Common",
|
common: "Common",
|
||||||
@@ -72,20 +74,31 @@ const SLOT_LABEL: Record<EquipmentType, string> = {
|
|||||||
|
|
||||||
export const EquipmentPanel = (): React.JSX.Element => {
|
export const EquipmentPanel = (): React.JSX.Element => {
|
||||||
const { state } = useGame();
|
const { state } = useGame();
|
||||||
|
const [showLocked, setShowLocked] = useState(true);
|
||||||
|
|
||||||
if (!state) return <section className="panel"><p>Loading...</p></section>;
|
if (!state) return <section className="panel"><p>Loading...</p></section>;
|
||||||
|
|
||||||
const equipment = state.equipment ?? [];
|
const equipment = state.equipment ?? [];
|
||||||
|
const unownedCount = equipment.filter((e) => !e.owned).length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="panel equipment-panel">
|
<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">
|
<p className="equipment-intro">
|
||||||
Equipment drops from bosses and grants passive bonuses. Only one item per slot can be equipped at a time.
|
Equipment drops from bosses and grants passive bonuses. Only one item per slot can be equipped at a time.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{SLOT_ORDER.map((slotType) => {
|
{SLOT_ORDER.map((slotType) => {
|
||||||
const items = equipment.filter((e) => e.type === slotType);
|
const items = equipment.filter(
|
||||||
|
(e) => e.type === slotType && (showLocked || e.owned),
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<div key={slotType} className="equipment-slot-section">
|
<div key={slotType} className="equipment-slot-section">
|
||||||
<h3 className="slot-heading">{SLOT_LABEL[slotType]}</h3>
|
<h3 className="slot-heading">{SLOT_LABEL[slotType]}</h3>
|
||||||
@@ -93,6 +106,9 @@ export const EquipmentPanel = (): React.JSX.Element => {
|
|||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<EquipmentCard key={item.id} item={item} />
|
<EquipmentCard key={item.id} item={item} />
|
||||||
))}
|
))}
|
||||||
|
{items.length === 0 && (
|
||||||
|
<p className="empty-zone">No items to show in this slot.</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Quest } from "@elysium/types";
|
import type { Quest } from "@elysium/types";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useGame } from "../../context/GameContext.js";
|
import { useGame } from "../../context/GameContext.js";
|
||||||
|
import { LockToggle } from "../ui/LockToggle.js";
|
||||||
import { ZoneSelector } from "./ZoneSelector.js";
|
import { ZoneSelector } from "./ZoneSelector.js";
|
||||||
|
|
||||||
const formatDuration = (seconds: number): string => {
|
const formatDuration = (seconds: number): string => {
|
||||||
@@ -65,15 +66,27 @@ const QuestCard = ({ quest }: QuestCardProps): React.JSX.Element => {
|
|||||||
export const QuestPanel = (): React.JSX.Element => {
|
export const QuestPanel = (): React.JSX.Element => {
|
||||||
const { state } = useGame();
|
const { state } = useGame();
|
||||||
const [activeZoneId, setActiveZoneId] = useState("verdant_vale");
|
const [activeZoneId, setActiveZoneId] = useState("verdant_vale");
|
||||||
|
const [showLocked, setShowLocked] = useState(true);
|
||||||
|
|
||||||
if (!state) return <section className="panel"><p>Loading...</p></section>;
|
if (!state) return <section className="panel"><p>Loading...</p></section>;
|
||||||
|
|
||||||
const zones = state.zones ?? [];
|
const zones = state.zones ?? [];
|
||||||
const zoneQuests = state.quests.filter((q) => q.zoneId === activeZoneId);
|
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 (
|
return (
|
||||||
<section className="panel quest-panel">
|
<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
|
<ZoneSelector
|
||||||
activeZoneId={activeZoneId}
|
activeZoneId={activeZoneId}
|
||||||
@@ -82,11 +95,11 @@ export const QuestPanel = (): React.JSX.Element => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="quest-list">
|
<div className="quest-list">
|
||||||
{zoneQuests.map((quest) => (
|
{visibleQuests.map((quest) => (
|
||||||
<QuestCard key={quest.id} quest={quest} />
|
<QuestCard key={quest.id} quest={quest} />
|
||||||
))}
|
))}
|
||||||
{zoneQuests.length === 0 && (
|
{visibleQuests.length === 0 && (
|
||||||
<p className="empty-zone">No quests in this zone yet.</p>
|
<p className="empty-zone">No quests to show in this zone.</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { Upgrade } from "@elysium/types";
|
import type { Upgrade } from "@elysium/types";
|
||||||
|
import { useState } from "react";
|
||||||
import { useGame } from "../../context/GameContext.js";
|
import { useGame } from "../../context/GameContext.js";
|
||||||
|
import { LockToggle } from "../ui/LockToggle.js";
|
||||||
|
|
||||||
interface UpgradeCardProps {
|
interface UpgradeCardProps {
|
||||||
upgrade: Upgrade;
|
upgrade: Upgrade;
|
||||||
@@ -63,6 +65,7 @@ const UpgradeCard = ({ upgrade, currentGold, currentEssence }: UpgradeCardProps)
|
|||||||
|
|
||||||
export const UpgradePanel = (): React.JSX.Element => {
|
export const UpgradePanel = (): React.JSX.Element => {
|
||||||
const { state } = useGame();
|
const { state } = useGame();
|
||||||
|
const [showLocked, setShowLocked] = useState(true);
|
||||||
|
|
||||||
if (!state) return <section className="panel"><p>Loading...</p></section>;
|
if (!state) return <section className="panel"><p>Loading...</p></section>;
|
||||||
|
|
||||||
@@ -72,7 +75,14 @@ export const UpgradePanel = (): React.JSX.Element => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="panel upgrade-panel">
|
<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>
|
<p className="upgrade-progress">{purchased.length} / {state.upgrades.length} purchased</p>
|
||||||
{state.upgrades.length === 0 ? (
|
{state.upgrades.length === 0 ? (
|
||||||
<p className="empty-state">No upgrades available yet — keep adventuring!</p>
|
<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}
|
currentEssence={state.resources.essence}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{locked.map((upgrade) => (
|
{showLocked && locked.map((upgrade) => (
|
||||||
<UpgradeCard
|
<UpgradeCard
|
||||||
key={upgrade.id}
|
key={upgrade.id}
|
||||||
upgrade={upgrade}
|
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;
|
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 ─────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
.zone-selector {
|
.zone-selector {
|
||||||
|
|||||||
Reference in New Issue
Block a user