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">
|
<aside id="holdemSidebar">
|
||||||
<section id="seatEditorPanel" class="sidebarPanel seatEditorPanel">
|
<section id="seatEditorPanel" class="sidebarPanel seatEditorPanel">
|
||||||
|
<div class="seatSheetGrip" aria-hidden="true"></div>
|
||||||
|
<div class="seatEditorHeader">
|
||||||
<h2 id="seatEditorTitle">Select a seat</h2>
|
<h2 id="seatEditorTitle">Select a seat</h2>
|
||||||
|
<button id="closeSeatEditorBtn" class="seatSheetClose" aria-label="Done editing this seat">Done</button>
|
||||||
|
</div>
|
||||||
<div id="seatEditorBody">
|
<div id="seatEditorBody">
|
||||||
<p class="fieldHint">Click a seat at the table to edit their name, cards, chips and anything else.</p>
|
<p class="fieldHint">Click a seat at the table to edit their name, cards, chips and anything else.</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -206,6 +210,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</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="potWizardOverlay" hidden>
|
||||||
<div id="potWizardModal">
|
<div id="potWizardModal">
|
||||||
<div class="potWizardHeader">
|
<div class="potWizardHeader">
|
||||||
@@ -222,6 +239,7 @@
|
|||||||
|
|
||||||
@section Scripts {
|
@section Scripts {
|
||||||
<script src="~/js/casino/holdem/money.js?v=@ViewData["cssVersion"]"></script>
|
<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/potCalculator.js?v=@ViewData["cssVersion"]"></script>
|
||||||
<script src="~/js/casino/holdem/state.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>
|
<script src="~/js/casino/holdem/render.js?v=@ViewData["cssVersion"]"></script>
|
||||||
|
|||||||
@@ -376,6 +376,27 @@ body {
|
|||||||
padding: 0.9rem;
|
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 {
|
.seatEditorPanel h2 {
|
||||||
margin: 0 0 0.6rem;
|
margin: 0 0 0.6rem;
|
||||||
padding-bottom: 0.5rem;
|
padding-bottom: 0.5rem;
|
||||||
@@ -732,6 +753,135 @@ body {
|
|||||||
|
|
||||||
/* ---------- Pot wizard ---------- */
|
/* ---------- 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 {
|
#potWizardOverlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
@@ -843,3 +993,206 @@ body {
|
|||||||
min-height: 44px;
|
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);
|
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 => {
|
document.getElementById("seatCountInput").addEventListener("change", e => {
|
||||||
HoldemState.resizeSeats(state, Number(e.target.value));
|
HoldemState.resizeSeats(state, Number(e.target.value));
|
||||||
hooks.save(state);
|
hooks.save(state);
|
||||||
@@ -49,38 +72,63 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById("nextHandBtn").addEventListener("click", () => {
|
document.getElementById("nextHandBtn").addEventListener("click", () => {
|
||||||
if (!confirm("Start the next hand? This moves the button, clears cards and chip counts for every seat.")) return;
|
// startNextHand moves the button on by one; say where it landed so the
|
||||||
HoldemState.startNextHand(state);
|
// dealer can confirm the rotation without hunting for the badge.
|
||||||
hooks.save(state);
|
const nextDealer = HoldemState.nextSeatIndex(state, state.dealerSeatIndex, 1);
|
||||||
Render.renderAll();
|
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", () => {
|
document.getElementById("resetTableBtn").addEventListener("click", async () => {
|
||||||
if (!confirm("Reset the whole table? This clears every seat, chip legend and note.")) return;
|
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();
|
HoldemState.clear();
|
||||||
Object.assign(state, HoldemState.createDefault());
|
Object.assign(state, HoldemState.createDefault());
|
||||||
Render.mount(state, hooks);
|
Render.mount(state, hooks);
|
||||||
|
Toast.show("Table reset.", { duration: 2500 });
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById("resolvePotBtn").addEventListener("click", () => {
|
document.getElementById("resolvePotBtn").addEventListener("click", () => {
|
||||||
PotWizard.open(state, payouts => {
|
PotWizard.open(state, payouts => {
|
||||||
|
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);
|
if (payouts.length > 0) HoldemState.recordHand(state, payouts);
|
||||||
HoldemState.startNextHand(state);
|
HoldemState.startNextHand(state);
|
||||||
hooks.save(state);
|
|
||||||
Render.renderAll();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("closeSeatEditorBtn").addEventListener("click", () => Render.closeSeatEditor());
|
||||||
|
|
||||||
document.getElementById("closePotWizardBtn").addEventListener("click", () => PotWizard.close());
|
document.getElementById("closePotWizardBtn").addEventListener("click", () => PotWizard.close());
|
||||||
document.getElementById("potWizardOverlay").addEventListener("click", e => {
|
document.getElementById("potWizardOverlay").addEventListener("click", e => {
|
||||||
if (e.target.id === "potWizardOverlay") PotWizard.close();
|
if (e.target.id === "potWizardOverlay") PotWizard.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById("clearHistoryBtn").addEventListener("click", () => {
|
document.getElementById("clearHistoryBtn").addEventListener("click", async () => {
|
||||||
if (!confirm("Clear the hand history and reset the hand counter to 1? This can't be undone.")) return;
|
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);
|
HoldemState.clearHistory(state);
|
||||||
hooks.save(state);
|
hooks.save(state);
|
||||||
Render.renderAll();
|
Render.renderAll();
|
||||||
|
Toast.show("Hand history cleared.", { duration: 2500 });
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById("exportHistoryBtn").addEventListener("click", () => {
|
document.getElementById("exportHistoryBtn").addEventListener("click", () => {
|
||||||
|
|||||||
@@ -106,7 +106,10 @@ const Render = {
|
|||||||
node.classList.toggle("seatSelected", index === this.selectedSeatIndex);
|
node.classList.toggle("seatSelected", index === this.selectedSeatIndex);
|
||||||
|
|
||||||
node.addEventListener("click", () => {
|
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.renderSeats();
|
||||||
this.renderSeatEditor();
|
this.renderSeatEditor();
|
||||||
});
|
});
|
||||||
@@ -175,10 +178,20 @@ const Render = {
|
|||||||
|
|
||||||
// ---- Seat editor panel (the full form for whichever seat is selected) ----
|
// ---- Seat editor panel (the full form for whichever seat is selected) ----
|
||||||
|
|
||||||
|
closeSeatEditor() {
|
||||||
|
this.selectedSeatIndex = null;
|
||||||
|
this.renderSeats();
|
||||||
|
this.renderSeatEditor();
|
||||||
|
},
|
||||||
|
|
||||||
renderSeatEditor() {
|
renderSeatEditor() {
|
||||||
const title = document.getElementById("seatEditorTitle");
|
const title = document.getElementById("seatEditorTitle");
|
||||||
const body = document.getElementById("seatEditorBody");
|
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) {
|
if (this.selectedSeatIndex === null) {
|
||||||
title.textContent = "Select a seat";
|
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>';
|
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