feat: add D4 play grouping with tab navigation and improve header layout
Some checks failed
lotteryfront CI / build (push) Has been cancelled

- Introduced D4 play groups (big/small, position, combo, roll/straight, head/tail/odd/even, digit big/small) with tab-based filtering to improve betting grid usability
- Updated PlayerPanel header grid layout from percentage-based to auto/1fr/auto columns with increased gap for better responsiveness
- Removed "placed" status from ticket orders list filter options
- Added D4 group translations in English, Nepali, and Chinese
- Refactored play columns
This commit is contained in:
2026-06-24 11:18:45 +08:00
parent 588d62a381
commit f46ccaac6e
6 changed files with 90 additions and 10 deletions

View File

@@ -47,7 +47,7 @@ export function PlayerPanel({
<header
className={cn(
playerPageHeader,
"sticky z-50 grid grid-cols-[minmax(0,38%)_minmax(0,1fr)_minmax(0,48%)] items-center gap-1 bg-white/95 pt-2 pb-2 -mx-3 px-3 backdrop-blur",
"sticky z-50 grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-2 bg-white/95 pt-2 pb-2 -mx-3 px-3 backdrop-blur",
)}
style={stickyTopStyle}
>

View File

@@ -135,6 +135,23 @@ const D4_PLAY_ORDER = [
"digit_small",
] as const;
type D4PlayGroupId =
| "big_small"
| "position"
| "combo"
| "roll_straight"
| "head_tail_odd_even"
| "digit_big_small";
const D4_PLAY_GROUPS: { id: D4PlayGroupId; labelKey: string; playCodes: readonly string[] }[] = [
{ id: "big_small", labelKey: "hall.d4Group.big_small", playCodes: ["big", "small"] },
{ id: "position", labelKey: "hall.d4Group.position", playCodes: ["pos_4a", "pos_4b", "pos_4c", "pos_4d", "pos_4e"] },
{ id: "combo", labelKey: "hall.d4Group.combo", playCodes: ["box", "ibox", "mbox"] },
{ id: "roll_straight", labelKey: "hall.d4Group.roll_straight", playCodes: ["roll", "straight"] },
{ id: "head_tail_odd_even", labelKey: "hall.d4Group.head_tail_odd_even", playCodes: ["head", "tail", "odd", "even"] },
{ id: "digit_big_small", labelKey: "hall.d4Group.digit_big_small", playCodes: ["digit_big", "digit_small"] },
];
const categoryPlayOrders: Record<Exclude<HallCategory, "JACKPOT">, readonly string[]> = {
D2: D2_PLAY_ORDER,
D3: D3_PLAY_ORDER,
@@ -446,6 +463,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
const creditMode = isCreditFundingPlayer(usePlayerSessionStore((state) => state.profile));
const [activeCategory, setActiveCategory] = useState<HallCategory>("D2");
const [d4PlayGroup, setD4PlayGroup] = useState<D4PlayGroupId>("big_small");
const [rows, setRows] = useState<DraftRow[]>(() => [newDraftRow()]);
const [activeRowId, setActiveRowId] = useState<string | null>(null);
const [catalogState, setCatalogState] = useState<
@@ -557,11 +575,19 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
);
}, [activeCategory, catalogState, openPlays]);
const playColumns = useMemo(() => {
const allPlayColumns = useMemo(() => {
if (activeCategory === "JACKPOT") return [];
return playColumnsForCategory(categoryPlays, activeCategory);
}, [activeCategory, categoryPlays]);
const playColumns = useMemo(() => {
if (activeCategory !== "D4") return allPlayColumns;
const group = D4_PLAY_GROUPS.find((g) => g.id === d4PlayGroup);
if (!group) return allPlayColumns;
const allowedCodes = new Set(group.playCodes);
return allPlayColumns.filter((col) => allowedCodes.has(col.play.play_code));
}, [activeCategory, allPlayColumns, d4PlayGroup]);
const tableMinWidthPx = useMemo(() => {
const indexCol = 40;
const numberCol = activeCategory === "D4" ? 112 : activeCategory === "D3" ? 88 : 72;
@@ -570,7 +596,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
return indexCol + numberCol + playColumns.length * amountCol + deleteCol;
}, [activeCategory, playColumns.length]);
const showWideTableHint = activeCategory === "D4" && playColumns.length > 8;
const showWideTableHint = activeCategory === "D4" && allPlayColumns.length > 8 && playColumns.length > 8;
const activeRow = useMemo(
() => rows.find((row) => row.id === activeRowId) ?? rows[0] ?? null,
@@ -598,7 +624,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
if (activeCategory !== "D4") return defaultNumberPlaceholder;
const targetRow = activeRow ?? rows[0];
if (!targetRow) return defaultNumberPlaceholder;
const hasRollStake = playColumns.some((column) => {
const hasRollStake = allPlayColumns.some((column) => {
if (column.play.play_code !== "roll") return false;
const amount = parseDecimalInputToMinor(targetRow.amounts[column.key] ?? "", currencyCode);
return amount !== null && amount > 0;
@@ -609,11 +635,19 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
activeRow,
currencyCode,
defaultNumberPlaceholder,
playColumns,
allPlayColumns,
rows,
t,
]);
const availableD4Groups = useMemo(() => {
if (activeCategory !== "D4") return [];
return D4_PLAY_GROUPS.filter((group) => {
const allowedCodes = new Set(group.playCodes);
return allPlayColumns.some((col) => allowedCodes.has(col.play.play_code));
});
}, [activeCategory, allPlayColumns]);
const updateRowNumber = (id: string, value: string) => {
setRows((current) =>
current.map((row) =>
@@ -809,7 +843,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
if (activeCategory === "JACKPOT") return [];
const issues: DraftLineIssue[] = [];
rows.forEach((row, rowIndex) => {
playColumns.forEach((column) => {
allPlayColumns.forEach((column) => {
const amount = parseDecimalInputToMinor(row.amounts[column.key] ?? "", currencyCode);
if (amount === null || amount <= 0) return;
const reason = draftLineIssueReason(column.play.play_code, row.number, column.digitSlot);
@@ -823,7 +857,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
});
});
return issues;
}, [activeCategory, currencyCode, playColumns, rows]);
}, [activeCategory, currencyCode, allPlayColumns, rows]);
const formatDraftLineIssue = useCallback(
(issue: DraftLineIssue): string => {
@@ -837,7 +871,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
if (activeCategory === "JACKPOT") return [];
const entries: DraftEntry[] = [];
rows.forEach((row, rowIndex) => {
playColumns.forEach((column) => {
allPlayColumns.forEach((column) => {
const amount = parseDecimalInputToMinor(row.amounts[column.key] ?? "", currencyCode);
if (amount === null || amount <= 0) return;
const line = lineForPlay(activeCategory, column.play, row.number, amount, column.digitSlot);
@@ -855,7 +889,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
});
});
return entries;
}, [activeCategory, currencyCode, playColumns, rows]);
}, [activeCategory, currencyCode, allPlayColumns, rows]);
const draftEntries = collectEntries();
const draftSummary = useMemo(() => {
@@ -1155,6 +1189,29 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
</div>
</div>
{activeCategory === "D4" && availableD4Groups.length > 1 ? (
<div className="flex gap-1 overflow-x-auto overscroll-x-contain pb-0.5">
{availableD4Groups.map((group) => {
const active = d4PlayGroup === group.id;
return (
<button
key={group.id}
type="button"
onClick={() => setD4PlayGroup(group.id)}
className={cn(
"shrink-0 rounded-full px-3 py-1.5 text-xs font-bold transition-colors",
active
? "bg-[#07459f] text-white"
: "border border-[#dce7f7] bg-white text-[#32518d] hover:bg-[#f4f7fb]",
)}
>
{t(group.labelKey)}
</button>
);
})}
</div>
) : null}
{jackpot?.enabled ? (
<div className="rounded-xl border border-amber-200 bg-gradient-to-r from-amber-50 via-white to-[#f8fbff] px-3 py-2.5">
<div className="flex flex-wrap items-center justify-between gap-2">

View File

@@ -42,7 +42,6 @@ const ORDERS_PAGE_SIZE = 20;
const STATUS_OPTIONS = [
"pending_confirm",
"partial_pending_confirm",
"placed",
"pending_draw",
"partial_failed",
"pending_payout",

View File

@@ -213,6 +213,14 @@
"boxTitle": "Box",
"boxDesc": "Multiply all by amount"
},
"d4Group": {
"big_small": "Big / Small",
"position": "Position",
"combo": "Combo",
"roll_straight": "Roll / Straight",
"head_tail_odd_even": "Head / Tail / Odd / Even",
"digit_big_small": "Digit Big / Small"
},
"table": {
"no": "No.",
"number": "Number",

View File

@@ -213,6 +213,14 @@
"boxTitle": "Box",
"boxDesc": "रकम सबैसँग गुणा गर्नुहोस्"
},
"d4Group": {
"big_small": "ठूलो / सानो",
"position": "स्थान",
"combo": "कम्बो",
"roll_straight": "रोल / स्ट्रेट",
"head_tail_odd_even": "टाउको / पुच्छर / बिजोर / जोर",
"digit_big_small": "डिजिट ठूलो / सानो"
},
"table": {
"no": "नं.",
"number": "नम्बर",

View File

@@ -211,6 +211,14 @@
"boxTitle": "Box",
"boxDesc": "按金额乘以全部组合"
},
"d4Group": {
"big_small": "大小盘",
"position": "位置",
"combo": "组合",
"roll_straight": "滚轮 / 直通",
"head_tail_odd_even": "头尾单双",
"digit_big_small": "各位大小"
},
"table": {
"no": "序号",
"number": "号码",