feat: add jackpot animations and enhance currency handling across components
- Introduced new CSS animations for jackpot effects to improve visual engagement. - Integrated CurrencySwitcher into PlayerPanel and HallScreen for better currency management. - Updated various components to utilize active player currency for consistent display. - Enhanced event handling for currency changes to ensure real-time updates across the application.
This commit is contained in:
@@ -21,14 +21,13 @@ import {
|
||||
ticketNumberSpec,
|
||||
} from "@/features/hall/hall-bet-rules";
|
||||
import type { HallDrawLiveSnapshot } from "@/features/hall/use-hall-draw-live";
|
||||
import { useCurrencyCatalog } from "@/hooks/use-currency-catalog";
|
||||
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
|
||||
import { triggerWalletPollingAfterBet } from "@/hooks/use-wallet-polling";
|
||||
import { getLotteryEcho } from "@/lib/lottery-echo";
|
||||
import { getLotteryRequestLocale } from "@/lib/lottery-locale";
|
||||
import { formatMinorAsCurrency, parseDecimalInputToMinor } from "@/lib/money";
|
||||
import { resolvePlayerCurrency } from "@/lib/player-currency";
|
||||
import { PLAYER_CURRENCY_CHANGE_EVENT } from "@/lib/player-currency-preference";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { usePlayerSessionStore } from "@/stores/player-session-store";
|
||||
import { LotteryApiBizError } from "@/types/api/errors";
|
||||
import type { PlayEffectivePayload, PlayEffectivePlayRow } from "@/types/api/play-effective";
|
||||
import type { TicketLineInput, TicketPlaceData, TicketPreviewData } from "@/types/api/ticket";
|
||||
@@ -77,6 +76,12 @@ type OddsUpdateWsEvent = {
|
||||
message?: string;
|
||||
};
|
||||
|
||||
type RiskSoldOutWsEvent = {
|
||||
draw_id?: number;
|
||||
draw_no?: string;
|
||||
normalized_number?: string;
|
||||
};
|
||||
|
||||
type CellRiskState = "open" | "warning" | "sold_out";
|
||||
type QuickFillState = Record<HallCategory, { favorites: string[]; history: string[] }>;
|
||||
|
||||
@@ -128,10 +133,7 @@ function isPlayOpenForPlayer(row: PlayEffectivePlayRow): boolean {
|
||||
}
|
||||
|
||||
function pickDisplayName(row: PlayEffectivePlayRow): string {
|
||||
const loc = getLotteryRequestLocale();
|
||||
if (loc === "zh") return row.display_name_zh ?? row.display_name_en ?? row.play_code;
|
||||
if (loc === "ne") return row.display_name_ne ?? row.display_name_en ?? row.play_code;
|
||||
return row.display_name_en ?? row.display_name_zh ?? row.play_code;
|
||||
return row.display_name?.trim() || row.play_code;
|
||||
}
|
||||
|
||||
function digitSlotOptions(category: Exclude<HallCategory, "JACKPOT">): number[] {
|
||||
@@ -367,13 +369,19 @@ function cellRiskState(
|
||||
rowNumber: string,
|
||||
category: Exclude<HallCategory, "JACKPOT">,
|
||||
alertRows: DrawCurrentRiskPoolAlert[] | undefined,
|
||||
liveSoldOutNumbers: ReadonlySet<string>,
|
||||
digitSlot?: number,
|
||||
): CellRiskState {
|
||||
const alerts = alertRows ?? [];
|
||||
if (alerts.length === 0) return "open";
|
||||
const normalizedRow = rowNumber.trim().toUpperCase();
|
||||
if (!normalizedRow) return "open";
|
||||
|
||||
if (liveSoldOutNumbers.has(normalizedRow)) {
|
||||
return "sold_out";
|
||||
}
|
||||
|
||||
const alerts = alertRows ?? [];
|
||||
if (alerts.length === 0) return "open";
|
||||
|
||||
for (const alert of alerts) {
|
||||
if (matchesRiskAlert(alert.normalized_number, play.play_code, normalizedRow, category, digitSlot)) {
|
||||
return alert.is_sold_out ? "sold_out" : "warning";
|
||||
@@ -393,7 +401,7 @@ function quickFillKeys(category: HallCategory): { favorites: string; history: st
|
||||
export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }) {
|
||||
const { display, isBettable, reload: reloadDraw } = drawLive;
|
||||
const { t } = useTranslation("player");
|
||||
const profile = usePlayerSessionStore((s) => s.profile);
|
||||
const { activeCurrency: currencyParam } = useActivePlayerCurrency();
|
||||
|
||||
const [activeCategory, setActiveCategory] = useState<HallCategory>("D2");
|
||||
const [rows, setRows] = useState<DraftRow[]>(() => [newDraftRow()]);
|
||||
@@ -411,16 +419,13 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
const [resultOpen, setResultOpen] = useState(false);
|
||||
const [resultData, setResultData] = useState<TicketPlaceData | null>(null);
|
||||
const [quickFillState, setQuickFillState] = useState<QuickFillState>(() => loadQuickFillState());
|
||||
const [liveSoldOutNumbers, setLiveSoldOutNumbers] = useState<Set<string>>(() => new Set());
|
||||
const [debouncedSummary, setDebouncedSummary] = useState({ bet: 0, rebate: 0, actual: 0 });
|
||||
const holdFavoriteRef = useRef<{ timer: number | null; number: string | null; longPress: boolean }>({
|
||||
timer: null,
|
||||
number: null,
|
||||
longPress: false,
|
||||
});
|
||||
useCurrencyCatalog();
|
||||
|
||||
const currencyParam = useMemo(() => resolvePlayerCurrency(profile), [profile]);
|
||||
|
||||
const loadCatalog = useCallback(async () => {
|
||||
setCatalogState((s) => (s.kind === "ok" ? s : { kind: "loading" }));
|
||||
try {
|
||||
@@ -448,6 +453,15 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
});
|
||||
}, [loadCatalog, refreshWallet]);
|
||||
|
||||
useEffect(() => {
|
||||
const onCurrencyChange = () => {
|
||||
void loadCatalog();
|
||||
void refreshWallet();
|
||||
};
|
||||
window.addEventListener(PLAYER_CURRENCY_CHANGE_EVENT, onCurrencyChange);
|
||||
return () => window.removeEventListener(PLAYER_CURRENCY_CHANGE_EVENT, onCurrencyChange);
|
||||
}, [loadCatalog, refreshWallet]);
|
||||
|
||||
useEffect(() => {
|
||||
const id = window.setInterval(() => {
|
||||
void loadCatalog();
|
||||
@@ -490,6 +504,12 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
[activeRowId, rows],
|
||||
);
|
||||
|
||||
const drawNo = display?.draw_no ?? null;
|
||||
|
||||
useEffect(() => {
|
||||
setLiveSoldOutNumbers(new Set());
|
||||
}, [drawNo]);
|
||||
|
||||
const alertRows = display?.risk_pool_alerts ?? [];
|
||||
const jackpot = display?.jackpot;
|
||||
const currentQuickFill = quickFillState[activeCategory] ?? { favorites: [], history: [] };
|
||||
@@ -657,14 +677,30 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
toast.message(evt.message ?? t("hall.playConfig.oddsUpdated"));
|
||||
};
|
||||
|
||||
const onRiskSoldOut = (evt: RiskSoldOutWsEvent) => {
|
||||
const normalized = evt.normalized_number?.trim().toUpperCase();
|
||||
if (!normalized) return;
|
||||
if (drawNo !== null && evt.draw_no !== undefined && evt.draw_no !== drawNo) {
|
||||
return;
|
||||
}
|
||||
setLiveSoldOutNumbers((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(normalized);
|
||||
return next;
|
||||
});
|
||||
void reloadDraw();
|
||||
};
|
||||
|
||||
channel.listen(".play.toggle", onPlayToggle);
|
||||
channel.listen(".odds.update", onOddsUpdate);
|
||||
channel.listen(".risk.sold_out", onRiskSoldOut);
|
||||
|
||||
return () => {
|
||||
channel.stopListening(".play.toggle");
|
||||
channel.stopListening(".odds.update");
|
||||
channel.stopListening(".risk.sold_out");
|
||||
};
|
||||
}, [clearAmountsForPlay, loadCatalog, t]);
|
||||
}, [clearAmountsForPlay, drawNo, loadCatalog, reloadDraw, t]);
|
||||
|
||||
const collectEntries = useCallback((): DraftEntry[] => {
|
||||
if (activeCategory === "JACKPOT") return [];
|
||||
@@ -1163,6 +1199,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
row.number,
|
||||
activeCategory as Exclude<HallCategory, "JACKPOT">,
|
||||
alertRows,
|
||||
liveSoldOutNumbers,
|
||||
column.digitSlot,
|
||||
);
|
||||
const disabled = tableDisabled || status === "sold_out" || (play.config !== null && !play.config.is_enabled);
|
||||
|
||||
Reference in New Issue
Block a user