about summary refs log tree commit diff stats
path: root/js/scripting-lang/web/src/app.js
diff options
context:
space:
mode:
Diffstat (limited to 'js/scripting-lang/web/src/app.js')
-rw-r--r--js/scripting-lang/web/src/app.js286
1 files changed, 286 insertions, 0 deletions
diff --git a/js/scripting-lang/web/src/app.js b/js/scripting-lang/web/src/app.js
new file mode 100644
index 0000000..086cba1
--- /dev/null
+++ b/js/scripting-lang/web/src/app.js
@@ -0,0 +1,286 @@
+// 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();
+  }
+}