Skip to content

Build a game browser

This guide builds three small pieces of a game browser: a “new games” list, a search box and a category menu. The code is JavaScript for Node.js 18 or later and uses the api helper from the JavaScript guide.

Ask for the newest games, then turn them into HTML. Titles are written by creators, so escape them. Use the picture address exactly as given and leave it out when it is null:

const { games } = await api("/catalog/browse", { sort: "new", limit: 6 });
const escapeHtml = (text) =>
text.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
const items = games.map((game) => {
const picture = game.thumbnail_url
? `<img src="${escapeHtml(game.thumbnail_url)}" alt="" width="128" height="128">`
: "";
return `<li>${picture}<a href="https://bazoodle.com/game/${encodeURIComponent(game.id)}">${escapeHtml(game.title)}</a></li>`;
});
console.log(`<ul>\n${items.join("\n")}\n</ul>`);

Handle the quiet cases: the list can be empty on a brand-new site (draw nothing), and a game can have no picture yet (the code above skips the <img>).

The rules that keep a search box fast, kind to the servers and private:

  1. Wait until typing pauses. Never search on every key press. Wait about 300 milliseconds after the last key and ignore text shorter than two characters.
  2. Ask for a few results.
  3. Do not send record=1. It is only for Bazoodle’s own box (Search).
  4. Keep recent searches on the person’s own device if you want them, never on a server.
function debounce(fn, ms = 300) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), ms);
};
}
async function suggest(text) {
const q = text.trim();
if (q.length < 2) return [];
const { games } = await api("/catalog/search", { q, limit: 5 });
return games.map((g) => ({ id: g.id, title: g.title }));
}
console.log(await suggest("coi"));
const { terms } = await api("/catalog/search/trending");
console.log(terms.map((t) => t.term));

Search matches the start of title words, tags and creator names, so coi finds “Coin Grab”. An empty answer means no match, not an error. Trending has at most eight words and is often empty on a quiet site: show nothing in that case. Search has its own rate limit group of 120 requests a minute per address, so with a 300 ms pause you stay far below it. If you serve many people from one server, cache identical queries for 15 seconds.

Read the categories once and build the menu from the response instead of typing the names, so it follows changes:

const { tags, filters } = await api("/catalog/categories");
console.log(tags.map((t) => `${t.id} = ${t.label}`).join("\n"));
console.log(filters.map((f) => `${f.id} = ${f.label}`).join("\n"));
async function gamesFor({ tag, filter, sort = "popular" }) {
const { games, next_offset } = await api("/catalog/browse", { tag, filter, sort, limit: 12 });
return { games, more: next_offset !== null };
}
const party = await gamesFor({ tag: "party", sort: "top" });
console.log(party.games.map((g) => g.title), "more:", party.more);

tag, filter and sort work together (Browse). For a ready-made set of shelves ask Home for the categories you want:

const home = await api("/catalog/home", { cats: "obby,puzzle" });
for (const row of home.rows) console.log(row.id, row.title, row.games.length);

A category can have no games yet and a shelf can be empty: say “No games here yet” instead of drawing an empty box. A tag or filter that is not in the categories response answers 400, which is another reason to build the menu from the response.