feat(player): PC 端列表/布局适配与分页
玩家端桌面:全宽居中 Logo、圆角侧栏与主内容面板;开奖结果/注单改 shadcn Table+分页(移动仍无限滚动),注单组默认折叠;信用/钱包筛选与分页;详情页与奖号网格密度优化。
This commit is contained in:
@@ -11,6 +11,14 @@ import { getWalletBalance } from "@/api/wallet";
|
||||
import { postTicketPlace, postTicketPreview } from "@/api/ticket";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { isHallSealedCountdownUi } from "@/features/draw/draw-status-meta";
|
||||
@@ -25,6 +33,13 @@ import {
|
||||
ticketNumberSpec,
|
||||
type DraftLineIssueReason,
|
||||
} from "@/features/hall/hall-bet-rules";
|
||||
import {
|
||||
isHighCostSelectionType,
|
||||
resolveSelectionTotalBet,
|
||||
selectionCombinationCount,
|
||||
selectionTypesForCategory,
|
||||
type SelectionType,
|
||||
} from "@/features/hall/selection-type";
|
||||
import type { HallDrawLiveSnapshot } from "@/features/hall/use-hall-draw-live";
|
||||
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
|
||||
import { triggerWalletPollingAfterBet } from "@/hooks/use-wallet-polling";
|
||||
@@ -64,9 +79,13 @@ type DraftRow = {
|
||||
number: string;
|
||||
amounts: Record<string, string>;
|
||||
providerCodes: string[];
|
||||
selectionType: "straight" | "reverse" | "full_cover" | "full_play" | "half_play";
|
||||
selectionType: SelectionType;
|
||||
};
|
||||
|
||||
type PendingSelectionChange =
|
||||
| { mode: "row"; rowId: string; next: SelectionType; prev: SelectionType }
|
||||
| { mode: "all"; next: SelectionType };
|
||||
|
||||
type DraftEntry = {
|
||||
rowId: string;
|
||||
rowNo: number;
|
||||
@@ -174,9 +193,24 @@ const D4_PLAY_ORDER = [
|
||||
"digit_big",
|
||||
"digit_small",
|
||||
] as const;
|
||||
/** 按大厅 Tab 对应的玩法顺序;汇总条默认以当前 Tab 玩法开头 */
|
||||
const PLAY_ORDER_BY_CATEGORY: Record<PlayHallCategory, readonly string[]> = {
|
||||
D2: D2_PLAY_ORDER,
|
||||
D3: D3_PLAY_ORDER,
|
||||
D4: D4_PLAY_ORDER,
|
||||
};
|
||||
const CATEGORY_ORDER: readonly PlayHallCategory[] = ["D4", "D3", "D2"];
|
||||
const MOBILE_QUICK_AMOUNT_PRESETS = ["10", "50", "100"] as const;
|
||||
const DEFAULT_DRAFT_ROW_COUNT = 20;
|
||||
|
||||
function playOrderForActiveCategory(activeCategory: PlayHallCategory): readonly string[] {
|
||||
const rest = CATEGORY_ORDER.filter((category) => category !== activeCategory);
|
||||
return [
|
||||
...PLAY_ORDER_BY_CATEGORY[activeCategory],
|
||||
...rest.flatMap((category) => PLAY_ORDER_BY_CATEGORY[category]),
|
||||
];
|
||||
}
|
||||
|
||||
function newDraftRows(count = DEFAULT_DRAFT_ROW_COUNT): DraftRow[] {
|
||||
return Array.from({ length: count }, newDraftRow);
|
||||
}
|
||||
@@ -290,7 +324,7 @@ function lineForPlay(
|
||||
displayNumber: string,
|
||||
amountMinor: number,
|
||||
digitSlot?: number,
|
||||
selectionType: DraftRow["selectionType"] = "straight",
|
||||
selectionType: SelectionType = "straight",
|
||||
): TicketLineInput | null {
|
||||
const number = normalizeNumberForPlay(displayNumber, play.play_code);
|
||||
if (draftLineIssueReason(play.play_code, displayNumber, digitSlot) !== null) {
|
||||
@@ -504,6 +538,9 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
const [liveSoldOutNumbers, setLiveSoldOutNumbers] = useState<Set<string>>(() => new Set());
|
||||
const [liveWarningNumbers, setLiveWarningNumbers] = useState<Set<string>>(() => new Set());
|
||||
const [debouncedSummary, setDebouncedSummary] = useState({ bet: 0, rebate: 0, actual: 0 });
|
||||
const [pendingSelectionChange, setPendingSelectionChange] = useState<PendingSelectionChange | null>(
|
||||
null,
|
||||
);
|
||||
const holdFavoriteRef = useRef<{ timer: number | null; number: string | null; longPress: boolean }>({
|
||||
timer: null,
|
||||
number: null,
|
||||
@@ -511,6 +548,8 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
});
|
||||
/** 单次预览→确认共用,重试 place 复用,避免重复扣款 */
|
||||
const placeTraceIdRef = useRef<string | null>(null);
|
||||
/** 玩法汇总条:切 Tab 后滚回左侧,保证当前维度玩法从开头可见 */
|
||||
const playSummaryScrollRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const newPlaceTraceId = (): string =>
|
||||
typeof crypto !== "undefined" && crypto.randomUUID
|
||||
@@ -607,7 +646,8 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
|
||||
const openPlays = useMemo(() => {
|
||||
if (catalogState.kind !== "ok") return [];
|
||||
const order: readonly string[] = [...D2_PLAY_ORDER, ...D3_PLAY_ORDER, ...D4_PLAY_ORDER];
|
||||
// 4D / 3D / 2D 汇总条与列顺序以当前 Tab 玩法为先,避免切到 4D 仍从 2A 起显示
|
||||
const order = playOrderForActiveCategory(activeCategory);
|
||||
const orderSet = new Set(order);
|
||||
return sortByPlayOrder(
|
||||
catalogState.data.plays
|
||||
@@ -615,7 +655,12 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
.filter((p) => orderSet.has(p.play_code)),
|
||||
order,
|
||||
);
|
||||
}, [catalogState]);
|
||||
}, [activeCategory, catalogState]);
|
||||
|
||||
useEffect(() => {
|
||||
const el = playSummaryScrollRef.current;
|
||||
if (el) el.scrollLeft = 0;
|
||||
}, [activeCategory]);
|
||||
|
||||
const activeCategoryPlays = useMemo(
|
||||
() => openPlays.filter((play) => TRADITIONAL_PLAY_CODES[activeCategory].includes(play.play_code)),
|
||||
@@ -712,14 +757,57 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
setActiveRowId(id);
|
||||
}, [activeCategory]);
|
||||
|
||||
const updateRowSelectionType = useCallback((id: string, selectionType: DraftRow["selectionType"]) => {
|
||||
setRows((current) => current.map((row) => row.id === id ? { ...row, selectionType } : row));
|
||||
const applyRowSelectionType = useCallback((id: string, selectionType: SelectionType) => {
|
||||
setRows((current) => current.map((row) => (row.id === id ? { ...row, selectionType } : row)));
|
||||
}, []);
|
||||
|
||||
const updateAllSelectionTypes = useCallback((selectionType: DraftRow["selectionType"]) => {
|
||||
const applyAllSelectionTypes = useCallback((selectionType: SelectionType) => {
|
||||
setRows((current) => current.map((row) => ({ ...row, selectionType })));
|
||||
}, []);
|
||||
|
||||
const requestRowSelectionType = useCallback(
|
||||
(id: string, selectionType: SelectionType) => {
|
||||
const row = rows.find((item) => item.id === id);
|
||||
if (!row || row.selectionType === selectionType) return;
|
||||
if (isHighCostSelectionType(selectionType) && !isHighCostSelectionType(row.selectionType)) {
|
||||
setPendingSelectionChange({
|
||||
mode: "row",
|
||||
rowId: id,
|
||||
next: selectionType,
|
||||
prev: row.selectionType,
|
||||
});
|
||||
return;
|
||||
}
|
||||
applyRowSelectionType(id, selectionType);
|
||||
},
|
||||
[applyRowSelectionType, rows],
|
||||
);
|
||||
|
||||
const requestAllSelectionTypes = useCallback(
|
||||
(selectionType: SelectionType) => {
|
||||
if (isHighCostSelectionType(selectionType)) {
|
||||
setPendingSelectionChange({ mode: "all", next: selectionType });
|
||||
return;
|
||||
}
|
||||
applyAllSelectionTypes(selectionType);
|
||||
},
|
||||
[applyAllSelectionTypes],
|
||||
);
|
||||
|
||||
const confirmPendingSelectionChange = useCallback(() => {
|
||||
if (!pendingSelectionChange) return;
|
||||
if (pendingSelectionChange.mode === "row") {
|
||||
applyRowSelectionType(pendingSelectionChange.rowId, pendingSelectionChange.next);
|
||||
} else {
|
||||
applyAllSelectionTypes(pendingSelectionChange.next);
|
||||
}
|
||||
setPendingSelectionChange(null);
|
||||
}, [applyAllSelectionTypes, applyRowSelectionType, pendingSelectionChange]);
|
||||
|
||||
const cancelPendingSelectionChange = useCallback(() => {
|
||||
setPendingSelectionChange(null);
|
||||
}, []);
|
||||
|
||||
const updateAmount = useCallback((rowId: string, playCode: string, value: string) => {
|
||||
const amount = sanitizeAmount(value);
|
||||
const column = allPlayColumns.find((item) => item.key === playCode);
|
||||
@@ -1076,18 +1164,84 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
const draftSummary = useMemo(() => {
|
||||
return draftEntries.reduce(
|
||||
(acc, entry) => {
|
||||
const selectionType = entry.line.selection_type ?? "straight";
|
||||
const comboCount = selectionCombinationCount(entry.number, selectionType);
|
||||
const totalBet = resolveSelectionTotalBet(entry.amountMinor, selectionType, comboCount);
|
||||
const rebateRate = parseRebateRate(entry.play.odds?.rebate_rate);
|
||||
const rebate = Math.round(entry.amountMinor * rebateRate);
|
||||
const rebate = Math.round(totalBet * rebateRate);
|
||||
const providerCount = entry.line.provider_codes?.length ?? 0;
|
||||
acc.bet += entry.amountMinor * providerCount;
|
||||
acc.bet += totalBet * providerCount;
|
||||
acc.rebate += rebate * providerCount;
|
||||
acc.actual += Math.max(0, entry.amountMinor - rebate) * providerCount;
|
||||
acc.actual += Math.max(0, totalBet - rebate) * providerCount;
|
||||
return acc;
|
||||
},
|
||||
{ bet: 0, rebate: 0, actual: 0 },
|
||||
);
|
||||
}, [draftEntries]);
|
||||
|
||||
const selectionTypeOptions = useMemo(
|
||||
() => selectionTypesForCategory(activeCategory),
|
||||
[activeCategory],
|
||||
);
|
||||
|
||||
const pendingSelectionPreview = useMemo(() => {
|
||||
if (!pendingSelectionChange) return null;
|
||||
const next = pendingSelectionChange.next;
|
||||
|
||||
const rowStake = (
|
||||
row: DraftRow,
|
||||
selectionType: SelectionType,
|
||||
mode: "resolved" | "raw",
|
||||
): number => {
|
||||
const comboCount = selectionCombinationCount(row.number, selectionType);
|
||||
const providerCount = Math.max(1, row.providerCodes.length);
|
||||
const stake = playColumns.reduce((total, column) => {
|
||||
if (draftLineIssueReason(column.play.play_code, row.number, column.digitSlot) !== null) {
|
||||
return total;
|
||||
}
|
||||
const amount = parseDecimalInputToMinor(row.amounts[column.key] ?? "", currencyCode) ?? 0;
|
||||
if (amount <= 0) return total;
|
||||
if (mode === "raw") return total + amount;
|
||||
return total + resolveSelectionTotalBet(amount, selectionType, comboCount);
|
||||
}, 0);
|
||||
return stake * providerCount;
|
||||
};
|
||||
|
||||
if (pendingSelectionChange.mode === "row") {
|
||||
const row = rows.find((item) => item.id === pendingSelectionChange.rowId);
|
||||
if (!row) return null;
|
||||
const comboCount = selectionCombinationCount(row.number, next);
|
||||
const fromMinor = rowStake(row, pendingSelectionChange.prev, "resolved");
|
||||
const toMinor =
|
||||
next === "full_cover" ? rowStake(row, next, "raw") : rowStake(row, next, "resolved");
|
||||
return {
|
||||
next,
|
||||
comboCount,
|
||||
number: row.number,
|
||||
fromMinor,
|
||||
toMinor,
|
||||
scope: "row" as const,
|
||||
};
|
||||
}
|
||||
|
||||
let fromMinor = 0;
|
||||
let toMinor = 0;
|
||||
let maxCombo = 1;
|
||||
rows.forEach((row) => {
|
||||
maxCombo = Math.max(maxCombo, selectionCombinationCount(row.number, next));
|
||||
fromMinor += rowStake(row, row.selectionType, "resolved");
|
||||
toMinor += next === "full_cover" ? rowStake(row, next, "raw") : rowStake(row, next, "resolved");
|
||||
});
|
||||
return {
|
||||
next,
|
||||
comboCount: maxCombo,
|
||||
number: "",
|
||||
fromMinor,
|
||||
toMinor,
|
||||
scope: "all" as const,
|
||||
};
|
||||
}, [currencyCode, pendingSelectionChange, playColumns, rows]);
|
||||
|
||||
useEffect(() => {
|
||||
const id = window.setTimeout(() => {
|
||||
setDebouncedSummary(draftSummary);
|
||||
@@ -1377,30 +1531,105 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"bg-white",
|
||||
isMobile
|
||||
? "rounded-lg border border-[#e8eef7] px-3 py-2.5"
|
||||
: "rounded-xl border border-[#e3ebf7] px-3 py-3 shadow-[0_8px_24px_rgba(15,23,42,0.045)]",
|
||||
!isMobile &&
|
||||
"flex items-center gap-2 rounded-xl border border-[#e3ebf7] bg-white px-2 py-2 shadow-[0_6px_18px_rgba(15,23,42,0.04)]",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-bold leading-5 text-slate-950">
|
||||
{t("hall.quickFill.title")}
|
||||
</p>
|
||||
<div
|
||||
className={cn(
|
||||
"bg-white",
|
||||
isMobile
|
||||
? "rounded-lg border border-[#e8eef7] px-3 py-2.5"
|
||||
: "min-w-0 flex-1 px-2 py-0.5",
|
||||
)}
|
||||
>
|
||||
<div className={cn("flex gap-3", isMobile ? "items-start justify-between" : "items-center justify-between")}>
|
||||
<div className={cn("min-w-0", !isMobile && "flex flex-1 items-center gap-4")}>
|
||||
<div className={cn("shrink-0", !isMobile && "min-w-[5.5rem]")}>
|
||||
<p className="text-sm font-bold leading-5 text-slate-950">
|
||||
{t("hall.quickFill.title")}
|
||||
</p>
|
||||
{isMobile ? (
|
||||
<p className="mt-0.5 text-[11px] leading-5 text-slate-500">
|
||||
{t("hall.mobile.quickFillSummary", {
|
||||
defaultValue: "收藏 {{favorites}} 个,历史 {{history}} 个",
|
||||
favorites: favorites.length,
|
||||
history: historyNumbers.length,
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{!isMobile ? (
|
||||
<p className="mt-0.5 text-xs leading-5 text-slate-500">
|
||||
{t("hall.quickFill.description")}
|
||||
</p>
|
||||
) : (
|
||||
<p className="mt-0.5 text-[11px] leading-5 text-slate-500">
|
||||
{t("hall.mobile.quickFillSummary", {
|
||||
defaultValue: "收藏 {{favorites}} 个,历史 {{history}} 个",
|
||||
favorites: favorites.length,
|
||||
history: historyNumbers.length,
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-x-4 gap-y-1.5">
|
||||
{favoriteChips.length > 0 ? (
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
<span className="mr-0.5 text-[11px] font-semibold text-[#d81435]">
|
||||
{t("hall.quickFill.favorites")}
|
||||
</span>
|
||||
{favoriteChips.map((number) => (
|
||||
<button
|
||||
key={`fav-${number}`}
|
||||
type="button"
|
||||
className="inline-flex h-7 touch-manipulation items-center gap-1 rounded-full border border-[#ffd7db] bg-[#fff3f5] px-2.5 text-xs font-semibold text-[#d81435] transition-colors hover:bg-[#ffe9ed]"
|
||||
onPointerDown={() => {
|
||||
const current = holdFavoriteRef.current;
|
||||
current.number = number;
|
||||
current.longPress = false;
|
||||
if (current.timer) window.clearTimeout(current.timer);
|
||||
current.timer = window.setTimeout(() => {
|
||||
current.longPress = true;
|
||||
toggleFavoriteNumber(number);
|
||||
}, 500);
|
||||
}}
|
||||
onPointerUp={() => {
|
||||
const current = holdFavoriteRef.current;
|
||||
if (current.timer) {
|
||||
window.clearTimeout(current.timer);
|
||||
current.timer = null;
|
||||
}
|
||||
if (!current.longPress) {
|
||||
fillCurrentRow(number);
|
||||
}
|
||||
current.longPress = false;
|
||||
}}
|
||||
onPointerLeave={() => {
|
||||
const current = holdFavoriteRef.current;
|
||||
if (current.timer) {
|
||||
window.clearTimeout(current.timer);
|
||||
current.timer = null;
|
||||
}
|
||||
current.longPress = false;
|
||||
}}
|
||||
>
|
||||
{number}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-1.5">
|
||||
<span className="mr-0.5 text-[11px] font-semibold text-slate-400">
|
||||
{t("hall.quickFill.history")}
|
||||
</span>
|
||||
{historyChips.length > 0 ? (
|
||||
historyChips.map((number) => (
|
||||
<button
|
||||
key={`his-${number}`}
|
||||
type="button"
|
||||
className="inline-flex h-7 min-w-9 items-center justify-center rounded-full border border-[#d7e5f8] bg-[#f8fbff] px-2.5 text-xs font-bold text-[#07459f] transition-colors hover:border-[#b9d0f3] hover:bg-[#eef6ff]"
|
||||
onClick={() => fillCurrentRow(number)}
|
||||
>
|
||||
{number}
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<span className="inline-flex h-7 items-center rounded-full border border-dashed border-slate-200 bg-slate-50 px-2.5 text-xs text-slate-400">
|
||||
{t("hall.quickFill.emptyHistory")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-1.5">
|
||||
{isMobile ? (
|
||||
@@ -1425,167 +1654,176 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
</Button>
|
||||
) : null}
|
||||
{activeRow?.number ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label={
|
||||
favorites.includes(activeRow.number)
|
||||
? t("hall.quickFill.unfavorite")
|
||||
: t("hall.quickFill.favorite")
|
||||
}
|
||||
aria-pressed={favorites.includes(activeRow.number)}
|
||||
title={
|
||||
favorites.includes(activeRow.number)
|
||||
? t("hall.quickFill.unfavorite")
|
||||
: t("hall.quickFill.favorite")
|
||||
}
|
||||
className={cn(
|
||||
"size-8 rounded-full border border-[#ffd7db] bg-[#fff7f8] text-[#d81435] hover:bg-[#fff1f3] hover:text-[#b80f2b]",
|
||||
favorites.includes(activeRow.number) &&
|
||||
"border-[#d81435] bg-[#d81435] text-white hover:bg-[#c51230] hover:text-white",
|
||||
)}
|
||||
onClick={() => toggleFavoriteNumber(activeRow.number)}
|
||||
>
|
||||
<Star
|
||||
className={cn(
|
||||
"size-4",
|
||||
favorites.includes(activeRow.number) && "fill-current",
|
||||
)}
|
||||
aria-hidden
|
||||
/>
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label={t("hall.quickFill.clearAll")}
|
||||
title={t("hall.quickFill.clearAll")}
|
||||
className="size-8 rounded-full border border-[#dfe6f0] bg-[#f8fafc] text-slate-500 hover:bg-slate-100 hover:text-slate-900"
|
||||
onClick={clearAllRows}
|
||||
>
|
||||
<Trash2 className="size-4" aria-hidden />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label={
|
||||
favorites.includes(activeRow.number)
|
||||
? t("hall.quickFill.unfavorite")
|
||||
: t("hall.quickFill.favorite")
|
||||
}
|
||||
aria-pressed={favorites.includes(activeRow.number)}
|
||||
title={
|
||||
favorites.includes(activeRow.number)
|
||||
? t("hall.quickFill.unfavorite")
|
||||
: t("hall.quickFill.favorite")
|
||||
}
|
||||
className={cn(
|
||||
"size-8 rounded-full border border-[#ffd7db] bg-[#fff7f8] text-[#d81435] hover:bg-[#fff1f3] hover:text-[#b80f2b]",
|
||||
favorites.includes(activeRow.number) &&
|
||||
"border-[#d81435] bg-[#d81435] text-white hover:bg-[#c51230] hover:text-white",
|
||||
)}
|
||||
onClick={() => toggleFavoriteNumber(activeRow.number)}
|
||||
>
|
||||
<Star
|
||||
className={cn(
|
||||
"size-4",
|
||||
favorites.includes(activeRow.number) && "fill-current",
|
||||
)}
|
||||
aria-hidden
|
||||
/>
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label={t("hall.quickFill.clearAll")}
|
||||
title={t("hall.quickFill.clearAll")}
|
||||
className="size-8 rounded-full border border-[#dfe6f0] bg-[#f8fafc] text-slate-500 hover:bg-slate-100 hover:text-slate-900"
|
||||
onClick={clearAllRows}
|
||||
>
|
||||
<Trash2 className="size-4" aria-hidden />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={cn("space-y-2", isMobile ? "mt-2" : "mt-3")}>
|
||||
{isMobile && quickFillExpanded ? (
|
||||
<div className="rounded-xl border border-[#e7eef8] bg-[#f8fbff] px-3 py-2.5">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-[11px] font-semibold text-[#5b6f95]">
|
||||
{t("hall.quickFill.batchTitle", { defaultValue: "Quick amount" })}
|
||||
</span>
|
||||
{MOBILE_QUICK_AMOUNT_PRESETS.map((preset) => (
|
||||
<button
|
||||
key={preset}
|
||||
type="button"
|
||||
disabled={tableDisabled}
|
||||
onClick={() => applyQuickAmountToActiveRow(preset)}
|
||||
className="inline-flex h-8 min-w-12 items-center justify-center rounded-full border border-[#cfe0fb] bg-white px-3 text-xs font-bold text-[#0b4ab3] transition-colors hover:border-[#aac7f5] hover:bg-[#edf5ff] disabled:opacity-40"
|
||||
>
|
||||
{preset}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
disabled={tableDisabled || !activeRow?.number}
|
||||
onClick={copyPreviousRowToActiveRow}
|
||||
className="inline-flex h-8 items-center justify-center rounded-full border border-[#dfe6f0] bg-white px-3 text-xs font-semibold text-slate-600 transition-colors hover:bg-slate-50 disabled:opacity-40"
|
||||
>
|
||||
{t("hall.quickFill.copyPrev", { defaultValue: "Copy prev row" })}
|
||||
</button>
|
||||
{isMobile ? (
|
||||
<div className="mt-2 space-y-2">
|
||||
{quickFillExpanded ? (
|
||||
<div className="rounded-xl border border-[#e7eef8] bg-[#f8fbff] px-3 py-2.5">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-[11px] font-semibold text-[#5b6f95]">
|
||||
{t("hall.quickFill.batchTitle", { defaultValue: "Quick amount" })}
|
||||
</span>
|
||||
{MOBILE_QUICK_AMOUNT_PRESETS.map((preset) => (
|
||||
<button
|
||||
key={preset}
|
||||
type="button"
|
||||
disabled={tableDisabled}
|
||||
onClick={clearActiveRowAmounts}
|
||||
className="inline-flex h-8 items-center justify-center rounded-full border border-[#ffe0e5] bg-white px-3 text-xs font-semibold text-[#d81435] transition-colors hover:bg-[#fff4f6] disabled:opacity-40"
|
||||
onClick={() => applyQuickAmountToActiveRow(preset)}
|
||||
className="inline-flex h-8 min-w-12 items-center justify-center rounded-full border border-[#cfe0fb] bg-white px-3 text-xs font-bold text-[#0b4ab3] transition-colors hover:border-[#aac7f5] hover:bg-[#edf5ff] disabled:opacity-40"
|
||||
>
|
||||
{t("hall.quickFill.clearRow", { defaultValue: "Clear row amounts" })}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!isMobile || quickFillExpanded ? (
|
||||
<>
|
||||
{favoriteChips.length > 0 ? (
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
<span className="mr-0.5 text-[11px] font-semibold text-[#d81435]">
|
||||
{t("hall.quickFill.favorites")}
|
||||
</span>
|
||||
{favoriteChips.map((number) => (
|
||||
<button
|
||||
key={`fav-${number}`}
|
||||
type="button"
|
||||
className="inline-flex h-8 touch-manipulation items-center gap-1 rounded-full border border-[#ffd7db] bg-[#fff3f5] px-2.5 text-xs font-semibold text-[#d81435] transition-colors hover:bg-[#ffe9ed]"
|
||||
onPointerDown={() => {
|
||||
const current = holdFavoriteRef.current;
|
||||
current.number = number;
|
||||
current.longPress = false;
|
||||
if (current.timer) window.clearTimeout(current.timer);
|
||||
current.timer = window.setTimeout(() => {
|
||||
current.longPress = true;
|
||||
toggleFavoriteNumber(number);
|
||||
}, 500);
|
||||
}}
|
||||
onPointerUp={() => {
|
||||
const current = holdFavoriteRef.current;
|
||||
if (current.timer) {
|
||||
window.clearTimeout(current.timer);
|
||||
current.timer = null;
|
||||
}
|
||||
if (!current.longPress) {
|
||||
fillCurrentRow(number);
|
||||
}
|
||||
current.longPress = false;
|
||||
}}
|
||||
onPointerLeave={() => {
|
||||
const current = holdFavoriteRef.current;
|
||||
if (current.timer) {
|
||||
window.clearTimeout(current.timer);
|
||||
current.timer = null;
|
||||
}
|
||||
current.longPress = false;
|
||||
}}
|
||||
>
|
||||
{number}
|
||||
<span className="text-[11px] font-medium opacity-70">
|
||||
{t("hall.quickFill.tapHold")}
|
||||
</span>
|
||||
{preset}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
disabled={tableDisabled || !activeRow?.number}
|
||||
onClick={copyPreviousRowToActiveRow}
|
||||
className="inline-flex h-8 items-center justify-center rounded-full border border-[#dfe6f0] bg-white px-3 text-xs font-semibold text-slate-600 transition-colors hover:bg-slate-50 disabled:opacity-40"
|
||||
>
|
||||
{t("hall.quickFill.copyPrev", { defaultValue: "Copy prev row" })}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={tableDisabled}
|
||||
onClick={clearActiveRowAmounts}
|
||||
className="inline-flex h-8 items-center justify-center rounded-full border border-[#ffe0e5] bg-white px-3 text-xs font-semibold text-[#d81435] transition-colors hover:bg-[#fff4f6] disabled:opacity-40"
|
||||
>
|
||||
{t("hall.quickFill.clearRow", { defaultValue: "Clear row amounts" })}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
<span className="mr-0.5 text-[11px] font-semibold text-slate-400">
|
||||
{t("hall.quickFill.history")}
|
||||
</span>
|
||||
{historyChips.length > 0 ? (
|
||||
historyChips.map((number) => (
|
||||
<button
|
||||
key={`his-${number}`}
|
||||
type="button"
|
||||
className="inline-flex h-8 min-w-10 items-center justify-center rounded-full border border-[#d7e5f8] bg-[#f8fbff] px-3 text-xs font-bold text-[#07459f] transition-colors hover:border-[#b9d0f3] hover:bg-[#eef6ff]"
|
||||
onClick={() => fillCurrentRow(number)}
|
||||
>
|
||||
{number}
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<span className="inline-flex h-8 items-center rounded-full border border-dashed border-slate-200 bg-slate-50 px-3 text-xs text-slate-400">
|
||||
{t("hall.quickFill.emptyHistory")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className={cn("overflow-x-auto pb-1", isMobile ? "-mx-1 flex gap-1.5 px-1" : "flex items-end gap-2")}>
|
||||
{quickFillExpanded ? (
|
||||
<>
|
||||
{favoriteChips.length > 0 ? (
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
<span className="mr-0.5 text-[11px] font-semibold text-[#d81435]">
|
||||
{t("hall.quickFill.favorites")}
|
||||
</span>
|
||||
{favoriteChips.map((number) => (
|
||||
<button
|
||||
key={`fav-${number}`}
|
||||
type="button"
|
||||
className="inline-flex h-8 touch-manipulation items-center gap-1 rounded-full border border-[#ffd7db] bg-[#fff3f5] px-2.5 text-xs font-semibold text-[#d81435] transition-colors hover:bg-[#ffe9ed]"
|
||||
onPointerDown={() => {
|
||||
const current = holdFavoriteRef.current;
|
||||
current.number = number;
|
||||
current.longPress = false;
|
||||
if (current.timer) window.clearTimeout(current.timer);
|
||||
current.timer = window.setTimeout(() => {
|
||||
current.longPress = true;
|
||||
toggleFavoriteNumber(number);
|
||||
}, 500);
|
||||
}}
|
||||
onPointerUp={() => {
|
||||
const current = holdFavoriteRef.current;
|
||||
if (current.timer) {
|
||||
window.clearTimeout(current.timer);
|
||||
current.timer = null;
|
||||
}
|
||||
if (!current.longPress) {
|
||||
fillCurrentRow(number);
|
||||
}
|
||||
current.longPress = false;
|
||||
}}
|
||||
onPointerLeave={() => {
|
||||
const current = holdFavoriteRef.current;
|
||||
if (current.timer) {
|
||||
window.clearTimeout(current.timer);
|
||||
current.timer = null;
|
||||
}
|
||||
current.longPress = false;
|
||||
}}
|
||||
>
|
||||
{number}
|
||||
<span className="text-[11px] font-medium opacity-70">
|
||||
{t("hall.quickFill.tapHold")}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
<span className="mr-0.5 text-[11px] font-semibold text-slate-400">
|
||||
{t("hall.quickFill.history")}
|
||||
</span>
|
||||
{historyChips.length > 0 ? (
|
||||
historyChips.map((number) => (
|
||||
<button
|
||||
key={`his-${number}`}
|
||||
type="button"
|
||||
className="inline-flex h-8 min-w-10 items-center justify-center rounded-full border border-[#d7e5f8] bg-[#f8fbff] px-3 text-xs font-bold text-[#07459f] transition-colors hover:border-[#b9d0f3] hover:bg-[#eef6ff]"
|
||||
onClick={() => fillCurrentRow(number)}
|
||||
>
|
||||
{number}
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<span className="inline-flex h-8 items-center rounded-full border border-dashed border-slate-200 bg-slate-50 px-3 text-xs text-slate-400">
|
||||
{t("hall.quickFill.emptyHistory")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"overflow-x-auto",
|
||||
isMobile
|
||||
? "-mx-1 flex gap-1.5 px-1 pb-1"
|
||||
: "order-first inline-flex w-fit max-w-full shrink-0 items-center gap-1 rounded-lg bg-[#f3f6fb] p-1",
|
||||
)}
|
||||
>
|
||||
{categoryTabs.map((tab) => {
|
||||
const hasPlays = openPlays.some(
|
||||
(play) => playCategory(play.play_code) === tab.value,
|
||||
@@ -1599,18 +1837,22 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
disabled={!hasPlays}
|
||||
onClick={() => setActiveCategory(tab.value)}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center border font-bold transition-colors",
|
||||
"inline-flex items-center justify-center font-bold transition-colors",
|
||||
isMobile
|
||||
? "min-w-[5.25rem] rounded-t-xl border-b-0 px-4 py-3 text-sm"
|
||||
: "min-w-[6rem] rounded-t-xl px-4 py-3 text-sm",
|
||||
active
|
||||
? "border-[#d7e1f3] border-b-white bg-white text-[#21335b] shadow-[0_-1px_0_rgba(255,255,255,0.8)]"
|
||||
: "border-transparent bg-transparent text-[#6d8fd6] hover:text-[#2d63e2]",
|
||||
? "min-w-[5.25rem] rounded-t-xl border border-b-0 px-4 py-3 text-sm"
|
||||
: "min-w-[4.5rem] rounded-lg px-3.5 py-2 text-sm",
|
||||
isMobile
|
||||
? active
|
||||
? "border-[#d7e1f3] border-b-white bg-white text-[#21335b] shadow-[0_-1px_0_rgba(255,255,255,0.8)]"
|
||||
: "border-transparent bg-transparent text-[#6d8fd6] hover:text-[#2d63e2]"
|
||||
: active
|
||||
? "bg-[#2d63e2] text-white shadow-[0_4px_12px_rgba(45,99,226,0.28)]"
|
||||
: "bg-transparent text-[#5b7fbf] hover:bg-[#f3f7ff] hover:text-[#2d63e2]",
|
||||
!hasPlays && "cursor-not-allowed opacity-40",
|
||||
)}
|
||||
aria-pressed={active}
|
||||
>
|
||||
<span className="mr-2 text-[13px]" aria-hidden>
|
||||
<span className={cn("mr-2 text-[13px]", !isMobile && active && "text-white/90")} aria-hidden>
|
||||
▦
|
||||
</span>
|
||||
{tab.label}
|
||||
@@ -1618,6 +1860,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{activeCategoryPlays.length === 0 ? (
|
||||
<div
|
||||
@@ -1637,7 +1880,10 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
showWideTableHint && "player-table-scroll-wrap",
|
||||
)}
|
||||
>
|
||||
<div className="mb-3 overflow-x-auto rounded border border-[#dae3f3] shadow-sm">
|
||||
<div
|
||||
ref={playSummaryScrollRef}
|
||||
className="mb-3 overflow-x-auto rounded border border-[#dae3f3] shadow-sm"
|
||||
>
|
||||
<table className="w-full border-collapse text-center text-[11px] tabular-nums">
|
||||
<thead>
|
||||
<tr className="bg-[#2d63e2] text-white">
|
||||
@@ -1686,9 +1932,9 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="player-table-scroll flex overflow-x-auto overscroll-x-contain">
|
||||
<div className="player-table-scroll overflow-x-auto overscroll-x-contain">
|
||||
<table
|
||||
className="w-max border-collapse text-[11px]"
|
||||
className="mx-auto w-max border-collapse text-[11px]"
|
||||
style={{ width: tableWidthPx }}
|
||||
>
|
||||
<thead>
|
||||
@@ -1745,8 +1991,8 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
defaultValue=""
|
||||
disabled={tableDisabled}
|
||||
onChange={(event) => {
|
||||
const selectionType = event.target.value as DraftRow["selectionType"] | "";
|
||||
if (selectionType) updateAllSelectionTypes(selectionType);
|
||||
const selectionType = event.target.value as SelectionType | "";
|
||||
if (selectionType) requestAllSelectionTypes(selectionType);
|
||||
event.currentTarget.value = "";
|
||||
}}
|
||||
className="mx-auto mt-1 h-5 w-full rounded border border-[#d7e1f3] bg-white px-0.5 text-[10px] font-semibold text-[#304f86]"
|
||||
@@ -1754,11 +2000,11 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
title={t("hall.table.setAllTypes")}
|
||||
>
|
||||
<option value="">{t("hall.table.selectAll")}</option>
|
||||
<option value="straight">{t("hall.table.selectionTypes.straight")}</option>
|
||||
<option value="reverse">{t("hall.table.selectionTypes.reverse")}</option>
|
||||
<option value="full_cover">{t("hall.table.selectionTypes.full_cover")}</option>
|
||||
<option value="full_play">{t("hall.table.selectionTypes.full_play")}</option>
|
||||
{activeCategory === "D4" ? <option value="half_play">{t("hall.table.selectionTypes.half_play")}</option> : null}
|
||||
{selectionTypeOptions.map((type) => (
|
||||
<option key={type} value={type}>
|
||||
{t(`hall.table.selectionTypes.${type}`)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</th>
|
||||
) : null}
|
||||
@@ -1803,7 +2049,8 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
liveSoldOutNumbers={liveSoldOutNumbers}
|
||||
liveWarningNumbers={liveWarningNumbers}
|
||||
updateRowNumber={updateRowNumber}
|
||||
updateRowSelectionType={updateRowSelectionType}
|
||||
updateRowSelectionType={requestRowSelectionType}
|
||||
selectionTypeOptions={selectionTypeOptions}
|
||||
updateAmount={updateAmount}
|
||||
toggleRowProvider={toggleRowProvider}
|
||||
setActiveRowId={setActiveRowId}
|
||||
@@ -1826,20 +2073,20 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
</div>
|
||||
|
||||
{!isMobile ? (
|
||||
<aside className="sticky top-3 z-20 w-64 shrink-0 self-start rounded-xl border border-[#e6edf8] bg-[#f8fbff] px-4 py-4 text-[#304f86] shadow-[0_8px_24px_rgba(15,23,42,0.05)]">
|
||||
<aside className="sticky top-3 z-20 w-52 shrink-0 self-start rounded-xl border border-[#e6edf8] bg-[#f8fbff] px-3.5 py-3.5 text-[#304f86] shadow-[0_8px_24px_rgba(15,23,42,0.05)]">
|
||||
<p className="text-xs font-black">{t("hall.table.orderSummary", { defaultValue: "Order summary" })}</p>
|
||||
<dl className="mt-5 space-y-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<dl className="mt-4 space-y-3">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<dt className="text-xs text-slate-500">{t("hall.table.lineCount", { defaultValue: "Lines" })}</dt>
|
||||
<dd className="font-mono text-base font-black tabular-nums">{draftEntries.length}</dd>
|
||||
<dd className="font-mono text-sm font-black tabular-nums">{draftEntries.length}</dd>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<dt className="text-xs text-slate-500">{t("hall.table.providerCount", { defaultValue: "Providers" })}</dt>
|
||||
<dd className="font-mono text-base font-black tabular-nums">{selectedProviderCount}</dd>
|
||||
<dd className="font-mono text-sm font-black tabular-nums">{selectedProviderCount}</dd>
|
||||
</div>
|
||||
<div className="border-t border-[#dce6f5] pt-4">
|
||||
<div className="border-t border-[#dce6f5] pt-3">
|
||||
<dt className="text-xs text-slate-500">{t("hall.table.actualTotal")}</dt>
|
||||
<dd className="mt-1 text-lg font-black tabular-nums text-[#0b3f96]">
|
||||
<dd className="mt-1 text-base font-black tabular-nums text-[#0b3f96]">
|
||||
{formatMinorAsCurrency(submitActualMinor, currencyCode)}
|
||||
</dd>
|
||||
</div>
|
||||
@@ -1849,7 +2096,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
disabled={!canSubmit || previewLoading}
|
||||
onClick={() => void handlePreview()}
|
||||
className={cn(
|
||||
"mt-6 h-11 w-full gap-2 rounded-lg border-0 text-sm font-bold text-white",
|
||||
"mt-4 h-10 w-full gap-2 rounded-lg border-0 text-sm font-bold text-white",
|
||||
!isBettable
|
||||
? "bg-slate-500 shadow-none hover:bg-slate-500 disabled:opacity-100"
|
||||
: "bg-[#e5002c] shadow-[0_8px_20px_rgba(229,0,44,0.26)] hover:bg-[#d10028]",
|
||||
@@ -1956,6 +2203,79 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
jackpotEnabled={Boolean(jackpot?.enabled)}
|
||||
creditMode={creditMode}
|
||||
/>
|
||||
|
||||
<Dialog
|
||||
open={pendingSelectionChange !== null}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) cancelPendingSelectionChange();
|
||||
}}
|
||||
>
|
||||
<DialogContent className="max-w-[380px] rounded-2xl border border-[#dfe8f6] bg-white p-0 shadow-[0_24px_70px_rgba(15,23,42,0.18)]">
|
||||
<DialogHeader className="gap-2 border-b border-[#eef2f8] px-5 py-4 text-left">
|
||||
<DialogTitle className="text-base font-black text-[#0b3f96]">
|
||||
{t("hall.table.selectionConfirm.title")}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-sm leading-relaxed text-slate-600">
|
||||
{pendingSelectionPreview
|
||||
? pendingSelectionPreview.next === "full_cover"
|
||||
? t("hall.table.selectionConfirm.fullCoverBody", {
|
||||
type: t(`hall.table.selectionTypes.${pendingSelectionPreview.next}`),
|
||||
count: pendingSelectionPreview.comboCount,
|
||||
amount: formatMinorAsCurrency(pendingSelectionPreview.toMinor || pendingSelectionPreview.fromMinor, currencyCode),
|
||||
})
|
||||
: pendingSelectionPreview.fromMinor > 0 &&
|
||||
pendingSelectionPreview.toMinor > pendingSelectionPreview.fromMinor
|
||||
? t("hall.table.selectionConfirm.increaseBody", {
|
||||
type: t(`hall.table.selectionTypes.${pendingSelectionPreview.next}`),
|
||||
count: pendingSelectionPreview.comboCount,
|
||||
from: formatMinorAsCurrency(pendingSelectionPreview.fromMinor, currencyCode),
|
||||
to: formatMinorAsCurrency(pendingSelectionPreview.toMinor, currencyCode),
|
||||
})
|
||||
: t("hall.table.selectionConfirm.genericBody", {
|
||||
type: t(`hall.table.selectionTypes.${pendingSelectionPreview.next}`),
|
||||
count: pendingSelectionPreview.comboCount,
|
||||
})
|
||||
: t("hall.table.selectionConfirm.genericBody", {
|
||||
type: "",
|
||||
count: 1,
|
||||
})}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{pendingSelectionPreview && pendingSelectionPreview.comboCount > 1 ? (
|
||||
<div className="space-y-1 px-5 py-3 text-xs text-slate-600">
|
||||
<p className="font-bold text-[#304f86]">
|
||||
{t("hall.table.selectionConfirm.comboHint", {
|
||||
count: pendingSelectionPreview.comboCount,
|
||||
})}
|
||||
</p>
|
||||
{pendingSelectionPreview.number ? (
|
||||
<p className="font-mono text-[11px] text-slate-500">
|
||||
{t("hall.table.selectionConfirm.numberHint", {
|
||||
number: pendingSelectionPreview.number,
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<DialogFooter className="gap-2 border-t border-[#eef2f8] px-5 py-4 sm:justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="rounded-lg"
|
||||
onClick={cancelPendingSelectionChange}
|
||||
>
|
||||
{t("hall.table.selectionConfirm.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
className="rounded-lg bg-[#e5002c] text-white hover:bg-[#d10028]"
|
||||
onClick={confirmPendingSelectionChange}
|
||||
>
|
||||
{t("hall.table.selectionConfirm.confirm")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1975,6 +2295,7 @@ const DraftRowItem = memo(function DraftRowItem({
|
||||
liveWarningNumbers,
|
||||
updateRowNumber,
|
||||
updateRowSelectionType,
|
||||
selectionTypeOptions,
|
||||
updateAmount,
|
||||
toggleRowProvider,
|
||||
setActiveRowId,
|
||||
@@ -1983,45 +2304,51 @@ const DraftRowItem = memo(function DraftRowItem({
|
||||
indexColClass,
|
||||
numberColClass,
|
||||
stickyNumberLeftClass,
|
||||
selectionTypeColClass,
|
||||
showSelectionTypeColumn,
|
||||
providerColClass,
|
||||
rowTotalColClass,
|
||||
currencyCode,
|
||||
}: {
|
||||
row: DraftRow;
|
||||
index: number;
|
||||
rowActive: boolean;
|
||||
tableDisabled: boolean;
|
||||
numberPlaceholder: string;
|
||||
activeCategory: PlayHallCategory;
|
||||
numberMaxChars: number;
|
||||
betProviders: BetProviderRow[];
|
||||
playColumns: PlayColumn[];
|
||||
alertRows: DrawCurrentRiskPoolAlert[];
|
||||
liveSoldOutNumbers: Set<string>;
|
||||
liveWarningNumbers: Set<string>;
|
||||
selectionTypeColClass,
|
||||
showSelectionTypeColumn,
|
||||
providerColClass,
|
||||
rowTotalColClass,
|
||||
currencyCode,
|
||||
}: {
|
||||
row: DraftRow;
|
||||
index: number;
|
||||
rowActive: boolean;
|
||||
tableDisabled: boolean;
|
||||
numberPlaceholder: string;
|
||||
activeCategory: PlayHallCategory;
|
||||
numberMaxChars: number;
|
||||
betProviders: BetProviderRow[];
|
||||
playColumns: PlayColumn[];
|
||||
alertRows: DrawCurrentRiskPoolAlert[];
|
||||
liveSoldOutNumbers: Set<string>;
|
||||
liveWarningNumbers: Set<string>;
|
||||
updateRowNumber: (id: string, value: string) => void;
|
||||
updateRowSelectionType: (id: string, value: DraftRow["selectionType"]) => void;
|
||||
updateRowSelectionType: (id: string, value: SelectionType) => void;
|
||||
selectionTypeOptions: SelectionType[];
|
||||
updateAmount: (rowId: string, playCode: string, value: string) => void;
|
||||
toggleRowProvider: (rowId: string, code: string) => void;
|
||||
setActiveRowId: (id: string) => void;
|
||||
t: HallTranslate;
|
||||
isMobile: boolean;
|
||||
indexColClass: string;
|
||||
numberColClass: string;
|
||||
stickyNumberLeftClass: string;
|
||||
selectionTypeColClass: string;
|
||||
showSelectionTypeColumn: boolean;
|
||||
providerColClass: string;
|
||||
rowTotalColClass: string;
|
||||
currencyCode: string;
|
||||
}) {
|
||||
const displayNumber = sanitizeNumber(row.number, activeCategory);
|
||||
const rowTotalMinor = playColumns.reduce((total, column) => {
|
||||
if (draftLineIssueReason(column.play.play_code, row.number, column.digitSlot) !== null) return total;
|
||||
return total + (parseDecimalInputToMinor(row.amounts[column.key] ?? "", currencyCode) ?? 0);
|
||||
}, 0) * row.providerCodes.length;
|
||||
numberColClass: string;
|
||||
stickyNumberLeftClass: string;
|
||||
selectionTypeColClass: string;
|
||||
showSelectionTypeColumn: boolean;
|
||||
providerColClass: string;
|
||||
rowTotalColClass: string;
|
||||
currencyCode: string;
|
||||
}) {
|
||||
const displayNumber = sanitizeNumber(row.number, activeCategory);
|
||||
const comboCount = selectionCombinationCount(displayNumber, row.selectionType);
|
||||
const rowTotalMinor =
|
||||
playColumns.reduce((total, column) => {
|
||||
if (draftLineIssueReason(column.play.play_code, row.number, column.digitSlot) !== null) {
|
||||
return total;
|
||||
}
|
||||
const amount = parseDecimalInputToMinor(row.amounts[column.key] ?? "", currencyCode) ?? 0;
|
||||
return total + resolveSelectionTotalBet(amount, row.selectionType, comboCount);
|
||||
}, 0) * row.providerCodes.length;
|
||||
|
||||
return (
|
||||
<tr
|
||||
@@ -2132,16 +2459,21 @@ const DraftRowItem = memo(function DraftRowItem({
|
||||
<select
|
||||
value={row.selectionType}
|
||||
disabled={tableDisabled}
|
||||
onChange={(event) => updateRowSelectionType(row.id, event.target.value as DraftRow["selectionType"])}
|
||||
onChange={(event) => updateRowSelectionType(row.id, event.target.value as SelectionType)}
|
||||
className="h-8 w-full rounded-md border border-[#e1e8f3] bg-white px-1 text-[10px] font-semibold text-[#304f86]"
|
||||
aria-label={t("hall.table.selectionTypeForRow", { row: index + 1 })}
|
||||
>
|
||||
<option value="straight">{t("hall.table.selectionTypes.straight")}</option>
|
||||
<option value="reverse">{t("hall.table.selectionTypes.reverse")}</option>
|
||||
<option value="full_cover">{t("hall.table.selectionTypes.full_cover")}</option>
|
||||
<option value="full_play">{t("hall.table.selectionTypes.full_play")}</option>
|
||||
{activeCategory === "D4" ? <option value="half_play">{t("hall.table.selectionTypes.half_play")}</option> : null}
|
||||
{selectionTypeOptions.map((type) => (
|
||||
<option key={type} value={type}>
|
||||
{t(`hall.table.selectionTypes.${type}`)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{comboCount > 1 && displayNumber.length >= 2 ? (
|
||||
<p className="mt-0.5 text-[9px] font-bold leading-none text-[#0b56b7]">
|
||||
{t("hall.table.comboCount", { count: comboCount })}
|
||||
</p>
|
||||
) : null}
|
||||
</td>
|
||||
) : null}
|
||||
{betProviders.map((provider, providerIndex) => (
|
||||
|
||||
Reference in New Issue
Block a user