diff options
Diffstat (limited to 'js/scripting-lang/web/src/view.js')
-rw-r--r-- | js/scripting-lang/web/src/view.js | 196 |
1 files changed, 196 insertions, 0 deletions
diff --git a/js/scripting-lang/web/src/view.js b/js/scripting-lang/web/src/view.js new file mode 100644 index 0000000..6d591cf --- /dev/null +++ b/js/scripting-lang/web/src/view.js @@ -0,0 +1,196 @@ +// view.js +// Pure view functions + +/** + * Pure view functions for the application. + * + * Why pure functions returning HTML strings? Because Elm does it, tbh. + * - Keeps rendering logic stateless and easy to test. + * - Ensures the UI is always a direct function of state, which should in theory totally avoid bugs from incremental DOM updates. + * - Using template literals is minimal and browser-native, with no dependencies, and is fun. + * + * 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>Baba Yaga's 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. eevee" 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 ? renderPokemonResult(state.pokemon) : ''} + ${state.pokemon ? '<div class="workflow-arrow">⬇</div>' : ''} + ${state.evolutionChain ? renderEvolutionSection(state) : ''} + ${state.evolutionChain ? '<div class="workflow-arrow">⬇</div>' : ''} + ${renderBabaYagaSection(state)} + </container> + `; +} + +function renderPokemonResult(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 renderEvolutionSection(state) { + return ` + <div class="evolution-section"> + <h3>Evolution Chain</h3> + ${renderEvolutionTree(state.evolutionChain)} + </div> + `; +} + +function renderEvolutionTree(evolutionChain) { + if (!evolutionChain || !evolutionChain.evolutionChain || !evolutionChain.evolutionChain.chain) { + return '<p>No evolution data available.</p>'; + } + + const chain = evolutionChain.evolutionChain.chain; + let html = '<div class="evolution-tree">'; + + // Render the base species + html += `<div class="evolution-stage base">`; + html += `<div class="pokemon-node">${capitalize(chain.species.name)}</div>`; + html += `</div>`; + + // Render evolution stages + if (chain.evolves_to && chain.evolves_to.length > 0) { + html += renderEvolutionStages(chain.evolves_to, 1); + } + + html += '</div>'; + return html; +} + +function renderEvolutionStages(evolutions, level) { + if (!evolutions || evolutions.length === 0) return ''; + + let html = `<div class="evolution-stage level-${level}">`; + + evolutions.forEach((evolution, index) => { + html += '<div class="evolution-branch">'; + + // Evolution details + if (evolution.evolution_details && evolution.evolution_details.length > 0) { + const detail = evolution.evolution_details[0]; + html += `<div class="evolution-detail">`; + html += `<span class="evolution-method">${capitalize(detail.trigger.name)}`; + if (detail.min_level) { + html += ` (Level ${detail.min_level})`; + } + html += `</span>`; + html += `</div>`; + } + + // Pokemon node + html += `<div class="pokemon-node">${capitalize(evolution.species.name)}</div>`; + + // Recursive evolution stages + if (evolution.evolves_to && evolution.evolves_to.length > 0) { + html += renderEvolutionStages(evolution.evolves_to, level + 1); + } + + html += '</div>'; + }); + + html += '</div>'; + return html; +} + +function renderBabaYagaSection(state) { + return ` + <div class="baba-yaga-section"> + <h3>Baba Yaga Data Transformation</h3> + <div class="script-editor"> + <label for="baba-yaga-script">Transformation Script:</label> + <textarea + id="baba-yaga-script" + placeholder="Write your Baba Yaga script here..." + rows="8" + >${escape(state.babaYagaScript)}</textarea> + <div class="script-controls"> + <button id="execute-script" type="button" ${!state.evolutionChain || !state.babaYagaScript.trim() ? 'disabled' : ''}> + Execute Script + </button> + <button id="clear-script" type="button">Clear</button> + <select id="example-scripts"> + <option value="">Load Example...</option> + <option value="Basic Evolution Stages">Basic Evolution Stages</option> + <option value="Evolution Methods">Evolution Methods</option> + <option value="Filter by Evolution Method">Filter by Evolution Method</option> + </select> + </div> + ${!state.evolutionChain ? '<p class="help-text">💡 Search for a Pokémon to automatically load its evolution chain and enable script execution.</p>' : ''} + ${state.evolutionChain && !state.babaYagaScript.trim() ? '<p class="help-text">💡 Write a Baba Yaga script or load an example to enable execution.</p>' : ''} + </div> + + ${state.scriptError ? `<div class="script-error" role="alert">${escape(state.scriptError)}</div>` : ''} + ${state.scriptOutput ? renderScriptOutput(state.scriptOutput) : ''} + </div> + `; +} + +function renderScriptOutput(scriptOutput) { + let html = '<div class="script-output">'; + html += '<h4>Script Results:</h4>'; + + // Display emitted data + if (scriptOutput.emitted && Object.keys(scriptOutput.emitted).length > 0) { + html += '<div class="emitted-data">'; + html += '<h5>Emitted Data:</h5>'; + Object.entries(scriptOutput.emitted).forEach(([event, data]) => { + html += `<div class="emitted-event">`; + html += `<h6>${escape(event)}:</h6>`; + html += `<pre>${escape(JSON.stringify(data, null, 2))}</pre>`; + html += `</div>`; + }); + html += '</div>'; + } + + // Display script result + if (scriptOutput.result !== undefined) { + html += '<div class="script-result">'; + html += '<h5>Script Result:</h5>'; + html += `<pre>${escape(JSON.stringify(scriptOutput.result, null, 2))}</pre>`; + html += '</div>'; + } + + html += '</div>'; + return html; +} + +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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); +} + +function capitalize(str) { + return str.charAt(0).toUpperCase() + str.slice(1); +} \ No newline at end of file |