Pass saved alias to platform on launch; copy to clipboard

- platforms table gets url_alias_param column (configurable per platform)
- Admin game form has new "Username URL Param" field — leave blank if platform
  doesn't support it, or set to e.g. "username" if it does
- Platform cards now use onclick openPlatform() instead of plain href:
  copies player's saved alias to clipboard on every click, and if
  url_alias_param is set appends ?param=alias to the launch URL
- Toast notification confirms "Alias copied — paste into login"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-06 11:13:45 +00:00
parent 44a98eba15
commit 483026fd07
3 changed files with 79 additions and 49 deletions
+10 -4
View File
@@ -811,6 +811,10 @@ tr:hover td{background:rgba(255,255,255,.015)}
<label class="gm-edit-label">Player URL * <span style="font-weight:400;color:var(--text2)">shown to players — opens the game</span></label> <label class="gm-edit-label">Player URL * <span style="font-weight:400;color:var(--text2)">shown to players — opens the game</span></label>
<input class="fi-sm" id="gf-player-url" type="url" placeholder="https://game.example.com/play" style="width:100%;padding:10px 12px"> <input class="fi-sm" id="gf-player-url" type="url" placeholder="https://game.example.com/play" style="width:100%;padding:10px 12px">
</div> </div>
<div style="margin-bottom:10px">
<label class="gm-edit-label">Username URL Param <span style="font-weight:400;color:var(--text2)">— optional: query param name the platform uses to pre-fill login (e.g. <code style="background:rgba(255,255,255,.07);padding:1px 5px;border-radius:3px">username</code>)</span></label>
<input class="fi-sm" id="gf-alias-param" type="text" placeholder="e.g. username, user, login" maxlength="50" style="width:100%;padding:10px 12px">
</div>
<!-- Agent Fields — EDIT mode (master admin only) --> <!-- Agent Fields — EDIT mode (master admin only) -->
<div id="gf-agent-edit" style="background:rgba(155,93,229,0.06);border:1px solid rgba(155,93,229,0.2);border-radius:8px;padding:12px 14px;margin-bottom:10px;display:none"> <div id="gf-agent-edit" style="background:rgba(155,93,229,0.06);border:1px solid rgba(155,93,229,0.2);border-radius:8px;padding:12px 14px;margin-bottom:10px;display:none">
<div style="font-size:12px;font-weight:700;color:var(--purple);letter-spacing:1px;text-transform:uppercase;margin-bottom:10px">🔐 Agent Info — Admin Only</div> <div style="font-size:12px;font-weight:700;color:var(--purple);letter-spacing:1px;text-transform:uppercase;margin-bottom:10px">🔐 Agent Info — Admin Only</div>
@@ -3051,8 +3055,9 @@ function editGame(id) {
document.getElementById('gf-name').value = g.name; document.getElementById('gf-name').value = g.name;
document.getElementById('gf-slug').value = g.slug; document.getElementById('gf-slug').value = g.slug;
document.getElementById('gf-slug').disabled = true; document.getElementById('gf-slug').disabled = true;
document.getElementById('gf-player-url').value = g.player_url; document.getElementById('gf-player-url').value = g.player_url;
document.getElementById('gf-color').value = g.color || '#f0c040'; document.getElementById('gf-alias-param').value = g.url_alias_param || '';
document.getElementById('gf-color').value = g.color || '#f0c040';
document.getElementById('gf-color-hex').value = g.color || '#f0c040'; document.getElementById('gf-color-hex').value = g.color || '#f0c040';
document.getElementById('gf-sort').value = g.sort_order; document.getElementById('gf-sort').value = g.sort_order;
document.getElementById('gf-active').value = g.is_active; document.getElementById('gf-active').value = g.is_active;
@@ -3171,8 +3176,9 @@ async function saveGame() {
id: id ? parseInt(id) : undefined, id: id ? parseInt(id) : undefined,
name: document.getElementById('gf-name').value.trim(), name: document.getElementById('gf-name').value.trim(),
slug: document.getElementById('gf-slug').value.trim(), slug: document.getElementById('gf-slug').value.trim(),
player_url: document.getElementById('gf-player-url').value.trim(), player_url: document.getElementById('gf-player-url').value.trim(),
agent_link: document.getElementById('gf-agent-link').value.trim(), url_alias_param: document.getElementById('gf-alias-param').value.trim(),
agent_link: document.getElementById('gf-agent-link').value.trim(),
agent_login: document.getElementById('gf-agent-login').value.trim(), agent_login: document.getElementById('gf-agent-login').value.trim(),
agent_password: document.getElementById('gf-agent-password').value.trim(), agent_password: document.getElementById('gf-agent-password').value.trim(),
games_link: document.getElementById('gf-games-link').value.trim(), games_link: document.getElementById('gf-games-link').value.trim(),
+45 -43
View File
@@ -12,14 +12,14 @@ switch ($action) {
// ── Public: active platforms for player app ─────────── // ── Public: active platforms for player app ───────────
case 'list': case 'list':
$stmt = db()->query("SELECT slug,name,player_url,color,icon_path FROM platforms WHERE is_active=1 AND is_deleted=0 ORDER BY sort_order ASC, id ASC"); $stmt = db()->query("SELECT slug,name,player_url,url_alias_param,color,icon_path FROM platforms WHERE is_active=1 AND is_deleted=0 ORDER BY sort_order ASC, id ASC");
$rows = $stmt->fetchAll(); $rows = $stmt->fetchAll();
// Normalize to match old CFG format
$out = array_map(fn($r) => [ $out = array_map(fn($r) => [
'id' => $r['slug'], 'id' => $r['slug'],
'name' => $r['name'], 'name' => $r['name'],
'url' => $r['player_url'], 'url' => $r['player_url'],
'color' => $r['color'], 'alias_param' => $r['url_alias_param'] ?? '',
'color' => $r['color'],
], $rows); ], $rows);
echo json_encode(['success'=>true, 'platforms'=>$out]); echo json_encode(['success'=>true, 'platforms'=>$out]);
break; break;
@@ -35,25 +35,26 @@ switch ($action) {
case 'create': case 'create':
if (!$isAdmin || $_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; } if (!$isAdmin || $_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; }
$d = json_decode(file_get_contents('php://input'), true); $d = json_decode(file_get_contents('php://input'), true);
$slug = preg_replace('/[^a-z0-9_]/', '', strtolower(trim($d['slug'] ?? ''))); $slug = preg_replace('/[^a-z0-9_]/', '', strtolower(trim($d['slug'] ?? '')));
$name = substr(trim($d['name'] ?? ''), 0, 100); $name = substr(trim($d['name'] ?? ''), 0, 100);
$player_url = substr(trim($d['player_url'] ?? ''), 0, 500); $player_url = substr(trim($d['player_url'] ?? ''), 0, 500);
$agent_link = substr(trim($d['agent_link'] ?? ''), 0, 500); $url_alias_param = preg_replace('/[^a-zA-Z0-9_\-]/', '', trim($d['url_alias_param'] ?? ''));
$agent_login = substr(trim($d['agent_login'] ?? ''), 0, 200); $agent_link = substr(trim($d['agent_link'] ?? ''), 0, 500);
$agent_password = substr(trim($d['agent_password'] ?? ''), 0, 200); $agent_login = substr(trim($d['agent_login'] ?? ''), 0, 200);
$games_link = substr(trim($d['games_link'] ?? ''), 0, 500); $agent_password = substr(trim($d['agent_password'] ?? ''), 0, 200);
$agent_guide = trim($d['agent_guide'] ?? ''); $games_link = substr(trim($d['games_link'] ?? ''), 0, 500);
$sub_agent_login = substr(trim($d['sub_agent_login'] ?? ''), 0, 200); $agent_guide = trim($d['agent_guide'] ?? '');
$sub_agent_password= substr(trim($d['sub_agent_password'] ?? ''), 0, 200); $sub_agent_login = substr(trim($d['sub_agent_login'] ?? ''), 0, 200);
$cashier_login = substr(trim($d['cashier_login'] ?? ''), 0, 200); $sub_agent_password = substr(trim($d['sub_agent_password'] ?? ''), 0, 200);
$cashier_password = substr(trim($d['cashier_password'] ?? ''), 0, 200); $cashier_login = substr(trim($d['cashier_login'] ?? ''), 0, 200);
$color = preg_match('/^#[0-9a-fA-F]{3,8}$/', $d['color'] ?? '') ? $d['color'] : '#f0c040'; $cashier_password = substr(trim($d['cashier_password'] ?? ''), 0, 200);
$sort_order = (int)($d['sort_order'] ?? 99); $color = preg_match('/^#[0-9a-fA-F]{3,8}$/', $d['color'] ?? '') ? $d['color'] : '#f0c040';
$is_active = isset($d['is_active']) ? (int)(bool)$d['is_active'] : 1; $sort_order = (int)($d['sort_order'] ?? 99);
$is_active = isset($d['is_active']) ? (int)(bool)$d['is_active'] : 1;
if (!$slug || !$name || !$player_url) { echo json_encode(['success'=>false,'error'=>'Slug, name, and player URL are required']); exit; } if (!$slug || !$name || !$player_url) { echo json_encode(['success'=>false,'error'=>'Slug, name, and player URL are required']); exit; }
try { try {
$stmt = db()->prepare("INSERT INTO platforms (slug,name,player_url,agent_link,agent_login,agent_password,games_link,agent_guide,sub_agent_login,sub_agent_password,cashier_login,cashier_password,color,sort_order,is_active) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"); $stmt = db()->prepare("INSERT INTO platforms (slug,name,player_url,url_alias_param,agent_link,agent_login,agent_password,games_link,agent_guide,sub_agent_login,sub_agent_password,cashier_login,cashier_password,color,sort_order,is_active) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)");
$stmt->execute([$slug,$name,$player_url,$agent_link,$agent_login,$agent_password,$games_link,$agent_guide,$sub_agent_login,$sub_agent_password,$cashier_login,$cashier_password,$color,$sort_order,$is_active]); $stmt->execute([$slug,$name,$player_url,$url_alias_param,$agent_link,$agent_login,$agent_password,$games_link,$agent_guide,$sub_agent_login,$sub_agent_password,$cashier_login,$cashier_password,$color,$sort_order,$is_active]);
echo json_encode(['success'=>true,'id'=>db()->lastInsertId()]); echo json_encode(['success'=>true,'id'=>db()->lastInsertId()]);
} catch (Exception $e) { } catch (Exception $e) {
echo json_encode(['success'=>false,'error'=>'Slug already exists or DB error']); echo json_encode(['success'=>false,'error'=>'Slug already exists or DB error']);
@@ -64,30 +65,31 @@ switch ($action) {
case 'update': case 'update':
if (!$isAdmin || $_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; } if (!$isAdmin || $_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; }
$d = json_decode(file_get_contents('php://input'), true); $d = json_decode(file_get_contents('php://input'), true);
$id = (int)($d['id'] ?? 0); $id = (int)($d['id'] ?? 0);
$name = substr(trim($d['name'] ?? ''), 0, 100); $name = substr(trim($d['name'] ?? ''), 0, 100);
$player_url = substr(trim($d['player_url'] ?? ''), 0, 500); $player_url = substr(trim($d['player_url'] ?? ''), 0, 500);
$agent_link = substr(trim($d['agent_link'] ?? ''), 0, 500); $url_alias_param = preg_replace('/[^a-zA-Z0-9_\-]/', '', trim($d['url_alias_param'] ?? ''));
$agent_login = substr(trim($d['agent_login'] ?? ''), 0, 200); $agent_link = substr(trim($d['agent_link'] ?? ''), 0, 500);
$agent_password = substr(trim($d['agent_password'] ?? ''), 0, 200); $agent_login = substr(trim($d['agent_login'] ?? ''), 0, 200);
$games_link = substr(trim($d['games_link'] ?? ''), 0, 500); $agent_password = substr(trim($d['agent_password'] ?? ''), 0, 200);
$agent_guide = trim($d['agent_guide'] ?? ''); $games_link = substr(trim($d['games_link'] ?? ''), 0, 500);
$sub_agent_login = substr(trim($d['sub_agent_login'] ?? ''), 0, 200); $agent_guide = trim($d['agent_guide'] ?? '');
$sub_agent_password= substr(trim($d['sub_agent_password'] ?? ''), 0, 200); $sub_agent_login = substr(trim($d['sub_agent_login'] ?? ''), 0, 200);
$cashier_login = substr(trim($d['cashier_login'] ?? ''), 0, 200); $sub_agent_password = substr(trim($d['sub_agent_password'] ?? ''), 0, 200);
$cashier_password = substr(trim($d['cashier_password'] ?? ''), 0, 200); $cashier_login = substr(trim($d['cashier_login'] ?? ''), 0, 200);
$color = preg_match('/^#[0-9a-fA-F]{3,8}$/', $d['color'] ?? '') ? $d['color'] : '#f0c040'; $cashier_password = substr(trim($d['cashier_password'] ?? ''), 0, 200);
$sort_order = (int)($d['sort_order'] ?? 99); $color = preg_match('/^#[0-9a-fA-F]{3,8}$/', $d['color'] ?? '') ? $d['color'] : '#f0c040';
$is_active = (int)(bool)($d['is_active'] ?? 1); $sort_order = (int)($d['sort_order'] ?? 99);
$is_active = (int)(bool)($d['is_active'] ?? 1);
if (!$id || !$name || !$player_url) { echo json_encode(['success'=>false,'error'=>'ID, name, and player URL required']); exit; } if (!$id || !$name || !$player_url) { echo json_encode(['success'=>false,'error'=>'ID, name, and player URL required']); exit; }
if ($isMasterAdmin) { if ($isMasterAdmin) {
// Master admin: update all fields including agent info // Master admin: update all fields including agent info
db()->prepare("UPDATE platforms SET name=?,player_url=?,agent_link=?,agent_login=?,agent_password=?,games_link=?,agent_guide=?,sub_agent_login=?,sub_agent_password=?,cashier_login=?,cashier_password=?,color=?,sort_order=?,is_active=? WHERE id=?") db()->prepare("UPDATE platforms SET name=?,player_url=?,url_alias_param=?,agent_link=?,agent_login=?,agent_password=?,games_link=?,agent_guide=?,sub_agent_login=?,sub_agent_password=?,cashier_login=?,cashier_password=?,color=?,sort_order=?,is_active=? WHERE id=?")
->execute([$name,$player_url,$agent_link,$agent_login,$agent_password,$games_link,$agent_guide,$sub_agent_login,$sub_agent_password,$cashier_login,$cashier_password,$color,$sort_order,$is_active,$id]); ->execute([$name,$player_url,$url_alias_param,$agent_link,$agent_login,$agent_password,$games_link,$agent_guide,$sub_agent_login,$sub_agent_password,$cashier_login,$cashier_password,$color,$sort_order,$is_active,$id]);
} else { } else {
// Regular admin: update only non-sensitive fields // Regular admin: update non-sensitive fields including alias param
db()->prepare("UPDATE platforms SET name=?,player_url=?,color=?,sort_order=?,is_active=? WHERE id=?") db()->prepare("UPDATE platforms SET name=?,player_url=?,url_alias_param=?,color=?,sort_order=?,is_active=? WHERE id=?")
->execute([$name,$player_url,$color,$sort_order,$is_active,$id]); ->execute([$name,$player_url,$url_alias_param,$color,$sort_order,$is_active,$id]);
} }
echo json_encode(['success'=>true]); echo json_encode(['success'=>true]);
break; break;
+24 -2
View File
@@ -661,6 +661,7 @@ body::before{content:'';position:fixed;inset:0;background-image:linear-gradient(
</div> </div>
<div class="section-title"><span>PLAY NOW</span></div> <div class="section-title"><span>PLAY NOW</span></div>
<div class="platform-grid" id="platform-grid"></div> <div class="platform-grid" id="platform-grid"></div>
<div id="platform-alias-toast" style="position:fixed;bottom:80px;left:50%;transform:translateX(-50%) translateY(10px);background:rgba(0,0,0,.88);color:#fff;font-size:14px;font-weight:700;padding:10px 18px;border-radius:24px;white-space:nowrap;opacity:0;transition:opacity .3s,transform .3s;pointer-events:none;z-index:999;border:1px solid rgba(0,229,255,.25)"></div>
</div> </div>
<!-- ═══ BUY TOKENS (milkyswipe style) ═══ --> <!-- ═══ BUY TOKENS (milkyswipe style) ═══ -->
@@ -1405,19 +1406,40 @@ async function refreshUser() {
} }
// ─── PLATFORMS ───────────────────────────────────────────── // ─── PLATFORMS ─────────────────────────────────────────────
function openPlatform(slug, url, aliasParam) {
const alias = savedAliases[slug] || '';
let launchUrl = url;
if (alias && aliasParam) {
const sep = url.includes('?') ? '&' : '?';
launchUrl = url + sep + encodeURIComponent(aliasParam) + '=' + encodeURIComponent(alias);
}
window.open(launchUrl, '_blank', 'noopener');
if (alias) {
navigator.clipboard?.writeText(alias).catch(() => {});
const t = document.getElementById('platform-alias-toast');
if (t) {
t.textContent = '📋 "' + alias + '" copied — paste into login';
t.style.opacity = '1'; t.style.transform = 'translateY(0)';
clearTimeout(t._timer);
t._timer = setTimeout(() => { t.style.opacity='0'; t.style.transform='translateY(10px)'; }, 3000);
}
}
}
function buildPlatforms() { function buildPlatforms() {
if (!CFG.platforms || !CFG.platforms.length) return; if (!CFG.platforms || !CFG.platforms.length) return;
const grid = document.getElementById('platform-grid'); const grid = document.getElementById('platform-grid');
if (grid) { if (grid) {
grid.innerHTML = CFG.platforms.map(p => ` grid.innerHTML = CFG.platforms.map(p => `
<a href="${p.url}" target="_blank" class="platform-card" style="--p-color:${p.color}"> <div class="platform-card" style="--p-color:${p.color};cursor:pointer"
onclick="openPlatform('${p.id}',${JSON.stringify(p.url)},${JSON.stringify(p.alias_param||'')})">
<div class="platform-img-wrap"> <div class="platform-img-wrap">
<img src="/assets/img/${p.id}.svg" alt="${p.name}" onerror="this.style.display='none'"> <img src="/assets/img/${p.id}.svg" alt="${p.name}" onerror="this.style.display='none'">
</div> </div>
<div class="platform-name">${p.name}</div> <div class="platform-name">${p.name}</div>
<div class="play-btn">TAP TO PLAY →</div> <div class="play-btn">TAP TO PLAY →</div>
</a>`).join(''); </div>`).join('');
} }
// Populate selects — clear dynamic options first to prevent duplicates on re-call // Populate selects — clear dynamic options first to prevent duplicates on re-call