// 各ページ共通のユーティリティ (function () { 'use strict'; function lsGet(key) { try { return localStorage.getItem(key); } catch { return null; } } function lsSet(key, value) { try { localStorage.setItem(key, value); } catch { /* プライベートモード等 */ } } // 非HTTPS環境でも動くよう crypto.randomUUID ではなく getRandomValues を使う function randomHex(bytes) { const a = new Uint8Array(bytes); crypto.getRandomValues(a); return Array.from(a, (b) => b.toString(16).padStart(2, '0')).join(''); } const Store = { get name() { return lsGet('poker.name') || ''; }, set name(v) { lsSet('poker.name', v); }, // 席の再接続用。ブラウザごとに固定 get token() { let t = lsGet('poker.token'); if (!t || !/^[0-9a-f]{32}$/.test(t)) { t = randomHex(16); lsSet('poker.token', t); } return t; }, }; function esc(s) { return String(s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); } function chips(n) { return '$' + Number(n).toLocaleString('en-US'); } let toastTimer; function toast(msg) { let el = document.querySelector('.toast'); if (!el) { el = document.createElement('div'); el.className = 'toast'; document.body.appendChild(el); } el.textContent = msg; el.hidden = false; clearTimeout(toastTimer); toastTimer = setTimeout(() => { el.hidden = true; }, 2500); } async function api(method, url, body) { const res = await fetch(url, { method, headers: body ? { 'Content-Type': 'application/json' } : undefined, body: body ? JSON.stringify(body) : undefined, credentials: 'same-origin', }); if (res.status === 401 && url !== '/api/auth') { location.href = '/key?next=' + encodeURIComponent(location.pathname); throw new Error('unauthorized'); } const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data.error || 'エラーが発生しました'); return data; } // サーバは WebSocket のみ受け付ける。接続を拒否されたら鍵が無効になっていないか確認する function connectSocket() { const socket = io({ transports: ['websocket'] }); socket.on('connect_error', () => { fetch('/api/tables', { credentials: 'same-origin' }) .then((res) => { if (res.status === 401) location.href = '/key?next=' + encodeURIComponent(location.pathname); }) .catch(() => {}); }); return socket; } // 自サイト内のパスだけ許可("//x" や "/\x" は外部サイト扱いになるので拒否) function safeNext(next) { return typeof next === 'string' && /^\/(?![/\\])[^\\\s]*$/.test(next) && next.length <= 200 ? next : '/'; } const PHASE_LABELS = { WAITING: 'WAITING', PREFLOP: 'PREFLOP', FLOP: 'FLOP', TURN: 'TURN', RIVER: 'RIVER', SHOWDOWN: 'SHOWDOWN', }; window.App = { Store, esc, chips, toast, api, connectSocket, safeNext, PHASE_LABELS }; })();