mirror of
https://github.com/myronblair/tomtomgames
synced 2026-06-30 17:51:08 -05:00
8238db3026
Players now paste the URL of their post instead of just clicking a platform button. The server fetches the URL and looks for the player's referral code in the page content. If found, the share is auto-approved and tokens are awarded immediately. If not (login wall, private page, code missing), it falls into the pending queue with a reason so admins can click the link directly for manual review. - api/referrals.php: replace submit_share with URL-accepting version; add scrapeForReferralCode() (SSRF-guarded cURL, 8s timeout, 512KB cap) and inferPlatformFromUrl() helpers - db/schema.sql: add share_url, auto_verified, verify_result columns - index.php: replace platform buttons with URL input form; show auto- verify result inline; shares list shows URL and auto-verify badge - admin/index.php: share cards show clickable URL, auto-check result label, and auto-verified tag Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3303 lines
186 KiB
PHP
3303 lines
186 KiB
PHP
<?php
|
||
ob_start();
|
||
require_once __DIR__ . '/../includes/config.php';
|
||
require_once __DIR__ . '/../includes/square.php';
|
||
ob_end_clean();
|
||
$_appVersion = '1.0.2';
|
||
?>
|
||
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||
<meta name="mobile-web-app-capable" content="yes">
|
||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||
<meta name="theme-color" content="#0a0a12">
|
||
|
||
<!-- ═══ PRIMARY SEO ═══════════════════════════════════════════ -->
|
||
<title>TomTomGames — Buy Game Tokens | VBlink777, Fire Kirin, Milky Way & More</title>
|
||
<meta name="description" content="TomTomGames is your trusted token portal for top fish table and skill games. Buy tokens instantly for VBlink777, Fire Kirin, Milky Way, Ultra Panda, Panda Master, Noble777 & eGame99. Fast deposits, secure payments, 24/7 support.">
|
||
<meta name="keywords" content="buy game tokens, fish table games, VBlink777 tokens, Fire Kirin tokens, Milky Way game, Ultra Panda game, Panda Master tokens, Noble777, eGame99, online skill games, token portal, game credits">
|
||
<meta name="robots" content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1">
|
||
<meta name="author" content="TomTomGames">
|
||
<link rel="canonical" href="https://tomtomgames.com/">
|
||
|
||
<!-- ═══ OPEN GRAPH (Facebook, Discord, iMessage) ═══════════════ -->
|
||
<meta property="og:type" content="website">
|
||
<meta property="og:url" content="https://tomtomgames.com/">
|
||
<meta property="og:title" content="TomTomGames — Buy Game Tokens Instantly">
|
||
<meta property="og:description" content="The fastest way to buy tokens for VBlink777, Fire Kirin, Milky Way, Ultra Panda, Panda Master, Noble777 & eGame99. Secure payments, instant delivery.">
|
||
<meta property="og:image" content="https://tomtomgames.com/assets/img/og-image.png">
|
||
<meta property="og:image:width" content="1200">
|
||
<meta property="og:image:height" content="630">
|
||
<meta property="og:site_name" content="TomTomGames">
|
||
<meta property="og:locale" content="en_US">
|
||
|
||
<!-- ═══ TWITTER / X CARD ══════════════════════════════════════ -->
|
||
<meta name="twitter:card" content="summary_large_image">
|
||
<meta name="twitter:title" content="TomTomGames — Buy Game Tokens Instantly">
|
||
<meta name="twitter:description" content="Buy tokens for Fish Table games — VBlink777, Fire Kirin, Milky Way & more. Fast, secure, 24/7.">
|
||
<meta name="twitter:image" content="https://tomtomgames.com/assets/img/og-image.png">
|
||
|
||
<!-- ═══ STRUCTURED DATA (JSON-LD Schema) ═══════════════════════ -->
|
||
<script type="application/ld+json">
|
||
{
|
||
"@context": "https://schema.org",
|
||
"@graph": [
|
||
{
|
||
"@type": "Organization",
|
||
"@id": "https://tomtomgames.com/#organization",
|
||
"name": "TomTomGames",
|
||
"url": "https://tomtomgames.com",
|
||
"logo": {
|
||
"@type": "ImageObject",
|
||
"url": "https://tomtomgames.com/assets/img/logo-icon.svg"
|
||
},
|
||
"contactPoint": {
|
||
"@type": "ContactPoint",
|
||
"contactType": "customer support",
|
||
"availableLanguage": "English"
|
||
},
|
||
"sameAs": []
|
||
},
|
||
{
|
||
"@type": "WebSite",
|
||
"@id": "https://tomtomgames.com/#website",
|
||
"url": "https://tomtomgames.com",
|
||
"name": "TomTomGames",
|
||
"description": "Token portal for fish table and skill games including VBlink777, Fire Kirin, Milky Way, Ultra Panda, Panda Master, Noble777 and eGame99.",
|
||
"publisher": {"@id": "https://tomtomgames.com/#organization"},
|
||
"potentialAction": {
|
||
"@type": "SearchAction",
|
||
"target": "https://tomtomgames.com/?q={search_term_string}",
|
||
"query-input": "required name=search_term_string"
|
||
}
|
||
},
|
||
{
|
||
"@type": "WebApplication",
|
||
"@id": "https://tomtomgames.com/#app",
|
||
"name": "TomTomGames Token Portal",
|
||
"url": "https://tomtomgames.com",
|
||
"applicationCategory": "GameApplication",
|
||
"operatingSystem": "Web, iOS, Android",
|
||
"description": "Buy and manage game tokens for popular fish table games. Supports VBlink777, Fire Kirin, Milky Way, Ultra Panda, Panda Master, Noble777, and eGame99.",
|
||
"offers": {
|
||
"@type": "AggregateOffer",
|
||
"priceCurrency": "USD",
|
||
"lowPrice": "5",
|
||
"highPrice": "500",
|
||
"offerCount": "6"
|
||
},
|
||
"featureList": [
|
||
"Instant token delivery",
|
||
"Secure Square payments",
|
||
"24/7 support chat",
|
||
"Multiple game platforms supported",
|
||
"Mobile-first design"
|
||
]
|
||
},
|
||
{
|
||
"@type": "FAQPage",
|
||
"mainEntity": [
|
||
{
|
||
"@type": "Question",
|
||
"name": "What games does TomTomGames support?",
|
||
"acceptedAnswer": {
|
||
"@type": "Answer",
|
||
"text": "TomTomGames supports VBlink777, Fire Kirin, Milky Way, Ultra Panda, Panda Master, Noble777, and eGame99 fish table and skill game platforms."
|
||
}
|
||
},
|
||
{
|
||
"@type": "Question",
|
||
"name": "How do I buy game tokens?",
|
||
"acceptedAnswer": {
|
||
"@type": "Answer",
|
||
"text": "Create an account, select your game platform, choose a token package (5 to 100+ tokens), and pay securely by credit card, Venmo, Cash App, Chime, or Zelle. Tokens are credited to your account instantly after payment."
|
||
}
|
||
},
|
||
{
|
||
"@type": "Question",
|
||
"name": "How fast are tokens delivered?",
|
||
"acceptedAnswer": {
|
||
"@type": "Answer",
|
||
"text": "Card payments are processed instantly. Manual payments via Venmo, Zelle, Cash App, or Chime are credited within minutes after we confirm receipt."
|
||
}
|
||
},
|
||
{
|
||
"@type": "Question",
|
||
"name": "Is TomTomGames safe?",
|
||
"acceptedAnswer": {
|
||
"@type": "Answer",
|
||
"text": "Yes. All card transactions are processed through Square, a PCI-compliant payment processor. We never store your full card details."
|
||
}
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
</script>
|
||
|
||
<!-- ═══ PWA / APP META ════════════════════════════════════════ -->
|
||
<link rel="manifest" href="/manifest.json">
|
||
<link href="https://fonts.googleapis.com/css2?family=Exo+2:wght@400;600;700;900&family=Rajdhani:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||
<style>
|
||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||
:root{
|
||
--bg:#0a0a12;--bg2:#10101e;--bg3:#181828;--card:#1a1a2e;
|
||
--border:rgba(255,255,255,0.07);--gold:#f0c040;--gold2:#ffdc73;
|
||
--cyan:#00e5ff;--purple:#9b5de5;--green:#00e676;--red:#ff4444;
|
||
--yellow:#ffd60a;--text:#e8e8f0;--text2:#8888aa;
|
||
--radius:16px;--rsm:10px;
|
||
/* ── Typography Scale ── */
|
||
--fs-xs:11px;--fs-sm:13px;--fs-base:15px;--fs-md:16px;
|
||
--fs-lg:18px;--fs-xl:20px;--fs-2xl:24px;--fs-3xl:28px;
|
||
--lh:1.6;--lh-tight:1.3;
|
||
--glow-gold:0 0 20px rgba(240,192,64,0.4);
|
||
--max:480px;--nav-h:64px
|
||
}
|
||
html{-webkit-tap-highlight-color:transparent}
|
||
body{background:var(--bg);color:var(--text);font-family:'Rajdhani',sans-serif;font-size:17px;min-height:100dvh;overflow-x:hidden;-webkit-overflow-scrolling:touch}
|
||
body::before{content:'';position:fixed;inset:0;background-image:linear-gradient(rgba(0,229,255,0.03) 1px,transparent 1px),linear-gradient(90deg,rgba(0,229,255,0.03) 1px,transparent 1px);background-size:40px 40px;pointer-events:none;z-index:0}
|
||
|
||
/* AUTH */
|
||
.auth-screen{display:flex;flex-direction:column;min-height:100vh;min-height:100dvh;background:var(--bg);padding:40px 24px 32px;max-width:var(--max);margin:0 auto;width:100%}
|
||
.auth-logo{text-align:center;margin-bottom:36px}
|
||
.auth-logo-text{font-family:'Exo 2',sans-serif;font-weight:900;font-size:32px;display:flex;align-items:center;justify-content:center;gap:12px}.auth-logo-text span{background:linear-gradient(135deg,var(--gold),var(--cyan));-webkit-background-clip:text;-webkit-text-fill-color:transparent}
|
||
.auth-logo-sub{font-size:15px;color:var(--text2);margin-top:4px;letter-spacing:1px}
|
||
.auth-tabs{display:flex;margin-bottom:24px;background:var(--bg3);border-radius:var(--rsm);padding:4px}
|
||
.auth-tab{flex:1;padding:11px;border:none;background:none;color:var(--text2);font-family:'Exo 2',sans-serif;font-weight:700;font-size:15px;border-radius:8px;cursor:pointer;transition:all .2s;letter-spacing:.5px}
|
||
.auth-tab.active{background:linear-gradient(135deg,rgba(240,192,64,.15),rgba(0,229,255,.08));color:var(--gold)}
|
||
|
||
/* MAIN */
|
||
#main-app{display:none;flex-direction:column;min-height:100vh;max-width:var(--max);margin:0 auto;position:relative;z-index:1;padding-bottom:0}
|
||
.topbar{position:sticky;top:0;z-index:100;background:rgba(10,10,18,.96);backdrop-filter:blur(12px);border-bottom:1px solid var(--border);padding:0 16px;height:58px;display:flex;align-items:center;justify-content:space-between}
|
||
.topbar-logo{font-family:'Exo 2',sans-serif;font-weight:900;font-size:18px;display:flex;align-items:center;gap:7px;letter-spacing:.5px}.topbar-logo span{background:linear-gradient(135deg,var(--gold),var(--cyan));-webkit-background-clip:text;-webkit-text-fill-color:transparent}
|
||
.topbar-right{display:flex;align-items:center;gap:10px}
|
||
.token-badge{background:linear-gradient(135deg,#1a1a2e,#252540);border:1px solid var(--gold);border-radius:20px;padding:4px 12px;display:flex;align-items:center;gap:5px;font-family:'Exo 2',sans-serif;font-weight:700;font-size:15px;color:var(--gold2);box-shadow:var(--glow-gold)}
|
||
.avatar-btn{width:34px;height:34px;border-radius:50%;background:linear-gradient(135deg,var(--purple),var(--cyan));border:none;cursor:pointer;font-family:'Exo 2',sans-serif;font-weight:700;font-size:15px;color:#fff;display:flex;align-items:center;justify-content:center}
|
||
|
||
/* PAGES */
|
||
.page{display:none;padding:16px;padding-bottom:84px}
|
||
/* ─── TOKEN COIN ──────────────────────────────────────────── */
|
||
.token-coin{display:inline-flex;align-items:center;justify-content:center;vertical-align:middle;flex-shrink:0}
|
||
.token-coin svg{display:block}
|
||
|
||
.page.active{display:block}
|
||
.page.active{display:block;animation:fadeUp .2s ease}
|
||
@keyframes fadeUp{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
|
||
|
||
/* NAV */
|
||
.navbar{position:fixed;bottom:0;left:50%;transform:translateX(-50%);width:100%;max-width:var(--max);height:var(--nav-h);background:rgba(10,10,18,.98);backdrop-filter:blur(16px);border-top:1px solid var(--border);display:flex;align-items:center;justify-content:space-around;z-index:100;padding-bottom:env(safe-area-inset-bottom)}
|
||
.nav-btn{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;border:none;background:none;color:var(--text2);cursor:pointer;padding:8px 4px;transition:color .2s;font-family:'Rajdhani',sans-serif;font-weight:600;font-size:12px;letter-spacing:.5px}
|
||
.nav-btn svg{width:21px;height:21px}
|
||
.nav-btn.active{color:var(--gold)}
|
||
.nav-btn.active svg{filter:drop-shadow(0 0 5px rgba(240,192,64,.6))}
|
||
|
||
/* SECTION TITLE */
|
||
.section-title{font-family:'Exo 2',sans-serif;font-weight:700;font-size:18px;margin-bottom:16px;display:flex;align-items:center;gap:8px}
|
||
.section-title span{background:linear-gradient(135deg,var(--gold),var(--cyan));-webkit-background-clip:text;-webkit-text-fill-color:transparent}
|
||
|
||
/* CARDS */
|
||
.card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:18px;margin-bottom:14px}
|
||
|
||
/* HERO BALANCE */
|
||
.hero-balance{background:linear-gradient(135deg,#1a1228,#121a28);border:1px solid rgba(240,192,64,.2);border-radius:var(--radius);padding:22px 20px;text-align:center;margin-bottom:20px;position:relative;overflow:hidden;box-shadow:0 0 0 1px rgba(240,192,64,.15),0 8px 32px rgba(0,0,0,.4)}
|
||
.hero-balance::before{content:'';position:absolute;top:-50px;right:-50px;width:160px;height:160px;background:radial-gradient(circle,rgba(240,192,64,.12) 0%,transparent 70%)}
|
||
.balance-label{font-size:13px;font-weight:700;color:var(--text2);letter-spacing:2px;text-transform:uppercase;margin-bottom:6px}
|
||
.balance-amount{font-family:'Exo 2',sans-serif;font-weight:900;font-size:48px;color:var(--gold);line-height:1;text-shadow:var(--glow-gold)}
|
||
.balance-unit{font-size:15px;color:var(--text2);margin-top:2px;letter-spacing:1px}
|
||
.balance-alias{margin-top:12px;font-size:15px;color:var(--text2)}
|
||
.balance-alias strong{color:var(--cyan)}
|
||
|
||
/* PLATFORM GRID */
|
||
.platform-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}
|
||
.platform-card{background:linear-gradient(145deg,#1a1a2e,#111120);border:1px solid var(--border);border-radius:14px;padding:14px 10px 12px;text-align:center;cursor:pointer;transition:all .2s;text-decoration:none;display:flex;flex-direction:column;align-items:center;gap:8px;position:relative;overflow:hidden}
|
||
.platform-card::before{content:'';position:absolute;top:0;left:0;right:0;height:3px;background:var(--p-color);box-shadow:0 0 10px var(--p-color)}
|
||
.platform-card:active{transform:scale(.96)}
|
||
.platform-img-wrap{width:58px;height:58px;border-radius:14px;overflow:hidden;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.3);border:1px solid rgba(255,255,255,.08)}
|
||
.platform-img-wrap img{width:58px;height:58px;object-fit:cover}
|
||
.platform-name{font-family:'Exo 2',sans-serif;font-weight:700;font-size:14px;color:var(--text)}
|
||
.play-btn{font-size:12px;color:var(--text2);font-weight:600;letter-spacing:.5px}
|
||
|
||
/* ─── PAYMENT FORM (milkyswipe style) ─── */
|
||
.pay-form{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;margin-bottom:14px}
|
||
.pay-form-header{background:linear-gradient(135deg,#0d1a2e,#0a1220);padding:16px 18px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px}
|
||
.pay-form-icon{width:36px;height:36px;border-radius:10px;background:linear-gradient(135deg,var(--gold),#d4a017);display:flex;align-items:center;justify-content:center;font-size:18px;flex-shrink:0}
|
||
.pay-form-title{font-family:'Exo 2',sans-serif;font-weight:700;font-size:17px;color:var(--text)}
|
||
.pay-form-sub{font-size:14px;color:var(--text2);margin-top:1px}
|
||
.pay-form-body{padding:18px}
|
||
|
||
/* Package pills */
|
||
.pkg-scroll{display:flex;gap:8px;overflow-x:auto;padding-bottom:4px;margin-bottom:18px;-webkit-overflow-scrolling:touch;scrollbar-width:none}
|
||
.pkg-scroll::-webkit-scrollbar{display:none}
|
||
.pkg-pill{flex-shrink:0;background:var(--bg3);border:1.5px solid var(--border);border-radius:30px;padding:8px 16px;cursor:pointer;transition:all .2s;text-align:center;min-width:72px}
|
||
.pkg-pill.popular{border-color:var(--gold)}
|
||
.pkg-pill.selected{background:linear-gradient(135deg,rgba(240,192,64,.15),rgba(0,229,255,.08));border-color:var(--gold);box-shadow:0 0 12px rgba(240,192,64,.2)}
|
||
.pkg-pill-tokens{font-family:'Exo 2',sans-serif;font-weight:900;font-size:17px;color:var(--gold2)}
|
||
.pkg-pill-price{font-size:13px;color:var(--text2);font-weight:600;margin-top:1px}
|
||
.pkg-pill.popular .pkg-pill-tokens::after{content:' ★';font-size:12px;color:var(--gold)}
|
||
|
||
/* Amount display */
|
||
.amount-display{background:var(--bg3);border:1px solid var(--border);border-radius:var(--rsm);padding:14px 16px;margin-bottom:14px;display:flex;align-items:center;justify-content:space-between}
|
||
.amount-display-label{font-size:14px;color:var(--text2);font-weight:700;text-transform:uppercase;letter-spacing:1px}
|
||
.amount-display-value{font-family:'Exo 2',sans-serif;font-weight:900;font-size:22px;color:var(--gold)}
|
||
|
||
/* Form fields */
|
||
.fg{margin-bottom:14px}
|
||
.fg label{display:block;font-size:13px;font-weight:700;color:var(--text2);letter-spacing:1px;text-transform:uppercase;margin-bottom:7px}
|
||
.fi{width:100%;background:var(--bg3);border:1.5px solid var(--border);border-radius:var(--rsm);padding:13px 14px;color:var(--text);font-family:'Rajdhani',sans-serif;font-size:17px;font-weight:500;outline:none;transition:border-color .2s;-webkit-appearance:none;appearance:none}
|
||
.fi:focus{border-color:var(--cyan);box-shadow:0 0 0 3px rgba(0,229,255,.08)}
|
||
.fi-select{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M0 0l6 8 6-8z' fill='%238888aa'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 14px center;padding-right:36px}
|
||
.fi-row{display:grid;grid-template-columns:1fr 1fr;gap:12px}
|
||
|
||
/* Custom token amount */
|
||
.custom-amt-row{background:linear-gradient(135deg,rgba(240,192,64,.06),rgba(0,229,255,.03));border:1.5px dashed rgba(240,192,64,.3);border-radius:var(--rsm);padding:12px 14px;margin-top:8px;display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}
|
||
.custom-amt-label{font-size:13px;font-weight:700;color:var(--gold);letter-spacing:.5px;margin-bottom:6px}
|
||
.custom-amt-input-wrap{display:flex;align-items:center;background:var(--bg3);border:1.5px solid rgba(240,192,64,.3);border-radius:8px;overflow:hidden}
|
||
.custom-amt-dollar{padding:0 10px;font-family:'Exo 2',sans-serif;font-weight:700;font-size:18px;color:var(--gold);background:rgba(240,192,64,.08);border-right:1px solid rgba(240,192,64,.2)}
|
||
.custom-amt-input{background:transparent;border:none;padding:10px 12px;color:var(--text);font-family:'Exo 2',sans-serif;font-size:18px;font-weight:700;width:110px;outline:none;-moz-appearance:textfield}
|
||
.custom-amt-input::-webkit-outer-spin-button,.custom-amt-input::-webkit-inner-spin-button{-webkit-appearance:none}
|
||
.custom-amt-input::placeholder{color:var(--text2);font-size:15px;font-weight:400}
|
||
.custom-amt-equiv{font-size:15px;color:var(--text2);white-space:nowrap}
|
||
.custom-amt-equiv strong{color:var(--gold2);font-family:'Exo 2',sans-serif;font-size:18px}
|
||
|
||
/* Order summary box */
|
||
.order-summary{background:linear-gradient(135deg,#1a1228,#0d1820);border:1px solid rgba(240,192,64,.2);border-radius:var(--rsm);padding:14px 16px;margin-bottom:16px}
|
||
.order-summary-row{display:flex;justify-content:space-between;align-items:center;padding:4px 0}
|
||
.order-summary-label{font-size:14px;color:var(--text2);font-weight:600;letter-spacing:.5px}
|
||
.order-summary-val{font-size:15px;color:var(--text);font-weight:600}
|
||
.order-summary-divider{height:1px;background:rgba(255,255,255,.08);margin:8px 0}
|
||
.order-summary-total{font-family:'Exo 2',sans-serif;font-weight:900;font-size:24px;color:var(--gold)}
|
||
|
||
/* Payment method tabs */
|
||
.pay-method-tabs{display:grid;grid-template-columns:repeat(5,1fr);gap:6px;margin-bottom:16px}
|
||
.pmt{background:var(--bg3);border:1.5px solid var(--border);border-radius:var(--rsm);padding:10px 4px;text-align:center;cursor:pointer;transition:all .2s}
|
||
.pmt.active{border-color:var(--cyan);background:rgba(0,229,255,.06)}
|
||
.pmt-icon{font-size:20px;display:block;margin-bottom:3px}
|
||
.pmt-label{font-size:11px;font-weight:700;color:var(--text2);letter-spacing:.3px;line-height:1.2}
|
||
.pmt.active .pmt-label{color:var(--cyan)}
|
||
|
||
/* Card container */
|
||
#card-container{background:#fff;border:1.5px solid var(--border);border-radius:var(--rsm);padding:4px;margin-bottom:14px;min-height:90px;overflow:hidden}
|
||
|
||
/* Manual pay instructions */
|
||
.manual-box{background:linear-gradient(135deg,rgba(0,229,255,.04),rgba(0,0,0,.2));border:1px solid rgba(0,229,255,.15);border-radius:var(--rsm);padding:14px;margin-bottom:14px}
|
||
.manual-box-title{font-weight:700;font-size:15px;color:var(--cyan);margin-bottom:8px;display:flex;align-items:center;gap:6px}
|
||
.manual-step{display:flex;gap:10px;margin-bottom:8px;align-items:flex-start}
|
||
.manual-step-num{width:20px;height:20px;border-radius:50%;background:var(--cyan);color:#000;font-size:13px;font-weight:700;display:flex;align-items:center;justify-content:center;flex-shrink:0;margin-top:1px}
|
||
.manual-step-text{font-size:15px;color:var(--text);line-height:1.5}
|
||
.manual-step-text strong{color:var(--gold2)}
|
||
.manual-step-text .handle{background:rgba(240,192,64,.1);border:1px solid rgba(240,192,64,.3);border-radius:6px;padding:1px 8px;font-family:'Exo 2',sans-serif;font-weight:700;color:var(--gold);font-size:15px;letter-spacing:.5px}
|
||
|
||
/* Buttons */
|
||
.btn{width:100%;padding:15px;border:none;border-radius:var(--rsm);font-family:'Exo 2',sans-serif;font-weight:700;font-size:16px;letter-spacing:1px;cursor:pointer;transition:all .2s;text-transform:uppercase;display:flex;align-items:center;justify-content:center;gap:8px}
|
||
.btn:active{transform:scale(.98)}
|
||
.btn-gold{background:linear-gradient(135deg,var(--gold),#d4a017);color:#000;box-shadow:0 4px 20px rgba(240,192,64,.4)}
|
||
.btn-outline{background:transparent;border:1.5px solid var(--gold);color:var(--gold)}
|
||
.btn-cyan{background:linear-gradient(135deg,var(--cyan),#0099cc);color:#000;box-shadow:0 4px 20px rgba(0,229,255,.3)}
|
||
.btn-danger{background:linear-gradient(135deg,var(--red),#cc0000);color:#fff}
|
||
.btn-green{background:linear-gradient(135deg,var(--green),#00aa55);color:#000}
|
||
|
||
/* Alerts */
|
||
.alert{padding:12px 14px;border-radius:var(--rsm);margin-bottom:14px;font-weight:600;font-size:15px;display:none;line-height:1.4}
|
||
.alert.show{display:block}
|
||
.alert-error{background:rgba(255,68,68,.1);border:1px solid rgba(255,68,68,.3);color:var(--red)}
|
||
.alert-success{background:rgba(0,230,118,.1);border:1px solid rgba(0,230,118,.3);color:var(--green)}
|
||
.alert-info{background:rgba(0,229,255,.08);border:1px solid rgba(0,229,255,.2);color:var(--cyan)}
|
||
|
||
/* Divider */
|
||
.divider{display:flex;align-items:center;gap:10px;margin:18px 0;color:var(--text2);font-size:13px;font-weight:700;letter-spacing:1px}
|
||
.divider::before,.divider::after{content:'';flex:1;height:1px;background:var(--border)}
|
||
|
||
/* Cashout items */
|
||
.cashout-item{background:var(--bg3);border:1px solid var(--border);border-radius:var(--rsm);padding:13px 14px;margin-bottom:8px;display:flex;align-items:center;justify-content:space-between;gap:8px}
|
||
.badge{display:inline-block;font-size:12px;font-weight:700;padding:3px 9px;border-radius:20px;letter-spacing:.3px}
|
||
.badge-pending{background:rgba(255,214,10,.15);color:var(--yellow)}
|
||
.badge-approved{background:rgba(0,230,118,.15);color:var(--green)}
|
||
.badge-rejected{background:rgba(255,68,68,.15);color:var(--red)}
|
||
.badge-completed{background:rgba(0,229,255,.15);color:var(--cyan)}
|
||
|
||
/* Profile */
|
||
.profile-header{text-align:center;padding:20px 0 14px}
|
||
.profile-avatar{width:76px;height:76px;border-radius:50%;background:linear-gradient(135deg,var(--purple),var(--cyan));display:flex;align-items:center;justify-content:center;font-family:'Exo 2',sans-serif;font-weight:900;font-size:30px;margin:0 auto 10px;border:3px solid var(--gold);box-shadow:var(--glow-gold)}
|
||
|
||
/* Modal */
|
||
.modal-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.85);z-index:200;align-items:flex-end;backdrop-filter:blur(4px)}
|
||
.modal-overlay.show{display:flex}
|
||
.modal-sheet{background:var(--bg2);border-radius:22px 22px 0 0;border-top:1px solid var(--border);padding:20px;width:100%;max-width:var(--max);margin:0 auto;max-height:90dvh;overflow-y:auto;animation:slideUp .3s ease}
|
||
@keyframes slideUp{from{transform:translateY(100%)}to{transform:translateY(0)}}
|
||
.modal-handle{width:40px;height:4px;background:var(--border);border-radius:2px;margin:0 auto 18px}
|
||
|
||
/* Spinner */
|
||
.spinner{width:18px;height:18px;border:2px solid rgba(255,255,255,.2);border-top-color:currentColor;border-radius:50%;animation:spin .8s linear infinite;display:inline-block;flex-shrink:0}
|
||
@keyframes spin{to{transform:rotate(360deg)}}
|
||
|
||
/* Order ref badge */
|
||
.ref-badge{background:rgba(240,192,64,.1);border:1px solid rgba(240,192,64,.3);border-radius:8px;padding:10px 14px;margin-bottom:14px;font-size:15px;color:var(--text2)}
|
||
.ref-badge strong{color:var(--gold2);font-family:'Exo 2',sans-serif}
|
||
|
||
/* ─── PROFILE TABS ───────────────────────────────────── */
|
||
.form-label{font-size:13px;font-weight:700;color:var(--text2);letter-spacing:1px;text-transform:uppercase;margin-bottom:6px;display:block}
|
||
.profile-tabs{display:flex;flex-wrap:wrap;gap:3px;background:var(--bg3);border-radius:var(--rsm);padding:4px;margin-bottom:14px}
|
||
.profile-tab{flex:1 1 calc(25% - 3px);min-width:72px;padding:10px 4px;border:none;background:none;color:var(--text2);font-family:'Exo 2',sans-serif;font-weight:700;font-size:13px;border-radius:8px;cursor:pointer;transition:all .2s;text-align:center}
|
||
.profile-tab.active{background:linear-gradient(135deg,rgba(240,192,64,.15),rgba(0,229,255,.08));color:var(--gold)}
|
||
.profile-tab-panel{display:none}
|
||
.profile-tab-panel.active{display:block;animation:fadeUp .2s ease}
|
||
|
||
/* ─── SAVED CARD CHIP ────────────────────────────────── */
|
||
.saved-card-chip{background:linear-gradient(135deg,#1c1c3a,#2d1b69);border-radius:16px;padding:20px;margin-bottom:12px;position:relative;overflow:hidden;border:1px solid rgba(255,255,255,.1)}
|
||
.saved-card-chip::before{content:'';position:absolute;top:-40px;right:-40px;width:140px;height:140px;border-radius:50%;background:rgba(255,255,255,.04)}
|
||
.saved-card-chip-top{display:flex;justify-content:space-between;align-items:center;margin-bottom:24px}
|
||
.saved-card-brand{font-family:'Exo 2',sans-serif;font-weight:900;font-size:18px;color:#fff;letter-spacing:2px;text-transform:uppercase}
|
||
.saved-card-number{font-family:'Courier New',monospace;font-size:18px;font-weight:700;color:#fff;letter-spacing:4px;margin-bottom:20px}
|
||
.saved-card-bottom{display:flex;justify-content:space-between;color:#fff}
|
||
|
||
/* ─── PAYMENT PROCESSING OVERLAY ─────────────────────── */
|
||
@keyframes rotateRing{to{transform:rotate(360deg)}}
|
||
@keyframes pulseIcon{0%,100%{transform:scale(1)}50%{transform:scale(1.08)}}
|
||
@keyframes stepFadeIn{from{opacity:0;transform:translateX(-8px)}to{opacity:1;transform:translateX(0)}}
|
||
@keyframes successBurst{0%{transform:scale(.5);opacity:0}60%{transform:scale(1.1)}100%{transform:scale(1);opacity:1}}
|
||
|
||
.pay-proc-ring{width:110px;height:110px;position:relative;margin-bottom:28px;flex-shrink:0}
|
||
.pay-proc-spinner{position:absolute;inset:0;border-radius:50%;border:3px solid rgba(240,192,64,.15);border-top-color:var(--gold);border-right-color:var(--gold);animation:rotateRing 1s linear infinite}
|
||
.pay-proc-icon{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:40px;animation:pulseIcon 2s ease infinite}
|
||
.pay-proc-title{font-family:'Exo 2',sans-serif;font-weight:900;font-size:24px;color:var(--text);margin-bottom:8px;text-align:center}
|
||
.pay-proc-sub{font-size:15px;color:var(--text2);margin-bottom:32px;text-align:center}
|
||
.pay-proc-steps{display:flex;flex-direction:column;gap:10px;width:100%;max-width:260px}
|
||
.pay-proc-step{font-size:15px;font-weight:600;color:rgba(255,255,255,.2);padding:8px 14px;border-radius:8px;transition:all .4s;display:flex;align-items:center;gap:8px}
|
||
.pay-proc-step.active{color:var(--text);background:rgba(240,192,64,.08);border-left:3px solid var(--gold);animation:stepFadeIn .3s ease}
|
||
.pay-proc-step.done{color:var(--green);background:rgba(0,230,118,.06)}
|
||
|
||
/* ─── PAYMENT RESULT OVERLAY ─────────────────────────── */
|
||
.pay-result-wrap{text-align:center;width:100%;max-width:380px}
|
||
.pay-result-icon{font-size:80px;margin-bottom:20px;display:block;animation:successBurst .5s ease}
|
||
.pay-result-title{font-family:'Exo 2',sans-serif;font-weight:900;font-size:28px;margin-bottom:8px}
|
||
.pay-result-title.success{background:linear-gradient(135deg,var(--gold),var(--green));-webkit-background-clip:text;-webkit-text-fill-color:transparent}
|
||
.pay-result-title.error{color:var(--red)}
|
||
.pay-result-title.pending{color:var(--gold)}
|
||
.pay-balance-bump{font-family:'Exo 2',sans-serif;font-weight:900;font-size:56px;color:var(--gold);line-height:1;margin:12px 0;text-shadow:0 0 30px rgba(240,192,64,.5)}
|
||
.pay-result-detail{background:rgba(255,255,255,.04);border:1px solid var(--border);border-radius:12px;padding:14px;margin:16px 0;font-size:15px;color:var(--text2);line-height:1.8;text-align:left}
|
||
.pay-result-detail strong{color:var(--text)}
|
||
.pay-result-btns{display:flex;flex-direction:column;gap:10px;margin-top:20px}
|
||
|
||
/* ─── GAME ALIAS CARDS ────────────────────────────────── */
|
||
.game-alias-card{background:var(--card);border:1px solid var(--border);border-radius:var(--rsm);padding:12px 14px;margin-bottom:10px;display:flex;align-items:center;gap:12px}
|
||
.game-alias-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}
|
||
.game-alias-name{font-family:'Exo 2',sans-serif;font-weight:700;font-size:15px;color:var(--text);margin-bottom:5px}
|
||
.game-alias-input{width:100%;background:var(--bg3);border:1px solid var(--border);border-radius:8px;padding:8px 12px;color:var(--text);font-family:'Rajdhani',sans-serif;font-size:15px;outline:none;transition:border-color .15s;box-sizing:border-box}
|
||
.game-alias-input:focus{border-color:var(--cyan)}
|
||
.game-alias-input::placeholder{color:var(--text2)}
|
||
|
||
/* ─── ACTIVITY DASHBOARD ──────────────────────────────── */
|
||
.activity-ftab{background:var(--bg3);border:1px solid var(--border);color:var(--text2);border-radius:20px;padding:6px 14px;font-family:'Exo 2',sans-serif;font-weight:700;font-size:14px;cursor:pointer;white-space:nowrap;transition:all .15s;flex-shrink:0}
|
||
.activity-ftab.active{background:rgba(240,192,64,.12);border-color:rgba(240,192,64,.35);color:var(--gold)}
|
||
.activity-card{background:var(--bg3);border:1px solid var(--border);border-radius:var(--rsm);padding:12px 14px;margin-bottom:8px;display:flex;gap:12px;align-items:flex-start;transition:border-color .2s}
|
||
.activity-card:hover{border-color:rgba(255,255,255,.12)}
|
||
.activity-icon{width:36px;height:36px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:17px;flex-shrink:0}
|
||
.activity-body{flex:1;min-width:0}
|
||
.activity-title{font-family:'Exo 2',sans-serif;font-weight:700;font-size:15px;color:var(--text);margin-bottom:2px}
|
||
.activity-meta{font-size:13px;color:var(--text2)}
|
||
.activity-status{display:inline-block;font-size:12px;font-weight:700;letter-spacing:.4px;text-transform:uppercase;border-radius:4px;padding:2px 7px;margin-top:4px}
|
||
.ac-pending{background:rgba(240,192,64,.12);color:var(--gold)}
|
||
.ac-locked{background:rgba(0,229,255,.1);color:var(--cyan)}
|
||
.ac-completed,.ac-sent,.ac-approved{background:rgba(0,230,118,.1);color:var(--green)}
|
||
.ac-rejected,.ac-failed,.ac-deleted{background:rgba(255,68,68,.1);color:var(--red)}
|
||
.ac-new{background:rgba(240,192,64,.15);color:var(--gold)}
|
||
.ac-read{background:rgba(255,255,255,.06);color:var(--text2)}
|
||
.activity-amount{font-family:'Exo 2',sans-serif;font-weight:700;font-size:15px;text-align:right;flex-shrink:0}
|
||
.summary-pill{background:var(--bg3);border:1px solid var(--border);border-radius:8px;padding:6px 12px;font-size:14px;font-family:'Exo 2',sans-serif;font-weight:700;white-space:nowrap}
|
||
|
||
/* ─── CASHOUT REQUEST CARDS ───────────────────────────── */
|
||
.co-card{background:var(--card);border:1px solid var(--border);border-radius:var(--rsm);padding:14px;margin-bottom:10px;transition:border-color .2s}
|
||
.co-card.pending{border-color:rgba(240,192,64,.25)}
|
||
.co-card.locked{border-color:rgba(0,229,255,.25);background:linear-gradient(135deg,rgba(0,229,255,.03),var(--card))}
|
||
.co-card.sent,.co-card.approved{border-color:rgba(0,230,118,.25)}
|
||
.co-card.rejected{border-color:rgba(255,68,68,.2);opacity:.75}
|
||
.co-card.deleted{opacity:.45}
|
||
.co-status-pill{display:inline-block;font-size:12px;font-weight:700;letter-spacing:.5px;text-transform:uppercase;border-radius:4px;padding:3px 8px;margin-left:8px}
|
||
.co-status-pending{background:rgba(240,192,64,.12);color:var(--gold)}
|
||
.co-status-locked{background:rgba(0,229,255,.1);color:var(--cyan)}
|
||
.co-status-sent,.co-status-approved{background:rgba(0,230,118,.1);color:var(--green)}
|
||
.co-status-rejected{background:rgba(255,68,68,.1);color:var(--red)}
|
||
.co-status-deleted{background:rgba(255,255,255,.05);color:var(--text2)}
|
||
.co-edit-row{display:flex;gap:6px;margin-top:10px;flex-wrap:wrap}
|
||
|
||
/* ─── REFERRAL STYLES ─────────────────────────────────── */
|
||
.ref-share-btn{border-radius:8px;padding:8px 14px;font-size:14px;font-weight:700;cursor:pointer;white-space:nowrap;transition:opacity .15s}
|
||
.ref-share-btn:hover{opacity:.8}
|
||
.ref-status-pending{color:var(--gold)}
|
||
.ref-status-verified{color:var(--green)}
|
||
.ref-status-denied{color:var(--red)}
|
||
|
||
/* ─── PLATFORM ACCOUNTS ───────────────────────────────── */
|
||
.pa-card{background:var(--bg3);border:1px solid var(--border);border-radius:var(--rsm);padding:14px;margin-bottom:10px}
|
||
.pa-card.approved{border-color:rgba(0,230,118,.3);background:rgba(0,230,118,.03)}
|
||
.pa-card.pending{border-color:rgba(240,192,64,.2)}
|
||
.pa-card.denied{border-color:rgba(255,68,68,.2);opacity:.75}
|
||
.pa-cred-box{background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:10px 14px;margin-top:10px;font-family:monospace;font-size:15px}
|
||
.pa-cred-row{display:flex;justify-content:space-between;align-items:center;margin-bottom:4px}
|
||
.pa-cred-label{font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:var(--text2)}
|
||
.pa-cred-val{font-size:15px;font-weight:700;color:var(--green)}
|
||
.pa-cred-pass{filter:blur(4px);transition:filter .2s;cursor:pointer;user-select:all}
|
||
.pa-cred-pass:hover,.pa-cred-pass.revealed{filter:none}
|
||
/* Onboarding modal */
|
||
#onboarding-modal{position:fixed;inset:0;background:rgba(0,0,0,.85);z-index:900;display:flex;align-items:center;justify-content:center;padding:20px}
|
||
.onboarding-box{background:var(--bg3);border:1px solid rgba(240,192,64,.3);border-radius:16px;padding:28px 24px;max-width:420px;width:100%;text-align:center}
|
||
.onboarding-title{font-family:'Exo 2',sans-serif;font-weight:900;font-size:22px;color:var(--gold);margin-bottom:10px}
|
||
.onboarding-sub{font-size:15px;color:var(--text2);margin-bottom:24px;line-height:1.6}
|
||
|
||
/* ─── BROADCAST STYLES ────────────────────────────────── */
|
||
.bc-card{background:var(--card);border:1px solid var(--border);border-radius:var(--rsm);margin-bottom:12px;overflow:hidden;transition:border-color .2s}
|
||
.bc-card.unread{border-color:rgba(240,192,64,.3);background:linear-gradient(135deg,rgba(240,192,64,.04),var(--card))}
|
||
.bc-header{padding:14px 16px 10px;display:flex;gap:10px;align-items:flex-start}
|
||
.bc-avatar{width:38px;height:38px;border-radius:50%;background:linear-gradient(135deg,var(--gold),#d4a017);display:flex;align-items:center;justify-content:center;font-size:18px;flex-shrink:0}
|
||
.bc-subject{font-family:'Exo 2',sans-serif;font-weight:700;font-size:15px;color:var(--text)}
|
||
.bc-meta{font-size:13px;color:var(--text2);margin-top:2px}
|
||
.bc-body{padding:0 16px 12px;font-size:15px;color:var(--text2);line-height:1.6;white-space:pre-wrap}
|
||
.bc-footer{padding:8px 16px;border-top:1px solid var(--border);display:flex;gap:10px;align-items:center}
|
||
.bc-reply-wrap{padding:0 16px 14px;display:none}
|
||
.bc-reply-wrap.open{display:block}
|
||
.bc-replies-list{max-height:220px;overflow-y:auto;margin-bottom:10px}
|
||
.bc-reply-row{display:flex;gap:8px;margin-bottom:8px}
|
||
.bc-reply-bubble{background:var(--bg3);border:1px solid var(--border);border-radius:8px;padding:8px 12px;flex:1}
|
||
.bc-reply-who{font-size:13px;font-weight:700;color:var(--cyan);margin-bottom:3px}
|
||
.bc-reply-who.admin{color:var(--gold)}
|
||
.bc-reply-msg{font-size:15px;color:var(--text)}
|
||
|
||
/* ─── CHAT STYLES ─────────────────────────────────────── */
|
||
@keyframes pulse2{from{opacity:.5}to{opacity:1}}
|
||
@keyframes dotBounce{0%,60%,100%{transform:translateY(0)}30%{transform:translateY(-6px)}}
|
||
|
||
.chat-header-avatar{width:40px;height:40px;border-radius:50%;background:linear-gradient(135deg,var(--gold),#d4a017);display:flex;align-items:center;justify-content:center;flex-shrink:0;box-shadow:var(--glow-gold)}
|
||
.chat-status-dot{display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--green);margin-right:4px;animation:pulse2 2s ease infinite alternate}
|
||
|
||
/* Quick reply chips */
|
||
.chat-quick-replies{display:flex;flex-wrap:wrap;gap:8px;padding:8px 14px 4px;flex-shrink:0}
|
||
.chat-chip{background:rgba(240,192,64,.08);border:1px solid rgba(240,192,64,.25);border-radius:20px;padding:7px 14px;color:var(--gold);font-family:'Exo 2',sans-serif;font-weight:600;font-size:14px;cursor:pointer;transition:all .15s;white-space:nowrap}
|
||
.chat-chip:active{background:rgba(240,192,64,.18);transform:scale(.97)}
|
||
|
||
/* Loading dots */
|
||
.chat-loading{display:flex;justify-content:center;align-items:center;padding:30px;color:var(--text2);font-size:15px}
|
||
.chat-loading-dots{display:flex;gap:5px}
|
||
.chat-loading-dots span{width:8px;height:8px;border-radius:50%;background:var(--text2);animation:dotBounce 1.2s ease infinite}
|
||
.chat-loading-dots span:nth-child(2){animation-delay:.2s}
|
||
.chat-loading-dots span:nth-child(3){animation-delay:.4s}
|
||
|
||
/* Send button pulse when has text */
|
||
.chat-send-btn.has-text{background:linear-gradient(135deg,var(--cyan),#0088cc);box-shadow:0 0 16px rgba(0,229,255,.35)}
|
||
#page-chat{display:none;flex-direction:column;padding:0;padding-bottom:0;position:fixed;top:0;left:0;right:0;bottom:var(--nav-h);max-width:var(--max);margin:0 auto;overflow:hidden;z-index:10}
|
||
#page-chat.active{display:flex}
|
||
.chat-header{display:flex;align-items:center;gap:12px;padding:12px 16px;background:rgba(16,16,30,.98);border-bottom:1px solid var(--border);flex-shrink:0}
|
||
.chat-header-avatar{width:40px;height:40px;border-radius:50%;background:linear-gradient(135deg,var(--gold),#d4a017);display:flex;align-items:center;justify-content:center;font-family:'Exo 2',sans-serif;font-weight:900;font-size:18px;color:#000;flex-shrink:0;box-shadow:var(--glow-gold)}
|
||
.chat-header-name{font-family:'Exo 2',sans-serif;font-weight:700;font-size:16px;color:var(--text)}
|
||
.chat-header-status{font-size:13px;color:var(--green);font-weight:600;margin-top:2px}
|
||
.chat-messages{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:8px;-webkit-overflow-scrolling:touch;padding-bottom:8px}
|
||
.chat-loading{color:var(--text2);font-size:15px;text-align:center;padding:20px}
|
||
.chat-bubble-wrap{display:flex;flex-direction:column;max-width:78%}
|
||
.chat-bubble-wrap.mine{align-self:flex-end;align-items:flex-end}
|
||
.chat-bubble-wrap.theirs{align-self:flex-start;align-items:flex-start}
|
||
.chat-bubble{padding:10px 14px;border-radius:18px;font-size:15px;line-height:1.45;word-break:break-word;position:relative}
|
||
.chat-bubble.mine{background:linear-gradient(135deg,var(--gold),#d4a017);color:#000;border-bottom-right-radius:4px;font-weight:500}
|
||
.chat-bubble.theirs{background:var(--card);color:var(--text);border:1px solid var(--border);border-bottom-left-radius:4px}
|
||
.chat-time{font-size:12px;color:var(--text2);margin-top:3px;padding:0 4px}
|
||
.chat-date-divider{text-align:center;font-size:13px;color:var(--text2);font-weight:700;letter-spacing:.5px;margin:8px 0;display:flex;align-items:center;gap:8px}
|
||
.chat-date-divider::before,.chat-date-divider::after{content:'';flex:1;height:1px;background:var(--border)}
|
||
.chat-input-bar{display:flex;gap:8px;padding:10px 12px;background:rgba(10,10,18,.98);border-top:1px solid var(--border);flex-shrink:0;padding-bottom:calc(10px + env(safe-area-inset-bottom));padding-bottom:max(10px,calc(10px + env(safe-area-inset-bottom)))}
|
||
.chat-input{flex:1;background:var(--bg3);border:1.5px solid var(--border);border-radius:24px;padding:10px 16px;color:var(--text);font-family:'Rajdhani',sans-serif;font-size:16px;line-height:1.6;outline:none;transition:border-color .2s}
|
||
.chat-input:focus{border-color:var(--cyan)}
|
||
.chat-send-btn{width:42px;height:42px;border-radius:50%;background:linear-gradient(135deg,var(--gold),#d4a017);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:all .2s;color:#000}
|
||
.chat-send-btn:active{transform:scale(.93)}
|
||
.chat-send-btn svg{stroke:#000}
|
||
.chat-nav-badge{position:absolute;top:4px;right:14px;background:var(--red);color:#fff;font-size:11px;font-weight:700;min-width:16px;height:16px;border-radius:8px;display:flex;align-items:center;justify-content:center;padding:0 4px}
|
||
.chat-empty{text-align:center;padding:40px 20px;color:var(--text2);flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center}
|
||
.chat-empty-icon{font-size:52px;margin-bottom:14px}
|
||
.chat-empty-text{font-size:15px;line-height:1.7}
|
||
.chat-sender-label{font-size:12px;font-weight:700;color:var(--text2);letter-spacing:.5px;text-transform:uppercase;margin-bottom:3px;padding:0 4px}
|
||
|
||
/* ── Responsive Typography ─────────────────────────────── */
|
||
/* Tablet (≥768px) */
|
||
@media (min-width: 768px) {
|
||
body { font-size: 17px; }
|
||
.card { font-size: 16px; padding: 22px; }
|
||
.page-title { font-size: 26px !important; }
|
||
.card-title { font-size: 18px; }
|
||
.btn { font-size: 16px; padding: 14px 24px; }
|
||
.activity-item { font-size: 15px; }
|
||
}
|
||
/* Desktop (≥1200px) */
|
||
@media (min-width: 1200px) {
|
||
body { font-size: 18px; }
|
||
.card { font-size: 17px; padding: 24px; }
|
||
.page-title { font-size: 28px !important; }
|
||
.btn { font-size: 17px; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- AUTH -->
|
||
<!-- ONBOARDING MODAL -->
|
||
<div id="onboarding-overlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.88);z-index:200;overflow-y:auto;padding:20px;-webkit-overflow-scrolling:touch">
|
||
<div style="max-width:440px;margin:0 auto;background:var(--card);border:1px solid rgba(240,192,64,.25);border-radius:18px;padding:28px 24px;position:relative;top:10%">
|
||
<div id="ob-step1">
|
||
<div style="text-align:center;margin-bottom:20px">
|
||
<div style="font-size:52px;margin-bottom:10px">🎮</div>
|
||
<div style="font-family:'Exo 2',sans-serif;font-weight:900;font-size:22px;color:var(--gold);margin-bottom:8px">Welcome to TomTomGames!</div>
|
||
<div style="font-size:14px;color:var(--text2);line-height:1.7">Do you already have a username/login<br>on any of our game platforms?</div>
|
||
</div>
|
||
<div style="display:flex;flex-direction:column;gap:10px">
|
||
<button class="btn btn-gold" onclick="obShowStep2()" style="font-size:15px;padding:14px">✅ Yes — I have existing logins</button>
|
||
<button class="btn" onclick="obRequestNew()" style="background:rgba(0,229,255,.07);border:1px solid rgba(0,229,255,.2);color:var(--cyan);padding:14px;font-size:15px">🆕 No — I want to request new logins</button>
|
||
<button onclick="obDismiss()" style="background:none;border:none;color:var(--text2);font-size:14px;cursor:pointer;margin-top:2px;padding:8px">Skip for now →</button>
|
||
</div>
|
||
</div>
|
||
<div id="ob-step2" style="display:none">
|
||
<div style="display:flex;align-items:center;gap:10px;margin-bottom:18px">
|
||
<button onclick="document.getElementById('ob-step2').style.display='none';document.getElementById('ob-step1').style.display='block';" style="background:none;border:none;color:var(--cyan);cursor:pointer;font-size:22px;padding:0;line-height:1">←</button>
|
||
<div>
|
||
<div style="font-family:'Exo 2',sans-serif;font-weight:900;font-size:17px">Request Game Accounts</div>
|
||
<div style="font-size:14px;color:var(--text2);margin-top:2px">Select platforms — our team will create your logins</div>
|
||
</div>
|
||
</div>
|
||
<div id="ob-platforms-list" style="margin-bottom:14px;max-height:300px;overflow-y:auto"></div>
|
||
<div id="ob-alert" class="alert" style="margin-bottom:10px"></div>
|
||
<div style="display:flex;gap:8px">
|
||
<button class="btn btn-gold" onclick="obSubmitRequests()" style="flex:1">📨 Submit Requests</button>
|
||
<button class="btn btn-outline" onclick="obDismiss()" style="width:80px;font-size:15px">Done</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="auth-screen" class="auth-screen" style="display:flex">
|
||
<div class="auth-logo">
|
||
<div class="auth-logo-text" style="display:flex;align-items:center;justify-content:center;gap:12px"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48" style="display:inline-block;vertical-align:middle;flex-shrink:0"><defs><linearGradient id="al1" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#f0c040"/><stop offset="100%" stop-color="#ff6b35"/></linearGradient><linearGradient id="al2" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#00e5ff"/><stop offset="100%" stop-color="#7b2fbe"/></linearGradient><filter id="agl"><feGaussianBlur stdDeviation="1.5" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs><rect x="6" y="16" width="36" height="22" rx="11" fill="url(#al1)" filter="url(#agl)"/><rect x="12" y="23" width="8" height="3" rx="1.5" fill="rgba(0,0,0,0.45)"/><rect x="15" y="20" width="3" height="8" rx="1.5" fill="rgba(0,0,0,0.45)"/><circle cx="32" cy="22" r="2.2" fill="#e63946" opacity=".9"/><circle cx="36" cy="25" r="2.2" fill="#2ec4b6" opacity=".9"/><circle cx="32" cy="28" r="2.2" fill="#7b2fbe" opacity=".9"/><circle cx="28" cy="25" r="2.2" fill="#f4a261" opacity=".9"/><rect x="21" y="24" width="6" height="3" rx="1.5" fill="rgba(0,0,0,0.3)"/><rect x="8" y="30" width="8" height="7" rx="4" fill="url(#al2)" opacity=".7"/><rect x="32" y="30" width="8" height="7" rx="4" fill="url(#al2)" opacity=".7"/><rect x="14" y="13" width="8" height="5" rx="2.5" fill="url(#al1)" opacity=".8"/><rect x="26" y="13" width="8" height="5" rx="2.5" fill="url(#al1)" opacity=".8"/><circle cx="24" cy="7" r="2" fill="#f0c040" opacity=".9"/><circle cx="39" cy="10" r="1.2" fill="#00e5ff" opacity=".8"/><circle cx="9" cy="10" r="1.2" fill="#f0c040" opacity=".7"/></svg><span>TomTomGames</span></div>
|
||
<div class="auth-logo-sub">YOUR ULTIMATE GAMING PORTAL</div>
|
||
</div>
|
||
<div class="auth-tabs">
|
||
<button class="auth-tab active" onclick="switchTab('login')">LOGIN</button>
|
||
<button class="auth-tab" onclick="switchTab('register')">CREATE ACCOUNT</button>
|
||
</div>
|
||
<div id="tab-login">
|
||
<div id="login-alert" class="alert"></div>
|
||
<!-- Unverified notice (hidden by default) -->
|
||
<div id="login-unverified-box" style="display:none;background:rgba(255,214,10,.08);border:1px solid rgba(255,214,10,.25);border-radius:10px;padding:14px;margin-bottom:14px">
|
||
<div style="font-size:15px;color:#ffd60a;font-weight:700;margin-bottom:6px">📧 Email Not Verified</div>
|
||
<div style="font-size:14px;color:#cccc88;line-height:1.6;margin-bottom:10px">Check your inbox for the verification link, or resend it below.</div>
|
||
<div id="login-resend-alert" class="alert" style="margin-bottom:8px"></div>
|
||
<button class="btn btn-outline" onclick="resendFromLogin()" id="login-resend-btn" style="padding:10px;font-size:14px">Resend Verification Email</button>
|
||
</div>
|
||
<div class="fg"><label>Username</label><input class="fi" id="login-user" type="text" placeholder="your_username" autocomplete="username" autocapitalize="none"></div>
|
||
<div class="fg"><label>Password</label><input class="fi" id="login-pass" type="password" placeholder="••••••••" autocomplete="current-password"></div>
|
||
<button class="btn btn-gold" onclick="doLogin()" style="margin-top:6px" id="login-btn">LOGIN</button>
|
||
</div>
|
||
<div id="tab-register" style="display:none">
|
||
<!-- FORM STATE -->
|
||
<div id="reg-form-state">
|
||
<div id="reg-alert" class="alert"></div>
|
||
<div class="fg"><label>Username</label><input class="fi" id="reg-user" type="text" placeholder="choose_a_username" autocapitalize="none" autocomplete="username"></div>
|
||
<div class="fg"><label>Your Display Name <span style="font-size:15px;color:var(--text2);font-weight:400">— what you want to be known as</span></label><input class="fi" id="reg-alias" type="text" placeholder="e.g. LuckyAce, GameKing22..." autocomplete="nickname"></div>
|
||
<div class="fg">
|
||
<label>Email Address <span style="color:var(--red)">*</span></label>
|
||
<input class="fi" id="reg-email" type="email" placeholder="your@email.com" autocomplete="email">
|
||
<div style="font-size:15px;color:var(--text2);margin-top:5px">📧 Required — we'll send a verification link</div>
|
||
</div>
|
||
<div class="fg"><label>Password</label><input class="fi" id="reg-pass" type="password" placeholder="Min 6 characters" autocomplete="new-password"></div>
|
||
<button class="btn btn-gold" onclick="doRegister()" style="margin-top:6px" id="reg-btn">CREATE ACCOUNT</button>
|
||
</div>
|
||
<!-- VERIFY PENDING STATE (shown after registration) -->
|
||
<div id="reg-pending-state" style="display:none">
|
||
<div style="text-align:center;padding:12px 0 20px">
|
||
<div style="font-size:52px;margin-bottom:16px">📧</div>
|
||
<div style="font-family:'Exo 2',sans-serif;font-weight:700;font-size:20px;color:var(--gold);margin-bottom:10px">Check Your Email!</div>
|
||
<p style="font-size:14px;color:var(--text2);line-height:1.7;margin-bottom:6px">
|
||
We sent a verification link to:<br>
|
||
<strong style="color:var(--cyan)" id="reg-pending-email">—</strong>
|
||
</p>
|
||
<p style="font-size:15px;color:var(--text2);line-height:1.6;margin-bottom:24px">
|
||
Open the email and click the link to activate your account. Check your spam folder if you don't see it.
|
||
</p>
|
||
<div style="background:rgba(0,229,255,.06);border:1px solid rgba(0,229,255,.15);border-radius:10px;padding:14px;margin-bottom:20px;font-size:15px;color:var(--text2);text-align:left;line-height:1.7">
|
||
<strong style="color:var(--cyan)">Steps:</strong><br>
|
||
1. Open your email inbox<br>
|
||
2. Find email from <strong style="color:var(--text)">noreply@tomtomgames.com</strong><br>
|
||
3. Click <strong style="color:var(--gold)">VERIFY MY ACCOUNT</strong><br>
|
||
4. You'll be automatically logged in ✅
|
||
</div>
|
||
<div id="resend-alert" class="alert" style="margin-bottom:12px"></div>
|
||
<button class="btn btn-outline" onclick="resendVerify()" id="resend-btn" style="margin-bottom:10px">Resend Verification Email</button>
|
||
<button class="btn" onclick="showRegForm()" style="background:none;color:var(--text2);font-size:15px;padding:8px">← Use different email</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- MAIN APP -->
|
||
<div id="main-app" style="display:none">
|
||
<div class="topbar">
|
||
<div class="topbar-logo" style="display:flex;align-items:center;gap:8px"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="32" height="32" style="display:inline-block;vertical-align:middle;flex-shrink:0"><defs><linearGradient id="li1" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#f0c040"/><stop offset="100%" stop-color="#ff6b35"/></linearGradient><linearGradient id="li2" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#00e5ff"/><stop offset="100%" stop-color="#7b2fbe"/></linearGradient></defs><rect x="6" y="16" width="36" height="22" rx="11" fill="url(#li1)"/><rect x="12" y="23" width="8" height="3" rx="1.5" fill="rgba(0,0,0,0.45)"/><rect x="15" y="20" width="3" height="8" rx="1.5" fill="rgba(0,0,0,0.45)"/><circle cx="32" cy="22" r="2.2" fill="#e63946" opacity=".9"/><circle cx="36" cy="25" r="2.2" fill="#2ec4b6" opacity=".9"/><circle cx="32" cy="28" r="2.2" fill="#7b2fbe" opacity=".9"/><circle cx="28" cy="25" r="2.2" fill="#f4a261" opacity=".9"/><rect x="21" y="24" width="6" height="3" rx="1.5" fill="rgba(0,0,0,0.3)"/><rect x="8" y="30" width="8" height="7" rx="4" fill="url(#li2)" opacity=".7"/><rect x="32" y="30" width="8" height="7" rx="4" fill="url(#li2)" opacity=".7"/><rect x="14" y="13" width="8" height="5" rx="2.5" fill="url(#li1)" opacity=".8"/><rect x="26" y="13" width="8" height="5" rx="2.5" fill="url(#li1)" opacity=".8"/></svg><span>TomTomGames</span></div>
|
||
<div class="topbar-right">
|
||
<div class="token-badge" id="header-tokens-wrap"><span id="header-tokens">0</span></div>
|
||
<button class="avatar-btn" id="avatar-btn" onclick="showPage('profile')">?</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- HOME -->
|
||
<div class="page active" id="page-home">
|
||
<div class="hero-balance">
|
||
<div class="balance-label">YOUR TOKENS</div>
|
||
<div class="balance-amount" id="home-balance">0</div>
|
||
<div class="balance-unit">TOKENS</div>
|
||
<div class="balance-alias">Playing as <strong id="home-alias">—</strong></div>
|
||
<div style="display:flex;gap:10px;margin-top:16px">
|
||
<button class="btn btn-gold" style="flex:1;padding:12px" onclick="showPage('buy')">BUY</button>
|
||
<button class="btn btn-outline" style="flex:1;padding:12px" onclick="showPage('cashout')">💸 CASH OUT</button>
|
||
</div>
|
||
</div>
|
||
<div class="section-title"><span>PLAY NOW</span></div>
|
||
<div class="platform-grid" id="platform-grid"></div>
|
||
</div>
|
||
|
||
<!-- ═══ BUY TOKENS (milkyswipe style) ═══ -->
|
||
<div class="page" id="page-buy">
|
||
<div class="section-title"><span>BUY TOKENS</span></div>
|
||
|
||
<div id="buy-alert" class="alert"></div>
|
||
|
||
<!-- Payment Form Card -->
|
||
<div class="pay-form">
|
||
<div class="pay-form-header">
|
||
<div class="pay-form-icon" id="buy-form-icon" style="font-size:0"><!-- coin --></div>
|
||
<div>
|
||
<div class="pay-form-title">Token Purchase</div>
|
||
<div class="pay-form-sub">$1 = 1 Token · All payments secure</div>
|
||
</div>
|
||
</div>
|
||
<div class="pay-form-body">
|
||
|
||
<!-- 1. Select Platform -->
|
||
<div class="fg">
|
||
<label>1. Select Your Gaming Platform</label>
|
||
<select class="fi fi-select" id="buy-platform" onchange="updatePayForm();prefillBuyAlias()">
|
||
<option value="">— Choose Platform —</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- 2. Game Alias -->
|
||
<div class="fg">
|
||
<label>2. Your In-Game Username / Alias</label>
|
||
<input class="fi" id="buy-alias" type="text" placeholder="e.g. LuckyAce777" oninput="updatePayForm()" onblur="saveAliasFromBuyPage()">
|
||
</div>
|
||
|
||
<!-- 3. Token Package -->
|
||
<div class="fg">
|
||
<label>3. Select Token Package <span style="color:var(--text2);font-weight:400;font-size:15px">($1 = 1 Token)</span></label>
|
||
<div class="pkg-scroll" id="pkg-scroll"></div>
|
||
<!-- Custom amount row -->
|
||
<div class="custom-amt-row" id="custom-pkg-row">
|
||
<div class="custom-amt-wrap">
|
||
<div class="custom-amt-label">✏️ Custom Amount</div>
|
||
<div class="custom-amt-input-wrap">
|
||
<span class="custom-amt-dollar">$</span>
|
||
<input class="custom-amt-input" id="custom-amt" type="number" min="1" max="500" step="1"
|
||
placeholder="Enter amount" oninput="selectCustomPkg(this.value)">
|
||
</div>
|
||
</div>
|
||
<div class="custom-amt-equiv" id="custom-amt-equiv" style="display:none">
|
||
= <strong id="custom-token-count">0</strong> Tokens
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Amount / Order Summary display -->
|
||
<div class="order-summary" id="order-summary" style="display:none">
|
||
<div class="order-summary-row">
|
||
<span class="order-summary-label">Package</span>
|
||
<span class="order-summary-val" id="summary-pkg">—</span>
|
||
</div>
|
||
<div class="order-summary-row">
|
||
<span class="order-summary-label">Tokens</span>
|
||
<span class="order-summary-val" style="color:var(--gold2)" id="summary-tokens">0</span>
|
||
</div>
|
||
<div class="order-summary-divider"></div>
|
||
<div class="order-summary-row">
|
||
<span class="order-summary-label" style="font-weight:700;color:var(--text)">Total Due</span>
|
||
<span class="order-summary-total" id="summary-total">$0.00</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 4. Payment Method -->
|
||
<div class="fg">
|
||
<label>4. Payment Method</label>
|
||
<div class="pay-method-tabs" id="manual-payment-btns">
|
||
<!-- Populated dynamically by buildPaymentMethods() -->
|
||
<div class="pmt active" onclick="selectPMT(this,'card')" title="Credit/Debit Card">
|
||
<span class="pmt-icon">💳</span><span class="pmt-label">Card</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- CARD fields (Square Web Payments) -->
|
||
<div id="section-card">
|
||
<div class="fg">
|
||
<label>Card Details <span style="font-size:14px;color:var(--green);font-weight:400" id="card-ready-label"></span></label>
|
||
<div id="card-container"></div>
|
||
</div>
|
||
<div class="fi-row">
|
||
<div class="fg">
|
||
<label>First Name</label>
|
||
<input class="fi" id="card-first-name" type="text" placeholder="First" autocomplete="given-name">
|
||
</div>
|
||
<div class="fg">
|
||
<label>Last Name</label>
|
||
<input class="fi" id="card-last-name" type="text" placeholder="Last" autocomplete="family-name">
|
||
</div>
|
||
</div>
|
||
<div class="fg">
|
||
<label>Billing Address</label>
|
||
<input class="fi" id="card-address" type="text" placeholder="Street address" autocomplete="street-address" style="margin-bottom:8px">
|
||
<div style="display:grid;grid-template-columns:1fr 60px 90px;gap:8px">
|
||
<input class="fi" id="card-city" type="text" placeholder="City" autocomplete="address-level2">
|
||
<input class="fi" id="card-state" type="text" placeholder="ST" maxlength="2" autocomplete="address-level1" style="text-transform:uppercase">
|
||
<input class="fi" id="card-zip" type="text" placeholder="ZIP" maxlength="10" autocomplete="postal-code">
|
||
</div>
|
||
</div>
|
||
<div class="fg">
|
||
<label>Email for Receipt</label>
|
||
<input class="fi" id="card-email" type="email" placeholder="your@email.com" autocomplete="email">
|
||
</div>
|
||
</div>
|
||
|
||
<!-- MANUAL payment instructions -->
|
||
<div id="section-manual" style="display:none">
|
||
<div class="manual-box" id="manual-box"></div>
|
||
<div class="ref-badge" id="ref-badge" style="display:none">
|
||
📋 Reference: <strong id="ref-platform">—</strong> | Player: <strong id="ref-alias">—</strong> | Include this in your payment note!
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Save billing checkbox -->
|
||
<div id="save-billing-wrap" style="display:flex;align-items:center;gap:10px;margin-bottom:14px;padding:10px 14px;background:rgba(0,229,255,.04);border:1px solid rgba(0,229,255,.12);border-radius:var(--rsm)">
|
||
<input type="checkbox" id="save-billing-cb" style="width:16px;height:16px;accent-color:var(--cyan);cursor:pointer" checked>
|
||
<label for="save-billing-cb" style="font-size:15px;color:var(--text2);cursor:pointer;line-height:1.4">
|
||
Save billing info for faster checkout next time
|
||
</label>
|
||
</div>
|
||
|
||
<!-- Submit -->
|
||
<button class="btn btn-gold" id="pay-submit-btn" onclick="submitPayment()">
|
||
COMPLETE PURCHASE
|
||
</button>
|
||
<div style="display:flex;align-items:center;justify-content:center;gap:8px;margin-top:10px">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#8888aa" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
||
<p style="font-size:15px;color:var(--text2)">256-bit SSL · Secured by Square</p>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- CASHOUT -->
|
||
<div class="page" id="page-cashout">
|
||
<div class="section-title"><span>CASH OUT</span></div>
|
||
<div class="pay-form">
|
||
<div class="pay-form-header">
|
||
<div class="pay-form-icon">💸</div>
|
||
<div>
|
||
<div class="pay-form-title">Cashout Request</div>
|
||
<div class="pay-form-sub">Request withdrawal of your token balance</div>
|
||
</div>
|
||
</div>
|
||
<div class="pay-form-body">
|
||
<div style="display:flex;align-items:center;justify-content:space-between;background:var(--bg3);border:1px solid rgba(240,192,64,.2);border-radius:var(--rsm);padding:14px;margin-bottom:16px">
|
||
<div>
|
||
<div style="font-size:15px;color:var(--text2);font-weight:700;letter-spacing:1px;text-transform:uppercase">Available Balance</div>
|
||
<div style="font-family:'Exo 2',sans-serif;font-weight:900;font-size:28px;color:var(--gold)"><span id="cashout-balance">0</span> <span style="font-size:14px;color:var(--text2)">TOKENS</span></div>
|
||
</div>
|
||
<span style="font-size:36px">💰</span>
|
||
</div>
|
||
<div id="cashout-alert" class="alert"></div>
|
||
|
||
<!-- Payout Method Selector -->
|
||
<div class="fg">
|
||
<label>Send Payment To
|
||
<a onclick="showPage('profile');setTimeout(()=>{switchProfileTab('payout',document.querySelector('.profile-tab[onclick*=payout]'))},100)"
|
||
style="font-size:15px;color:var(--cyan);cursor:pointer;margin-left:8px;font-weight:400">+ Manage Methods</a>
|
||
</label>
|
||
<div id="cashout-payout-list" style="margin-bottom:8px">
|
||
<div style="color:var(--text2);font-size:15px;padding:10px;text-align:center">Loading payout methods...</div>
|
||
</div>
|
||
<div id="cashout-no-payout" style="display:none;background:rgba(240,192,64,.07);border:1px solid rgba(240,192,64,.2);border-radius:8px;padding:12px;font-size:15px;color:var(--gold);text-align:center">
|
||
No payout method saved. <a onclick="showPage('profile');setTimeout(()=>{switchProfileTab('payout',document.querySelector('[onclick*=payout]'))},100)" style="cursor:pointer;text-decoration:underline">Add one in your profile →</a>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="fg"><label>Select Platform</label><select class="fi fi-select" id="cashout-platform" onchange="prefillCashoutAlias(this.value)"><option value="">— Choose Platform —</option></select></div>
|
||
<div class="fg"><label>Your In-Game Alias / Username</label><input class="fi" id="cashout-alias" type="text" placeholder="Your alias on that platform" onblur="saveCashoutAlias()"></div>
|
||
<div class="fg"><label>Tokens to Cash Out</label><input class="fi" id="cashout-tokens" type="number" min="1" placeholder="Enter amount (min: 1)"></div>
|
||
<button class="btn btn-cyan" onclick="submitCashout()">💸 SUBMIT CASHOUT REQUEST</button>
|
||
<p style="text-align:center;font-size:15px;color:var(--text2);margin-top:10px">Cashouts are reviewed and paid out within a few minutes</p>
|
||
</div>
|
||
</div>
|
||
<div class="divider">MY CASHOUT REQUESTS</div>
|
||
<div id="cashout-history">
|
||
<div style="color:var(--text2);font-size:15px;text-align:center;padding:20px">Loading your requests...</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- PROFILE -->
|
||
<div class="page" id="page-profile">
|
||
<!-- Profile header -->
|
||
<div class="profile-header">
|
||
<div class="profile-avatar" id="profile-avatar">?</div>
|
||
<div style="font-family:'Exo 2',sans-serif;font-weight:700;font-size:22px;color:var(--text)" id="profile-username">—</div>
|
||
<div style="color:var(--text2);font-size:14px;margin-top:2px" id="profile-alias">—</div>
|
||
</div>
|
||
<!-- Balance card -->
|
||
<div class="card" style="margin-bottom:10px;background:linear-gradient(135deg,#1a1228,#121a28);border-color:rgba(240,192,64,.2)">
|
||
<div style="display:flex;justify-content:space-between;align-items:center">
|
||
<div>
|
||
<div style="font-size:15px;color:var(--text2);font-weight:700;letter-spacing:1px;text-transform:uppercase;margin-bottom:4px">Token Balance</div>
|
||
<div style="display:flex;align-items:center;gap:6px" id="profile-tokens"><span style="font-family:'Exo 2',sans-serif;font-weight:900;font-size:28px;color:var(--gold)">0</span></div>
|
||
</div>
|
||
<button class="btn btn-gold" style="width:auto;padding:10px 18px;font-size:15px" onclick="showPage('buy')">BUY</button>
|
||
</div>
|
||
</div>
|
||
<!-- Profile tabs -->
|
||
<div class="profile-tabs" id="profile-tabs">
|
||
<button class="profile-tab active" onclick="switchProfileTab('activity',this)">Activity</button>
|
||
<button class="profile-tab" onclick="switchProfileTab('games',this)">🎮 Games</button>
|
||
<button class="profile-tab" onclick="switchProfileTab('logins',this)">🔑 Logins</button>
|
||
<button class="profile-tab" onclick="switchProfileTab('payout',this)">💸 Payout</button>
|
||
<button class="profile-tab" onclick="switchProfileTab('referrals',this)">🎁 Refer</button>
|
||
<button class="profile-tab" onclick="switchProfileTab('billing',this)">Billing</button>
|
||
<button class="profile-tab" onclick="switchProfileTab('account',this)">Account</button>
|
||
</div>
|
||
<!-- TAB: ACTIVITY -->
|
||
<div class="profile-tab-panel active" id="ptab-activity">
|
||
|
||
<!-- Filter tabs -->
|
||
<div style="display:flex;gap:6px;margin-bottom:14px;overflow-x:auto;padding-bottom:2px">
|
||
<button class="activity-ftab active" onclick="filterActivity('all',this)">All</button>
|
||
<button class="activity-ftab" onclick="filterActivity('purchases',this)">💳 Purchases</button>
|
||
<button class="activity-ftab" onclick="filterActivity('cashouts',this)">💸 Cashouts</button>
|
||
<button class="activity-ftab" onclick="filterActivity('broadcasts',this)">📢 News</button>
|
||
</div>
|
||
|
||
<!-- Status summary bar -->
|
||
<div id="activity-summary" style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:14px"></div>
|
||
|
||
<!-- Live feed -->
|
||
<div id="activity-feed">
|
||
<div style="text-align:center;padding:24px;color:var(--text2);font-size:15px">
|
||
<span class="spinner" style="border-top-color:var(--gold)"></span>
|
||
</div>
|
||
</div>
|
||
|
||
<button class="btn btn-outline" onclick="showPage('cashout')" style="margin-top:12px;width:100%">💸 Request Cash Out</button>
|
||
</div>
|
||
|
||
<!-- TAB: PLATFORM LOGINS -->
|
||
<!-- TAB: LOGINS / PLATFORM ACCOUNTS -->
|
||
<div class="profile-tab-panel" id="ptab-logins">
|
||
<div style="font-size:14px;font-weight:700;color:var(--text2);text-transform:uppercase;letter-spacing:1px;margin-bottom:4px">Platform Logins</div>
|
||
<div style="font-size:14px;color:var(--text2);margin-bottom:14px;line-height:1.5">Your game platform accounts. Approved logins show your credentials below. Tap password to reveal.</div>
|
||
|
||
<!-- Accounts list - shows all requests + approved credentials -->
|
||
<div id="platform-logins-list">
|
||
<div style="text-align:center;padding:20px;color:var(--text2);font-size:15px">Loading...</div>
|
||
</div>
|
||
|
||
<!-- Request new account -->
|
||
<div class="card" style="margin-top:14px">
|
||
<div class="card-title" style="font-size:15px">➕ Request Platform Account</div>
|
||
<div style="font-size:14px;color:var(--text2);margin-bottom:10px">Don't have a login for a platform? Request one and our team will set it up for you.</div>
|
||
<div id="req-platform-alert" class="alert" style="margin-bottom:8px"></div>
|
||
<select class="fi fi-select" id="req-platform-slug" style="width:100%;margin-bottom:10px">
|
||
<option value="">— Select Platform —</option>
|
||
</select>
|
||
<button class="btn btn-gold" onclick="requestPlatformLogin()" style="width:100%">📨 REQUEST ACCOUNT</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- TAB: REFERRALS -->
|
||
<div class="profile-tab-panel" id="ptab-referrals">
|
||
<div style="text-align:center;padding:16px 0 20px">
|
||
<div style="font-size:36px;margin-bottom:8px">🎁</div>
|
||
<div style="font-family:'Exo 2',sans-serif;font-weight:900;font-size:18px;color:var(--gold)">Refer & Earn Tokens!</div>
|
||
<div style="font-size:15px;color:var(--text2);margin-top:6px;line-height:1.6">Share your referral link. Earn tokens for every friend who joins and gets verified.</div>
|
||
</div>
|
||
|
||
<!-- Referral link -->
|
||
<div class="card" style="margin-bottom:12px;border-color:rgba(240,192,64,.2)">
|
||
<div style="font-size:15px;font-weight:700;color:var(--text2);text-transform:uppercase;letter-spacing:1px;margin-bottom:8px">Your Referral Link</div>
|
||
<div style="display:flex;gap:8px;align-items:center">
|
||
<input class="fi" id="referral-link-input" type="text" readonly style="flex:1;font-size:14px;cursor:pointer" onclick="this.select()">
|
||
<button onclick="copyReferralLink()" style="background:rgba(240,192,64,.1);border:1px solid rgba(240,192,64,.25);color:var(--gold);border-radius:8px;padding:10px 14px;font-size:14px;font-weight:700;cursor:pointer;white-space:nowrap">📋 Copy</button>
|
||
</div>
|
||
<div id="ref-copy-msg" style="font-size:15px;color:var(--green);margin-top:6px;display:none">✓ Copied to clipboard!</div>
|
||
</div>
|
||
|
||
<!-- Stats -->
|
||
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;margin-bottom:14px">
|
||
<div class="card" style="text-align:center;padding:12px 8px">
|
||
<div style="font-family:'Exo 2',sans-serif;font-weight:900;font-size:22px;color:var(--gold)" id="ref-count">0</div>
|
||
<div style="font-size:14px;color:var(--text2);text-transform:uppercase;letter-spacing:.5px">Referrals</div>
|
||
</div>
|
||
<div class="card" style="text-align:center;padding:12px 8px">
|
||
<div style="font-family:'Exo 2',sans-serif;font-weight:900;font-size:22px;color:var(--green)" id="ref-earned">0</div>
|
||
<div style="font-size:14px;color:var(--text2);text-transform:uppercase;letter-spacing:.5px">Tokens Earned</div>
|
||
</div>
|
||
<div class="card" style="text-align:center;padding:12px 8px">
|
||
<div style="font-family:'Exo 2',sans-serif;font-weight:900;font-size:16px;color:var(--cyan)" id="ref-tier">—</div>
|
||
<div style="font-size:14px;color:var(--text2);text-transform:uppercase;letter-spacing:.5px">Current Tier</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Next tier progress -->
|
||
<div id="ref-next-tier" style="margin-bottom:14px;display:none">
|
||
<div class="card" style="padding:12px 14px">
|
||
<div style="font-size:14px;color:var(--text2);margin-bottom:6px" id="ref-next-label">Next tier progress</div>
|
||
<div style="background:rgba(255,255,255,.08);border-radius:4px;height:6px;overflow:hidden">
|
||
<div id="ref-progress-bar" style="height:6px;border-radius:4px;background:linear-gradient(90deg,var(--gold),var(--cyan));transition:width .5s"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Social share -->
|
||
<div class="card" style="margin-bottom:12px">
|
||
<div style="font-size:14px;font-weight:700;color:var(--text2);margin-bottom:6px;text-transform:uppercase;letter-spacing:1px">Share & Earn Bonus Tokens</div>
|
||
<div style="font-size:14px;color:var(--text2);margin-bottom:12px;line-height:1.5">Share your referral link anywhere — Reddit, Twitter/X, a forum, your blog. Paste the link to your post below and we'll try to verify it automatically.</div>
|
||
<div style="display:flex;gap:8px;align-items:center;margin-bottom:6px">
|
||
<input class="fi" id="share-url-input" type="url" placeholder="https://reddit.com/r/gaming/comments/…" style="flex:1;font-size:14px">
|
||
<button onclick="submitShareUrl()" style="background:var(--gold);border:none;color:#000;border-radius:8px;padding:10px 16px;font-size:14px;font-weight:700;cursor:pointer;white-space:nowrap">🔍 Submit</button>
|
||
</div>
|
||
<div style="font-size:13px;color:var(--text2)">Public posts (Reddit, forums, blogs) verify instantly. Facebook/Instagram/TikTok need manual review.</div>
|
||
<div id="ref-share-alert" class="alert" style="margin-top:8px"></div>
|
||
<div id="ref-shares-list" style="margin-top:10px"></div>
|
||
</div>
|
||
|
||
<!-- Tiers list -->
|
||
<div class="card">
|
||
<div style="font-size:14px;font-weight:700;color:var(--text2);margin-bottom:10px;text-transform:uppercase;letter-spacing:1px">Referral Tiers</div>
|
||
<div id="ref-tiers-list"></div>
|
||
</div>
|
||
|
||
<!-- My referrals -->
|
||
<div class="card" style="margin-top:12px">
|
||
<div style="font-size:14px;font-weight:700;color:var(--text2);margin-bottom:10px;text-transform:uppercase;letter-spacing:1px">My Referrals</div>
|
||
<div id="ref-list"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- TAB: PAYOUT METHODS -->
|
||
<div class="profile-tab-panel" id="ptab-payout">
|
||
<div style="font-size:14px;font-weight:700;color:var(--text2);text-transform:uppercase;letter-spacing:1px;margin-bottom:4px">Payout Methods</div>
|
||
<div style="font-size:14px;color:var(--text2);margin-bottom:14px;line-height:1.5">Where to send your cashout payments. The default method is used automatically.</div>
|
||
<div id="payout-methods-list"></div>
|
||
<!-- Add Method Form -->
|
||
<div class="card" style="margin-top:12px">
|
||
<div class="card-title" style="font-size:15px">➕ Add Payout Method</div>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px">
|
||
<div>
|
||
<label style="font-size:15px;color:var(--text2);font-weight:700;display:block;margin-bottom:4px">Type</label>
|
||
<select class="fi" id="payout-type-new" style="font-size:15px">
|
||
<option value="">Loading...</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label style="font-size:15px;color:var(--text2);font-weight:700;display:block;margin-bottom:4px">Label</label>
|
||
<input class="fi" id="payout-label-new" type="text" placeholder="e.g. My Venmo" style="font-size:15px">
|
||
</div>
|
||
</div>
|
||
<div style="margin-bottom:10px">
|
||
<label style="font-size:15px;color:var(--text2);font-weight:700;display:block;margin-bottom:4px">Handle / Phone / Email</label>
|
||
<input class="fi" id="payout-handle-new" type="text" placeholder="e.g. @username, phone, or email">
|
||
</div>
|
||
<div id="payout-add-alert" class="alert" style="margin-bottom:8px"></div>
|
||
<button class="btn btn-gold" onclick="addPayoutMethod()" style="width:100%">💾 SAVE METHOD</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- TAB: PLATFORM ACCOUNTS -->
|
||
<!-- TAB: GAMES -->
|
||
<div class="profile-tab-panel" id="ptab-games">
|
||
<div style="font-size:14px;font-weight:700;color:var(--text2);text-transform:uppercase;letter-spacing:1px;margin-bottom:4px">Your Game Aliases</div>
|
||
<div style="font-size:14px;color:var(--text2);margin-bottom:14px;line-height:1.5">Save your in-game username for each platform. Your alias auto-fills when you buy tokens.</div>
|
||
<div id="game-aliases-list"><div style="color:var(--text2);text-align:center;padding:20px;font-size:15px">Loading games...</div></div>
|
||
<div id="game-aliases-alert" class="alert" style="margin-top:10px"></div>
|
||
<button class="btn btn-gold" onclick="saveAllAliases()" style="margin-top:12px">💾 SAVE ALL ALIASES</button>
|
||
</div>
|
||
<!-- TAB: BILLING -->
|
||
<div class="profile-tab-panel" id="ptab-billing">
|
||
<!-- Saved card display -->
|
||
<div id="saved-card-display" style="display:none">
|
||
<div class="saved-card-chip">
|
||
<div class="saved-card-chip-top">
|
||
<span class="saved-card-brand" id="saved-card-brand">VISA</span>
|
||
<span id="saved-card-dots">💳</span>
|
||
</div>
|
||
<div class="saved-card-number" id="saved-card-number">···· ···· ···· ····</div>
|
||
<div class="saved-card-bottom">
|
||
<div><div style="font-size:15px;color:rgba(255,255,255,.5);margin-bottom:2px">CARD HOLDER</div><div id="saved-card-name" style="font-size:15px;font-weight:700">—</div></div>
|
||
<div style="text-align:right"><div style="font-size:15px;color:rgba(255,255,255,.5);margin-bottom:2px">EXPIRES</div><div id="saved-card-exp" style="font-size:15px;font-weight:700">—</div></div>
|
||
</div>
|
||
</div>
|
||
<button class="btn btn-danger" onclick="clearSavedCard()" style="margin-top:10px;padding:10px;font-size:14px">🗑 Remove Saved Card</button>
|
||
</div>
|
||
<!-- Billing form -->
|
||
<div id="billing-form-section">
|
||
<div style="font-size:14px;font-weight:700;color:var(--text2);text-transform:uppercase;letter-spacing:1px;margin-bottom:12px">Billing Information</div>
|
||
<div id="billing-alert" class="alert" style="margin-bottom:12px"></div>
|
||
<div class="fi-row" style="margin-bottom:10px">
|
||
<div><label class="form-label">First Name</label><input class="fi" id="p-first" type="text" placeholder="First" autocomplete="given-name"></div>
|
||
<div><label class="form-label">Last Name</label><input class="fi" id="p-last" type="text" placeholder="Last" autocomplete="family-name"></div>
|
||
</div>
|
||
<div class="fg"><label class="form-label">Email</label><input class="fi" id="p-email" type="email" placeholder="your@email.com" autocomplete="email"></div>
|
||
<div class="fg"><label class="form-label">Street Address</label><input class="fi" id="p-address" type="text" placeholder="123 Main St" autocomplete="street-address"></div>
|
||
<div style="display:grid;grid-template-columns:1fr 60px 90px;gap:8px;margin-bottom:10px">
|
||
<div><label class="form-label">City</label><input class="fi" id="p-city" type="text" placeholder="City" autocomplete="address-level2"></div>
|
||
<div><label class="form-label">ST</label><input class="fi" id="p-state" type="text" placeholder="TX" maxlength="2" autocomplete="address-level1" style="text-transform:uppercase"></div>
|
||
<div><label class="form-label">ZIP</label><input class="fi" id="p-zip" type="text" placeholder="75001" maxlength="10" autocomplete="postal-code"></div>
|
||
</div>
|
||
<button class="btn btn-cyan" onclick="saveBillingProfile()" style="margin-bottom:8px">💾 SAVE BILLING INFO</button>
|
||
<button class="btn" onclick="clearAllBilling()" style="background:rgba(255,68,68,.08);border:1px solid rgba(255,68,68,.2);color:var(--red);font-size:14px;padding:10px">🗑 Clear All Saved Info</button>
|
||
</div>
|
||
</div>
|
||
<!-- TAB: ACCOUNT -->
|
||
<div class="profile-tab-panel" id="ptab-account">
|
||
<div style="font-size:14px;font-weight:700;color:var(--text2);text-transform:uppercase;letter-spacing:1px;margin-bottom:12px">Account Info</div>
|
||
<div class="card" style="margin-bottom:10px">
|
||
<div style="display:flex;flex-direction:column;gap:10px">
|
||
<div style="display:flex;justify-content:space-between"><span style="color:var(--text2);font-size:15px">Username</span><span id="acct-username" style="font-weight:700;color:var(--text)">—</span></div>
|
||
<div style="display:flex;justify-content:space-between"><span style="color:var(--text2);font-size:15px">Alias</span><span id="acct-alias" style="font-weight:700;color:var(--cyan)">—</span></div>
|
||
<div style="display:flex;justify-content:space-between"><span style="color:var(--text2);font-size:15px">Email</span><span id="acct-email" style="font-weight:700;font-size:15px">—</span></div>
|
||
<div style="display:flex;justify-content:space-between"><span style="color:var(--text2);font-size:15px">Member Since</span><span id="acct-joined" style="font-weight:700;color:var(--text2);font-size:14px">—</span></div>
|
||
</div>
|
||
</div>
|
||
<button class="btn btn-outline" onclick="showPage('buy')" style="margin-bottom:8px">Buy Tokens</button>
|
||
<button class="btn btn-outline" onclick="showPage('cashout')" style="margin-bottom:8px">💸 Cash Out</button>
|
||
<button class="btn btn-outline" onclick="showPage('chat')" style="margin-bottom:8px">💬 Contact Support</button>
|
||
<button class="btn btn-danger" onclick="doLogout()" style="margin-top:4px">LOG OUT</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ONBOARDING MODAL -->
|
||
<div id="onboarding-modal" style="display:none">
|
||
<div class="onboarding-box" id="onboarding-step-1">
|
||
<div style="font-size:48px;margin-bottom:12px">🎮</div>
|
||
<div class="onboarding-title">Welcome to TomTomGames!</div>
|
||
<div class="onboarding-sub">Do you already have a login account on any of our game platforms?</div>
|
||
<div style="display:flex;gap:10px;justify-content:center">
|
||
<button class="btn btn-gold" onclick="onboardingHasAccount()" style="flex:1;max-width:160px">Yes, I do</button>
|
||
<button class="btn btn-outline" onclick="onboardingNoAccount()" style="flex:1;max-width:160px">No, not yet</button>
|
||
</div>
|
||
</div>
|
||
<div class="onboarding-box" id="onboarding-step-2" style="display:none">
|
||
<div style="font-size:40px;margin-bottom:12px">📋</div>
|
||
<div class="onboarding-title">Request Platform Login</div>
|
||
<div class="onboarding-sub">Select which platform you want an account created on. Our team will set it up and send you your login details.</div>
|
||
<div id="onboarding-alert" class="alert" style="margin-bottom:12px"></div>
|
||
<div class="fg" style="margin-bottom:14px;text-align:left">
|
||
<label style="font-size:14px;font-weight:700;color:var(--text2);display:block;margin-bottom:6px">Select Platform</label>
|
||
<select class="fi fi-select" id="onboarding-platform" style="width:100%">
|
||
<option value="">— Choose a platform —</option>
|
||
</select>
|
||
</div>
|
||
<div style="display:flex;gap:10px">
|
||
<button class="btn btn-gold" onclick="onboardingRequestAccount()" style="flex:1">Request Account</button>
|
||
<button class="btn btn-outline" onclick="onboardingDone()" style="flex:1">Skip for now</button>
|
||
</div>
|
||
</div>
|
||
<div class="onboarding-box" id="onboarding-step-3" style="display:none">
|
||
<div style="font-size:48px;margin-bottom:12px">✅</div>
|
||
<div class="onboarding-title">Request Submitted!</div>
|
||
<div class="onboarding-sub" id="onboarding-confirm-text">Your account request has been sent. You can see the status in your Profile under Games.</div>
|
||
<div style="display:flex;gap:10px;justify-content:center">
|
||
<button class="btn btn-gold" onclick="onboardingRequestAnother()" style="flex:1;max-width:160px">+ Another Platform</button>
|
||
<button class="btn btn-outline" onclick="onboardingDone()" style="flex:1;max-width:160px">Go to Home</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- BROADCASTS PAGE -->
|
||
<div class="page" id="page-broadcasts">
|
||
<div class="section-title"><span>📢 NEWS & ANNOUNCEMENTS</span></div>
|
||
<div id="broadcasts-list">
|
||
<div style="color:var(--text2);text-align:center;padding:32px;font-size:15px">Loading...</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- CHAT PAGE -->
|
||
<div class="page" id="page-chat">
|
||
<div class="chat-header">
|
||
<div class="chat-header-avatar">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="20" height="20"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
||
</div>
|
||
<div style="flex:1">
|
||
<div class="chat-header-name">TomTomGames Support</div>
|
||
<div class="chat-header-status" id="chat-status-text">
|
||
<span class="chat-status-dot"></span> We reply within minutes
|
||
</div>
|
||
</div>
|
||
<div id="chat-typing-indicator" style="display:none;font-size:15px;color:var(--cyan);font-weight:600"></div>
|
||
</div>
|
||
<div class="chat-messages" id="chat-messages">
|
||
<div class="chat-loading">Loading messages...</div>
|
||
</div>
|
||
<div class="chat-quick-replies" id="chat-quick-replies" style="display:none">
|
||
<button class="chat-chip" onclick="quickReply('I need help with my tokens')">🎮 Token issue</button>
|
||
<button class="chat-chip" onclick="quickReply('I have a cashout question')">💸 Cashout help</button>
|
||
<button class="chat-chip" onclick="quickReply('I need help with my account')">👤 Account help</button>
|
||
<button class="chat-chip" onclick="quickReply('I have a payment question')">💳 Payment help</button>
|
||
</div>
|
||
<div class="chat-input-bar">
|
||
<input class="chat-input" id="chat-input" type="text" placeholder="Type your message to support..." maxlength="1000"
|
||
onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendChatMsg();}">
|
||
<button class="chat-send-btn" onclick="sendChatMsg()" id="chat-send-btn" title="Send message">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="18" height="18"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<nav class="navbar">
|
||
<button class="nav-btn active" data-page="home" onclick="showPage('home')">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>HOME
|
||
</button>
|
||
<button class="nav-btn" data-page="buy" onclick="showPage('buy')">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg>BUY
|
||
</button>
|
||
<button class="nav-btn" data-page="cashout" onclick="showPage('cashout')">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>CASH OUT
|
||
</button>
|
||
<button class="nav-btn chat-nav-btn" data-page="chat" onclick="showPage('chat')" style="position:relative">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
||
<span class="chat-nav-badge" id="chat-nav-badge" style="display:none"></span>
|
||
SUPPORT
|
||
</button>
|
||
<button class="nav-btn" data-page="broadcasts" onclick="showPage('broadcasts')" style="position:relative">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07A19.5 19.5 0 0 1 4.69 11.5 19.79 19.79 0 0 1 1.61 2.84 2 2 0 0 1 3.58 1H6.5a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L7.91 8.5a16 16 0 0 0 5.54 5.54l.86-.87a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 21 15.5"/><path d="m22 2-7 7"/><path d="M15 2h7v7"/></svg>
|
||
<span class="chat-nav-badge" id="broadcast-nav-badge" style="display:none"></span>
|
||
NEWS
|
||
</button>
|
||
<button class="nav-btn" data-page="profile" onclick="showPage('profile')">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>PROFILE
|
||
</button>
|
||
</nav>
|
||
<div id="copyright-footer" style="position:fixed;bottom:0;left:50%;transform:translateX(-50%);width:100%;max-width:var(--max);text-align:center;padding-bottom:calc(var(--nav-h) + 2px);pointer-events:none;z-index:99">
|
||
<span style="font-size:11px;color:rgba(255,255,255,.18);font-family:'Exo 2',sans-serif;letter-spacing:.3px">© 2026 TomTomGames.com, a division of TomTom Enterprises · v<?= $_appVersion ?></span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- SUCCESS MODAL -->
|
||
<div class="modal-overlay" id="success-modal">
|
||
<div class="modal-sheet">
|
||
<div class="modal-handle"></div>
|
||
<div style="text-align:center;padding:16px 0 8px">
|
||
<div style="font-size:56px;margin-bottom:14px" id="modal-icon">🎉</div>
|
||
<div style="font-family:'Exo 2',sans-serif;font-weight:700;font-size:20px;color:var(--text);margin-bottom:16px" id="modal-title">Tokens Added!</div>
|
||
<div id="modal-card-content"></div>
|
||
<button class="btn btn-gold" style="margin-top:20px" id="modal-action-btn">AWESOME!</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- PAYMENT PROCESSING OVERLAY -->
|
||
<div id="pay-processing-overlay" style="display:none;position:fixed;inset:0;background:rgba(5,5,15,.96);z-index:500;flex-direction:column;align-items:center;justify-content:center;gap:0;backdrop-filter:blur(8px)">
|
||
<div class="pay-proc-ring">
|
||
<div class="pay-proc-spinner"></div>
|
||
<div class="pay-proc-icon" id="pay-proc-icon">🔒</div>
|
||
</div>
|
||
<div class="pay-proc-title" id="pay-proc-title">Processing Payment</div>
|
||
<div class="pay-proc-sub" id="pay-proc-sub">Please wait — do not close the app</div>
|
||
<div class="pay-proc-steps" id="pay-proc-steps">
|
||
<div class="pay-proc-step active" id="step-1">🔐 Securing connection</div>
|
||
<div class="pay-proc-step" id="step-2">💳 Authorizing card</div>
|
||
<div class="pay-proc-step" id="step-3">✅ Confirming payment</div>
|
||
<div class="pay-proc-step" id="step-4">Crediting tokens</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- PAYMENT SUCCESS OVERLAY (replaces old modal) -->
|
||
<div id="pay-success-overlay" style="display:none;position:fixed;inset:0;background:rgba(5,5,15,.97);z-index:501;flex-direction:column;align-items:center;justify-content:center;padding:32px;backdrop-filter:blur(8px)">
|
||
<div class="pay-result-wrap" id="pay-result-wrap"><!-- filled by JS --></div>
|
||
</div>
|
||
|
||
<!-- Square SDK loads async — won't block app startup -->
|
||
<script src="<?= \SquarePayment::sdkUrl() ?>"></script>
|
||
<script>
|
||
const CFG = {
|
||
sqAppId: '<?= SQUARE_APP_ID ?>',
|
||
sqLocId: '<?= SQUARE_LOCATION_ID ?>',
|
||
sqEnv: '<?= SQUARE_ENV ?>',
|
||
packages: <?= TOKEN_PACKAGES ?>,
|
||
platforms: [], // loaded dynamically from /api/platforms.php
|
||
pay: {
|
||
venmo: '<?= PAY_VENMO ?>',
|
||
chime: '<?= PAY_CHIME ?>',
|
||
cashapp: '<?= PAY_CASHAPP ?>',
|
||
zelle: '<?= PAY_ZELLE ?>',
|
||
}
|
||
};
|
||
|
||
let S = { user:null, pkg:null, method:'card', sqCard:null };
|
||
|
||
async function doLogin() {
|
||
const btn=document.getElementById('login-btn'); const al=document.getElementById('login-alert');
|
||
const u=document.getElementById('login-user').value.trim(); const p=document.getElementById('login-pass').value;
|
||
if (!u||!p){showAlert(al,'Please fill in all fields.','error');return;}
|
||
document.getElementById('login-unverified-box').style.display='none';
|
||
btn.innerHTML='<span class="spinner"></span>'; btn.disabled=true;
|
||
try {
|
||
const d=await api('/api/login.php',{username:u,password:p});
|
||
console.log('Login response:', JSON.stringify(d));
|
||
if(d.success){
|
||
if (d.user && d.user.is_admin == 1) {
|
||
window.location.href = '/admin/';
|
||
return;
|
||
}
|
||
console.log('Login success, calling showApp...');
|
||
S.user=d.user; showApp();
|
||
} else if(d.unverified) {
|
||
// Show unverified box with resend option
|
||
hideAlert(al);
|
||
S._unverifiedEmail = d.email || '';
|
||
document.getElementById('login-unverified-box').style.display='block';
|
||
} else {
|
||
showAlert(al,d.error||'Login failed.','error');
|
||
}
|
||
} catch{showAlert(al,'Network error.','error');}
|
||
btn.innerHTML='LOGIN'; btn.disabled=false;
|
||
}
|
||
|
||
// ─── INIT ──────────────────────────────────────────────────
|
||
window.addEventListener('DOMContentLoaded', function() {
|
||
// Load platforms and payment methods dynamically, then build UI
|
||
Promise.all([
|
||
fetch('/api/platforms.php?action=list').then(r=>r.json()).catch(()=>({success:false})),
|
||
fetch('/api/payment_settings.php?action=list').then(r=>r.json()).catch(()=>({success:false}))
|
||
]).then(function(results) {
|
||
if (results[0].success) CFG.platforms = results[0].platforms;
|
||
if (results[1].success) CFG.payMethods = results[1].methods;
|
||
try { buildPlatforms(); } catch(e) {}
|
||
try { buildPkgPills(); } catch(e) {}
|
||
try { buildCashoutPlatforms(); } catch(e) {}
|
||
try { buildPaymentMethods(); } catch(e) {}
|
||
}).catch(function() {
|
||
try { buildPlatforms(); } catch(e) {}
|
||
try { buildPkgPills(); } catch(e) {}
|
||
try { buildCashoutPlatforms(); } catch(e) {}
|
||
});
|
||
|
||
fetch('/api/me.php')
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(d) {
|
||
if (d && d.success && d.user) {
|
||
if (d.user.is_admin == 1) {
|
||
window.location.href = '/admin/';
|
||
} else {
|
||
S.user = d.user; showApp();
|
||
}
|
||
} else showAuth();
|
||
})
|
||
.catch(function() { showAuth(); });
|
||
});
|
||
|
||
function api(url, body) {
|
||
const opts = { method: body ? 'POST' : 'GET', headers: { 'Content-Type': 'application/json' } };
|
||
if (body) opts.body = JSON.stringify(body);
|
||
return fetch(url, opts).then(r => r.json());
|
||
}
|
||
|
||
function showApp() {
|
||
var auth = document.getElementById('auth-screen');
|
||
var app = document.getElementById('main-app');
|
||
if (auth) auth.setAttribute('style', 'display:none');
|
||
if (app) app.setAttribute('style', 'display:block;min-height:100vh;');
|
||
console.log('showApp: auth hidden, app visible');
|
||
try { updateUI(); } catch(e) { console.warn('updateUI err:', e); }
|
||
try { loadCashoutHistory(); } catch(e) { console.warn('cashout err:', e); }
|
||
try { pollChatBadge(); } catch(e) { console.warn('badge err:', e); }
|
||
// Load aliases for auto-prefill on buy page
|
||
api('/api/game_aliases.php?action=get').then(d => {
|
||
if (d.success) savedAliases = d.aliases || {};
|
||
}).catch(() => {});
|
||
loadCashoutMethodTypes();
|
||
setTimeout(pollBroadcastBadge, 1000);
|
||
setInterval(pollBroadcastBadge, 60000);
|
||
setTimeout(checkOnboarding, 1500);
|
||
// Inject coin into static elements
|
||
setTimeout(function() {
|
||
var bfi = document.getElementById('buy-form-icon');
|
||
if (bfi) bfi.innerHTML = tokenCoin(48);
|
||
}, 100);
|
||
}
|
||
function showAuth() {
|
||
var auth = document.getElementById('auth-screen');
|
||
var app = document.getElementById('main-app');
|
||
if (app) app.setAttribute('style', 'display:none;');
|
||
if (auth) auth.setAttribute('style', 'display:flex');
|
||
}
|
||
function switchTab(t) {
|
||
document.getElementById('tab-login').style.display = t === 'login' ? 'block' : 'none';
|
||
document.getElementById('tab-register').style.display = t === 'register' ? 'block' : 'none';
|
||
document.querySelectorAll('.auth-tab').forEach((el,i) =>
|
||
el.classList.toggle('active',(i===0&&t==='login')||(i===1&&t==='register'))
|
||
);
|
||
}
|
||
function showPage(n) {
|
||
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
|
||
document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
|
||
document.getElementById('page-'+n)?.classList.add('active');
|
||
document.querySelector(`.nav-btn[data-page="${n}"]`)?.classList.add('active');
|
||
const _cr=document.getElementById('copyright-footer'); if(_cr) _cr.style.display=n==='home'?'block':'none';
|
||
if (n === 'buy') { updatePayForm(); initBuyPageBilling(); setTimeout(initSquare, 150); }
|
||
if (n === 'broadcasts') { loadBroadcasts_player(); }
|
||
if (n === 'cashout') {
|
||
updateUI();
|
||
loadCashoutHistory();
|
||
loadCashoutPayoutMethods();
|
||
// Prefill alias for currently selected platform if any
|
||
const pid = document.getElementById('cashout-platform')?.value;
|
||
if (pid) prefillCashoutAlias(pid);
|
||
}
|
||
if (n === 'profile') { loadActivityDashboard(); switchProfileTab('activity', document.querySelector('.profile-tab')); }
|
||
if (n === 'chat') {
|
||
// Clear badge when opening chat
|
||
document.getElementById('chat-nav-badge').style.display = 'none';
|
||
initChat();
|
||
setTimeout(scrollChat, 100);
|
||
}
|
||
}
|
||
|
||
function setText(id, val) { var el=document.getElementById(id); if(el) el.textContent=val; }
|
||
function displayName(u) { return u ? (u.alias || u.username || '?') : '?'; }
|
||
function updateUI() {
|
||
if (!S.user) return;
|
||
const t = parseFloat(S.user.tokens) || 0;
|
||
const name = displayName(S.user);
|
||
const init = name.charAt(0).toUpperCase();
|
||
const setHTML = (id, html) => { const el=document.getElementById(id); if(el) el.innerHTML=html; };
|
||
setText('home-alias', name);
|
||
setText('cashout-balance', t);
|
||
setText('profile-username', name);
|
||
setText('profile-alias', S.user.username ? '@' + S.user.username : '');
|
||
setText('avatar-btn', init);
|
||
setText('profile-avatar', init);
|
||
setHTML('header-tokens', t + ' ' + tokenCoin(18));
|
||
setHTML('home-balance', '<span style="font-family:\'Exo 2\',sans-serif">' + t + '</span>');
|
||
setHTML('profile-tokens', t + ' ' + tokenCoin(24));
|
||
}
|
||
async function refreshUser() {
|
||
const d = await api('/api/me.php');
|
||
if (d.success) { S.user = d.user; updateUI(); }
|
||
}
|
||
|
||
// ─── PLATFORMS ─────────────────────────────────────────────
|
||
function buildPlatforms() {
|
||
document.getElementById('platform-grid').innerHTML = CFG.platforms.map(p => `
|
||
<a href="${p.url}" target="_blank" class="platform-card" style="--p-color:${p.color}">
|
||
<div class="platform-img-wrap">
|
||
<img src="/assets/img/${p.id}.svg" alt="${p.name}" onerror="this.style.display='none'">
|
||
</div>
|
||
<div class="platform-name">${p.name}</div>
|
||
<div class="play-btn">TAP TO PLAY →</div>
|
||
</a>`).join('');
|
||
|
||
// Also populate buy-platform select
|
||
const sel = document.getElementById('buy-platform');
|
||
CFG.platforms.forEach(p => {
|
||
const o = document.createElement('option');
|
||
o.value = p.id; o.textContent = p.name; sel.appendChild(o);
|
||
});
|
||
}
|
||
function buildCashoutPlatforms() {
|
||
const sel = document.getElementById('cashout-platform');
|
||
CFG.platforms.forEach(p => {
|
||
const o = document.createElement('option'); o.value = p.id; o.textContent = p.name; sel.appendChild(o);
|
||
});
|
||
}
|
||
|
||
// ─── TOKEN PACKAGES ────────────────────────────────────────
|
||
function buildPkgPills() {
|
||
const container = document.getElementById('pkg-scroll');
|
||
container.innerHTML = CFG.packages.map((pkg, i) => `
|
||
<div class="pkg-pill${pkg.popular?' popular':''}" onclick="selectPkg(${i},this)">
|
||
<div class="pkg-pill-tokens">${pkg.tokens}</div>
|
||
<div class="pkg-pill-price">$${pkg.price}</div>
|
||
</div>`).join('');
|
||
const popularIdx = CFG.packages.findIndex(p => p.popular);
|
||
const idx = popularIdx >= 0 ? popularIdx : 0;
|
||
selectPkg(idx, container.querySelectorAll('.pkg-pill')[idx]);
|
||
}
|
||
function selectPkg(i, el) {
|
||
S.pkg = CFG.packages[i]; S.customPkg = false;
|
||
document.querySelectorAll('.pkg-pill').forEach(p => p.classList.remove('selected'));
|
||
el?.classList.add('selected');
|
||
const ci = document.getElementById('custom-amt');
|
||
if (ci) ci.value = '';
|
||
document.getElementById('custom-amt-equiv').style.display = 'none';
|
||
updateOrderSummary();
|
||
}
|
||
function selectCustomPkg(val) {
|
||
const amt = parseInt(val);
|
||
if (!amt || amt < 1) {
|
||
document.getElementById('custom-amt-equiv').style.display = 'none';
|
||
S.pkg = null; S.customPkg = false; updateOrderSummary(); return;
|
||
}
|
||
document.querySelectorAll('.pkg-pill').forEach(p => p.classList.remove('selected'));
|
||
S.customPkg = true;
|
||
S.pkg = { tokens: amt, price: amt, label: amt + ' Tokens (Custom)', popular: false };
|
||
document.getElementById('custom-token-count').textContent = amt;
|
||
document.getElementById('custom-amt-equiv').style.display = 'flex';
|
||
updateOrderSummary();
|
||
}
|
||
function updateOrderSummary() {
|
||
const el = document.getElementById('order-summary');
|
||
if (!S.pkg) { el.style.display='none'; return; }
|
||
document.getElementById('summary-pkg').textContent = S.pkg.label || S.pkg.tokens+' Tokens';
|
||
const sumTok = document.getElementById('summary-tokens'); if(sumTok) sumTok.innerHTML = S.pkg.tokens + ' ' + tokenCoin(18);
|
||
document.getElementById('summary-total').textContent = '$'+S.pkg.price+'.00';
|
||
el.style.display = 'block';
|
||
}
|
||
function updateAmountDisplay() { updateOrderSummary(); }
|
||
function updatePayForm() {
|
||
updateAmountDisplay();
|
||
const alias = document.getElementById('buy-alias').value.trim();
|
||
const platformId = document.getElementById('buy-platform').value;
|
||
document.getElementById('ref-alias').textContent = alias || '—';
|
||
document.getElementById('ref-platform').textContent = platformId ? CFG.platforms.find(p=>p.id===platformId)?.name : '—';
|
||
if (S.method !== 'card') renderManualInstructions();
|
||
}
|
||
|
||
// ─── PAYMENT METHOD ────────────────────────────────────────
|
||
function selectPMT(el, method) {
|
||
document.querySelectorAll('.pmt').forEach(m => m.classList.remove('active'));
|
||
el.classList.add('active');
|
||
S.method = method;
|
||
document.getElementById('section-card').style.display = method === 'card' ? 'block' : 'none';
|
||
document.getElementById('section-manual').style.display = method !== 'card' ? 'block' : 'none';
|
||
if (method !== 'card') renderManualInstructions();
|
||
}
|
||
|
||
function renderManualInstructions() {
|
||
const method = S.method;
|
||
const pkg = S.pkg;
|
||
const alias = document.getElementById('buy-alias').value.trim();
|
||
const platformId = document.getElementById('buy-platform').value;
|
||
const platformName = platformId ? CFG.platforms.find(p => p.id === platformId)?.name || platformId : '(your platform)';
|
||
const amt = pkg ? `$${pkg.price}.00` : '(select a package)';
|
||
const handle = CFG.pay[method] || '—';
|
||
|
||
const iconMap = { venmo:'💙', chime:'🟢', cashapp:'💚', zelle:'💜' };
|
||
const nameMap = {
|
||
venmo: (CFG.payLabels&&CFG.payLabels.venmo)||'Venmo',
|
||
chime: (CFG.payLabels&&CFG.payLabels.chime)||'Chime',
|
||
cashapp:(CFG.payLabels&&CFG.payLabels.cashapp)||'Cash App',
|
||
zelle: (CFG.payLabels&&CFG.payLabels.zelle)||'Zelle',
|
||
};
|
||
|
||
document.getElementById('manual-box').innerHTML = `
|
||
<div class="manual-box-title">${iconMap[method]} ${nameMap[method]} Instructions</div>
|
||
<div class="manual-step">
|
||
<div class="manual-step-num">1</div>
|
||
<div class="manual-step-text">Open your <strong>${nameMap[method]}</strong> app and send <strong>${amt}</strong> to <span class="handle">${handle}</span></div>
|
||
</div>
|
||
<div class="manual-step">
|
||
<div class="manual-step-num">2</div>
|
||
<div class="manual-step-text">In the <strong>note/memo</strong>, include: <strong>${platformName}</strong> — <strong>${alias || 'your alias'}</strong></div>
|
||
</div>
|
||
<div class="manual-step">
|
||
<div class="manual-step-num">3</div>
|
||
<div class="manual-step-text">Click <strong>Submit Request</strong> below. We'll verify your payment and credit your tokens within a few hours.</div>
|
||
</div>`;
|
||
|
||
const refBadge = document.getElementById('ref-badge');
|
||
if (alias && platformId) { refBadge.style.display = 'block'; }
|
||
else { refBadge.style.display = 'none'; }
|
||
}
|
||
|
||
// ─── SQUARE ────────────────────────────────────────────────
|
||
async function initSquare() {
|
||
if (!window.Square) {
|
||
console.warn('Square SDK not loaded — card unavailable');
|
||
document.getElementById('card-container').innerHTML =
|
||
'<div style="color:#ff6666;font-size:14px;padding:8px">Card form unavailable. Please refresh the page.</div>';
|
||
return;
|
||
}
|
||
try {
|
||
// Destroy existing instance before re-attaching
|
||
if (S.sqCard) {
|
||
try { await S.sqCard.destroy(); } catch(e) {}
|
||
S.sqCard = null;
|
||
}
|
||
// Clear container so Square has a clean div to inject into
|
||
var container = document.getElementById('card-container');
|
||
container.innerHTML = '';
|
||
|
||
var pay = window.Square.payments(CFG.sqAppId, CFG.sqLocId);
|
||
S.sqCard = await pay.card({
|
||
style: {
|
||
'.input-container' : { borderColor: '#dddddd', borderRadius: '8px' },
|
||
'.input-container.is-focus': { borderColor: '#0095d9' },
|
||
'input' : { color: '#1a1a2e', fontSize: '16px' },
|
||
'input::placeholder' : { color: '#aaaaaa' },
|
||
}
|
||
});
|
||
await S.sqCard.attach('#card-container');
|
||
console.log('Square card attached OK');
|
||
var lbl = document.getElementById('card-ready-label');
|
||
if (lbl) lbl.textContent = '✓ Ready';
|
||
} catch(e) {
|
||
console.error('Square init error:', e);
|
||
document.getElementById('card-container').innerHTML =
|
||
'<div style="color:#ff9999;font-size:14px;padding:8px">Card form error: ' + e.message + '</div>';
|
||
}
|
||
}
|
||
|
||
// ─── SUBMIT PAYMENT ────────────────────────────────────────
|
||
// ─── PAYMENT PROCESSING OVERLAY ──────────────────────────
|
||
const PROC_STEPS = ['step-1','step-2','step-3','step-4'];
|
||
let procTimer = null;
|
||
|
||
function showProcessingOverlay() {
|
||
const ov = document.getElementById('pay-processing-overlay');
|
||
ov.style.display = 'flex';
|
||
PROC_STEPS.forEach(s => {
|
||
const el = document.getElementById(s);
|
||
el.classList.remove('active','done');
|
||
});
|
||
document.getElementById('pay-proc-icon').textContent = '🔒';
|
||
document.getElementById('pay-proc-title').textContent = 'Processing Payment';
|
||
document.getElementById('pay-proc-sub').textContent = 'Please wait — do not close the app';
|
||
// Animate steps
|
||
let i = 0;
|
||
procTimer = setInterval(() => {
|
||
if (i > 0) document.getElementById(PROC_STEPS[i-1]).classList.replace('active','done');
|
||
if (i < PROC_STEPS.length) {
|
||
document.getElementById(PROC_STEPS[i]).classList.add('active');
|
||
i++;
|
||
} else { clearInterval(procTimer); }
|
||
}, 700);
|
||
}
|
||
|
||
function hideProcessingOverlay() {
|
||
clearInterval(procTimer);
|
||
document.getElementById('pay-processing-overlay').style.display = 'none';
|
||
}
|
||
|
||
function showPayResult(type, data) {
|
||
hideProcessingOverlay();
|
||
const ov = document.getElementById('pay-success-overlay');
|
||
const wrap = document.getElementById('pay-result-wrap');
|
||
ov.style.display = 'flex';
|
||
|
||
if (type === 'success') {
|
||
const newBal = parseFloat(S.user?.tokens || 0);
|
||
wrap.innerHTML = `
|
||
<span class="pay-result-icon">🎉</span>
|
||
<div class="pay-result-title success">Payment Approved!</div>
|
||
<div class="pay-balance-bump">+${data.tokens_added} ${tokenCoin(28)}</div>
|
||
<div style="font-size:14px;color:var(--text2);margin-bottom:4px">New Balance</div>
|
||
<div style="font-family:'Exo 2',sans-serif;font-weight:700;font-size:26px;color:var(--gold2);margin-bottom:16px">${data.new_balance !== undefined ? data.new_balance : parseFloat(S.user?.tokens||0)} Tokens</div>
|
||
<div class="pay-result-detail">
|
||
<strong>Amount charged:</strong> $${data.amount || ''}<br>
|
||
<strong>Platform:</strong> ${data.platform || ''}<br>
|
||
<strong>Game alias:</strong> <span style="color:var(--cyan)">${data.alias || ''}</span><br>
|
||
${data.card ? `<strong>Card:</strong> ${data.card}` : ''}
|
||
</div>
|
||
<div class="pay-result-btns">
|
||
<button class="btn btn-gold" onclick="closePayResult();showPage('home')">🎰 PLAY NOW!</button>
|
||
<button class="btn btn-outline" onclick="closePayResult();showPage('buy')" style="padding:10px;font-size:15px">Buy More Tokens</button>
|
||
</div>`;
|
||
} else if (type === 'manual') {
|
||
wrap.innerHTML = `
|
||
<span class="pay-result-icon">⏳</span>
|
||
<div class="pay-result-title pending">Request Submitted!</div>
|
||
<div style="font-size:14px;color:var(--text2);margin:12px 0 4px">Tokens pending — awaiting payment confirmation</div>
|
||
<div style="display:flex;align-items:center;justify-content:center;gap:8px;font-family:'Exo 2',sans-serif;font-weight:900;font-size:40px;color:var(--gold);margin-bottom:4px">${data.tokens}${tokenCoin(36)}</div>
|
||
<div style="font-size:14px;color:var(--text2);margin-bottom:16px">will be added to your account once payment is received</div>
|
||
<div class="pay-result-detail">
|
||
Order <strong style="color:var(--cyan)">#${data.purchase_id}</strong> received.<br><br>
|
||
<strong style="color:var(--gold)">📲 Next steps:</strong><br>
|
||
1. Send <strong>$${data.amount}</strong> via <strong>${data.method}</strong><br>
|
||
2. Include your order # <strong style="color:var(--cyan)">#${data.purchase_id}</strong> in the memo<br>
|
||
3. Tokens are credited <strong>within a few minutes</strong> after we confirm receipt<br><br>
|
||
<span style="color:var(--text2);font-size:14px">⚠️ Your token balance will not change until payment is verified by our team.</span>
|
||
</div>
|
||
<div class="pay-result-btns">
|
||
<button class="btn btn-gold" onclick="closePayResult();showPage('home')">GOT IT — HOME</button>
|
||
<button class="btn btn-outline" onclick="closePayResult();showPage('chat')" style="padding:10px;font-size:15px">💬 Contact Support</button>
|
||
</div>`;
|
||
} else {
|
||
wrap.innerHTML = `
|
||
<span class="pay-result-icon">❌</span>
|
||
<div class="pay-result-title error">Payment Failed</div>
|
||
<div class="pay-result-detail">${data.error || 'Your payment could not be processed. Please check your card details and try again.'}</div>
|
||
<div class="pay-result-btns">
|
||
<button class="btn btn-gold" onclick="closePayResult()">TRY AGAIN</button>
|
||
<button class="btn btn-outline" onclick="closePayResult();showPage('chat')" style="padding:10px;font-size:15px">💬 Contact Support</button>
|
||
</div>`;
|
||
}
|
||
}
|
||
|
||
function closePayResult() {
|
||
document.getElementById('pay-success-overlay').style.display = 'none';
|
||
}
|
||
|
||
async function submitPayment() {
|
||
const btn = document.getElementById('pay-submit-btn');
|
||
const al = document.getElementById('buy-alert');
|
||
|
||
const platformId = document.getElementById('buy-platform').value;
|
||
const gameAlias = document.getElementById('buy-alias').value.trim();
|
||
const platformName = platformId ? CFG.platforms.find(p=>p.id===platformId)?.name || platformId : '';
|
||
|
||
if (!platformId) { showAlert(al, 'Please select a gaming platform.', 'error'); return; }
|
||
if (!gameAlias) { showAlert(al, 'Please enter your in-game alias.', 'error'); return; }
|
||
if (!S.pkg) { showAlert(al, 'Please select a token package or enter a custom amount.', 'error'); return; }
|
||
if (S.pkg.price < 1) { showAlert(al, 'Minimum purchase is $1.', 'error'); return; }
|
||
|
||
const billing = {
|
||
first_name: (document.getElementById('card-first-name')?.value || '').trim(),
|
||
last_name: (document.getElementById('card-last-name')?.value || '').trim(),
|
||
address: (document.getElementById('card-address')?.value || '').trim(),
|
||
city: (document.getElementById('card-city')?.value || '').trim(),
|
||
state: (document.getElementById('card-state')?.value || '').trim().toUpperCase(),
|
||
zip: (document.getElementById('card-zip')?.value || '').trim(),
|
||
email: (document.getElementById('card-email')?.value || '').trim(),
|
||
};
|
||
|
||
if (S.method === 'card') {
|
||
if (!billing.first_name || !billing.last_name) { showAlert(al, 'Please enter the name on your card.', 'error'); return; }
|
||
if (!billing.zip) { showAlert(al, 'Billing ZIP code is required.', 'error'); return; }
|
||
}
|
||
|
||
btn.disabled = true;
|
||
hideAlert(al);
|
||
|
||
// ── Card tokenize BEFORE showing overlay (Square SDK needs DOM visible)
|
||
let sourceId = '';
|
||
if (S.method === 'card') {
|
||
if (!S.sqCard) { showAlert(al,'Card form not ready. Please refresh.','error'); btn.disabled=false; return; }
|
||
try {
|
||
const result = await S.sqCard.tokenize();
|
||
if (result.status !== 'OK') {
|
||
showAlert(al, result.errors?.[0]?.message || 'Card declined. Check your card details.', 'error');
|
||
btn.disabled=false; return;
|
||
}
|
||
sourceId = result.token;
|
||
} catch { showAlert(al,'Payment error. Try again.','error'); btn.disabled=false; return; }
|
||
}
|
||
|
||
// ── Show full-screen processing overlay
|
||
showProcessingOverlay();
|
||
|
||
try {
|
||
const d = await api('/api/purchase.php', {
|
||
source_id: sourceId,
|
||
tokens: S.pkg.tokens,
|
||
price_cents: S.pkg.price * 100,
|
||
method: S.method,
|
||
platform_id: platformId,
|
||
game_alias: gameAlias,
|
||
player_name: S.user?.alias || '',
|
||
is_custom: S.customPkg || false,
|
||
billing,
|
||
});
|
||
|
||
// Mark all steps done
|
||
PROC_STEPS.forEach(s => {
|
||
const el = document.getElementById(s);
|
||
el.classList.remove('active');
|
||
el.classList.add('done');
|
||
});
|
||
await new Promise(r => setTimeout(r, 600)); // brief pause for visual satisfaction
|
||
|
||
if (d.success) {
|
||
// Update local token balance immediately from API response
|
||
if (d.new_balance !== undefined && S.user) {
|
||
S.user.tokens = d.new_balance;
|
||
updateUI();
|
||
}
|
||
await refreshUser(); // then sync full user object from server
|
||
// Auto-save billing if checkbox checked
|
||
const saveCb = document.getElementById('save-billing-cb');
|
||
if (saveCb?.checked && billing.first_name && S.method === 'card') {
|
||
const cardBrand = d.card_brand || '';
|
||
const cardLast4 = d.card_last4 || '';
|
||
await api('/api/billing.php?action=save', { ...billing, card_brand: cardBrand, card_last4: cardLast4 });
|
||
}
|
||
// Auto-save game alias for this platform
|
||
if (gameAlias && platformId) {
|
||
savedAliases[platformId] = gameAlias;
|
||
api('/api/game_aliases.php?action=save', { platform_slug: platformId, alias: gameAlias }).catch(()=>{});
|
||
}
|
||
// Reset form
|
||
const ci = document.getElementById('custom-amt');
|
||
if (ci) ci.value = '';
|
||
document.getElementById('custom-amt-equiv').style.display = 'none';
|
||
document.getElementById('order-summary').style.display = 'none';
|
||
|
||
if (d.manual) {
|
||
// Manual payment (Venmo, Zelle, Cash App, Chime) — pending admin approval
|
||
const methodLabels = { venmo:'Venmo', chime:'Chime', cashapp:'Cash App', zelle:'Zelle' };
|
||
showPayResult('manual', {
|
||
tokens: S.pkg.tokens,
|
||
amount: (S.pkg.price).toFixed(2),
|
||
method: (CFG.payLabels && CFG.payLabels[S.method]) || methodLabels[S.method] || S.method,
|
||
purchase_id: d.purchase_id,
|
||
});
|
||
} else {
|
||
// Card payment — tokens credited immediately
|
||
const newBalance = d.new_balance !== undefined ? parseFloat(d.new_balance) : parseFloat(S.user?.tokens || 0);
|
||
showPayResult('success', {
|
||
tokens_added: d.tokens_added || S.pkg.tokens,
|
||
new_balance: newBalance,
|
||
amount: (S.pkg.price).toFixed(2),
|
||
platform: platformName,
|
||
alias: gameAlias,
|
||
card: d.card_brand ? d.card_brand + ' ····' + d.card_last4 : null,
|
||
});
|
||
}
|
||
} else {
|
||
showPayResult('error', { error: d.error || 'Payment failed.' });
|
||
}
|
||
} catch {
|
||
hideProcessingOverlay();
|
||
showAlert(al, 'Network error. Please try again.', 'error');
|
||
}
|
||
|
||
btn.disabled = false;
|
||
}
|
||
|
||
// ─── PROFILE TABS ─────────────────────────────────────────
|
||
function switchProfileTab(name, btn) {
|
||
document.querySelectorAll('.profile-tab').forEach(b => b.classList.remove('active'));
|
||
document.querySelectorAll('.profile-tab-panel').forEach(p => p.classList.remove('active'));
|
||
btn?.classList.add('active');
|
||
document.getElementById('ptab-'+name)?.classList.add('active');
|
||
if (name === 'billing') loadBillingProfile();
|
||
if (name === 'account') populateAccount();
|
||
if (name === 'games') loadGameAliases();
|
||
if (name === 'payout') { loadPayoutMethods(); loadCashoutMethodTypes(); }
|
||
if (name === 'activity') loadActivityDashboard();
|
||
if (name === 'referrals') loadReferralDashboard();
|
||
if (name === 'logins') { loadPlatformLogins(); populatePlatformSelect('req-platform-slug'); }
|
||
if (name === 'logins') loadPlatformLogins();
|
||
}
|
||
|
||
// ─── BROADCASTS (PLAYER) ───────────────────────────────────
|
||
async function loadBroadcasts_player() {
|
||
const el = document.getElementById('broadcasts-list');
|
||
const d = await api('/api/broadcast.php?action=list');
|
||
if (!d.success || !d.broadcasts.length) {
|
||
el.innerHTML = '<div style="text-align:center;padding:40px;color:var(--text2)"><div style="font-size:40px;margin-bottom:12px">📢</div><div style="font-size:14px">No announcements yet.</div><div style="font-size:14px;margin-top:4px">Check back later for news and updates.</div></div>';
|
||
return;
|
||
}
|
||
el.innerHTML = d.broadcasts.map(b => {
|
||
const unread = parseInt(b.is_read) === 0;
|
||
return `<div class="bc-card${unread?' unread':''}" id="bc-card-${b.id}">
|
||
<div class="bc-header">
|
||
<div class="bc-avatar">📢</div>
|
||
<div style="flex:1;min-width:0">
|
||
<div style="display:flex;align-items:center;gap:8px">
|
||
<div class="bc-subject">${escHtml(b.subject)}</div>
|
||
${unread?'<span style="font-size:15px;font-weight:700;color:var(--gold);background:rgba(240,192,64,.15);border:1px solid rgba(240,192,64,.3);border-radius:4px;padding:2px 6px">NEW</span>':''}
|
||
</div>
|
||
<div class="bc-meta">From <strong>${escHtml(b.sender_name)}</strong> · ${(b.sent_at||'').substring(0,16)}</div>
|
||
</div>
|
||
</div>
|
||
<div class="bc-body">${escHtml(b.message)}</div>
|
||
<div class="bc-footer">
|
||
<button onclick="toggleBcReplies(${b.id})" style="background:none;border:none;color:var(--cyan);font-size:14px;font-weight:700;cursor:pointer;padding:0">
|
||
💬 ${b.reply_count} repl${parseInt(b.reply_count)!==1?'ies':'y'}
|
||
</button>
|
||
<span style="color:var(--text2);font-size:15px">👁 ${b.read_count} read</span>
|
||
</div>
|
||
<div class="bc-reply-wrap" id="bc-replies-${b.id}">
|
||
<div class="bc-replies-list" id="bc-rlist-${b.id}"></div>
|
||
<div style="display:flex;flex-direction:column;gap:8px">
|
||
<textarea id="bc-rinput-${b.id}" rows="3" placeholder="Write a reply..." style="width:100%;box-sizing:border-box;background:var(--bg3);border:1px solid var(--border);border-radius:10px;padding:12px 14px;color:var(--text);font-family:'Rajdhani',sans-serif;font-size:14px;resize:none;outline:none;transition:border-color .15s" onfocus="this.style.borderColor='var(--cyan)'" onblur="this.style.borderColor=''" onkeydown="if(event.key==='Enter'&&event.ctrlKey){event.preventDefault();playerBcReply(${b.id});}"></textarea>
|
||
<div style="display:flex;justify-content:space-between;align-items:center">
|
||
<span style="font-size:15px;color:var(--text2)">Ctrl+Enter to send</span>
|
||
<button class="btn btn-gold" onclick="playerBcReply(${b.id})" style="padding:10px 20px;font-size:15px">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="14" height="14" style="margin-right:5px"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>SEND REPLY
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
}).join('');
|
||
// Mark all as read
|
||
d.broadcasts.forEach(b => {
|
||
if (!parseInt(b.is_read)) {
|
||
api('/api/broadcast.php?action=mark_read',{broadcast_id:b.id}).catch(()=>{});
|
||
}
|
||
});
|
||
pollBroadcastBadge();
|
||
}
|
||
|
||
async function toggleBcReplies(id) {
|
||
const wrap = document.getElementById('bc-replies-'+id);
|
||
const isOpen = wrap.classList.contains('open');
|
||
wrap.classList.toggle('open', !isOpen);
|
||
if (!isOpen) await loadBcReplies_player(id);
|
||
}
|
||
|
||
async function loadBcReplies_player(id) {
|
||
const el = document.getElementById('bc-rlist-'+id);
|
||
el.innerHTML = '<div style="color:var(--text2);font-size:14px;padding:8px">Loading...</div>';
|
||
const d = await api('/api/broadcast.php?action=replies&broadcast_id='+id);
|
||
if (!d.success||!d.replies.length) { el.innerHTML='<div style="color:var(--text2);font-size:14px;padding:8px;text-align:center">No replies yet — be the first!</div>'; return; }
|
||
el.innerHTML = d.replies.map(r => {
|
||
const isAdm = parseInt(r.is_admin);
|
||
return `<div class="bc-reply-row">
|
||
<div style="width:28px;height:28px;border-radius:50%;background:${isAdm?'linear-gradient(135deg,#f0c040,#d4a017)':'linear-gradient(135deg,#7b2fbe,#457b9d)'};display:flex;align-items:center;justify-content:center;font-family:'Exo 2',sans-serif;font-weight:700;font-size:14px;color:#fff;flex-shrink:0">${(r.username||'?').charAt(0).toUpperCase()}</div>
|
||
<div class="bc-reply-bubble">
|
||
<div class="bc-reply-who${isAdm?' admin':''}">${escHtml(r.username)}${isAdm?' 🔑':''} <span style="font-weight:400;color:var(--text2)">· ${(r.created_at||'').substring(11,16)}</span></div>
|
||
<div class="bc-reply-msg">${escHtml(r.message)}</div>
|
||
</div>
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
|
||
async function playerBcReply(id) {
|
||
const input = document.getElementById('bc-rinput-'+id);
|
||
const msg = input.value.trim();
|
||
if (!msg) return;
|
||
input.value = '';
|
||
input.style.borderColor = '';
|
||
const d = await api('/api/broadcast.php?action=reply',{broadcast_id:id,message:msg});
|
||
if (d.success) {
|
||
await loadBcReplies_player(id);
|
||
// Update reply count
|
||
const card = document.getElementById('bc-card-'+id);
|
||
if (card) {
|
||
const btn = card.querySelector('.bc-footer button');
|
||
if (btn) {
|
||
const current = parseInt(btn.textContent.match(/\d+/)?.[0]||0) + 1;
|
||
btn.textContent = `💬 ${current} repl${current!==1?'ies':'y'}`;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
async function pollBroadcastBadge() {
|
||
try {
|
||
const d = await api('/api/broadcast.php?action=unread_count');
|
||
const badge = document.getElementById('broadcast-nav-badge');
|
||
if (badge) badge.style.display = d.count > 0 ? 'block' : 'none';
|
||
} catch(e) {}
|
||
}
|
||
const PAYOUT_ICONS = { venmo:'💙', cashapp:'💚', zelle:'💜', chime:'🟢', bank:'🏦', other:'💰' };
|
||
const PAYOUT_LABELS = { venmo:'Venmo', cashapp:'Cash App', zelle:'Zelle', chime:'Chime', bank:'Bank Transfer', other:'Other' };
|
||
let payoutMethods = [];
|
||
|
||
function prefillCashoutAlias(platformId) {
|
||
const input = document.getElementById('cashout-alias');
|
||
if (!input) return;
|
||
input.value = (platformId && savedAliases[platformId]) ? savedAliases[platformId] : '';
|
||
}
|
||
|
||
function saveCashoutAlias() {
|
||
const platformId = document.getElementById('cashout-platform')?.value;
|
||
const alias = document.getElementById('cashout-alias')?.value.trim();
|
||
if (!platformId || !alias || !S.user) return;
|
||
savedAliases[platformId] = alias;
|
||
api('/api/game_aliases.php?action=save', { platform_slug: platformId, alias }).catch(() => {});
|
||
}
|
||
|
||
async function loadPayoutMethods() {
|
||
const el = document.getElementById('payout-methods-list');
|
||
const d = await api('/api/payout_methods.php?action=list');
|
||
payoutMethods = d.success ? (d.methods || []) : [];
|
||
renderPayoutMethodsList(el, payoutMethods);
|
||
}
|
||
|
||
async function loadCashoutPayoutMethods() {
|
||
const el = document.getElementById('cashout-payout-list');
|
||
const noEl = document.getElementById('cashout-no-payout');
|
||
const d = await api('/api/payout_methods.php?action=list');
|
||
payoutMethods = d.success ? (d.methods || []) : [];
|
||
if (!payoutMethods.length) {
|
||
if (el) el.innerHTML = '';
|
||
if (noEl) noEl.style.display = 'block';
|
||
return;
|
||
}
|
||
if (noEl) noEl.style.display = 'none';
|
||
if (el) {
|
||
el.innerHTML = payoutMethods.map(m => `
|
||
<label style="display:flex;align-items:center;gap:10px;background:${parseInt(m.is_default)?'rgba(0,229,255,.07)':'var(--bg3)'};border:1px solid ${parseInt(m.is_default)?'rgba(0,229,255,.25)':'var(--border)'};border-radius:8px;padding:10px 12px;margin-bottom:6px;cursor:pointer">
|
||
<input type="radio" name="cashout-payout" value="${m.id}" ${parseInt(m.is_default)?'checked':''} style="accent-color:var(--cyan)">
|
||
<span style="font-size:18px">${PAYOUT_ICONS[m.method_type]||'💰'}</span>
|
||
<div style="flex:1;min-width:0">
|
||
<div style="font-weight:700;font-size:15px">${escHtml(m.label)}</div>
|
||
<div style="font-size:14px;color:var(--text2)">${escHtml(m.account_handle)}</div>
|
||
</div>
|
||
${parseInt(m.is_default)?'<span style="font-size:14px;font-weight:700;color:var(--cyan);border:1px solid rgba(0,229,255,.2);border-radius:4px;padding:2px 7px">DEFAULT</span>':''}
|
||
</label>`).join('');
|
||
}
|
||
}
|
||
|
||
function renderPayoutMethodsList(el) {
|
||
if (!el) return;
|
||
if (!payoutMethods.length) {
|
||
el.innerHTML = '<div style="color:var(--text2);font-size:15px;text-align:center;padding:16px">No payout methods saved yet.</div>';
|
||
return;
|
||
}
|
||
el.innerHTML = payoutMethods.map(m => `
|
||
<div style="display:flex;align-items:center;gap:10px;background:${parseInt(m.is_default)?'rgba(0,229,255,.07)':'var(--bg3)'};border:1px solid ${parseInt(m.is_default)?'rgba(0,229,255,.25)':'var(--border)'};border-radius:10px;padding:12px 14px;margin-bottom:8px">
|
||
<span style="font-size:22px">${PAYOUT_ICONS[m.method_type]||'💰'}</span>
|
||
<div style="flex:1;min-width:0">
|
||
<div style="font-weight:700;font-size:14px">${escHtml(m.label)}
|
||
${parseInt(m.is_default)?'<span style="font-size:14px;font-weight:700;color:var(--cyan);border:1px solid rgba(0,229,255,.2);border-radius:4px;padding:2px 7px;margin-left:6px">DEFAULT</span>':''}
|
||
</div>
|
||
<div style="font-size:14px;color:var(--text2)">${PAYOUT_LABELS[m.method_type]||m.method_type} · ${escHtml(m.account_handle)}</div>
|
||
</div>
|
||
<div style="display:flex;gap:6px;flex-shrink:0">
|
||
${!parseInt(m.is_default)?`<button onclick="setDefaultPayout(${m.id})" style="background:rgba(0,229,255,.08);border:1px solid rgba(0,229,255,.2);color:var(--cyan);border-radius:6px;padding:5px 10px;font-size:15px;font-weight:700;cursor:pointer;border:none">Set Default</button>`:''}
|
||
<button onclick="deletePayoutMethod(${m.id})" style="background:rgba(255,68,68,.08);border:1px solid rgba(255,68,68,.2);color:var(--red);border-radius:6px;padding:5px 10px;font-size:15px;font-weight:700;cursor:pointer">✕</button>
|
||
</div>
|
||
</div>`).join('');
|
||
}
|
||
|
||
async function addPayoutMethod() {
|
||
const al = document.getElementById('payout-add-alert');
|
||
const type = document.getElementById('payout-type-new').value;
|
||
const label = document.getElementById('payout-label-new').value.trim();
|
||
const handle = document.getElementById('payout-handle-new').value.trim();
|
||
if (!label || !handle) { showAlert(al,'Please fill in all fields.','error'); return; }
|
||
const d = await api('/api/payout_methods.php?action=add', { method_type:type, label, account_handle:handle, is_default: payoutMethods.length===0 ? 1 : 0 });
|
||
if (d.success) {
|
||
document.getElementById('payout-label-new').value = '';
|
||
document.getElementById('payout-handle-new').value = '';
|
||
showAlert(al,'✓ Payout method saved!','success');
|
||
setTimeout(() => { al.className='alert'; al.textContent=''; }, 3000);
|
||
loadPayoutMethods();
|
||
} else showAlert(al, d.error||'Error saving','error');
|
||
}
|
||
|
||
async function setDefaultPayout(id) {
|
||
const d = await api('/api/payout_methods.php?action=set_default', { id });
|
||
if (d.success) { loadPayoutMethods(); loadCashoutPayoutMethods(); }
|
||
}
|
||
|
||
async function deletePayoutMethod(id) {
|
||
if (!confirm('Remove this payout method?')) return;
|
||
const d = await api('/api/payout_methods.php?action=delete', { id });
|
||
if (d.success) { loadPayoutMethods(); loadCashoutPayoutMethods(); }
|
||
}
|
||
|
||
function getSelectedPayoutId() {
|
||
const sel = document.querySelector('input[name="cashout-payout"]:checked');
|
||
return sel ? parseInt(sel.value) : null;
|
||
}
|
||
|
||
|
||
// ─── TOKEN COIN SVG ────────────────────────────────────────
|
||
function tokenCoin(size) {
|
||
size = size || 22;
|
||
const s = size, r = s/2, ri = r*0.72, sh = r*0.14;
|
||
const id = 'tcg' + Math.random().toString(36).slice(2,6);
|
||
return `<svg width="${s}" height="${s+Math.round(sh*1.5)}" viewBox="0 0 ${s} ${s+Math.round(sh*1.5)}" style="display:inline-block;vertical-align:middle;flex-shrink:0" aria-label="token">
|
||
<defs>
|
||
<radialGradient id="cf${id}" cx="42%" cy="38%" r="58%">
|
||
<stop offset="0%" stop-color="#ffe87a"/>
|
||
<stop offset="50%" stop-color="#f0c040"/>
|
||
<stop offset="100%" stop-color="#c8900a"/>
|
||
</radialGradient>
|
||
<radialGradient id="ce${id}" cx="50%" cy="50%" r="50%">
|
||
<stop offset="65%" stop-color="#b07a18"/>
|
||
<stop offset="100%" stop-color="#7a5008"/>
|
||
</radialGradient>
|
||
</defs>
|
||
<ellipse cx="${r}" cy="${r+sh*1.5}" rx="${ri*0.7}" ry="${sh*0.6}" fill="rgba(0,0,0,0.18)"/>
|
||
<circle cx="${r}" cy="${r+sh}" r="${ri}" fill="url(#ce${id})"/>
|
||
<circle cx="${r}" cy="${r}" r="${ri}" fill="url(#cf${id})"/>
|
||
<circle cx="${r}" cy="${r}" r="${ri}" fill="none" stroke="#ffe87a" stroke-width="${s*0.04}" opacity="0.55"/>
|
||
<circle cx="${r}" cy="${r}" r="${ri*0.73}" fill="none" stroke="#c8900a" stroke-width="${s*0.025}" opacity="0.4"/>
|
||
<text x="${r}" y="${r+s*0.1}" font-family="'Exo 2',sans-serif" font-weight="900" font-size="${s*0.42}px" fill="#7a5008" text-anchor="middle" opacity="0.9">T</text>
|
||
<ellipse cx="${r*0.6}" cy="${r*0.55}" rx="${ri*0.28}" ry="${ri*0.15}" fill="rgba(255,255,220,0.38)" transform="rotate(-28 ${r*0.6} ${r*0.55})"/>
|
||
</svg>`;
|
||
}
|
||
|
||
// Replace all 🪙 emoji in text nodes with coin SVG
|
||
function coinify(el) {
|
||
if (!el) return;
|
||
el.innerHTML = el.innerHTML.replace(/🪙/g, tokenCoin(20));
|
||
}
|
||
|
||
// ─── ONBOARDING MODAL ─────────────────────────────────────
|
||
let obSelectedPlatforms = new Set();
|
||
|
||
function showOnboardingIfNeeded() {
|
||
if (!S.user) return;
|
||
// Show once: only for verified players who haven't completed onboarding
|
||
if (S.user.email_verified && !parseInt(S.user.onboarding_done||0) && !S.user.is_admin) {
|
||
setTimeout(() => {
|
||
document.getElementById('onboarding-overlay').style.display = 'block';
|
||
obLoadPlatforms();
|
||
}, 800);
|
||
}
|
||
}
|
||
|
||
async function obLoadPlatforms() {
|
||
const d = await api('/api/platforms.php?action=list').catch(()=>({success:false}));
|
||
if (!d.success) return;
|
||
// Also load existing requests to skip already-requested ones
|
||
const existing = await api('/api/platform_accounts.php?action=my_accounts').catch(()=>({accounts:[]}));
|
||
const existingSlugs = new Set((existing.accounts||[]).filter(a=>['pending','approved'].includes(a.status)).map(a=>a.platform_slug));
|
||
const el = document.getElementById('ob-platforms-list');
|
||
if (!el || !d.platforms.length) return;
|
||
el.innerHTML = d.platforms.filter(p => !existingSlugs.has(p.id)).map(p => `
|
||
<label style="display:flex;align-items:center;gap:10px;padding:10px 12px;background:var(--bg3);border:1px solid var(--border);border-radius:8px;margin-bottom:6px;cursor:pointer" id="ob-plat-${p.id}">
|
||
<input type="checkbox" value="${p.id}" data-name="${escHtml(p.name)}"
|
||
onchange="obTogglePlatform('${p.id}',this.checked,'ob-plat-${p.id}')"
|
||
style="accent-color:var(--gold);width:16px;height:16px;flex-shrink:0">
|
||
<div style="width:10px;height:10px;border-radius:50%;background:${p.color};flex-shrink:0"></div>
|
||
<span style="font-family:'Exo 2',sans-serif;font-weight:700;font-size:14px">${escHtml(p.name)}</span>
|
||
</label>`).join('') || '<div style="color:var(--text2);text-align:center;padding:12px;font-size:15px">No platforms available to request.</div>';
|
||
}
|
||
|
||
function obTogglePlatform(id, checked, labelId) {
|
||
if (checked) obSelectedPlatforms.add(id);
|
||
else obSelectedPlatforms.delete(id);
|
||
const lbl = document.getElementById(labelId);
|
||
if (lbl) lbl.style.borderColor = checked ? 'rgba(240,192,64,.4)' : 'var(--border)';
|
||
}
|
||
|
||
function obShowStep2() {
|
||
document.getElementById('ob-step1').style.display = 'none';
|
||
document.getElementById('ob-step2').style.display = 'block';
|
||
obLoadPlatforms();
|
||
}
|
||
|
||
function obRequestNew() {
|
||
obShowStep2();
|
||
}
|
||
|
||
async function obSubmitRequests() {
|
||
const al = document.getElementById('ob-alert');
|
||
if (obSelectedPlatforms.size === 0) { showAlert(al,'Select at least one platform.','error'); return; }
|
||
let ok = 0;
|
||
for (const slug of obSelectedPlatforms) {
|
||
const name = document.querySelector(`input[value="${slug}"]`)?.dataset.name || slug;
|
||
const d = await api('/api/platform_accounts.php?action=request', { platform_slug: slug, platform_name: name });
|
||
if (d.success) ok++;
|
||
}
|
||
if (ok > 0) {
|
||
showAlert(al, `✓ ${ok} request${ok>1?'s':''} submitted! Our team will create your accounts shortly.`, 'success');
|
||
await api('/api/platform_accounts.php?action=skip_onboarding', {});
|
||
setTimeout(obDismiss, 2500);
|
||
} else showAlert(al, 'Something went wrong. Try again.', 'error');
|
||
}
|
||
|
||
async function obDismiss() {
|
||
document.getElementById('onboarding-overlay').style.display = 'none';
|
||
await api('/api/platform_accounts.php?action=skip_onboarding', {}).catch(()=>{});
|
||
if (S.user) S.user.onboarding_done = 1;
|
||
}
|
||
|
||
function obSkip() { obDismiss(); }
|
||
|
||
// ─── PLATFORM LOGINS (Profile tab) ────────────────────────
|
||
async function loadPlatformLogins() {
|
||
const el = document.getElementById('platform-logins-list');
|
||
if (!el) return;
|
||
el.innerHTML = '<div style="color:var(--text2);font-size:15px;text-align:center;padding:16px">Loading...</div>';
|
||
const d = await api('/api/platform_accounts.php?action=my_accounts');
|
||
if (!d.success) { el.innerHTML = '<div style="color:var(--text2);font-size:15px;text-align:center;padding:16px">Unable to load.</div>'; return; }
|
||
|
||
// Populate request dropdown - exclude already requested/approved
|
||
const existing = new Set(d.accounts.filter(a=>['pending','approved'].includes(a.status)).map(a=>a.platform_slug));
|
||
const sel = document.getElementById('req-platform-slug');
|
||
if (sel && CFG.platforms.length) {
|
||
sel.innerHTML = '<option value="">— Select Platform —</option>' +
|
||
CFG.platforms.filter(p => !existing.has(p.id)).map(p =>
|
||
`<option value="${p.id}" data-name="${escHtml(p.name)}">${escHtml(p.name)}</option>`
|
||
).join('');
|
||
}
|
||
|
||
if (!d.accounts.length) {
|
||
el.innerHTML = '<div style="color:var(--text2);font-size:15px;text-align:center;padding:20px">No platform account requests yet.<br><span style="font-size:15px">Use the form below to request one.</span></div>';
|
||
return;
|
||
}
|
||
|
||
const STATUS_INFO = {
|
||
pending: { icon:'⏳', label:'Awaiting Approval', cls:'ac-pending', msg:'Our team will review and create your account shortly.' },
|
||
approved: { icon:'✅', label:'Account Ready', cls:'ac-completed', msg:'' },
|
||
denied: { icon:'❌', label:'Request Denied', cls:'ac-failed', msg:'Contact support for more info.' },
|
||
deleted: { icon:'🗑', label:'Deleted', cls:'ac-failed', msg:'' },
|
||
};
|
||
|
||
el.innerHTML = d.accounts.map(a => {
|
||
const si = STATUS_INFO[a.status] || STATUS_INFO.pending;
|
||
const canDelete = a.status === 'pending';
|
||
const showCreds = a.status === 'approved' && a.provided_username;
|
||
return `<div class="activity-card" style="margin-bottom:10px;flex-direction:column;gap:0">
|
||
<div style="display:flex;gap:12px;align-items:flex-start;width:100%">
|
||
<div class="activity-icon" style="background:rgba(240,192,64,.1)">${si.icon}</div>
|
||
<div style="flex:1;min-width:0">
|
||
<div class="activity-title">${escHtml(a.display_name||a.platform_slug)}</div>
|
||
<span class="activity-status ${si.cls}">${si.label}</span>
|
||
${a.admin_note ? `<div style="font-size:15px;color:var(--gold);margin-top:4px">📝 ${escHtml(a.admin_note)}</div>` : ''}
|
||
${si.msg ? `<div style="font-size:15px;color:var(--text2);margin-top:4px">${si.msg}</div>` : ''}
|
||
</div>
|
||
${canDelete ? `<button onclick="deletePlatformRequest(${a.id})" style="background:none;border:none;color:var(--red);cursor:pointer;font-size:18px;padding:0;flex-shrink:0">✕</button>` : ''}
|
||
</div>
|
||
${showCreds ? `
|
||
<div style="margin-top:12px;background:rgba(0,229,255,.06);border:1px solid rgba(0,229,255,.2);border-radius:8px;padding:12px 14px;width:100%">
|
||
<div style="font-size:15px;font-weight:700;color:var(--cyan);margin-bottom:8px;letter-spacing:.5px">🔑 YOUR LOGIN CREDENTIALS</div>
|
||
<div style="display:grid;grid-template-columns:auto 1fr;gap:6px 12px;font-size:15px">
|
||
<span style="color:var(--text2)">Username:</span>
|
||
<span style="font-weight:700;color:var(--text);font-family:monospace">${escHtml(a.provided_username)}</span>
|
||
<span style="color:var(--text2)">Password:</span>
|
||
<span style="font-weight:700;color:var(--gold);font-family:monospace;cursor:pointer" onclick="this.style.filter='none'" style="filter:blur(5px)" title="Tap to reveal">${escHtml(a.provided_password)}</span>
|
||
</div>
|
||
<div style="font-size:14px;color:var(--text2);margin-top:8px">⚠️ Keep your credentials secure. Tap password to reveal.</div>
|
||
${a.player_url ? `<a href="${escHtml(a.player_url)}" target="_blank" class="btn btn-gold" style="display:block;text-align:center;margin-top:10px;padding:8px;font-size:15px;text-decoration:none">▶ Play ${escHtml(a.display_name)}</a>` : ''}
|
||
</div>` : ''}
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
|
||
async function requestPlatformAccount() {
|
||
const sel = document.getElementById('req-platform-slug');
|
||
const al = document.getElementById('req-platform-alert');
|
||
const slug = sel?.value;
|
||
const name = sel?.options[sel.selectedIndex]?.dataset.name || slug;
|
||
if (!slug) { showAlert(al,'Select a platform.','error'); return; }
|
||
const d = await api('/api/platform_accounts.php?action=request', { platform_slug: slug, platform_name: name });
|
||
if (d.success) {
|
||
showAlert(al,'✓ Request submitted! We\'ll create your account shortly.','success');
|
||
setTimeout(() => { al.className='alert'; al.textContent=''; }, 3500);
|
||
loadPlatformLogins();
|
||
} else showAlert(al, d.error||'Request failed','error');
|
||
}
|
||
|
||
async function deletePlatformRequest(id) {
|
||
if (!confirm('Cancel this account request?')) return;
|
||
const d = await api('/api/platform_accounts.php?action=delete_request', { id });
|
||
if (d.success) loadPlatformLogins();
|
||
}
|
||
let cashoutMethodTypes = [];
|
||
|
||
// ─── REFERRAL SYSTEM ──────────────────────────────────────
|
||
let _refData = null;
|
||
|
||
// Capture ?ref= code from URL on page load
|
||
const _urlRef = new URLSearchParams(window.location.search).get('ref') || '';
|
||
|
||
async function loadReferralDashboard() {
|
||
const d = await api('/api/referrals.php?action=status');
|
||
if (!d.success) {
|
||
const inp = document.getElementById('referral-link-input');
|
||
if (inp) inp.placeholder = d.error || 'Please log in to see your referral link';
|
||
return;
|
||
}
|
||
_refData = d;
|
||
|
||
// Referral link — build URL defensively
|
||
const inp = document.getElementById('referral-link-input');
|
||
if (inp) {
|
||
const code = d.referral_code || '';
|
||
const url = code
|
||
? (d.referral_url || ('https://tomtomgames.com/?ref=' + code))
|
||
: '';
|
||
inp.value = url;
|
||
inp.placeholder = code ? '' : 'Generating your referral link...';
|
||
// If no code yet, retry in 2 seconds
|
||
if (!code) setTimeout(loadReferralDashboard, 2000);
|
||
}
|
||
|
||
// Stats
|
||
setText('ref-count', d.verified_count || 0);
|
||
setText('ref-earned', d.total_earned || 0);
|
||
setText('ref-tier', d.current_tier?.name || '—');
|
||
|
||
// Next tier progress
|
||
if (d.next_tier) {
|
||
const nextEl = document.getElementById('ref-next-tier');
|
||
const labelEl = document.getElementById('ref-next-label');
|
||
const barEl = document.getElementById('ref-progress-bar');
|
||
const curr = d.current_tier?.min_referrals || 0;
|
||
const next = d.next_tier.min_referrals;
|
||
const count = d.verified_count || 0;
|
||
const pct = Math.min(100, Math.round(((count - curr) / (next - curr)) * 100));
|
||
if (nextEl) nextEl.style.display = 'block';
|
||
if (labelEl) labelEl.textContent = `${count}/${next} referrals to ${d.next_tier.name} (+${d.next_tier.tokens_per_ref} tokens/ref)`;
|
||
if (barEl) barEl.style.width = pct + '%';
|
||
}
|
||
|
||
// Tiers list
|
||
const tiersEl = document.getElementById('ref-tiers-list');
|
||
if (tiersEl) {
|
||
const tiers = await api('/api/referrals.php?action=tiers');
|
||
if (tiers.success) {
|
||
tiersEl.innerHTML = (tiers.tiers || []).map(t => `
|
||
<div style="display:flex;align-items:center;gap:10px;padding:10px 0;border-bottom:1px solid var(--border)">
|
||
<div style="flex:1">
|
||
<div style="font-family:'Exo 2',sans-serif;font-weight:700;font-size:15px;color:${d.current_tier?.id==t.id?'var(--gold)':'var(--text)'}">${escHtml(t.name)} ${d.current_tier?.id==t.id?'★':''}</div>
|
||
<div style="font-size:15px;color:var(--text2)">${escHtml(t.description||'')}</div>
|
||
</div>
|
||
<div style="text-align:right;font-size:14px">
|
||
<div style="font-weight:700;color:var(--green)">${t.tokens_per_ref} tokens/ref</div>
|
||
${t.bonus_tokens>0?'<div style="color:var(--gold);font-size:15px">+'+t.bonus_tokens+' bonus</div>':''}
|
||
<div style="color:var(--text2);font-size:14px">${t.min_referrals} referrals</div>
|
||
</div>
|
||
</div>`).join('') || '<div style="color:var(--text2);font-size:15px">No tiers configured.</div>';
|
||
}
|
||
}
|
||
|
||
// My referrals list
|
||
const refListEl = document.getElementById('ref-list');
|
||
if (refListEl) {
|
||
const refs = d.referrals || [];
|
||
if (!refs.length) {
|
||
refListEl.innerHTML = '<div style="color:var(--text2);font-size:15px;text-align:center;padding:12px">No referrals yet. Share your link!</div>';
|
||
} else {
|
||
refListEl.innerHTML = refs.map(r => `
|
||
<div style="display:flex;align-items:center;gap:10px;padding:9px 0;border-bottom:1px solid var(--border)">
|
||
<div style="flex:1">
|
||
<div style="font-size:15px;font-weight:700">${escHtml(r.alias||r.username)}</div>
|
||
<div style="font-size:15px;color:var(--text2)">${(r.created_at||'').substring(0,10)}</div>
|
||
</div>
|
||
<div style="text-align:right">
|
||
<div class="ref-status-${r.status}" style="font-size:15px;font-weight:700;text-transform:uppercase">${r.status}</div>
|
||
${r.tokens_awarded>0?'<div style="color:var(--green);font-size:15px">+'+r.tokens_awarded+' tokens</div>':''}
|
||
</div>
|
||
</div>`).join('');
|
||
}
|
||
}
|
||
|
||
// Social shares
|
||
const sharesList = document.getElementById('ref-shares-list');
|
||
if (sharesList) {
|
||
const shares = d.social_shares || [];
|
||
if (!shares.length) {
|
||
sharesList.innerHTML = '';
|
||
} else {
|
||
sharesList.innerHTML = '<div style="font-size:13px;font-weight:700;color:var(--text2);margin-bottom:6px;text-transform:uppercase;letter-spacing:.5px">Your Submitted Shares</div>' +
|
||
shares.map(s => {
|
||
const autoTag = parseInt(s.auto_verified) ? ' <span style="font-size:12px;background:rgba(0,255,128,.1);border:1px solid rgba(0,255,128,.2);color:var(--green);border-radius:4px;padding:1px 5px">⚡ auto</span>' : '';
|
||
const urlFrag = s.share_url
|
||
? `<div style="margin-top:3px"><a href="${escHtml(s.share_url)}" target="_blank" rel="noopener" style="color:var(--cyan);font-size:12px;word-break:break-all">${escHtml(s.share_url.length>60?s.share_url.substring(0,60)+'…':s.share_url)}</a></div>`
|
||
: '';
|
||
return `<div style="padding:8px 0;border-bottom:1px solid var(--border)">
|
||
<div style="font-size:14px"><strong>${escHtml(s.platform)}</strong> — <span class="ref-status-${s.status}">${s.status}</span>${autoTag}${s.status==='approved'?' <span style="color:var(--green)">+'+s.bonus_tokens+' tokens</span>':''}</div>
|
||
${urlFrag}
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
}
|
||
}
|
||
|
||
function copyReferralLink() {
|
||
const inp = document.getElementById('referral-link-input');
|
||
if (!inp) return;
|
||
inp.select();
|
||
navigator.clipboard?.writeText(inp.value).catch(() => {});
|
||
document.execCommand('copy');
|
||
const msg = document.getElementById('ref-copy-msg');
|
||
if (msg) { msg.style.display='block'; setTimeout(()=>msg.style.display='none', 2000); }
|
||
}
|
||
|
||
async function submitShareUrl() {
|
||
const al = document.getElementById('ref-share-alert');
|
||
const inp = document.getElementById('share-url-input');
|
||
const url = inp?.value.trim();
|
||
if (!url) { showAlert(al, 'Please paste the URL of your post.', 'error'); return; }
|
||
showAlert(al, '🔍 Checking your post…', 'info');
|
||
const d = await api('/api/referrals.php?action=submit_share', { url });
|
||
if (d.success) {
|
||
showAlert(al, d.message, d.auto_verified ? 'success' : 'info');
|
||
if (inp) inp.value = '';
|
||
loadReferralDashboard();
|
||
} else {
|
||
showAlert(al, d.error || 'Error submitting share.', 'error');
|
||
}
|
||
}
|
||
|
||
// Pass referral code during registration
|
||
function getUrlRefCode() { return _urlRef; }
|
||
|
||
// ─── PLATFORM LOGINS (unified) ───────────────────────────
|
||
async function loadPlatformLogins() {
|
||
const el = document.getElementById('platform-logins-list');
|
||
if (!el) return;
|
||
el.innerHTML = '<div style="text-align:center;padding:16px;color:var(--text2);font-size:15px"><span class="spinner" style="border-top-color:var(--gold)"></span></div>';
|
||
try {
|
||
const d = await api('/api/platform_accounts.php?action=list');
|
||
if (!d.success || !d.accounts.length) {
|
||
el.innerHTML = '<div style="text-align:center;padding:20px;color:var(--text2);font-size:15px">No platform account requests yet. Request one below!</div>';
|
||
return;
|
||
}
|
||
el.innerHTML = d.accounts.map(a => buildLoginCard(a)).join('');
|
||
} catch(e) {
|
||
el.innerHTML = '<div style="color:var(--red);text-align:center;padding:16px;font-size:15px">Unable to load. Try again.</div>';
|
||
}
|
||
}
|
||
|
||
function buildLoginCard(a) {
|
||
const statusLabel = { pending:'⏳ Request Pending', approved:'✅ Account Ready', denied:'❌ Request Denied', deleted:'🗑 Deleted' };
|
||
const statusCls = { pending:'ac-pending', approved:'ac-completed', denied:'ac-failed', deleted:'ac-failed' };
|
||
const note = a.admin_note ? `<div style="font-size:15px;color:var(--gold);margin-top:6px;padding:6px 8px;background:rgba(240,192,64,.07);border-radius:6px">📝 ${escHtml(a.admin_note)}</div>` : '';
|
||
|
||
let creds = '';
|
||
if (a.status === 'approved' && a.platform_username) {
|
||
const passHtml = a.platform_password
|
||
? `<div class="pa-cred-row">
|
||
<span class="pa-cred-label">Password</span>
|
||
<span class="pa-cred-val pa-cred-pass" onclick="this.classList.toggle('revealed')" title="Tap to reveal">${escHtml(a.platform_password)}</span>
|
||
</div>
|
||
<div style="font-size:14px;color:var(--text2);margin-top:3px">Tap password to reveal · Keep this safe!</div>`
|
||
: '';
|
||
creds = `<div class="pa-cred-box">
|
||
<div class="pa-cred-row"><span class="pa-cred-label">Username</span><span class="pa-cred-val">${escHtml(a.platform_username)}</span></div>
|
||
${passHtml}
|
||
</div>`;
|
||
}
|
||
|
||
const pendingMsg = a.status === 'pending'
|
||
? '<div style="font-size:15px;color:var(--text2);margin-top:8px;padding:8px;background:rgba(255,255,255,.03);border-radius:6px">Our team is setting up your account. Your login credentials will appear here once ready.</div>'
|
||
: '';
|
||
|
||
return `<div class="pa-card ${a.status}" style="margin-bottom:10px">
|
||
<div style="display:flex;align-items:center;justify-content:space-between;gap:10px">
|
||
<div>
|
||
<div style="font-family:'Exo 2',sans-serif;font-weight:700;font-size:15px">${escHtml(a.platform_name || a.platform_slug)}</div>
|
||
<div style="font-size:15px;color:var(--text2);margin-top:2px">${(a.requested_at||'').substring(0,10)}</div>
|
||
<span class="activity-status ${statusCls[a.status]||'ac-pending'}">${statusLabel[a.status]||a.status}</span>
|
||
</div>
|
||
${a.status === 'pending' ? '<div style="font-size:15px;color:var(--text2);text-align:right">Pending<br>review</div>' : ''}
|
||
</div>
|
||
${note}${creds}${pendingMsg}
|
||
</div>`;
|
||
}
|
||
|
||
async function requestPlatformLogin() {
|
||
const al = document.getElementById('req-platform-alert');
|
||
const slug = document.getElementById('req-platform-slug')?.value;
|
||
if (!slug) { showAlert(al, 'Please select a platform.', 'error'); return; }
|
||
const platName = CFG.platforms.find(p => p.id === slug)?.name || slug;
|
||
const d = await api('/api/platform_accounts.php?action=request', { platform_slug: slug });
|
||
if (d.success) {
|
||
showAlert(al, `✓ Request submitted for ${platName}! Check back here once approved.`, 'success');
|
||
setTimeout(() => { al.className='alert'; al.textContent=''; }, 4000);
|
||
document.getElementById('req-platform-slug').value = '';
|
||
loadPlatformLogins();
|
||
} else {
|
||
showAlert(al, d.error || 'Error submitting request.', 'error');
|
||
}
|
||
}
|
||
|
||
function populatePlatformSelect(selId) {
|
||
const sel = document.getElementById(selId);
|
||
if (!sel || !CFG.platforms.length) return;
|
||
sel.querySelectorAll('option:not([value=""])').forEach(o => o.remove());
|
||
CFG.platforms.forEach(p => {
|
||
const o = document.createElement('option');
|
||
o.value = p.id; o.textContent = p.name;
|
||
sel.appendChild(o);
|
||
});
|
||
}
|
||
|
||
// ─── ONBOARDING FLOW ──────────────────────────────────────
|
||
function showOnboarding() {
|
||
const modal = document.getElementById('onboarding-modal');
|
||
if (!modal) return;
|
||
populatePlatformSelect('onboarding-platform');
|
||
modal.style.display = 'flex';
|
||
}
|
||
function hideOnboarding() { const m=document.getElementById('onboarding-modal'); if(m)m.style.display='none'; }
|
||
function showOnboardingStep(n) {
|
||
[1,2,3].forEach(i => { const el=document.getElementById('onboarding-step-'+i); if(el) el.style.display=i===n?'block':'none'; });
|
||
}
|
||
function onboardingHasAccount() {
|
||
api('/api/platform_accounts.php?action=dismiss_onboarding',{}).catch(()=>{});
|
||
hideOnboarding();
|
||
}
|
||
function onboardingNoAccount() { showOnboardingStep(2); }
|
||
function onboardingRequestAnother() {
|
||
const al=document.getElementById('onboarding-alert'); if(al){al.className='alert';al.textContent='';}
|
||
document.getElementById('onboarding-platform').value='';
|
||
showOnboardingStep(2);
|
||
}
|
||
async function onboardingRequestAccount() {
|
||
const al=document.getElementById('onboarding-alert');
|
||
const slug=document.getElementById('onboarding-platform')?.value;
|
||
if (!slug) { showAlert(al,'Please select a platform.','error'); return; }
|
||
const platName=CFG.platforms.find(p=>p.id===slug)?.name||slug;
|
||
const d=await api('/api/platform_accounts.php?action=request',{platform_slug:slug});
|
||
if (d.success) {
|
||
const ct=document.getElementById('onboarding-confirm-text');
|
||
if (ct) ct.textContent=`Request for ${platName} submitted! Find your login details in Profile → Accounts once approved.`;
|
||
showOnboardingStep(3);
|
||
} else showAlert(al, d.error||'Error','error');
|
||
}
|
||
async function onboardingDone() {
|
||
api('/api/platform_accounts.php?action=dismiss_onboarding',{}).catch(()=>{});
|
||
hideOnboarding();
|
||
}
|
||
async function checkOnboarding() {
|
||
if (!S.user || S.user.is_admin) return;
|
||
try {
|
||
const d=await api('/api/platform_accounts.php?action=check_onboarding');
|
||
if (d.success && d.needs_onboarding) showOnboarding();
|
||
} catch(e){}
|
||
}
|
||
|
||
async function loadCashoutMethodTypes() {
|
||
try {
|
||
const d = await api('/api/cashout_method_types.php?action=list');
|
||
if (!d.success) return;
|
||
cashoutMethodTypes = d.types || [];
|
||
// Populate the payout type selector
|
||
const sel = document.getElementById('payout-type-new');
|
||
if (sel) {
|
||
sel.innerHTML = cashoutMethodTypes.map(t =>
|
||
`<option value="${escHtml(t.slug)}">${escHtml(t.icon)} ${escHtml(t.label)}</option>`
|
||
).join('');
|
||
}
|
||
// Update PAYOUT_ICONS and PAYOUT_LABELS from live data
|
||
cashoutMethodTypes.forEach(t => {
|
||
PAYOUT_ICONS[t.slug] = t.icon;
|
||
PAYOUT_LABELS[t.slug] = t.label;
|
||
});
|
||
} catch(e) { /* use defaults */ }
|
||
}
|
||
|
||
// Also load on app init so icons/labels are available everywhere
|
||
function populateAccount() {
|
||
const u = S.user;
|
||
if (!u) return;
|
||
document.getElementById('acct-username').textContent = u.username || '—';
|
||
document.getElementById('acct-alias').textContent = u.alias || '—';
|
||
document.getElementById('acct-email').textContent = u.email || 'Not set';
|
||
document.getElementById('acct-joined').textContent = (u.created_at||'').substring(0,10);
|
||
}
|
||
|
||
// ─── DYNAMIC PAYMENT METHODS ──────────────────────────────
|
||
function buildPaymentMethods() {
|
||
const methods = CFG.payMethods || [];
|
||
const container = document.getElementById('manual-payment-btns');
|
||
if (!container) return;
|
||
|
||
// Check if card is enabled from payMethods (or default to enabled if no card entry)
|
||
const cardSetting = methods.find(m => m.method_key === 'card');
|
||
const cardEnabled = cardSetting ? parseInt(cardSetting.is_enabled) : 1;
|
||
const manualMethods = methods.filter(m => m.method_key !== 'card' && parseInt(m.is_enabled));
|
||
|
||
let html = '';
|
||
|
||
// Card button — only show if enabled
|
||
if (cardEnabled) {
|
||
html += `<div class="pmt" onclick="selectPMT(this,'card')" title="Credit/Debit Card">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="22" height="22"><rect x="1" y="4" width="22" height="16" rx="2"/><line x1="1" y1="10" x2="23" y2="10"/></svg>
|
||
<span>Card</span>
|
||
</div>`;
|
||
}
|
||
|
||
const iconMap = { venmo:'💙', chime:'🟢', cashapp:'💚', zelle:'💜' };
|
||
manualMethods.forEach(m => {
|
||
html += `<div class="pmt" onclick="selectPMT(this,'${m.method_key}')" title="${escHtml(m.label)}">
|
||
<span style="font-size:20px">${iconMap[m.method_key]||'💰'}</span>
|
||
<span>${escHtml(m.label)}</span>
|
||
</div>`;
|
||
});
|
||
|
||
container.innerHTML = html;
|
||
|
||
// If card is disabled and current method is card, switch to first available
|
||
if (!cardEnabled && S.method === 'card') {
|
||
const firstManual = manualMethods[0];
|
||
if (firstManual) {
|
||
const firstBtn = container.querySelector('.pmt');
|
||
if (firstBtn) selectPMT(firstBtn, firstManual.method_key);
|
||
}
|
||
// Also hide the card section entirely
|
||
const cardSection = document.getElementById('section-card');
|
||
if (cardSection) cardSection.style.display = 'none';
|
||
const manSection = document.getElementById('section-manual');
|
||
if (manSection) manSection.style.display = 'block';
|
||
}
|
||
|
||
// If no methods at all, show message
|
||
if (!cardEnabled && !manualMethods.length) {
|
||
container.innerHTML = '<div style="font-size:15px;color:var(--red);padding:8px">No payment methods are currently available. Please contact support.</div>';
|
||
}
|
||
|
||
// Update CFG.pay handles from DB
|
||
methods.forEach(m => {
|
||
if (CFG.pay && m.handle) CFG.pay[m.method_key] = m.handle;
|
||
if (!CFG.payLabels) CFG.payLabels = {};
|
||
CFG.payLabels[m.method_key] = m.label;
|
||
if (m.instructions) {
|
||
if (!CFG.payInstructions) CFG.payInstructions = {};
|
||
CFG.payInstructions[m.method_key] = m.instructions;
|
||
}
|
||
});
|
||
}
|
||
|
||
// Save alias from buy page back to profile
|
||
async function saveAliasFromBuyPage() {
|
||
const platformId = document.getElementById('buy-platform')?.value;
|
||
const alias = document.getElementById('buy-alias')?.value.trim();
|
||
if (!platformId || !alias || !S.user) return;
|
||
savedAliases[platformId] = alias;
|
||
await api('/api/game_aliases.php?action=save', { platform_slug: platformId, alias });
|
||
}
|
||
let savedAliases = {}; // { platform_slug: alias }
|
||
|
||
async function loadGameAliases() {
|
||
const el = document.getElementById('game-alias-list') || document.getElementById('game-aliases-list');
|
||
try {
|
||
const [aliasRes, platRes] = await Promise.all([
|
||
api('/api/game_aliases.php?action=get'),
|
||
api('/api/platforms.php?action=list')
|
||
]);
|
||
if (aliasRes.success) savedAliases = aliasRes.aliases || {};
|
||
const platforms = platRes.success ? platRes.platforms : CFG.platforms;
|
||
renderGameAliasList(platforms);
|
||
} catch(e) {
|
||
if (el) el.innerHTML = '<div style="color:var(--text2);text-align:center;padding:16px;font-size:15px">Could not load games.</div>';
|
||
}
|
||
}
|
||
|
||
function renderGameAliasList(platforms) {
|
||
const el = document.getElementById('game-aliases-list');
|
||
if (!el) return;
|
||
if (!platforms || !platforms.length) {
|
||
el.innerHTML = '<div style="color:var(--text2);text-align:center;padding:16px;font-size:15px">No games configured yet.</div>';
|
||
return;
|
||
}
|
||
el.innerHTML = platforms.map(p => `
|
||
<div class="game-alias-card">
|
||
<div class="game-alias-dot" style="background:${p.color};box-shadow:0 0 8px ${p.color}"></div>
|
||
<div style="flex:1;min-width:0">
|
||
<div class="game-alias-name">${escHtml(p.name)}</div>
|
||
<input class="game-alias-input" id="alias-${p.id}"
|
||
type="text" maxlength="100"
|
||
placeholder="Your in-game username..."
|
||
value="${escHtml(savedAliases[p.id] || '')}"
|
||
onchange="saveOneAlias('${p.id}',this.value)">
|
||
</div>
|
||
<a href="${escHtml(p.url)}" target="_blank"
|
||
style="flex-shrink:0;width:36px;height:36px;border-radius:8px;background:rgba(255,255,255,.05);border:1px solid var(--border);display:flex;align-items:center;justify-content:center;text-decoration:none;font-size:16px"
|
||
title="Play ${escHtml(p.name)}">▶</a>
|
||
</div>`).join('');
|
||
}
|
||
|
||
async function saveOneAlias(slug, alias) {
|
||
savedAliases[slug] = alias.trim();
|
||
await api('/api/game_aliases.php?action=save', { platform_slug: slug, alias: alias.trim() });
|
||
}
|
||
|
||
async function saveAllAliases() {
|
||
const al = document.getElementById('game-aliases-alert');
|
||
const aliases = {};
|
||
(CFG.platforms || []).forEach(p => {
|
||
const el = document.getElementById('alias-' + p.id);
|
||
if (el) aliases[p.id] = el.value.trim();
|
||
});
|
||
// Also grab any inputs by the alias-* pattern
|
||
document.querySelectorAll('[id^="alias-"]').forEach(el => {
|
||
const slug = el.id.replace('alias-', '');
|
||
aliases[slug] = el.value.trim();
|
||
});
|
||
savedAliases = {...savedAliases, ...aliases};
|
||
const d = await api('/api/game_aliases.php?action=save_all', { aliases });
|
||
if (d.success) {
|
||
showAlert(al, '✓ All aliases saved!', 'success');
|
||
setTimeout(() => hideAlert(al), 3000);
|
||
} else {
|
||
showAlert(al, 'Save failed. Try again.', 'error');
|
||
}
|
||
}
|
||
|
||
// Auto-prefill alias on buy page when platform changes
|
||
function prefillBuyAlias() {
|
||
const platformId = document.getElementById('buy-platform')?.value;
|
||
const aliasInput = document.getElementById('buy-alias');
|
||
if (platformId && aliasInput && savedAliases[platformId]) {
|
||
aliasInput.value = savedAliases[platformId];
|
||
} else if (aliasInput && platformId && !savedAliases[platformId]) {
|
||
aliasInput.value = '';
|
||
}
|
||
}
|
||
|
||
function escHtml(s) { return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
||
async function loadBillingProfile() {
|
||
const d = await api('/api/billing.php?action=get');
|
||
if (d.success && d.billing) {
|
||
const b = d.billing;
|
||
// Show saved card chip if card on file
|
||
if (b.card_last4) {
|
||
document.getElementById('saved-card-brand').textContent = b.card_brand || 'CARD';
|
||
document.getElementById('saved-card-number').textContent = '···· ···· ···· ' + b.card_last4;
|
||
document.getElementById('saved-card-name').textContent = [b.first_name, b.last_name].filter(Boolean).join(' ') || '—';
|
||
const exp = (b.card_exp_month && b.card_exp_year) ? b.card_exp_month + '/' + b.card_exp_year.slice(-2) : '—';
|
||
document.getElementById('saved-card-exp').textContent = exp;
|
||
document.getElementById('saved-card-display').style.display = 'block';
|
||
} else {
|
||
document.getElementById('saved-card-display').style.display = 'none';
|
||
}
|
||
// Fill form fields
|
||
setVal('p-first', b.first_name);
|
||
setVal('p-last', b.last_name);
|
||
setVal('p-email', b.email);
|
||
setVal('p-address', b.address);
|
||
setVal('p-city', b.city);
|
||
setVal('p-state', b.state);
|
||
setVal('p-zip', b.zip);
|
||
// Also pre-fill buy form
|
||
prefillBuyForm(b);
|
||
} else {
|
||
document.getElementById('saved-card-display').style.display = 'none';
|
||
}
|
||
}
|
||
|
||
function prefillBuyForm(b) {
|
||
if (!b) return;
|
||
setVal('card-first-name', b.first_name);
|
||
setVal('card-last-name', b.last_name);
|
||
setVal('card-address', b.address);
|
||
setVal('card-city', b.city);
|
||
setVal('card-state', b.state);
|
||
setVal('card-zip', b.zip);
|
||
setVal('card-email', b.email);
|
||
}
|
||
|
||
function setVal(id, val) {
|
||
const el = document.getElementById(id);
|
||
if (el && val) el.value = val;
|
||
}
|
||
|
||
async function saveBillingProfile() {
|
||
const al = document.getElementById('billing-alert');
|
||
const data = {
|
||
first_name: (document.getElementById('p-first')?.value || '').trim(),
|
||
last_name: (document.getElementById('p-last')?.value || '').trim(),
|
||
email: (document.getElementById('p-email')?.value || '').trim(),
|
||
address: (document.getElementById('p-address')?.value || '').trim(),
|
||
city: (document.getElementById('p-city')?.value || '').trim(),
|
||
state: (document.getElementById('p-state')?.value || '').trim().toUpperCase(),
|
||
zip: (document.getElementById('p-zip')?.value || '').trim(),
|
||
};
|
||
const d = await api('/api/billing.php?action=save', data);
|
||
if (d.success) {
|
||
showAlert(al, 'Billing info saved!', 'success');
|
||
prefillBuyForm(data);
|
||
} else showAlert(al, d.error || 'Save failed.', 'error');
|
||
}
|
||
|
||
async function clearSavedCard() {
|
||
if (!confirm('Remove saved card info?')) return;
|
||
const d = await api('/api/billing.php?action=clear_card', {});
|
||
if (d.success) {
|
||
document.getElementById('saved-card-display').style.display = 'none';
|
||
// Clear card fields in buy form
|
||
['card-first-name','card-last-name'].forEach(id => { const el=document.getElementById(id); if(el) el.value=''; });
|
||
}
|
||
}
|
||
|
||
async function clearAllBilling() {
|
||
if (!confirm('Clear all saved billing information? This cannot be undone.')) return;
|
||
const d = await api('/api/billing.php?action=clear_all', {});
|
||
if (d.success) {
|
||
loadBillingProfile();
|
||
['p-first','p-last','p-email','p-address','p-city','p-state','p-zip'].forEach(id => {
|
||
const el=document.getElementById(id); if(el) el.value='';
|
||
});
|
||
const al = document.getElementById('billing-alert');
|
||
showAlert(al, 'All billing info cleared.', 'success');
|
||
}
|
||
}
|
||
|
||
// Auto-load billing when going to buy page
|
||
async function initBuyPageBilling() {
|
||
const d = await api('/api/billing.php?action=get');
|
||
if (d.success && d.billing) prefillBuyForm(d.billing);
|
||
}
|
||
|
||
// ─── CASHOUT ───────────────────────────────────────────────
|
||
async function submitCashout() {
|
||
const al = document.getElementById('cashout-alert');
|
||
const pid = document.getElementById('cashout-platform').value;
|
||
const alias = document.getElementById('cashout-alias').value.trim();
|
||
const toks = parseFloat(document.getElementById('cashout-tokens').value);
|
||
const payoutId = getSelectedPayoutId();
|
||
|
||
if (!payoutId) { showAlert(al,'Please select a payout method above.','error'); return; }
|
||
if (!pid) { showAlert(al,'Please select a platform.','error'); return; }
|
||
if (!alias) { showAlert(al,'Please enter your in-game alias.','error'); return; }
|
||
if (!toks || toks<1) { showAlert(al,'Enter a valid token amount.','error'); return; }
|
||
|
||
// Get payout method details
|
||
const pm = payoutMethods.find(m => m.id == payoutId);
|
||
|
||
try {
|
||
const d = await api('/api/cashout.php', {
|
||
platform_id: pid,
|
||
alias,
|
||
tokens: toks,
|
||
payout_method_id: payoutId,
|
||
payout_method_type: pm ? pm.method_type : '',
|
||
payout_handle: pm ? pm.account_handle : '',
|
||
});
|
||
if (d.success) {
|
||
showAlert(al, '✓ Request created! Review and submit to admin when ready.', 'success');
|
||
S.user.tokens = d.new_balance; updateUI();
|
||
document.getElementById('cashout-tokens').value = '';
|
||
document.getElementById('cashout-alias').value = '';
|
||
// Reload list and scroll to it
|
||
await loadCashoutHistory();
|
||
setTimeout(() => {
|
||
const hist = document.getElementById('cashout-history');
|
||
if (hist) hist.scrollIntoView({behavior:'smooth', block:'start'});
|
||
// Briefly highlight the first (newest) card
|
||
const first = hist?.querySelector('.co-card');
|
||
if (first) { first.style.boxShadow='0 0 0 2px var(--gold)'; setTimeout(()=>first.style.boxShadow='',2000); }
|
||
}, 200);
|
||
} else showAlert(al, d.error||'Request failed.','error');
|
||
} catch { showAlert(al,'Network error.','error'); }
|
||
}
|
||
|
||
async function loadCashoutHistory() {
|
||
const el = document.getElementById('cashout-history');
|
||
if (!el) return;
|
||
try {
|
||
const d = await api('/api/cashout.php?action=list');
|
||
if (!d.success || !d.requests.length) {
|
||
el.innerHTML = '<div style="color:var(--text2);font-size:15px;text-align:center;padding:20px">No cashout requests yet. Submit one above.</div>';
|
||
return;
|
||
}
|
||
el.innerHTML = d.requests.map(r => buildCashoutCard(r)).join('');
|
||
} catch(e) {
|
||
el.innerHTML = '<div style="color:var(--text2);font-size:15px;text-align:center;padding:16px">Unable to load requests.</div>';
|
||
}
|
||
}
|
||
|
||
function buildCashoutCard(r) {
|
||
const editable = r.status === 'pending';
|
||
const locked = r.status === 'locked';
|
||
const statusLabel = { pending:'⏳ Pending', locked:'🔒 Sent to Admin', sent:'✅ Sent', approved:'✅ Sent', rejected:'❌ Denied', deleted:'🗑 Deleted' };
|
||
const payout = r.payout_handle ? `<div style="font-size:15px;color:var(--cyan);margin-top:2px">${escHtml(r.payout_handle)}</div>` : '';
|
||
const adminNote= r.admin_note ? `<div style="font-size:15px;color:var(--gold);margin-top:4px;padding:6px 8px;background:rgba(240,192,64,.07);border-radius:6px">📝 ${escHtml(r.admin_note)}</div>` : '';
|
||
const actionBtns = editable ? `
|
||
<div class="co-edit-row">
|
||
<input class="fi" id="co-edit-tokens-${r.id}" type="number" min="1" value="${r.tokens}"
|
||
style="width:90px;padding:7px 10px;font-size:15px" placeholder="Tokens">
|
||
<input class="fi" id="co-edit-alias-${r.id}" type="text" value="${escHtml(r.alias||'')}"
|
||
style="flex:1;min-width:100px;padding:7px 10px;font-size:15px" placeholder="Game alias">
|
||
<button onclick="updateCashoutRequest(${r.id})"
|
||
style="background:rgba(0,229,255,.1);border:1px solid rgba(0,229,255,.25);color:var(--cyan);border-radius:8px;padding:7px 12px;font-size:14px;font-weight:700;cursor:pointer;white-space:nowrap">
|
||
✏️ Update
|
||
</button>
|
||
<button onclick="deleteCashoutRequest(${r.id})"
|
||
style="background:rgba(255,68,68,.08);border:1px solid rgba(255,68,68,.2);color:var(--red);border-radius:8px;padding:7px 12px;font-size:14px;font-weight:700;cursor:pointer">
|
||
🗑
|
||
</button>
|
||
<button onclick="lockCashoutRequest(${r.id})"
|
||
style="background:rgba(240,192,64,.1);border:1px solid rgba(240,192,64,.25);color:var(--gold);border-radius:8px;padding:7px 12px;font-size:14px;font-weight:700;cursor:pointer;white-space:nowrap">
|
||
🔒 Submit to Admin
|
||
</button>
|
||
</div>` : '';
|
||
const lockNote = locked ? `<div style="font-size:15px;color:var(--cyan);margin-top:8px;padding:7px 10px;background:rgba(0,229,255,.06);border-radius:6px;border:1px solid rgba(0,229,255,.15)">🔒 This request has been submitted to our team. They will process it shortly.</div>` : '';
|
||
|
||
return `<div class="co-card ${r.status}" id="co-card-${r.id}">
|
||
<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:10px">
|
||
<div style="flex:1;min-width:0">
|
||
<div style="display:flex;align-items:center;flex-wrap:wrap;gap:4px;margin-bottom:4px">
|
||
<span style="font-family:'Exo 2',sans-serif;font-weight:700;font-size:16px;color:var(--gold)">${r.tokens}</span>
|
||
<span style="margin-left:2px">${tokenCoin(18)}</span>
|
||
<span class="co-status-pill co-status-${r.status}">${statusLabel[r.status]||r.status}</span>
|
||
</div>
|
||
<div style="font-size:14px;color:var(--text2)">${escHtml(r.platform_name||r.platform_id)} · <span style="color:var(--cyan)">${escHtml(r.alias||'')}</span></div>
|
||
${payout}
|
||
${adminNote}
|
||
${lockNote}
|
||
</div>
|
||
<div style="text-align:right;flex-shrink:0">
|
||
<div style="font-size:15px;color:var(--text2)">${(r.created_at||'').substring(0,10)}</div>
|
||
<div style="font-size:14px;color:var(--text2);margin-top:2px">#${r.id}</div>
|
||
</div>
|
||
</div>
|
||
${actionBtns}
|
||
</div>`;
|
||
}
|
||
|
||
async function updateCashoutRequest(id) {
|
||
const tokens = parseFloat(document.getElementById('co-edit-tokens-'+id)?.value||0);
|
||
const alias = document.getElementById('co-edit-alias-'+id)?.value.trim();
|
||
if (!tokens||tokens<1) { showAlert(document.getElementById('cashout-alert'),'Enter a valid token amount.','error'); return; }
|
||
const d = await api('/api/cashout.php?action=update&id='+id+'&tokens='+tokens+'&alias='+encodeURIComponent(alias));
|
||
if (d.success) {
|
||
showAlert(document.getElementById('cashout-alert'),'✓ Request updated!','success');
|
||
setTimeout(()=>{ const al=document.getElementById('cashout-alert'); if(al){al.className='alert';al.textContent='';} }, 3000);
|
||
loadCashoutHistory();
|
||
} else showAlert(document.getElementById('cashout-alert'), d.error||'Update failed','error');
|
||
}
|
||
|
||
async function deleteCashoutRequest(id) {
|
||
if (!confirm('Cancel this cashout request?\n\nYour tokens will be returned to your balance immediately.')) return;
|
||
const d = await api('/api/cashout.php?action=delete&id='+id);
|
||
if (d.success) {
|
||
if (d.new_balance !== undefined && S.user) { S.user.tokens = d.new_balance; updateUI(); }
|
||
const card = document.getElementById('co-card-'+id);
|
||
if (card) { card.style.transition='opacity .3s'; card.style.opacity='0'; setTimeout(()=>card.remove(), 300); }
|
||
showAlert(document.getElementById('cashout-alert'),'✓ Request cancelled. Tokens returned.','success');
|
||
setTimeout(()=>{ const al=document.getElementById('cashout-alert'); if(al){al.className='alert';al.textContent='';} }, 3000);
|
||
} else showAlert(document.getElementById('cashout-alert'), d.error||'Delete failed','error');
|
||
}
|
||
|
||
async function lockCashoutRequest(id) {
|
||
if (!confirm('Submit this cashout request to our team for processing?\n\nYou won\'t be able to edit it after this.')) return;
|
||
const d = await api('/api/cashout.php?action=lock&id='+id);
|
||
if (d.success) {
|
||
loadCashoutHistory();
|
||
showAlert(document.getElementById('cashout-alert'),'✓ Submitted! Our team will process it shortly.','success');
|
||
setTimeout(()=>{ const al=document.getElementById('cashout-alert'); if(al){al.className='alert';al.textContent='';} }, 4000);
|
||
} else showAlert(document.getElementById('cashout-alert'), d.error||'Error','error');
|
||
}
|
||
|
||
// ─── PLAYER ACTIVITY DASHBOARD ────────────────────────────
|
||
let activityData = null;
|
||
let activityFilter = 'all';
|
||
let activityPollTimer = null;
|
||
|
||
async function loadProfileHistory() {
|
||
// Renamed alias kept for compatibility
|
||
await loadActivityDashboard();
|
||
}
|
||
|
||
async function loadActivityDashboard() {
|
||
const feed = document.getElementById('activity-feed');
|
||
const sumEl = document.getElementById('activity-summary');
|
||
if (!feed) return;
|
||
try {
|
||
const d = await api('/api/my_activity.php?action=all');
|
||
if (!d.success) throw new Error('Failed');
|
||
activityData = d;
|
||
renderActivitySummary(d, sumEl);
|
||
renderActivityFeed(activityFilter);
|
||
// Poll for real-time updates every 15s while profile is open
|
||
clearTimeout(activityPollTimer);
|
||
activityPollTimer = setTimeout(async () => {
|
||
const fresh = await api('/api/my_activity.php?action=all').catch(()=>null);
|
||
if (fresh?.success) { activityData = fresh; renderActivitySummary(fresh, sumEl); renderActivityFeed(activityFilter); }
|
||
}, 15000);
|
||
} catch(e) {
|
||
feed.innerHTML = '<div style="color:var(--text2);font-size:15px;text-align:center;padding:20px">Unable to load activity.</div>';
|
||
}
|
||
}
|
||
|
||
function filterActivity(filter, btn) {
|
||
activityFilter = filter;
|
||
document.querySelectorAll('.activity-ftab').forEach(b => b.classList.remove('active'));
|
||
if (btn) btn.classList.add('active');
|
||
renderActivityFeed(filter);
|
||
}
|
||
|
||
function renderActivitySummary(d, el) {
|
||
if (!el) return;
|
||
const purchases = d.purchases || [];
|
||
const cashouts = d.cashouts || [];
|
||
const pending_p = purchases.filter(p => p.status === 'pending').length;
|
||
const pending_c = cashouts.filter(c => c.status === 'pending' || c.status === 'locked').length;
|
||
const total_spent = purchases.filter(p => p.status !== 'failed').reduce((s,p) => s + (p.amount_cents/100), 0);
|
||
const total_cashed = cashouts.filter(c => c.status === 'sent' || c.status === 'approved').reduce((s,c) => s + parseFloat(c.tokens||0), 0);
|
||
const pills = [
|
||
total_spent > 0 ? `<div class="summary-pill">💳 $${total_spent.toFixed(2)} purchased</div>` : '',
|
||
total_cashed > 0 ? `<div class="summary-pill" style="color:var(--green)">💸 ${total_cashed} tokens cashed out</div>` : '',
|
||
pending_p > 0 ? `<div class="summary-pill" style="color:var(--gold);border-color:rgba(240,192,64,.3)">⏳ ${pending_p} purchase${pending_p>1?'s':''} pending</div>` : '',
|
||
pending_c > 0 ? `<div class="summary-pill" style="color:var(--cyan);border-color:rgba(0,229,255,.2)">⏳ ${pending_c} cashout${pending_c>1?'s':''} in review</div>` : '',
|
||
].filter(Boolean).join('');
|
||
el.innerHTML = pills;
|
||
}
|
||
|
||
function renderActivityFeed(filter) {
|
||
const feed = document.getElementById('activity-feed');
|
||
if (!feed || !activityData) return;
|
||
const items = buildActivityItems(activityData, filter);
|
||
if (!items.length) {
|
||
const labels = {all:'activity', purchases:'purchases', cashouts:'cashout requests', broadcasts:'announcements'};
|
||
feed.innerHTML = `<div style="text-align:center;padding:28px;color:var(--text2);font-size:15px">No ${labels[filter]||filter} yet.</div>`;
|
||
return;
|
||
}
|
||
feed.innerHTML = items.map(renderActivityCard).join('');
|
||
}
|
||
|
||
function buildActivityItems(d, filter) {
|
||
let items = [];
|
||
|
||
if (filter === 'all' || filter === 'purchases') {
|
||
(d.purchases||[]).forEach(p => items.push({ _type:'purchase', _ts: p.created_at, ...p }));
|
||
}
|
||
if (filter === 'all' || filter === 'cashouts') {
|
||
(d.cashouts||[]).forEach(c => items.push({ _type:'cashout', _ts: c.created_at, ...c }));
|
||
}
|
||
if (filter === 'all' || filter === 'broadcasts') {
|
||
(d.broadcasts||[]).forEach(b => items.push({ _type:'broadcast', _ts: b.sent_at, ...b }));
|
||
}
|
||
|
||
// Sort newest first
|
||
items.sort((a,b) => new Date(b._ts) - new Date(a._ts));
|
||
|
||
// For 'all', interleave with date dividers
|
||
if (filter === 'all') {
|
||
const grouped = []; let lastDate = '';
|
||
items.forEach(item => {
|
||
const dt = (item._ts||'').substring(0,10);
|
||
if (dt !== lastDate) {
|
||
lastDate = dt;
|
||
const d0 = new Date(dt+'T00:00:00'), now = new Date();
|
||
now.setHours(0,0,0,0);
|
||
const diff = Math.round((now-d0)/86400000);
|
||
const label = diff===0?'Today':diff===1?'Yesterday':d0.toLocaleDateString('en-US',{month:'short',day:'numeric'});
|
||
grouped.push({ _type:'divider', label });
|
||
}
|
||
grouped.push(item);
|
||
});
|
||
return grouped;
|
||
}
|
||
return items;
|
||
}
|
||
|
||
const PMT_ICON = { card:'💳', venmo:'💙', cashapp:'💚', zelle:'💜', chime:'🟢' };
|
||
|
||
function renderActivityCard(item) {
|
||
if (item._type === 'divider') {
|
||
return `<div style="font-size:15px;font-weight:700;color:var(--text2);letter-spacing:1px;text-transform:uppercase;padding:10px 0 6px;border-bottom:1px solid var(--border);margin-bottom:6px">${item.label}</div>`;
|
||
}
|
||
|
||
if (item._type === 'purchase') {
|
||
const statusMap = { completed:'✅ Completed', pending:'⏳ Awaiting Payment', failed:'❌ Failed', approved:'✅ Approved' };
|
||
const statusClass = { completed:'ac-completed', pending:'ac-pending', failed:'ac-failed', approved:'ac-completed' };
|
||
const icon = PMT_ICON[item.payment_method] || '💳';
|
||
const amt = item.amount_cents ? '$'+(item.amount_cents/100).toFixed(2) : '';
|
||
const card = item.card_last4 ? `${item.card_brand||'Card'} ····${item.card_last4}` : '';
|
||
const note = item.admin_note ? `<div style="font-size:15px;color:var(--gold);margin-top:4px">📝 ${escHtml(item.admin_note)}</div>` : '';
|
||
return `<div class="activity-card">
|
||
<div class="activity-icon" style="background:rgba(0,230,118,.1)">${icon}</div>
|
||
<div class="activity-body">
|
||
<div class="activity-title">Token Purchase · ${item.tokens} ${tokenCoin(14)}</div>
|
||
<div class="activity-meta">${pname(item.platform_id)}${item.game_alias?' · '+escHtml(item.game_alias):''}</div>
|
||
${card?`<div class="activity-meta">${card}</div>`:''}
|
||
<span class="activity-status ${statusClass[item.status]||'ac-pending'}">${statusMap[item.status]||item.status}</span>
|
||
${note}
|
||
</div>
|
||
<div class="activity-amount">
|
||
<div style="color:var(--green)">${amt}</div>
|
||
<div style="font-size:14px;color:var(--text2);font-weight:400;margin-top:2px">${(item.created_at||'').substring(0,10)}</div>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
if (item._type === 'cashout') {
|
||
const statusMap = {
|
||
pending:'⏳ Draft — not submitted',
|
||
locked:'🔒 Submitted to admin',
|
||
sent:'✅ Payment Sent',
|
||
approved:'✅ Payment Sent',
|
||
rejected:'❌ Denied',
|
||
deleted:'🗑 Deleted'
|
||
};
|
||
const statusClass = { pending:'ac-pending', locked:'ac-locked', sent:'ac-completed', approved:'ac-completed', rejected:'ac-failed', deleted:'ac-failed' };
|
||
const payout = item.payout_handle ? `<div class="activity-meta">${escHtml(item.payout_handle)}</div>` : '';
|
||
const note = item.admin_note ? `<div style="font-size:15px;color:var(--gold);margin-top:4px">📝 ${escHtml(item.admin_note)}</div>` : '';
|
||
const canEdit = item.status === 'pending';
|
||
return `<div class="activity-card" style="${item.status==='pending'?'border-color:rgba(240,192,64,.2)':item.status==='locked'?'border-color:rgba(0,229,255,.15)':''}">
|
||
<div class="activity-icon" style="background:rgba(240,192,64,.1)">💸</div>
|
||
<div class="activity-body">
|
||
<div class="activity-title">Cashout Request · ${item.tokens} ${tokenCoin(14)}</div>
|
||
<div class="activity-meta">${escHtml(item.platform_name||item.platform_id)}${item.alias?' · '+escHtml(item.alias):''}</div>
|
||
${payout}
|
||
<span class="activity-status ${statusClass[item.status]||'ac-pending'}">${statusMap[item.status]||item.status}</span>
|
||
${note}
|
||
${canEdit?`<div style="margin-top:8px"><button onclick="showPage('cashout')" style="background:none;border:none;color:var(--cyan);font-size:15px;font-weight:700;cursor:pointer;padding:0">✏️ Edit on Cashout page →</button></div>`:''}
|
||
</div>
|
||
<div class="activity-amount">
|
||
<div style="font-size:14px;color:var(--text2);font-weight:400;margin-top:2px">${(item.created_at||'').substring(0,10)}</div>
|
||
<div style="font-size:14px;color:var(--text2)">#${item.id}</div>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
if (item._type === 'broadcast') {
|
||
const isNew = !parseInt(item.is_read);
|
||
return `<div class="activity-card" style="${isNew?'border-color:rgba(240,192,64,.2)':''}">
|
||
<div class="activity-icon" style="background:rgba(240,192,64,.08)">📢</div>
|
||
<div class="activity-body">
|
||
<div class="activity-title">${escHtml(item.subject)}</div>
|
||
<div class="activity-meta">From ${escHtml(item.sender)} · ${(item.sent_at||'').substring(0,10)}</div>
|
||
<div style="font-size:14px;color:var(--text2);margin-top:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:220px">${escHtml(item.message)}</div>
|
||
${isNew?'<span class="activity-status ac-new">● New</span>':'<span class="activity-status ac-read">Read</span>'}
|
||
${parseInt(item.replied)?'<span class="activity-status ac-locked" style="margin-left:4px">Replied</span>':''}
|
||
</div>
|
||
<div>
|
||
<button onclick="showPage('broadcasts')" style="background:none;border:none;color:var(--cyan);font-size:15px;font-weight:700;cursor:pointer;padding:0;white-space:nowrap">View →</button>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
return '';
|
||
}
|
||
|
||
function pname(id) {
|
||
return CFG.platforms?.find(p=>p.id===id)?.name || id || '—';
|
||
}
|
||
|
||
// ─── AUTH ──────────────────────────────────────────────────
|
||
|
||
async function resendFromLogin() {
|
||
const btn=document.getElementById('login-resend-btn');
|
||
const al=document.getElementById('login-resend-alert');
|
||
const email=S._unverifiedEmail;
|
||
if(!email){showAlert(al,'Please register first.','error');return;}
|
||
btn.innerHTML='<span class="spinner"></span>'; btn.disabled=true;
|
||
try {
|
||
const d=await api('/api/resend_verify.php',{email});
|
||
if(d.success) showAlert(al,'Verification email resent! Check your inbox.','success');
|
||
else showAlert(al,d.error||'Could not resend.','error');
|
||
} catch{showAlert(al,'Network error.','error');}
|
||
btn.innerHTML='Resend Verification Email'; btn.disabled=false;
|
||
}
|
||
|
||
async function doRegister() {
|
||
const btn=document.getElementById('reg-btn'); const al=document.getElementById('reg-alert');
|
||
const u=document.getElementById('reg-user').value.trim();
|
||
const alias=document.getElementById('reg-alias').value.trim();
|
||
const email=document.getElementById('reg-email').value.trim();
|
||
const p=document.getElementById('reg-pass').value;
|
||
if(!u||!alias||!p){showAlert(al,'Username, alias and password are required.','error');return;}
|
||
if(!email){showAlert(al,'Email address is required for account verification.','error');return;}
|
||
btn.innerHTML='<span class="spinner"></span>'; btn.disabled=true;
|
||
try {
|
||
const d=await api('/api/register.php',{username:u,alias,email,password:p,referral_code:getUrlRefCode()});
|
||
if(d.success){
|
||
// Show pending verification state
|
||
document.getElementById('reg-pending-email').textContent = d.email || email;
|
||
S._pendingEmail = d.email || email;
|
||
showPendingState();
|
||
if (d.mail_warning) {
|
||
// Email failed to send - show resend option prominently
|
||
showAlert(document.getElementById('pending-alert')||al,
|
||
'Account created! We had trouble sending the verification email. Use the Resend button below.','error');
|
||
}
|
||
} else {
|
||
showAlert(al,d.error||'Registration failed.','error');
|
||
}
|
||
} catch{showAlert(al,'Network error.','error');}
|
||
btn.innerHTML='CREATE ACCOUNT'; btn.disabled=false;
|
||
}
|
||
|
||
function showPendingState() {
|
||
document.getElementById('reg-form-state').style.display='none';
|
||
document.getElementById('reg-pending-state').style.display='block';
|
||
}
|
||
|
||
function showRegForm() {
|
||
document.getElementById('reg-form-state').style.display='block';
|
||
document.getElementById('reg-pending-state').style.display='none';
|
||
}
|
||
|
||
async function resendVerify() {
|
||
const btn=document.getElementById('resend-btn');
|
||
const al=document.getElementById('resend-alert');
|
||
const email=S._pendingEmail;
|
||
if(!email){showAlert(al,'No email found. Please register again.','error');return;}
|
||
btn.innerHTML='<span class="spinner"></span>'; btn.disabled=true;
|
||
try {
|
||
const d=await api('/api/resend_verify.php',{email});
|
||
if(d.success) showAlert(al,'Verification email resent! Check your inbox.','success');
|
||
else showAlert(al,d.error||'Could not resend.','error');
|
||
} catch{showAlert(al,'Network error.','error');}
|
||
btn.innerHTML='Resend Verification Email'; btn.disabled=false;
|
||
}
|
||
|
||
async function doLogout() {
|
||
try { await fetch('/api/logout.php'); } catch(e) {}
|
||
S.user = null;
|
||
S.pkg = null;
|
||
// Clear any cached state
|
||
if (window._refData) window._refData = null;
|
||
showAuth();
|
||
}
|
||
|
||
// ─── HELPERS ──────────────────────────────────────────────
|
||
function showAlert(el,msg,type){el.textContent=msg;el.className='alert show alert-'+type;}
|
||
function hideAlert(el){el.className='alert';}
|
||
function showModal(id){document.getElementById(id).classList.add('show');}
|
||
function closeModal(id){document.getElementById(id).classList.remove('show');}
|
||
document.addEventListener('keydown',e=>{if(e.key==='Enter'){const t=document.getElementById('tab-login');if(t&&t.style.display!=='none')doLogin();}});
|
||
|
||
// ─── CHAT ─────────────────────────────────────────────────
|
||
let chatState = { lastId: 0, polling: null, initialized: false };
|
||
|
||
function initChat() {
|
||
// Load/reload messages every time chat opens
|
||
loadChatMessages(true);
|
||
if (chatState.initialized) return;
|
||
chatState.initialized = true;
|
||
// Auto-refresh every 3s while chat is open
|
||
chatState.polling = setInterval(() => {
|
||
if (document.getElementById('page-chat').classList.contains('active')) {
|
||
loadChatMessages(false);
|
||
}
|
||
pollChatBadge();
|
||
}, 3000);
|
||
}
|
||
|
||
async function loadChatMessages(scrollToBottom) {
|
||
try {
|
||
const d = await api('/api/chat.php?action=messages&since=' + chatState.lastId);
|
||
if (!d.success) return;
|
||
const container = document.getElementById('chat-messages');
|
||
const quickReplies = document.getElementById('chat-quick-replies');
|
||
|
||
if (d.messages.length === 0 && chatState.lastId === 0) {
|
||
container.innerHTML = '<div class="chat-empty">' +
|
||
'<div class="chat-empty-icon">💬</div>' +
|
||
'<div class="chat-empty-text">Have a question about tokens,<br>a cashout, or your account?<br>' +
|
||
'<strong style="color:var(--gold)">Send us a message below!</strong></div></div>';
|
||
if (quickReplies) quickReplies.style.display = 'flex';
|
||
return;
|
||
}
|
||
|
||
if (d.messages.length > 0) {
|
||
if (quickReplies) quickReplies.style.display = 'none';
|
||
const emptyEl = container.querySelector('.chat-empty, .chat-loading');
|
||
if (emptyEl) emptyEl.remove();
|
||
|
||
let lastDate = '';
|
||
d.messages.forEach(msg => {
|
||
// Skip if bubble already in DOM
|
||
if (container.querySelector('[data-id="' + msg.id + '"]')) return;
|
||
const msgDate = msg.created_at.substring(0,10);
|
||
if (msgDate !== lastDate) {
|
||
lastDate = msgDate;
|
||
const div = document.createElement('div');
|
||
div.className = 'chat-date-divider';
|
||
div.textContent = formatChatDate(msgDate);
|
||
container.appendChild(div);
|
||
}
|
||
const bubble = buildBubble(msg);
|
||
// Animate new incoming admin messages
|
||
if (msg.sender === 'admin' && chatState.lastId > 0) {
|
||
bubble.style.animation = 'fadeUp .3s ease';
|
||
}
|
||
container.appendChild(bubble);
|
||
chatState.lastId = Math.max(chatState.lastId, parseInt(msg.id));
|
||
});
|
||
|
||
if (scrollToBottom) scrollChat();
|
||
else {
|
||
const near = container.scrollHeight - container.scrollTop - container.clientHeight < 160;
|
||
if (near) scrollChat();
|
||
}
|
||
}
|
||
} catch(e) { console.warn('Chat poll error', e); }
|
||
}
|
||
|
||
function buildBubble(msg) {
|
||
const isMine = msg.sender === 'user';
|
||
const wrap = document.createElement('div');
|
||
wrap.className = 'chat-bubble-wrap ' + (isMine ? 'mine' : 'theirs');
|
||
wrap.dataset.id = msg.id;
|
||
const time = msg.created_at.substring(11,16);
|
||
const senderLabel = isMine ? '' : '<div class="chat-sender-label">Support</div>';
|
||
wrap.innerHTML = senderLabel +
|
||
'<div class="chat-bubble ' + (isMine ? 'mine' : 'theirs') + '">' + escHtml(msg.message) + '</div>' +
|
||
'<div class="chat-time">' + time + '</div>';
|
||
return wrap;
|
||
}
|
||
|
||
function scrollChat() {
|
||
const c = document.getElementById('chat-messages');
|
||
if (c) { c.scrollTop = c.scrollHeight; }
|
||
}
|
||
|
||
function onChatInput() {
|
||
const btn = document.getElementById('chat-send-btn');
|
||
const input = document.getElementById('chat-input');
|
||
if (btn) btn.classList.toggle('has-text', input.value.trim().length > 0);
|
||
}
|
||
|
||
function quickReply(text) {
|
||
const input = document.getElementById('chat-input');
|
||
if (input) {
|
||
input.value = text;
|
||
input.focus();
|
||
onChatInput();
|
||
}
|
||
}
|
||
|
||
async function sendChatMsg() {
|
||
const input = document.getElementById('chat-input');
|
||
const btn = document.getElementById('chat-send-btn');
|
||
const msg = input.value.trim();
|
||
if (!msg) return;
|
||
input.value = '';
|
||
if (btn) btn.classList.remove('has-text');
|
||
|
||
const container = document.getElementById('chat-messages');
|
||
const quickReplies = document.getElementById('chat-quick-replies');
|
||
const emptyEl = container.querySelector('.chat-empty, .chat-loading');
|
||
if (emptyEl) emptyEl.remove();
|
||
if (quickReplies) quickReplies.style.display = 'none';
|
||
|
||
// Optimistic bubble
|
||
const tempBubble = buildBubble({
|
||
id: 'temp_' + Date.now(), sender: 'user', message: msg,
|
||
created_at: new Date().toISOString().replace('T',' ').substring(0,19)
|
||
});
|
||
tempBubble.style.opacity = '0.65';
|
||
container.appendChild(tempBubble);
|
||
scrollChat();
|
||
|
||
try {
|
||
const d = await api('/api/chat.php?action=send', { message: msg });
|
||
if (d.success) {
|
||
tempBubble.style.opacity = '1';
|
||
tempBubble.dataset.id = d.id;
|
||
chatState.lastId = Math.max(chatState.lastId, d.id);
|
||
} else {
|
||
tempBubble.remove();
|
||
}
|
||
} catch { tempBubble.remove(); }
|
||
}
|
||
|
||
async function pollChatBadge() {
|
||
try {
|
||
const d = await api('/api/chat.php?action=unread');
|
||
const badge = document.getElementById('chat-nav-badge');
|
||
if (d.success && d.count > 0) {
|
||
badge.textContent = d.count > 9 ? '9+' : d.count;
|
||
badge.style.display = 'flex';
|
||
} else {
|
||
badge.style.display = 'none';
|
||
}
|
||
} catch(e) {}
|
||
}
|
||
|
||
function formatChatDate(dateStr) {
|
||
const d = new Date(dateStr + 'T00:00:00');
|
||
const today = new Date(); today.setHours(0,0,0,0);
|
||
const diff = Math.round((today - d) / 86400000);
|
||
if (diff === 0) return 'Today';
|
||
if (diff === 1) return 'Yesterday';
|
||
return d.toLocaleDateString('en-US', {month:'short', day:'numeric'});
|
||
}
|
||
|
||
function escHtml(s) {
|
||
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/\n/g,'<br>');
|
||
}
|
||
|
||
</script>
|
||
</body>
|
||
<!-- v<?= $_appVersion ?> --></html>
|