about summary refs log tree commit diff stats
path: root/js/seed/src/view.js
blob: 5feef6e23aa7b4b97688b9a00d847c97b7d4de22 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// view.js
// Pure view functions

/**
 * Pure view functions for the application.
 *
 * Why pure functions returning HTML strings?
 * - Keeps rendering logic stateless and easy to test.
 * - Ensures the UI is always a direct function of state, avoiding UI bugs from incremental DOM updates.
 * - Using template literals is minimal and browser-native, with no dependencies.
 *
 * Why escape output?
 * - Prevents XSS and ensures all user/content data is safely rendered.
 *
 * Why semantic/accessible HTML?
 * - Ensures the app is usable for all users, including those using assistive tech, and is easy to reason about.
 */
/**
 * Render the app UI as an HTML string
 * @param {object} state
 * @returns {string}
 */
export function view(state) {
  return `
    <h1>PokéDex</h1>
    <container>
      <form id="search-form" autocomplete="off">
        <label for="pokemon-query">Pokémon Name (or number)</label>
        <input id="pokemon-query" type="text" value="${escape(state.query)}" placeholder="e.g. pikachu" aria-label="Pokémon Name" required />
        <button type="submit" ${state.loading ? 'disabled' : ''}>${state.loading ? 'Loading...' : 'Search'}</button>
      </form>
      ${state.error ? `<div class="error" role="alert" tabindex="-1">${escape(state.error)}</div>` : ''}
      ${state.pokemon ? renderResult(state.pokemon) : ''}
    </container>
  `;
}

function renderResult(pokemon) {
  return `
    <div class="result">
      <h2>${capitalize(pokemon.name)} (#${pokemon.id})</h2>
      <img class="pokemon-sprite" src="${pokemon.sprites.front_default}" alt="${escape(pokemon.name)} sprite" />
      <ul>
        <li>Type: <b>${pokemon.types.map(t => capitalize(escape(t.type.name))).join(', ')}</b></li>
        <li>Height: <b>${pokemon.height / 10} m</b></li>
        <li>Weight: <b>${pokemon.weight / 10} kg</b></li>
      </ul>
    </div>
  `;
}

function escape(str) {
  /**
   * Escapes HTML special characters to prevent XSS.
   *
   * Why escape here? Keeps all rendering safe by default, so no accidental injection is possible.
   */
  return String(str).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}
function capitalize(str) {
  return str.charAt(0).toUpperCase() + str.slice(1);
}