Files
elysium/apps/web/src/components/ui/ResourceBar.tsx
T
hikari 5aae3eb389 feat: show save errors in the UI instead of console
Add syncError state to GameContext. forceSync now catches errors
and displays them in the ResourceBar for 5 seconds, replacing the
cloud save timestamp with ' Save failed'. Signature mismatches
are also cleared from localStorage so the next save can proceed.
Auto-save silently self-heals bad signatures without surfacing an error.
2026-03-06 19:18:29 -08:00

116 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { Resource } from "@elysium/types";
import { useGame } from "../../context/GameContext.js";
import { RESOURCE_CAP } from "../../engine/tick.js";
interface ResourceBarProps {
resources: Resource;
prestigeCount: number;
profileUrl: string;
onEditProfile: () => void;
lastSavedAt: number | null;
isSyncing: boolean;
onForceSync: () => Promise<void>;
}
const formatRelativeTime = (timestamp: number): string => {
const seconds = Math.floor((Date.now() - timestamp) / 1000);
if (seconds < 10) return "just now";
if (seconds < 60) return `${seconds}s ago`;
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
return `${hours}h ago`;
};
const RESOURCE_FULL_TOOLTIP = "This resource is full! Consider spending some or prestiging to keep earning.";
export const ResourceBar = ({
resources,
prestigeCount,
profileUrl,
onEditProfile,
lastSavedAt,
isSyncing,
onForceSync,
}: ResourceBarProps): React.JSX.Element => {
const { formatNumber, syncError } = useGame();
const anyFull = Object.values(resources).some((v) => v >= RESOURCE_CAP);
return (
<>
<header className="resource-bar">
<div className={`resource${resources.gold >= RESOURCE_CAP ? " resource-full" : ""}`}>
<span className="resource-icon">🪙</span>
<span className="resource-value">{formatNumber(resources.gold)}</span>
<span className="resource-label">Gold</span>
{resources.gold >= RESOURCE_CAP && <span className="resource-cap-badge" title={RESOURCE_FULL_TOOLTIP}>FULL</span>}
</div>
<div className={`resource${resources.essence >= RESOURCE_CAP ? " resource-full" : ""}`}>
<span className="resource-icon"></span>
<span className="resource-value">{formatNumber(resources.essence)}</span>
<span className="resource-label">Essence</span>
{resources.essence >= RESOURCE_CAP && <span className="resource-cap-badge" title={RESOURCE_FULL_TOOLTIP}>FULL</span>}
</div>
<div className={`resource${resources.crystals >= RESOURCE_CAP ? " resource-full" : ""}`}>
<span className="resource-icon">💎</span>
<span className="resource-value">{formatNumber(resources.crystals)}</span>
<span className="resource-label">Crystals</span>
{resources.crystals >= RESOURCE_CAP && <span className="resource-cap-badge" title={RESOURCE_FULL_TOOLTIP}>FULL</span>}
</div>
<div className={`resource${resources.runestones >= RESOURCE_CAP ? " resource-full" : ""}`}>
<span className="resource-icon">🔮</span>
<span className="resource-value">{formatNumber(resources.runestones)}</span>
<span className="resource-label">Runestones</span>
{resources.runestones >= RESOURCE_CAP && <span className="resource-cap-badge" title={RESOURCE_FULL_TOOLTIP}>FULL</span>}
</div>
{prestigeCount > 0 && (
<div className="prestige-badge">
Prestige {prestigeCount}
</div>
)}
<div className="profile-buttons">
{syncError !== null ? (
<span className="save-status save-error" title={syncError}>
Save failed
</span>
) : lastSavedAt !== null ? (
<span className="save-status" title={new Date(lastSavedAt).toLocaleString()}>
{formatRelativeTime(lastSavedAt)}
</span>
) : null}
<button
className="force-save-button"
disabled={isSyncing}
onClick={onForceSync}
title="Force cloud save"
type="button"
>
{isSyncing ? "⏳" : "💾"}
</button>
<a
className="profile-link-button"
href={profileUrl}
rel="noreferrer"
target="_blank"
title="View your public profile"
>
👤 Profile
</a>
<button
className="profile-edit-button"
onClick={onEditProfile}
title="Edit your profile"
type="button"
>
</button>
</div>
</header>
{anyFull && (
<div className="resource-cap-notice">
One or more resources are full! Consider spending some or prestiging to keep earning.
</div>
)}
</>
);
};