PageSourceSearch

https://webcade.fun/tabbar.js?v=1025436e

js webcade.fun collected 2026-10-08 08:26:45 UTC 8,285 bytes, 161 lines download raw bytes

1// The bottom tab bar, on phones.
2//
3// Every destination on this site lived behind a hamburger: zero of them were
4// visible without opening a menu, so reaching the leaderboard was two taps and
5// knowing it existed was a matter of curiosity. A phone app puts its rooms
6// along the bottom, always lit, always one tap — which is also the only way a
7// player discovers Duels and Crews without being told.
8//
9// The bar is built here for the same reason the header is: seventeen pages
10// would otherwise each carry a copy, and the last one would always be an edit
11// behind. Desktop keeps the inline nav row it already has; this is phones only.
12(() => {
13  // Matches the hamburger breakpoint in style.css. Above it the header already
14  // shows every link inline and a second copy along the bottom would be noise.
15  const NARROW = '(max-width: 860px)';
16  if (!window.matchMedia || !window.matchMedia(NARROW).matches) return;
17
18  const here = location.pathname.replace(/\/$/, '') || '/';
19
20  // The front door used to be excluded, and the reason was sound while it was
21  // true: / was a splash with one deliberate choice on it — ENTER — and a row
22  // of five alternatives along the bottom is an argument against the one thing
23  // that screen is asking for.
24  //
25  // It is not a splash any more. / is the homepage now: a hero, the live board,
26  // six playable games and four links out. Withholding the bar there would make
27  // the front door the ONE page on the site where a phone has no navigation,
28  // which is the exact problem this file was written to fix. So it gets the bar
29  // like everything else.
30
31  // Line icons rather than emoji: emoji render as somebody else's artwork at
32  // somebody else's weight, and five of them never look like one set.
33  const ICON = {
34    play: '<path d="M8 5.5v13l11-6.5z"/>',
35    ranks: '<rect x="3" y="13" width="4.5" height="8" rx="1"/>'
36         + '<rect x="9.75" y="8" width="4.5" height="13" rx="1"/>'
37         + '<rect x="16.5" y="4" width="4.5" height="17" rx="1"/>',
38    duels: '<path d="M4 4l9.5 9.5M20 4l-9.5 9.5" stroke-width="2" stroke-linecap="round" fill="none"/>'
39         + '<path d="M13.5 13.5l3.5 3.5-2 2-3.5-3.5M10.5 13.5L7 17l2 2 3.5-3.5" stroke-width="2" stroke-linejoin="round" fill="none"/>',
40    you: '<circle cx="12" cy="8" r="3.6" stroke-width="2" fill="none"/>'
41       + '<path d="M4.8 20a7.2 7.2 0 0 1 14.4 0" stroke-width="2" stroke-linecap="round" fill="none"/>',
42  };
43
44  // href may be a function, for a tab whose destination depends on who is
45  // asking. Order is by how often a player wants it, left to right, with the
46  // thing they came to do first.
47  //
48  // The labels are the words the desktop nav uses, not shorter synonyms for
49  // them. PLAY and RANKS read fine on their own but they named the same two
50  // destinations the bar at the top calls ARCADE and LEADERBOARD, so a player
51  // moving between a phone and a laptop had to learn the site twice. YOU has
52  // no counterpart in the top nav — it is the player's own page — so it keeps
53  // its name.
54  //
55  // `id` is an internal key, not a label: it selects the icon, and 'ranks'
56  // stays 'ranks' because renaming it would rename the icon it draws for no
57  // gain a player can see.
58  const TABS = [
59    { id: 'play',   label: 'Arcade',      href: '/arcade',      match: ['/arcade'] },
60    { id: 'ranks',  label: 'Leaderboard', href: '/leaderboard', match: ['/leaderboard'] },
61    { id: 'duels',  label: 'Duels',       href: '/duel',        match: ['/duel', '/crews'] },
62    // CLAIMS WAS THE FOURTH TAB and is gone with the desk it pointed at, which
63    // closed at 00:00 UTC on 20 Sep 2026. Its icon went with it. The route
64    // still answers 410 for a holder arriving on an old link — see the claim
65    // desk block in server.js — but a tab is furniture, and furniture pointing
66    // at a withdrawn page is a dead end one tap from every screen on a phone.
67    //
68    // DO NOT PASTE THE OLD ENTRY BACK INTO THIS COMMENT to show what it was.
69    // homeuitest counts the tabs by grepping this file for the literal opening
70    // of an entry, and searches it for the desk's path; a commented-out copy
71    // reads as a fifth tab and fails both. That is the same trap legacyuitest
72    // records for <main>. Four tabs, and the grid in style.css no longer
73    // writes the number down a second time.
74    { id: 'you',    label: 'You',         href: null,           match: ['/player'] },
75  ];
76
77  // "You" has no fixed address: it is the player's own page when we know who
78  // they are, and the way to become known when we do not.
79  function youHref() {
80    const w = window.ARCADE_WALLET;
81    const pk = (w && w.publicKey) || (() => {
82      try { return (JSON.parse(localStorage.getItem('webcade-wallet') || 'null') || {}).pk || null; }
83      catch (_) { return null; }
84    })();
85    // Same for the tab bar: a Telegram player has no pk, and without this
86    // their profile tab simply is not there.
87    const tgId = (window.ARCADE_TG && window.ARCADE_TG.id) || null;
88    const who = pk || tgId;
89    return who ? '/player?w=' + encodeURIComponent(who) : null;
90  }
91
92  const bar = document.createElement('nav');
93  bar.id = 'tabbar';
94  bar.setAttribute('aria-label', 'Sections');
95
96  bar.innerHTML = TABS.map(t => {
97    const on = t.match.some(m => here === m.replace(/\/$/, ''));
98    const href = t.id === 'you' ? youHref() : t.href;
99    // A tab with nowhere to go is still a tab — it connects a wallet instead,
100    // which is the one thing that gives it somewhere to go.
101    const tag = href ? 'a' : 'button';
102    const attrs = href ? ` href="${href}"` : ' type="button"';
103    return `<${tag} class="tb${on ? ' on' : ''}" data-tab="${t.id}"${attrs}${on ? ' aria-current="page"' : ''}>
104        <svg viewBox="0 0 24 24" aria-hidden="true">${ICON[t.id]}</svg>
105        <span>${t.label}</span>
106      </${tag}>`;
107  }).join('');
108
109  // Appended to <body>, not into <footer>: the bar is fixed to the viewport and
110  // has to outlive whatever a page does to its own layout.
111  document.body.appendChild(bar);
112  document.body.classList.add('has-tabbar');
113
114  const you = bar.querySelector('[data-tab="you"]');
115  if (you && you.tagName === 'BUTTON') {
116    you.addEventListener('click', () => {
117      // Ask again before reaching for a wallet. This file is injected into
118      // <head> by server.js and tg.js sits eleven scripts down the body, so the
119      // href was decided at a moment when ARCADE_TG could not exist yet — and
120      // for a Telegram player, who has no browser wallet either, that meant YOU
121      // became a button offering to install Phantom. By the time anyone taps
122      // it, the answer is available.
123      const href = youHref();
124      if (href) { location.href = href; return; }
125      const w = window.ARCADE_WALLET;
126      if (w && w.connect) w.connect();
127    });
128  }
129
130  // The wallet arrives after this runs — it reconnects asynchronously, and on
131  // the Seeker only once a CDN import resolves. So the tab is corrected when
132  // that lands rather than being left pointing nowhere for the whole visit.
133  function refreshYou() {
134    const href = youHref();
135    const el = bar.querySelector('[data-tab="you"]');
136    if (!href || !el || el.tagName === 'A') return;
137    const a = document.createElement('a');
138    a.className = el.className;
139    a.dataset.tab = 'you';
140    a.href = href;
141    a.innerHTML = el.innerHTML;
142    el.replaceWith(a);
143  }
144  if (window.ARCADE_WALLET && window.ARCADE_WALLET.onChange) {
145    window.ARCADE_WALLET.onChange(refreshYou);
146  } else {
147    // wallet.js may not have run yet; ask again once it has
148    document.addEventListener('DOMContentLoaded', () => {
149      // And ask outright, not only through the wallet. onChange was the ONLY
150      // thing that ever corrected this tab, and it fires when a WALLET
151      // arrives — which for a Telegram player never happens, because their
152      // identity is a signed id and not a wallet at all. So the one group of
153      // players whose tab bar was wrong was also the one group nothing was
154      // ever going to fix it for. tg.js has published ARCADE_TG by now.
155      refreshYou();
156      if (window.ARCADE_WALLET && window.ARCADE_WALLET.onChange) {
157        window.ARCADE_WALLET.onChange(refreshYou);
158      }
159    });
160  }
161})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.