Add App.confirm() in common.js: a promise-based dialog styled like the site's modals, with a title, multi-line message and custom button labels. Destructive actions use a red OK button and focus Cancel by default; Escape and backdrop clicks cancel, and Tab stays inside the dialog. Use it for folding when a check is possible, leaving the table, confirming showdown winners and deleting tables from the lobby. After the dialog closes, re-check that the action is still valid (turn not passed, pot not already awarded, table still listed) before sending it. Also add a "poker-test" launch config that runs on port 4672 with the example keys from .env.example, for local browser testing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
117 lines
4.3 KiB
JavaScript
117 lines
4.3 KiB
JavaScript
(function () {
|
|
'use strict';
|
|
|
|
const { Store, esc, chips } = App;
|
|
const nameInput = document.getElementById('name');
|
|
const nameError = document.getElementById('name-error');
|
|
const listEl = document.getElementById('list');
|
|
const countEl = document.getElementById('count');
|
|
|
|
nameInput.value = Store.name;
|
|
nameInput.addEventListener('input', () => {
|
|
Store.name = nameInput.value.trim();
|
|
nameInput.classList.remove('invalid');
|
|
nameError.textContent = '';
|
|
});
|
|
|
|
function requireName() {
|
|
if (Store.name) return true;
|
|
nameInput.classList.add('invalid');
|
|
nameError.textContent = '先に名前を入力してください。';
|
|
nameInput.focus();
|
|
return false;
|
|
}
|
|
|
|
let admin = false;
|
|
let tables = null;
|
|
|
|
// 管理者だけにハンドレンジ学習・CPU戦のレビューへのリンクとテーブルの削除ボタンを出す
|
|
// (ページ・API 自体もサーバ側で管理者以外は 404)
|
|
App.api('GET', '/api/me')
|
|
.then((me) => {
|
|
admin = Boolean(me.admin);
|
|
document.getElementById('range-link').hidden = !admin;
|
|
document.getElementById('review-link').hidden = !admin;
|
|
if (tables) render(tables);
|
|
})
|
|
.catch(() => {});
|
|
|
|
document.getElementById('create').addEventListener('click', () => {
|
|
if (requireName()) location.href = '/create';
|
|
});
|
|
|
|
listEl.addEventListener('click', (e) => {
|
|
const del = e.target.closest('[data-delete]');
|
|
if (del) return deleteTable(del.dataset.delete, del);
|
|
const btn = e.target.closest('[data-join]');
|
|
if (btn && requireName()) location.href = '/table/' + encodeURIComponent(btn.dataset.join);
|
|
});
|
|
|
|
async function deleteTable(id, btn) {
|
|
const t = tables && tables.find((x) => x.id === id);
|
|
if (!t) return;
|
|
const players = t.playerCount ? `
|
|
着席中の ${t.playerCount} 人はテーブルから退出させられます。` : '';
|
|
const ok = await App.confirm({
|
|
title: 'テーブルを削除',
|
|
message: `「${t.name}」を削除しますか?${players}`,
|
|
ok: '削除する',
|
|
danger: true,
|
|
});
|
|
if (!ok || !document.contains(btn)) return;
|
|
btn.disabled = true;
|
|
App.api('DELETE', '/api/tables/' + encodeURIComponent(id))
|
|
.then(() => App.toast('テーブルを削除しました'))
|
|
.catch((err) => {
|
|
btn.disabled = false;
|
|
App.toast(err.message);
|
|
});
|
|
}
|
|
|
|
const MODE_LABEL = { chips: '💰 キャッシュオンリー', friends: '🃏 友人戦', cpu: '🤖 CPU戦', review: '📝 レビュー付きCPU戦' };
|
|
|
|
function phaseBadge(t) {
|
|
if (t.paused) return '<span class="badge paused">PAUSED</span>';
|
|
if (t.phase === 'WAITING') return '<span class="badge">WAITING</span>';
|
|
return '<span class="badge live">PLAYING</span>';
|
|
}
|
|
|
|
function render(list) {
|
|
tables = list;
|
|
countEl.textContent = tables.length ? `${tables.length} テーブル` : '';
|
|
if (!tables.length) {
|
|
listEl.innerHTML = '<div class="empty">開催中のテーブルはありません。<br>テーブルを作成して友達を招待しよう。</div>';
|
|
return;
|
|
}
|
|
listEl.innerHTML = tables
|
|
.map((t) => {
|
|
const full = t.playerCount >= t.maxSeats;
|
|
const timer = t.timerEnabled ? `⏱ ${t.intervalMin}分` : '⏱ なし';
|
|
return `
|
|
<div class="table-item">
|
|
<div class="info">
|
|
<div class="name">${esc(t.name)}${phaseBadge(t)}</div>
|
|
<div class="meta">
|
|
<span>👤 <b>${t.playerCount}/${t.maxSeats}</b></span>
|
|
<span>開始 <b>${chips(t.startingStack)}</b></span>
|
|
<span class="gold">Lv.${t.level} <b class="gold">${chips(t.smallBlind)}/${chips(t.bigBlind)}</b></span>
|
|
<span>${timer}</span>
|
|
<span>${MODE_LABEL[t.mode] || ''}</span>
|
|
</div>
|
|
</div>
|
|
<div class="buttons">
|
|
${admin ? `<button class="btn btn-red btn-sm" data-delete="${esc(t.id)}" title="テーブルを削除">削除</button>` : ''}
|
|
<button class="btn btn-blue btn-sm" data-join="${esc(t.id)}" ${full ? 'title="満席(観戦のみ)"' : ''}>
|
|
${full ? '観戦' : '参加'}
|
|
</button>
|
|
</div>
|
|
</div>`;
|
|
})
|
|
.join('');
|
|
}
|
|
|
|
const socket = App.connectSocket();
|
|
socket.on('connect', () => socket.emit('lobby:subscribe'));
|
|
socket.on('lobby:tables', render);
|
|
})();
|