> ## Documentation Index
> Fetch the complete documentation index at: https://ej3mplo.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# The game page

> Description, Play or Install, favorite, and back.

Now, instead of launching the game when a cover is pressed, we will open a page
with its description and its buttons. The page is the `<section id="ficha" hidden
data-focus-trap>` from the HTML:

* **`hidden`**: it starts hidden.
* **`data-focus-trap`**: while it is visible, focus cannot leave it (the arrows
  do not escape to the grid behind it).

Change the card's `onclick` to `onclick: () => abrirFicha(g.id)` and add this to
`main.js` (also add `description`, `playtime` and `year` to the imports:
`import { description, playtime, year } from "/_sdk/kit/format.js";`):

```js theme={null}
let fichaId = null;
let turno = 0; // drop answers for a page that has already closed

const datos = (g) => [g.developer, year(g.releaseDate), playtime(g.playtime)].filter(Boolean).join(" · ");

async function abrirFicha(id) {
  const g = ejg.library.byId(id);
  if (!g) return;
  fichaId = id;
  const miTurno = ++turno;

  const fondo = g.media.hero || g.media.heroThumb;
  const texto = h("div", { class: "descripcion" }, h("p", null, "Cargando…"));
  const botones = h("div", { class: "botones" });
  ficha.replaceChildren(
    h("div", { class: "ficha-fondo", style: fondo ? `background-image: url("${fondo}")` : null }),
    h(
      "div",
      { class: "ficha-contenido" },
      h("div", { class: "ficha-caratula" }, img(g.media.cover || g.media.coverThumb)),
      h(
        "div",
        { class: "ficha-info" },
        h("h2", null, g.title),
        h("p", { class: "datos" }, datos(g)),
        botones,
        texto,
      ),
    ),
  );
  pintarBotones(g);
  ficha.hidden = false;
  ejg.sound.play("open");
  foco.first(botones);
  pistasFicha();

  // The long description is not in the library: it is requested separately.
  try {
    const d = await ejg.game.details(id);
    if (miTurno !== turno) return;
    texto.replaceChildren(description(d.description || d.shortDescription || "Sin descripción."));
  } catch {
    if (miTurno === turno) texto.replaceChildren(h("p", null, "No se ha podido cargar la descripción."));
  }
}
```

(and `const ficha = document.querySelector("#ficha");` next to the other constants).

* **`ejg.library.byId(id)`** returns the game instantly, with no waiting.
* **`ejg.game.details(id)`** asks for what the library does not include: the
  description, screenshots (`screenshots`), trailers (`trailers`) and recent
  sessions (`recentSessions`). It is asynchronous, so we paint the page first and
  fill in the text afterwards. `turno` keeps a game's description from landing on
  another game's page if the user opens and closes pages quickly.
* **`description(text)`** turns the description format (`## title`, `- bullet`
  and paragraphs separated by a blank line) into safe HTML elements. Do not use
  `innerHTML` with text that comes from outside.
* **`playtime(seconds)`** → "12 h 34 min" or "Not played". **`year(date)`** → "2017".
* Here we do use the large images (`cover`, `hero`): a page has only one.

The buttons are separate because they are repainted when something changes
(favorite, a session in progress):

```js theme={null}
function pintarBotones(g) {
  const caja = ficha.querySelector(".botones");
  if (!caja) return;
  const enMarcha = ejg.game.isRunning(g.id);
  const antes = foco.current?.dataset.accion;
  caja.replaceChildren(
    h(
      "button",
      { class: "boton principal", "data-focus": "", "data-accion": "jugar", onclick: () => jugar(g.id) },
      enMarcha ? "En marcha" : "Jugar",
    ),
    h("button", { class: "boton", "data-focus": "", "data-accion": "fav", onclick: () => ejg.game.favorite(g.id) }, g.favorite ? "★ Favorito" : "☆ Favorito"),
    h("button", { class: "boton", "data-focus": "", "data-accion": "editar", onclick: () => ejg.game.edit(g.id) }, "Editar"),
    h("button", { class: "boton", "data-focus": "", "data-accion": "volver", onclick: cerrarFicha }, "Volver"),
  );
  // After repainting, focus returns to the same button.
  const b = antes && caja.querySelector(`[data-accion="${antes}"]`);
  if (b) foco.focus(b, { noScroll: true, silent: true });
}

async function jugar(id) {
  if (ejg.game.isRunning(id)) return;
  const b = ficha.querySelector('[data-accion="jugar"]');
  if (b) b.textContent = "Iniciando…";
  try {
    await ejg.game.launch(id); // ejGames plays the launch sound
  } catch (e) {
    ejg.ui.toast(String(e.message || e), "error");
  }
  const g = ejg.library.byId(id);
  if (g && fichaId === id) pintarBotones(g);
}

function cerrarFicha() {
  if (ficha.hidden) return;
  const id = fichaId;
  fichaId = null;
  turno++;
  ficha.hidden = true;
  ficha.replaceChildren();
  ejg.sound.play("back");
  pistasRejilla();
  const t = rejilla.querySelector(`[data-id="${id}"]`);
  if (t) foco.focus(t, { silent: true });
  else foco.first(rejilla);
}
```

* **Play**, or **Running** if `ejg.game.isRunning(id)`. The example's source
  strings are `Jugar` and `En marcha`.
* **`ejg.game.edit(id)`** opens ejGames' game editor (name, art, executable…).
  When it closes, `focus-return` arrives and focus returns to the button.

What is left is to teach `bindNav` that **Back** closes the page and that the
favorite also works inside it, to change the hints depending on where you are,
and to repaint when something changes:

```js theme={null}
bindNav(foco, {
  // With no page open we return false, so ejGames handles "Back".
  back: () => {
    if (ficha.hidden) return false;
    cerrarFicha();
  },
  y: () => {
    const id = fichaId ?? Number(foco.current?.dataset.id);
    if (!id) return false;
    ejg.game.favorite(id);
  },
  menu: () => {
    ejg.ui.open("menu");
  },
});

const pistas = hints(document.querySelector("#pistas"), []);
const pistasRejilla = () => pistas.set([["accept", "Abrir"], ["y", "Favorito"], ["menu", "Menú"]]);
const pistasFicha = () => pistas.set([["accept", "Elegir"], ["back", "Volver"], ["y", "Favorito"]]);

const repintar = debounce(() => {
  pintar();
  if (fichaId) {
    const g = ejg.library.byId(fichaId);
    if (!g) return cerrarFicha();
    pintarBotones(g);
    ficha.querySelector(".datos").textContent = datos(g); // the hours, when the game closes
  }
}, 50);
ejg.library.onChange(repintar); // new games, favorites, covers…
ejg.on("running", repintar); // a game starts or ends

pintar();
pistasRejilla();
```

Replace your previous `bindNav`, the hint bar and the last two lines with this,
and add `debounce` to the `dom.js` import. It groups several notices in a row
into a single repaint. When a game closes, its new hours arrive through the same
notice, which is why `repintar` also updates the open page. The page CSS, in
short (the full file is in
[`examples/ejemplo-tema/style.css`](https://github.com/ej3mpl0/ejGames/blob/main/examples/ejemplo-tema/style.css)):

```css theme={null}
.ficha { position: fixed; inset: 0; z-index: 10; overflow-y: auto; background: var(--fondo); }
.ficha-fondo { position: absolute; inset: 0 0 auto; height: 75vh; background: center / cover no-repeat; opacity: 0.35;
               mask-image: linear-gradient(#000 35%, transparent); }
.ficha-contenido { position: relative; display: flex; align-items: flex-start; gap: 40px; padding: 90px 64px 64px; max-width: 1200px; }
.ficha-caratula { flex: none; width: 240px; aspect-ratio: 2 / 3; border-radius: 12px; overflow: hidden; background: var(--panel); }
.ficha-caratula img { width: 100%; height: 100%; object-fit: cover; }
.ficha-caratula img.img-empty, .ficha-caratula img.img-error { display: none; }
.botones { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 26px; }
.boton { padding: 10px 18px; border-radius: 8px; background: color-mix(in srgb, var(--texto) 10%, transparent); }
.boton.principal { padding-inline: 30px; background: var(--acento); color: #fff; font-weight: 600; }
.boton.is-focused { box-shadow: 0 0 0 3px var(--texto); }
```

> [!TIP]
>
> Want guides on your page? The kit view `createGuideView` brings the list of
> Steam community guides and a reader you can use with a controller. You mount it
> in a container and dress it with CSS. See [Community guides](https://ej3mplo.mintlify.site/referencia/guias.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.