generated from nhcarrigan/template
feat: add number format config, resource cap, and modal scroll fix
- Add user-configurable number format (suffix/scientific/engineering) - Suffix: K/M/B/T through Dc (1e33), then letter-based a/b/c... indefinitely - Scientific: 1.23e15 style via toExponential - Engineering: exponent always a multiple of 3 (1.23E15) - Stored in ProfileSettings, fetched from profile API on load - Picker UI in EditProfileModal with live examples - Cap all resource accumulation at 1e300 (RESOURCE_CAP constant) - Per-resource FULL badge with tooltip in ResourceBar - Amber notice strip when any resource is at cap - handleClick also respects the cap - Make EditProfileModal scrollable with viewport margin - Flex column layout with sticky header, scrollable form body - Bio textarea preserved as resizable with min-height - Fix ReferenceError: formatNumber not defined in BossPanel/AchievementPanel - Pass formatNumber as prop to BossCard and AchievementCard - Pass formatNumber as parameter to conditionDescription
This commit is contained in:
@@ -1,10 +1,9 @@
|
||||
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 conditionDescription = (achievement: Achievement, formatNumber: (n: number) => string): string => {
|
||||
const { condition } = achievement;
|
||||
switch (condition.type) {
|
||||
case "totalGoldEarned":
|
||||
@@ -26,9 +25,10 @@ const conditionDescription = (achievement: Achievement): string => {
|
||||
|
||||
interface AchievementCardProps {
|
||||
achievement: Achievement;
|
||||
formatNumber: (n: number) => string;
|
||||
}
|
||||
|
||||
const AchievementCard = ({ achievement }: AchievementCardProps): React.JSX.Element => {
|
||||
const AchievementCard = ({ achievement, formatNumber }: AchievementCardProps): React.JSX.Element => {
|
||||
const isUnlocked = achievement.unlockedAt !== null;
|
||||
|
||||
return (
|
||||
@@ -37,7 +37,7 @@ const AchievementCard = ({ achievement }: AchievementCardProps): React.JSX.Eleme
|
||||
<div className="achievement-info">
|
||||
<h3>{achievement.name}</h3>
|
||||
<p>{achievement.description}</p>
|
||||
<p className="achievement-condition">{conditionDescription(achievement)}</p>
|
||||
<p className="achievement-condition">{conditionDescription(achievement, formatNumber)}</p>
|
||||
{achievement.reward?.crystals != null && (
|
||||
<p className="achievement-reward">💎 +{achievement.reward.crystals} Crystals</p>
|
||||
)}
|
||||
@@ -54,7 +54,7 @@ const AchievementCard = ({ achievement }: AchievementCardProps): React.JSX.Eleme
|
||||
};
|
||||
|
||||
export const AchievementPanel = (): React.JSX.Element => {
|
||||
const { state } = useGame();
|
||||
const { state, formatNumber } = useGame();
|
||||
const [showLocked, setShowLocked] = useState(true);
|
||||
|
||||
if (!state) return <section className="panel"><p>Loading...</p></section>;
|
||||
@@ -79,7 +79,7 @@ export const AchievementPanel = (): React.JSX.Element => {
|
||||
</p>
|
||||
<div className="achievement-list">
|
||||
{visible.map((achievement) => (
|
||||
<AchievementCard key={achievement.id} achievement={achievement} />
|
||||
<AchievementCard key={achievement.id} achievement={achievement} formatNumber={formatNumber} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { BattleResult } from "../../context/GameContext.js";
|
||||
import { useGame } from "../../context/GameContext.js";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface BattleModalProps {
|
||||
@@ -6,17 +7,12 @@ interface BattleModalProps {
|
||||
onDismiss: () => void;
|
||||
}
|
||||
|
||||
const formatNumber = (n: number): string => {
|
||||
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
||||
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`;
|
||||
return Math.floor(n).toLocaleString();
|
||||
};
|
||||
|
||||
export const BattleModal = ({
|
||||
battle,
|
||||
onDismiss,
|
||||
}: BattleModalProps): React.JSX.Element => {
|
||||
const { result, bossName } = battle;
|
||||
const { formatNumber } = useGame();
|
||||
|
||||
const [phase, setPhase] = useState<"animating" | "result">("animating");
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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";
|
||||
|
||||
@@ -11,6 +10,7 @@ interface BossCardProps {
|
||||
onChallenge: (bossId: string) => void;
|
||||
isChallenging: boolean;
|
||||
unlockHint?: string | undefined;
|
||||
formatNumber: (n: number) => string;
|
||||
}
|
||||
|
||||
const BossCard = ({
|
||||
@@ -19,6 +19,7 @@ const BossCard = ({
|
||||
onChallenge,
|
||||
isChallenging,
|
||||
unlockHint,
|
||||
formatNumber,
|
||||
}: BossCardProps): React.JSX.Element => {
|
||||
const hpPercent = (boss.currentHp / boss.maxHp) * 100;
|
||||
const isPrestigeLocked = boss.prestigeRequirement > prestigeCount;
|
||||
@@ -92,7 +93,7 @@ const BossCard = ({
|
||||
};
|
||||
|
||||
export const BossPanel = (): React.JSX.Element => {
|
||||
const { state, challengeBoss } = useGame();
|
||||
const { state, challengeBoss, formatNumber } = useGame();
|
||||
const [challengingBossId, setChallengingBossId] = useState<string | null>(null);
|
||||
const [activeZoneId, setActiveZoneId] = useState("verdant_vale");
|
||||
const [showLocked, setShowLocked] = useState(true);
|
||||
@@ -212,6 +213,7 @@ export const BossPanel = (): React.JSX.Element => {
|
||||
<BossCard
|
||||
key={boss.id}
|
||||
boss={boss}
|
||||
formatNumber={formatNumber}
|
||||
isChallenging={challengingBossId === boss.id}
|
||||
prestigeCount={state.prestige.count}
|
||||
unlockHint={bossUnlockHints.get(boss.id)}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { useGame } from "../../context/GameContext.js";
|
||||
import { calculateClickPower } from "../../engine/tick.js";
|
||||
import { formatNumber } from "../../utils/format.js";
|
||||
|
||||
interface FloatText {
|
||||
id: number;
|
||||
@@ -11,7 +10,7 @@ interface FloatText {
|
||||
}
|
||||
|
||||
export const ClickArea = (): React.JSX.Element => {
|
||||
const { state, handleClick } = useGame();
|
||||
const { state, handleClick, formatNumber } = useGame();
|
||||
const [floats, setFloats] = useState<FloatText[]>([]);
|
||||
const nextIdRef = useRef(0);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ProfileSettings } from "@elysium/types";
|
||||
import type { NumberFormat, ProfileSettings } from "@elysium/types";
|
||||
import { DEFAULT_PROFILE_SETTINGS } from "@elysium/types";
|
||||
import { useEffect, useState } from "react";
|
||||
import { updateProfile } from "../../api/client.js";
|
||||
@@ -20,12 +20,15 @@ const STAT_TOGGLES: { key: keyof ProfileSettings; label: string; icon: string }[
|
||||
];
|
||||
|
||||
export const EditProfileModal = ({ onClose }: EditProfileModalProps): React.JSX.Element => {
|
||||
const { state } = useGame();
|
||||
const { state, numberFormat: currentNumberFormat, setNumberFormat } = useGame();
|
||||
const player = state?.player;
|
||||
|
||||
const [characterName, setCharacterName] = useState(player?.characterName ?? "");
|
||||
const [bio, setBio] = useState("");
|
||||
const [settings, setSettings] = useState<ProfileSettings>({ ...DEFAULT_PROFILE_SETTINGS });
|
||||
const [settings, setSettings] = useState<ProfileSettings>({
|
||||
...DEFAULT_PROFILE_SETTINGS,
|
||||
numberFormat: currentNumberFormat,
|
||||
});
|
||||
const [loadingProfile, setLoadingProfile] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -59,6 +62,7 @@ export const EditProfileModal = ({ onClose }: EditProfileModalProps): React.JSX.
|
||||
setError(null);
|
||||
try {
|
||||
await updateProfile({ characterName, bio, profileSettings: settings });
|
||||
setNumberFormat(settings.numberFormat);
|
||||
setSaved(true);
|
||||
setTimeout(onClose, 900);
|
||||
} catch (err: unknown) {
|
||||
@@ -139,6 +143,30 @@ export const EditProfileModal = ({ onClose }: EditProfileModalProps): React.JSX.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="edit-profile-section">
|
||||
<p className="edit-profile-label">Number Format</p>
|
||||
<p className="edit-profile-sublabel">How large numbers appear across the game.</p>
|
||||
<div className="number-format-picker">
|
||||
{(
|
||||
[
|
||||
{ value: "suffix", label: "Suffix", example: "1.23Qa" },
|
||||
{ value: "scientific", label: "Scientific", example: "1.23e15" },
|
||||
{ value: "engineering", label: "Engineering", example: "1.23E15" },
|
||||
] as { value: NumberFormat; label: string; example: string }[]
|
||||
).map(({ value, label, example }) => (
|
||||
<button
|
||||
key={value}
|
||||
className={`number-format-btn ${settings.numberFormat === value ? "number-format-active" : ""}`}
|
||||
onClick={() => { setSettings((prev) => ({ ...prev, numberFormat: value })); }}
|
||||
type="button"
|
||||
>
|
||||
<span className="number-format-label">{label}</span>
|
||||
<span className="number-format-example">{example}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && <p className="edit-profile-error">{error}</p>}
|
||||
|
||||
<div className="edit-profile-actions">
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { PublicProfileResponse } from "@elysium/types";
|
||||
import { useEffect, useState } from "react";
|
||||
import { formatNumber } from "../../utils/format.js";
|
||||
import { useGame } from "../../context/GameContext.js";
|
||||
|
||||
interface ProfilePageProps {
|
||||
discordId: string;
|
||||
}
|
||||
|
||||
export const ProfilePage = ({ discordId }: ProfilePageProps): React.JSX.Element => {
|
||||
const { formatNumber } = useGame();
|
||||
const [profile, setProfile] = useState<PublicProfileResponse | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { Quest } 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";
|
||||
|
||||
@@ -24,7 +23,7 @@ interface QuestCardProps {
|
||||
}
|
||||
|
||||
const QuestCard = ({ quest, unlockHint, zoneHint }: QuestCardProps): React.JSX.Element => {
|
||||
const { startQuest } = useGame();
|
||||
const { startQuest, formatNumber } = useGame();
|
||||
|
||||
return (
|
||||
<div className={`quest-card quest-${quest.status}`}>
|
||||
|
||||
Reference in New Issue
Block a user