Easter egg: give the mini-game a real mobile trigger
.nav__brand is display:none below 1024px, so "tap the wordmark" was desktop-only. Add a shared onTapCode() helper (src/lib/tap-code.js) and put the 7-tap shortcut on the dex feed's game-title (.feed-head__id), which is visible on every screen size. Keeps the wordmark tap for desktop. 3 tests for onTapCode (count, window reset, unbind). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
This commit is contained in:
parent
fb299d11f9
commit
1125278f76
@ -1,6 +1,7 @@
|
||||
import { el } from '../lib/dom.js';
|
||||
import { openGameSheet } from './GameSheet.js';
|
||||
import { settings } from '../store/settings.js';
|
||||
import { onTapCode } from '../lib/tap-code.js';
|
||||
|
||||
const ITEMS = [
|
||||
{ label: 'Dex', icon: '▦', href: '#/', match: (h) => h === '#/' || h === '' || h === '#' },
|
||||
@ -25,20 +26,6 @@ export function Nav() {
|
||||
const nav = el('nav', { class: 'nav', 'aria-label': 'Primary' });
|
||||
let links = [];
|
||||
|
||||
// Easter egg: seven quick taps on the wordmark opens the hidden game
|
||||
// (the Konami code does the same on a keyboard).
|
||||
let taps = 0;
|
||||
let tapTimer = null;
|
||||
function bumpBrand() {
|
||||
taps += 1;
|
||||
clearTimeout(tapTimer);
|
||||
tapTimer = setTimeout(() => (taps = 0), 1500);
|
||||
if (taps >= 7) {
|
||||
taps = 0;
|
||||
location.hash = '#/whos-that';
|
||||
}
|
||||
}
|
||||
|
||||
function build() {
|
||||
const st = settings.get();
|
||||
const items = ITEMS.filter((it) => !it.optional || st[it.optional]);
|
||||
@ -55,10 +42,13 @@ export function Nav() {
|
||||
)
|
||||
: el('a', { class: 'nav__link', href: item.href }, ...inner);
|
||||
});
|
||||
nav.replaceChildren(
|
||||
el('span', { class: 'nav__brand', onclick: bumpBrand }, 'Pocketdex'),
|
||||
...links,
|
||||
);
|
||||
// Easter egg: the wordmark only shows on wide screens; on mobile the
|
||||
// same shortcut lives on the dex feed's game title (see DexGrid).
|
||||
const brand = el('span', { class: 'nav__brand' }, 'Pocketdex');
|
||||
onTapCode(brand, () => {
|
||||
if ((location.hash || '') !== '#/whos-that') location.hash = '#/whos-that';
|
||||
});
|
||||
nav.replaceChildren(brand, ...links);
|
||||
nav._items = items;
|
||||
sync();
|
||||
}
|
||||
|
||||
23
src/lib/tap-code.js
Normal file
23
src/lib/tap-code.js
Normal file
@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Fire `fn` after `count` taps/clicks land on `node` within `windowMs` of
|
||||
* each other — a quiet way to hide a shortcut behind a bit of fidgeting.
|
||||
* Returns an unbind.
|
||||
*/
|
||||
export function onTapCode(node, fn, { count = 7, windowMs = 1500 } = {}) {
|
||||
let n = 0;
|
||||
let timer = null;
|
||||
const onClick = () => {
|
||||
n += 1;
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => (n = 0), windowMs);
|
||||
if (n >= count) {
|
||||
n = 0;
|
||||
fn();
|
||||
}
|
||||
};
|
||||
node.addEventListener('click', onClick);
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
node.removeEventListener('click', onClick);
|
||||
};
|
||||
}
|
||||
@ -8,6 +8,7 @@ import { Card } from '../components/Card.js';
|
||||
import { ProgressRing } from '../components/ProgressRing.js';
|
||||
import { Sprite } from '../components/Sprite.js';
|
||||
import { typesForGen } from '../lib/type-resolve.js';
|
||||
import { onTapCode } from '../lib/tap-code.js';
|
||||
|
||||
const TYPES = [
|
||||
'normal',
|
||||
@ -429,16 +430,18 @@ export async function DexGrid() {
|
||||
}
|
||||
syncToolsBadge();
|
||||
|
||||
// Easter egg: a flurry of taps on the game title opens the hidden game.
|
||||
// (The Konami code does the same with a keyboard.)
|
||||
const feedId = el('div', { class: 'feed-head__id' }, title, gameLabel);
|
||||
onTapCode(feedId, () => {
|
||||
if ((location.hash || '') !== '#/whos-that') location.hash = '#/whos-that';
|
||||
});
|
||||
|
||||
view.append(
|
||||
el(
|
||||
'header',
|
||||
{ class: 'feed-head' },
|
||||
el(
|
||||
'div',
|
||||
{ class: 'feed-head__top' },
|
||||
el('div', { class: 'feed-head__id' }, title, gameLabel),
|
||||
ring.node,
|
||||
),
|
||||
el('div', { class: 'feed-head__top' }, feedId, ring.node),
|
||||
el(
|
||||
'div',
|
||||
{ class: 'field-search' },
|
||||
|
||||
43
test/tap-code.test.js
Normal file
43
test/tap-code.test.js
Normal file
@ -0,0 +1,43 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { onTapCode } from '../src/lib/tap-code.js';
|
||||
|
||||
const tap = (node) => node.dispatchEvent(new Event('click'));
|
||||
|
||||
beforeEach(() => vi.useFakeTimers());
|
||||
afterEach(() => vi.useRealTimers());
|
||||
|
||||
describe('onTapCode', () => {
|
||||
it('fires after `count` taps inside the window', () => {
|
||||
const node = new EventTarget();
|
||||
const fn = vi.fn();
|
||||
onTapCode(node, fn, { count: 7, windowMs: 1500 });
|
||||
for (let i = 0; i < 6; i++) tap(node);
|
||||
expect(fn).not.toHaveBeenCalled();
|
||||
tap(node);
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('resets when taps are too far apart', () => {
|
||||
const node = new EventTarget();
|
||||
const fn = vi.fn();
|
||||
onTapCode(node, fn, { count: 3, windowMs: 500 });
|
||||
tap(node);
|
||||
tap(node);
|
||||
vi.advanceTimersByTime(600); // window elapsed -> counter clears
|
||||
tap(node);
|
||||
tap(node);
|
||||
expect(fn).not.toHaveBeenCalled();
|
||||
tap(node);
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('unbind stops it', () => {
|
||||
const node = new EventTarget();
|
||||
const fn = vi.fn();
|
||||
const off = onTapCode(node, fn, { count: 2 });
|
||||
off();
|
||||
tap(node);
|
||||
tap(node);
|
||||
expect(fn).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user