import { ChevronRight, ReceiptText } from 'lucide-react';
import {
type FocusEvent,
type MouseEvent,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import type { ProfileMudPointBalance } from '../contracts/runtime';
import MUD_POINT_ICON_SRC from '../icons/topbar-wallet.png';
import { formatMudPointCount } from '../utils/format';
export type PlatformMudPointWalletEntryProps = {
balance: number | null;
breakdown?: ProfileMudPointBalance | null;
isLoading?: boolean;
error?: string | null;
variant?: 'desktop' | 'mobile' | 'editor';
className?: string;
onRequestDetails: () => void;
onRecharge: () => void;
onOpenLedger: () => void;
};
function MudPointBalanceRow({
label,
points,
detail,
}: {
label: string;
points: number;
detail?: string | null;
}) {
return (
{label}
{detail ? (
{detail}
) : null}
{formatMudPointCount(points)}
);
}
export function PlatformMudPointWalletEntry({
balance,
breakdown,
isLoading = false,
variant = 'desktop',
className,
onRequestDetails,
onRecharge,
onOpenLedger,
}: PlatformMudPointWalletEntryProps) {
const rootRef = useRef(null);
const isOpenRef = useRef(false);
const requestedDetailsKeyForOpenRef = useRef(null);
const closeTimerRef = useRef(null);
const [isOpen, setIsOpen] = useState(false);
const isCompact = variant === 'mobile';
const displayedBalance = balance ?? breakdown?.totalPoints ?? null;
const balanceLabel =
displayedBalance === null
? '--'
: formatMudPointCount(displayedBalance, true);
const exactBalanceLabel =
displayedBalance === null ? '--' : formatMudPointCount(displayedBalance);
const detailsRequestKey = `balance:${balance ?? breakdown?.totalPoints ?? 'unknown'}`;
const cancelPendingClose = useCallback(() => {
if (closeTimerRef.current !== null) {
window.clearTimeout(closeTimerRef.current);
closeTimerRef.current = null;
}
}, []);
const closeDetails = useCallback(() => {
cancelPendingClose();
isOpenRef.current = false;
requestedDetailsKeyForOpenRef.current = null;
setIsOpen(false);
}, [cancelPendingClose]);
const requestDetailsIfNeeded = useCallback(() => {
if (
isLoading ||
requestedDetailsKeyForOpenRef.current === detailsRequestKey
) {
return;
}
requestedDetailsKeyForOpenRef.current = detailsRequestKey;
onRequestDetails();
}, [detailsRequestKey, isLoading, onRequestDetails]);
const requestAndOpen = useCallback(() => {
cancelPendingClose();
if (isOpenRef.current) {
return;
}
isOpenRef.current = true;
setIsOpen(true);
requestDetailsIfNeeded();
}, [cancelPendingClose, requestDetailsIfNeeded]);
useEffect(() => cancelPendingClose, [cancelPendingClose]);
useEffect(() => {
if (isOpen) {
requestDetailsIfNeeded();
}
}, [isOpen, requestDetailsIfNeeded]);
useEffect(() => {
if (!isOpen) {
return;
}
const handlePointerDown = (event: PointerEvent) => {
if (!rootRef.current?.contains(event.target as Node)) {
closeDetails();
}
};
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
closeDetails();
}
};
document.addEventListener('pointerdown', handlePointerDown);
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('pointerdown', handlePointerDown);
document.removeEventListener('keydown', handleKeyDown);
};
}, [closeDetails, isOpen]);
const closeAfterFocusLeaves = (event: FocusEvent) => {
const nextTarget = event.relatedTarget;
if (
!(nextTarget instanceof Node) ||
!event.currentTarget.contains(nextTarget)
) {
closeDetails();
}
};
const closeAfterPointerLeaves = (event: MouseEvent) => {
const nextTarget = event.relatedTarget;
if (
nextTarget instanceof Node &&
event.currentTarget.contains(nextTarget)
) {
return;
}
cancelPendingClose();
closeTimerRef.current = window.setTimeout(() => {
closeTimerRef.current = null;
closeDetails();
}, 120);
};
return (
{isOpen ? (
泥点 {exactBalanceLabel}
{breakdown ? (
<>
>
) : (
{isLoading ? '余额读取中' : '余额明细暂不可用'}
)}
) : null}
);
}