// app.js // Entrypoint for the app import { initialState, cloneState } from './state.js'; import { update } from './update.js'; import { view } from './view.js'; import { fetchPokemon, fetchEvolutionData, executeBabaYagaScript, getExampleScripts, getCurrentHarness } from './api.js'; import { initDevMode } from './dev.js'; const root = document.getElementById('app'); let state = cloneState(initialState); let dev; /** * Entrypoint for the app. * * This file implements a minimal Elm-style architecture using only browser APIs and ES modules. * - All state is immutable and updated by a pure update function. * - The entire UI is re-rendered as a string on each state change for simplicity and predictability. * - Event delegation is used to keep wiring minimal and functional. * - No 3rd party code: everything is browser-native for cozy portability and clarity. * * Why this approach? * - Functional, pure update/view logic is easier for me to reason about and test. * - Re-rendering the whole UI avoids bugs from manual DOM updates and keeps state/UI in sync. * - Minimal code and clear data flow make it easy to extend or adapt for new projects. */ // Enable devMode if ?dev=1 is in the URL /** * devMode enables logging of all actions and state transitions for debugging. * * Why? This makes the app's state flow transparent, helping you understand and debug the app without extra tooling. */ const devMode = window.location.search.includes('dev=1'); /** * Generalized render function for Elm-style apps. * * @param {Object} config - Render configuration * @param {HTMLElement} config.root - Root DOM element * @param {any} config.state - Current app state * @param {Function} config.view - View function (state => HTML string) * @param {Array} [config.events] - Array of { selector, event, handler } * @param {Function} [config.postRender] - Optional function({ root, state }) for post-render logic */ function render({ root, state, view, events = [], postRender }) { root.innerHTML = view(state); events.forEach(({ selector, event, handler }) => { const el = root.querySelector(selector); if (el) el.addEventListener(event, handler); }); if (typeof postRender === 'function') { postRender({ root, state }); } } // --- App-specific config for render --- function postRender({ root, state }) { // Preserve scroll position const scrollPosition = window.scrollY; const input = root.querySelector('#pokemon-query'); const error = root.querySelector('.error'); // Only handle error focus - don't interfere with user typing if (error) { error.focus(); } else if (input && !document.activeElement) { // Only auto-focus search input if nothing is currently focused input.focus(); input.value = state.query; input.setSelectionRange(input.value.length, input.value.length); } // Restore scroll position window.scrollTo(0, scrollPosition); } function doRender() { render({ root, state, view, events: [ { selector: '#search-form', event: 'submit', handler: handleSubmit }, { selector: '#pokemon-query', event: 'input', handler: handleInput }, { selector: '#execute-script', event: 'click', handler: handleExecuteScript }, { selector: '#clear-script', event: 'click', handler: handleClearScript }, { selector: '#example-scripts', event: 'change', handler: handleLoadExample }, { selector: '#baba-yaga-script', event: 'input', handler: handleScriptInput }, ], postRender, }); } /** * Dispatches an action to update state and re-render. * * Why centralize dispatch? This enforces a single source of truth for state changes, making the app predictable and easy to debug. * * Why log actions/state in devMode? This provides a transparent, time-travel-like view of app logic without needing any extra tooling. */ function dispatch(action) { const prevState = state; state = update(state, action); if (devMode && dev && typeof dev.pushState === 'function') { dev.pushState(state); console.groupCollapsed(`Action: ${action.type}`); console.log('Payload:', action.payload); console.log('Prev state:', prevState); console.log('Next state:', state); console.groupEnd(); } // Only re-render for actions that actually change the UI const shouldRender = [ 'FETCH_SUCCESS', 'FETCH_ERROR', 'FETCH_EVOLUTION_SUCCESS', 'FETCH_EVOLUTION_ERROR', 'EXECUTE_SCRIPT_SUCCESS', 'EXECUTE_SCRIPT_ERROR', 'CLEAR_SCRIPT_OUTPUT', 'UPDATE_BABA_YAGA_SCRIPT' // Only when loading examples ].includes(action.type); if (shouldRender) { doRender(); } } /** * Handles input events by updating state without re-rendering. */ function handleInput(e) { // Update state directly without triggering re-render state.query = e.target.value; } /** * Handles script input events by updating state without re-rendering. */ function handleScriptInput(e) { // Update state directly without triggering re-render state.babaYagaScript = e.target.value; } /** * Handles form submission, triggers async fetch, and dispatches state updates. * * Why handle async here? Keeps update/view pure and centralizes side-effect. */ async function handleSubmit(e) { e.preventDefault(); if (!state.query.trim()) return; dispatch({ type: 'FETCH_START' }); try { const data = await fetchPokemon(state.query.trim()); dispatch({ type: 'FETCH_SUCCESS', payload: data }); // Automatically fetch evolution chain after successful Pokémon search try { dispatch({ type: 'FETCH_EVOLUTION_START' }); const evolutionData = await fetchEvolutionData(data.name); dispatch({ type: 'FETCH_EVOLUTION_SUCCESS', payload: evolutionData }); } catch (evolutionErr) { dispatch({ type: 'FETCH_EVOLUTION_ERROR', payload: evolutionErr.message }); } } catch (err) { dispatch({ type: 'FETCH_ERROR', payload: err.message }); } } /** * Handles Baba Yaga script execution. */ async function handleExecuteScript(e) { e.preventDefault(); if (!state.evolutionChain || !state.babaYagaScript.trim()) return; dispatch({ type: 'EXECUTE_SCRIPT_START' }); try { // state.evolutionChain contains the wrapper object, pass it directly const result = await executeBabaYagaScript(state.babaYagaScript, state.evolutionChain); dispatch({ type: 'EXECUTE_SCRIPT_SUCCESS', payload: result }); // Update dev mode with the harness instance for enhanced debugging console.log('[App] Checking dev mode integration:', { devMode, dev: !!dev, updateHarness: dev ? typeof dev.updateHarness : 'no dev', updateHarnessValue: dev ? dev.updateHarness : 'no dev' }); if (devMode && dev && typeof dev.updateHarness === 'function') { const currentHarness = getCurrentHarness(); console.log('[App] Script executed, current harness:', !!currentHarness); try { dev.updateHarness(currentHarness); console.log('[App] updateHarness called successfully'); } catch (error) { console.error('[App] Error calling updateHarness:', error); } } else { console.log('[App] Dev mode or updateHarness not available:', { devMode, dev: !!dev, updateHarness: dev ? typeof dev.updateHarness : false }); // Try to access the function directly from window.dev if (window.dev && typeof window.dev.updateHarness === 'function') { console.log('[App] Found updateHarness on window.dev, trying direct call'); const currentHarness = getCurrentHarness(); try { window.dev.updateHarness(currentHarness); console.log('[App] Direct updateHarness call successful'); } catch (error) { console.error('[App] Error in direct updateHarness call:', error); } } } } catch (err) { dispatch({ type: 'EXECUTE_SCRIPT_ERROR', payload: err.message }); } } /** * Handles script clearing. */ function handleClearScript(e) { e.preventDefault(); dispatch({ type: 'UPDATE_BABA_YAGA_SCRIPT', payload: '' }); dispatch({ type: 'CLEAR_SCRIPT_OUTPUT' }); } /** * Handles loading example scripts. */ function handleLoadExample(e) { const selectedExample = e.target.value; if (!selectedExample) return; const examples = getExampleScripts(); if (examples[selectedExample]) { // Update state and trigger re-render to show the example state.babaYagaScript = examples[selectedExample]; doRender(); } // Reset the select e.target.value = ''; } // Initialize dev mode before first render if (devMode) { dev = initDevMode({ getState: () => state, setState: s => { state = s; }, render: doRender, harness: null // Will be updated when harness is created }); } // Initial render doRender(); function updateHistoryInfo() { if (!devMode || !dev) return; dev.update(); } function setHistoryPointer(idx) { const info = dev.getHistoryInfo(); if (idx < 1 || idx > info.length) return; const newState = dev.setPointer(idx - 1); if (newState) { state = newState; doRender(); updateHistoryInfo(); } }