Merge pull request 'Make the dealer table usable on a phone' (#16) from feature/casino-holdem-mobile-table into feature/casino-holdem-quick-split
Reviewed-on: #16
This commit was merged in pull request #16.
This commit is contained in:
@@ -38,7 +38,11 @@
|
||||
|
||||
<aside id="holdemSidebar">
|
||||
<section id="seatEditorPanel" class="sidebarPanel seatEditorPanel">
|
||||
<h2 id="seatEditorTitle">Select a seat</h2>
|
||||
<div class="seatSheetGrip" aria-hidden="true"></div>
|
||||
<div class="seatEditorHeader">
|
||||
<h2 id="seatEditorTitle">Select a seat</h2>
|
||||
<button id="closeSeatEditorBtn" class="seatSheetClose" aria-label="Done editing this seat">Done</button>
|
||||
</div>
|
||||
<div id="seatEditorBody">
|
||||
<p class="fieldHint">Click a seat at the table to edit their name, cards, chips and anything else.</p>
|
||||
</div>
|
||||
@@ -206,6 +210,19 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div id="toastHost" aria-live="polite"></div>
|
||||
|
||||
<div id="confirmOverlay" hidden>
|
||||
<div id="confirmModal" role="alertdialog" aria-labelledby="confirmTitle" aria-describedby="confirmMessage" tabindex="-1">
|
||||
<h2 id="confirmTitle"></h2>
|
||||
<p id="confirmMessage"></p>
|
||||
<div class="confirmActions">
|
||||
<button id="confirmCancelBtn" class="holdemBtn holdemBtnGhost" type="button">Cancel</button>
|
||||
<button id="confirmAcceptBtn" class="holdemBtn holdemBtnDanger" type="button">Confirm</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="potWizardOverlay" hidden>
|
||||
<div id="potWizardModal">
|
||||
<div class="potWizardHeader">
|
||||
@@ -222,6 +239,7 @@
|
||||
|
||||
@section Scripts {
|
||||
<script src="~/js/casino/holdem/money.js?v=@ViewData["cssVersion"]"></script>
|
||||
<script src="~/js/casino/holdem/feedback.js?v=@ViewData["cssVersion"]"></script>
|
||||
<script src="~/js/casino/holdem/potCalculator.js?v=@ViewData["cssVersion"]"></script>
|
||||
<script src="~/js/casino/holdem/state.js?v=@ViewData["cssVersion"]"></script>
|
||||
<script src="~/js/casino/holdem/render.js?v=@ViewData["cssVersion"]"></script>
|
||||
|
||||
@@ -376,6 +376,27 @@ body {
|
||||
padding: 0.9rem;
|
||||
}
|
||||
|
||||
.seatEditorHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.seatEditorHeader h2 {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Grip and Done button are the sheet's furniture — no reason for either on a
|
||||
desktop sidebar, where the panel is just a panel. */
|
||||
.seatSheetGrip,
|
||||
.seatSheetClose {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.seatEditorPanel h2 {
|
||||
margin: 0 0 0.6rem;
|
||||
padding-bottom: 0.5rem;
|
||||
@@ -732,6 +753,135 @@ body {
|
||||
|
||||
/* ---------- Pot wizard ---------- */
|
||||
|
||||
/* ---------- Toasts ---------- */
|
||||
|
||||
#toastHost {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
bottom: 1.25rem;
|
||||
transform: translateX(-50%);
|
||||
z-index: 80;
|
||||
width: min(560px, calc(100vw - 1.5rem));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.toast {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
background: linear-gradient(180deg, #2b2b2b, #202020);
|
||||
border: 1px solid #414141;
|
||||
border-left: 3px solid #d8b46a;
|
||||
border-radius: 10px;
|
||||
padding: 0.7rem 0.8rem 0.7rem 0.9rem;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.55);
|
||||
pointer-events: auto;
|
||||
opacity: 0;
|
||||
transform: translateY(12px);
|
||||
transition: opacity 0.18s ease, transform 0.18s ease;
|
||||
}
|
||||
|
||||
.toast.toastVisible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.toastText {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
color: #e9e9e9;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.toastAction {
|
||||
flex: none;
|
||||
min-height: 38px;
|
||||
padding: 0.35rem 0.9rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #d8b46a;
|
||||
background: rgba(216, 180, 106, 0.12);
|
||||
color: #f0d9a8;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toastAction:hover {
|
||||
background: rgba(216, 180, 106, 0.22);
|
||||
}
|
||||
|
||||
.toastClose {
|
||||
flex: none;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #888;
|
||||
font-size: 0.8rem;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toastClose:hover {
|
||||
background: #333;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
/* ---------- Confirmation dialog ---------- */
|
||||
|
||||
#confirmOverlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.72);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 90;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
#confirmOverlay[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#confirmModal {
|
||||
background: #1e1e1e;
|
||||
border: 1px solid #3a3a3a;
|
||||
border-radius: 12px;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
padding: 1.3rem 1.4rem 1.2rem;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#confirmTitle {
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 500;
|
||||
color: #f5f5f5;
|
||||
}
|
||||
|
||||
#confirmMessage {
|
||||
margin: 0 0 1.2rem;
|
||||
color: #a5a5a5;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.confirmActions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.confirmActions .holdemBtn {
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
#potWizardOverlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
@@ -843,3 +993,206 @@ body {
|
||||
min-height: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Phones ----------
|
||||
Two things break on a narrow screen: seat pucks ring the table on a radius
|
||||
measured from the wrap, so the left- and right-most ones hang off the edge of
|
||||
the viewport; and the seat editor lives far enough down the page that tapping a
|
||||
seat looks like it did nothing. Inset the ring by half a puck, and float the
|
||||
editor up as a bottom sheet over the table. */
|
||||
@media (max-width: 700px) {
|
||||
#holdemApp {
|
||||
padding: 1rem 0.75rem 2rem;
|
||||
}
|
||||
|
||||
#holdemHeader {
|
||||
margin-bottom: 1rem;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
#holdemHeader h1 {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.holdemSubtitle {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
#holdemHeaderActions {
|
||||
width: 100%;
|
||||
gap: 0.45rem;
|
||||
}
|
||||
|
||||
#holdemHeaderActions .holdemBtn {
|
||||
flex: 1 1 auto;
|
||||
justify-content: center;
|
||||
min-height: 46px;
|
||||
padding: 0.5rem 0.7rem;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
/* Keeps every puck inside the viewport: the ring is drawn within this box,
|
||||
so half a puck of padding is exactly enough to stop the overhang. */
|
||||
#seatsLayer {
|
||||
inset: 26px 44px;
|
||||
}
|
||||
|
||||
/* Narrower than the desktop puck: at a full ten-handed table the ring is
|
||||
flattest at the top and bottom, and wide pucks are what made neighbours
|
||||
overlap there. The puck is only a summary — the real form is the sheet. */
|
||||
.seat {
|
||||
width: 64px;
|
||||
min-height: 42px;
|
||||
padding: 0.25rem 0.3rem;
|
||||
}
|
||||
|
||||
#tableWatermark {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
#potBadge {
|
||||
padding: 0.5rem 1.1rem;
|
||||
}
|
||||
|
||||
#potBadgeAmount {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.tableHint {
|
||||
margin-top: 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* ---- Seat editor as a bottom sheet ---- */
|
||||
|
||||
#seatEditorPanel {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 60;
|
||||
margin: 0;
|
||||
max-height: 74vh;
|
||||
overflow-y: auto;
|
||||
border-radius: 16px 16px 0 0;
|
||||
border-bottom: none;
|
||||
padding: 0.35rem 1rem 1.25rem;
|
||||
background: linear-gradient(180deg, #242424, #1a1a1a);
|
||||
box-shadow: 0 -14px 40px rgba(0, 0, 0, 0.65);
|
||||
transform: translateY(105%);
|
||||
transition: transform 0.25s ease;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
body.seatSheetOpen #seatEditorPanel {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.seatSheetGrip {
|
||||
display: block;
|
||||
width: 38px;
|
||||
height: 4px;
|
||||
margin: 0.35rem auto 0.5rem;
|
||||
border-radius: 999px;
|
||||
background: #4a4a4a;
|
||||
}
|
||||
|
||||
.seatSheetClose {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex: none;
|
||||
min-height: 44px;
|
||||
padding: 0.35rem 1.1rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #3a3a3a;
|
||||
background: linear-gradient(180deg, #2e2e2e, #232323);
|
||||
color: #ddd;
|
||||
font-size: 0.8rem;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* The sheet covers the bottom of the page while it's open — give the sidebar
|
||||
room so nothing is stranded underneath it. */
|
||||
body.seatSheetOpen #holdemSidebar {
|
||||
padding-bottom: 40vh;
|
||||
}
|
||||
|
||||
/* The seat form was sized for a 320px desktop sidebar. In the sheet it has a
|
||||
whole phone width to work with, so everything grows to thumb size — and
|
||||
every field goes to 16px, below which iOS zooms the page on focus and the
|
||||
dealer has to pinch back out mid-hand. */
|
||||
.seatEditorForm .seatNameInput,
|
||||
.seatEditorForm .amountOverrideInput,
|
||||
.seatEditorForm .chipCountInput,
|
||||
.seatEditorForm .customFieldLabel,
|
||||
.seatEditorForm .customFieldValue {
|
||||
font-size: 16px;
|
||||
min-height: 44px;
|
||||
padding: 0.4rem 0.6rem;
|
||||
}
|
||||
|
||||
.seatEditorForm .chipCountInput {
|
||||
width: 56px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.seatEditorForm .chipCounts {
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.seatEditorForm .chipSwatch {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.seatEditorForm .amountOverrideInput {
|
||||
width: 110px;
|
||||
}
|
||||
|
||||
.seatEditorForm .cardInput {
|
||||
font-size: 1.35rem;
|
||||
min-height: 58px;
|
||||
}
|
||||
|
||||
.seatEditorForm .seatCardToggles {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.seatEditorForm .inHandToggle,
|
||||
.seatEditorForm .foldToggle {
|
||||
min-height: 44px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.seatEditorForm .inHandToggle input,
|
||||
.seatEditorForm .foldToggle input {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.seatEditorForm .amountOverrideRow,
|
||||
.seatEditorForm .seatTotal {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.seatEditorForm .addFieldBtn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
min-height: 44px;
|
||||
margin-top: 0.4rem;
|
||||
}
|
||||
|
||||
.toastAction {
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.confirmActions {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.confirmActions .holdemBtn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
// Toasts and confirmation dialogs, so the table never has to fall back on the
|
||||
// browser's own alert boxes. A system confirm() steals focus, looks nothing like
|
||||
// the rest of the page, and on a phone it's a full-screen interruption — all of
|
||||
// which is a bad trade for a dealer trying to keep a game moving.
|
||||
|
||||
// A short-lived message at the bottom of the screen, optionally with one action
|
||||
// (in practice: Undo). Only one is ever on screen; a second replaces the first.
|
||||
const Toast = {
|
||||
DEFAULT_DURATION_MS: 8000,
|
||||
timer: null,
|
||||
|
||||
show(message, options = {}) {
|
||||
const host = document.getElementById("toastHost");
|
||||
if (!host) return;
|
||||
|
||||
this.dismiss();
|
||||
|
||||
const toast = document.createElement("div");
|
||||
toast.className = "toast";
|
||||
toast.setAttribute("role", "status");
|
||||
|
||||
const text = document.createElement("span");
|
||||
text.className = "toastText";
|
||||
text.textContent = message;
|
||||
toast.appendChild(text);
|
||||
|
||||
if (options.actionLabel && options.onAction) {
|
||||
const action = document.createElement("button");
|
||||
action.type = "button";
|
||||
action.className = "toastAction";
|
||||
action.textContent = options.actionLabel;
|
||||
action.addEventListener("click", () => {
|
||||
this.dismiss();
|
||||
options.onAction();
|
||||
});
|
||||
toast.appendChild(action);
|
||||
}
|
||||
|
||||
const close = document.createElement("button");
|
||||
close.type = "button";
|
||||
close.className = "toastClose";
|
||||
close.setAttribute("aria-label", "Dismiss");
|
||||
close.textContent = "✕";
|
||||
close.addEventListener("click", () => this.dismiss());
|
||||
toast.appendChild(close);
|
||||
|
||||
host.appendChild(toast);
|
||||
// Next frame, so the entrance transition actually runs.
|
||||
requestAnimationFrame(() => toast.classList.add("toastVisible"));
|
||||
|
||||
this.timer = setTimeout(
|
||||
() => this.dismiss(),
|
||||
options.duration || this.DEFAULT_DURATION_MS
|
||||
);
|
||||
},
|
||||
|
||||
dismiss() {
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
const host = document.getElementById("toastHost");
|
||||
if (host) host.innerHTML = "";
|
||||
},
|
||||
};
|
||||
|
||||
// Styled stand-in for confirm(). Resolves true if the dealer goes through with it.
|
||||
const Dialog = {
|
||||
confirm({ title, message, confirmLabel = "Confirm", cancelLabel = "Cancel", danger = false }) {
|
||||
return new Promise(resolve => {
|
||||
const overlay = document.getElementById("confirmOverlay");
|
||||
const modal = overlay.querySelector("#confirmModal");
|
||||
overlay.querySelector("#confirmTitle").textContent = title;
|
||||
overlay.querySelector("#confirmMessage").textContent = message;
|
||||
|
||||
const confirmBtn = overlay.querySelector("#confirmAcceptBtn");
|
||||
const cancelBtn = overlay.querySelector("#confirmCancelBtn");
|
||||
confirmBtn.textContent = confirmLabel;
|
||||
cancelBtn.textContent = cancelLabel;
|
||||
confirmBtn.classList.toggle("holdemBtnDanger", danger);
|
||||
confirmBtn.classList.toggle("holdemBtnAccent", !danger);
|
||||
|
||||
const finish = result => {
|
||||
overlay.hidden = true;
|
||||
confirmBtn.removeEventListener("click", onAccept);
|
||||
cancelBtn.removeEventListener("click", onCancel);
|
||||
overlay.removeEventListener("click", onBackdrop);
|
||||
document.removeEventListener("keydown", onKey);
|
||||
resolve(result);
|
||||
};
|
||||
const onAccept = () => finish(true);
|
||||
const onCancel = () => finish(false);
|
||||
const onBackdrop = e => {
|
||||
if (e.target === overlay) finish(false);
|
||||
};
|
||||
const onKey = e => {
|
||||
if (e.key === "Escape") finish(false);
|
||||
if (e.key === "Enter") finish(true);
|
||||
};
|
||||
|
||||
confirmBtn.addEventListener("click", onAccept);
|
||||
cancelBtn.addEventListener("click", onCancel);
|
||||
overlay.addEventListener("click", onBackdrop);
|
||||
document.addEventListener("keydown", onKey);
|
||||
|
||||
overlay.hidden = false;
|
||||
modal.focus();
|
||||
confirmBtn.focus();
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
if (typeof module !== "undefined" && module.exports) {
|
||||
module.exports = { Toast, Dialog };
|
||||
}
|
||||
@@ -8,6 +8,29 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
|
||||
Render.mount(state, hooks);
|
||||
|
||||
// Moving to the next hand used to ask "are you sure?" every single time —
|
||||
// the most-pressed button in a session, gated behind a system dialog. It now
|
||||
// just happens, and the toast offers the way back for the few seconds anyone
|
||||
// would notice a misclick. The snapshot is deliberately in-memory only: an
|
||||
// undo is for the mistake you just made, not one from last night's session.
|
||||
const runUndoable = (message, mutate) => {
|
||||
const snapshot = JSON.parse(JSON.stringify(state));
|
||||
mutate();
|
||||
hooks.save(state);
|
||||
Render.renderAll();
|
||||
|
||||
Toast.show(message, {
|
||||
actionLabel: "Undo",
|
||||
onAction: () => {
|
||||
Object.keys(state).forEach(key => delete state[key]);
|
||||
Object.assign(state, snapshot);
|
||||
hooks.save(state);
|
||||
Render.renderAll();
|
||||
Toast.show("Undone.", { duration: 2500 });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
document.getElementById("seatCountInput").addEventListener("change", e => {
|
||||
HoldemState.resizeSeats(state, Number(e.target.value));
|
||||
hooks.save(state);
|
||||
@@ -49,38 +72,63 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
});
|
||||
|
||||
document.getElementById("nextHandBtn").addEventListener("click", () => {
|
||||
if (!confirm("Start the next hand? This moves the button, clears cards and chip counts for every seat.")) return;
|
||||
HoldemState.startNextHand(state);
|
||||
hooks.save(state);
|
||||
Render.renderAll();
|
||||
// startNextHand moves the button on by one; say where it landed so the
|
||||
// dealer can confirm the rotation without hunting for the badge.
|
||||
const nextDealer = HoldemState.nextSeatIndex(state, state.dealerSeatIndex, 1);
|
||||
const where = state.dedicatedDealer
|
||||
? `small blind on seat ${HoldemState.nextSeatIndex(state, nextDealer, 1) + 1}`
|
||||
: `button on seat ${nextDealer + 1}`;
|
||||
|
||||
runUndoable(`Next hand — ${where}. Cards and chip counts cleared.`, () =>
|
||||
HoldemState.startNextHand(state)
|
||||
);
|
||||
});
|
||||
|
||||
document.getElementById("resetTableBtn").addEventListener("click", () => {
|
||||
if (!confirm("Reset the whole table? This clears every seat, chip legend and note.")) return;
|
||||
document.getElementById("resetTableBtn").addEventListener("click", async () => {
|
||||
const ok = await Dialog.confirm({
|
||||
title: "Reset the whole table?",
|
||||
message: "Every seat, chip colour and note goes back to defaults. The hand history and session standings are cleared too.",
|
||||
confirmLabel: "Reset table",
|
||||
danger: true,
|
||||
});
|
||||
if (!ok) return;
|
||||
|
||||
HoldemState.clear();
|
||||
Object.assign(state, HoldemState.createDefault());
|
||||
Render.mount(state, hooks);
|
||||
Toast.show("Table reset.", { duration: 2500 });
|
||||
});
|
||||
|
||||
document.getElementById("resolvePotBtn").addEventListener("click", () => {
|
||||
PotWizard.open(state, payouts => {
|
||||
if (payouts.length > 0) HoldemState.recordHand(state, payouts);
|
||||
HoldemState.startNextHand(state);
|
||||
hooks.save(state);
|
||||
Render.renderAll();
|
||||
const paid = payouts.map(p => `${p.label} ${Money.format(p.amount)}`).join(", ");
|
||||
runUndoable(paid ? `Paid ${paid}.` : "Hand closed with nothing to pay out.", () => {
|
||||
if (payouts.length > 0) HoldemState.recordHand(state, payouts);
|
||||
HoldemState.startNextHand(state);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById("closeSeatEditorBtn").addEventListener("click", () => Render.closeSeatEditor());
|
||||
|
||||
document.getElementById("closePotWizardBtn").addEventListener("click", () => PotWizard.close());
|
||||
document.getElementById("potWizardOverlay").addEventListener("click", e => {
|
||||
if (e.target.id === "potWizardOverlay") PotWizard.close();
|
||||
});
|
||||
|
||||
document.getElementById("clearHistoryBtn").addEventListener("click", () => {
|
||||
if (!confirm("Clear the hand history and reset the hand counter to 1? This can't be undone.")) return;
|
||||
document.getElementById("clearHistoryBtn").addEventListener("click", async () => {
|
||||
const ok = await Dialog.confirm({
|
||||
title: "Clear the hand history?",
|
||||
message: "This wipes every logged hand and the session standings, and puts the hand counter back to #1. Export first if anyone might want the numbers.",
|
||||
confirmLabel: "Clear history",
|
||||
danger: true,
|
||||
});
|
||||
if (!ok) return;
|
||||
|
||||
HoldemState.clearHistory(state);
|
||||
hooks.save(state);
|
||||
Render.renderAll();
|
||||
Toast.show("Hand history cleared.", { duration: 2500 });
|
||||
});
|
||||
|
||||
document.getElementById("exportHistoryBtn").addEventListener("click", () => {
|
||||
|
||||
@@ -106,7 +106,10 @@ const Render = {
|
||||
node.classList.toggle("seatSelected", index === this.selectedSeatIndex);
|
||||
|
||||
node.addEventListener("click", () => {
|
||||
this.selectedSeatIndex = index;
|
||||
// Tapping the open seat again closes it — on a phone the editor is a
|
||||
// sheet over the table, and the seat you just tapped is the closest
|
||||
// thing to hand for dismissing it.
|
||||
this.selectedSeatIndex = this.selectedSeatIndex === index ? null : index;
|
||||
this.renderSeats();
|
||||
this.renderSeatEditor();
|
||||
});
|
||||
@@ -175,10 +178,20 @@ const Render = {
|
||||
|
||||
// ---- Seat editor panel (the full form for whichever seat is selected) ----
|
||||
|
||||
closeSeatEditor() {
|
||||
this.selectedSeatIndex = null;
|
||||
this.renderSeats();
|
||||
this.renderSeatEditor();
|
||||
},
|
||||
|
||||
renderSeatEditor() {
|
||||
const title = document.getElementById("seatEditorTitle");
|
||||
const body = document.getElementById("seatEditorBody");
|
||||
|
||||
// Drives the mobile bottom sheet: the panel only slides up over the table
|
||||
// while a seat is actually being edited.
|
||||
document.body.classList.toggle("seatSheetOpen", this.selectedSeatIndex !== null);
|
||||
|
||||
if (this.selectedSeatIndex === null) {
|
||||
title.textContent = "Select a seat";
|
||||
body.innerHTML = '<p class="fieldHint">Click a seat at the table to edit their name, cards, chips and anything else.</p>';
|
||||
|
||||
Reference in New Issue
Block a user