about summary refs log tree commit diff stats
path: root/js/seed/src/app.js
diff options
context:
space:
mode:
Diffstat (limited to 'js/seed/src/app.js')
-rw-r--r--js/seed/src/app.js114
1 files changed, 86 insertions, 28 deletions
diff --git a/js/seed/src/app.js b/js/seed/src/app.js
index c345ef7..34b4579 100644
--- a/js/seed/src/app.js
+++ b/js/seed/src/app.js
@@ -1,80 +1,105 @@
 // app.js
-// Entrypoint for the PokéAPI FRP/TEA app
+// Entrypoint for the app
 
 import { initialState, cloneState } from './state.js';
 import { update } from './update.js';
 import { view } from './view.js';
 import { fetchPokemon } from './api.js';
+import { initDevMode } from './dev.js';
 
 const root = document.getElementById('app');
 let state = cloneState(initialState);
+let dev;
 
 /**
- * Entrypoint for the PokéAPI FRP/TEA app.
+ * 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 via a pure update function.
+ * - 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 maximal portability and clarity.
+ * - No 3rd party code: everything is browser-native for cozy portability and clarity.
  *
  * Why this approach?
- * - Functional, pure update/view logic is easy to reason about and test.
+ * - 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 prototypes.
+ * - 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 developers understand and debug the app without extra tooling.
+ * 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');
 
 /**
- * Renders the app UI and wires up event handlers.
- *
- * Why re-render the whole UI? This ensures the DOM always matches the state, avoiding subtle bugs from incremental updates.
+ * Generalized render function for Elm-style apps.
  *
- * Why focus management? Accessibility: focusing error messages ensures screen readers announce them, and focusing the input improves keyboard UX.
+ * @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() {
+function render({ root, state, view, events = [], postRender }) {
   root.innerHTML = view(state);
-  const form = root.querySelector('#search-form');
+  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 }) {
   const input = root.querySelector('#pokemon-query');
   const error = root.querySelector('.error');
-  if (form) {
-    form.addEventListener('submit', handleSubmit);
-    input.addEventListener('input', handleInput);
-    // Focus management for accessibility
-    if (error) {
-      error.focus();
-    } else {
-      input.focus();
-      input.value = state.query;
-      input.setSelectionRange(input.value.length, input.value.length);
-    }
+  if (error) {
+    error.focus();
+  } else if (input) {
+    input.focus();
+    input.value = state.query;
+    input.setSelectionRange(input.value.length, input.value.length);
   }
 }
 
+function doRender() {
+  render({
+    root,
+    state,
+    view,
+    events: [
+      { selector: '#search-form', event: 'submit', handler: handleSubmit },
+      { selector: '#pokemon-query', event: 'input', handler: handleInput },
+    ],
+    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 for free.
+ * 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.pushState(state);
     console.groupCollapsed(`Action: ${action.type}`);
     console.log('Payload:', action.payload);
     console.log('Prev state:', prevState);
     console.log('Next state:', state);
     console.groupEnd();
   }
-  render();
+  doRender();
 }
 
 /**
@@ -89,7 +114,7 @@ function handleInput(e) {
 /**
  * Handles form submission, triggers async fetch, and dispatches state updates.
  *
- * Why handle async here? Keeps update/view pure and side-effect free, following functional programming best practices.
+ * Why handle async here? Keeps update/view pure and centralizes side-effect.
  */
 async function handleSubmit(e) {
   e.preventDefault();
@@ -104,4 +129,37 @@ async function handleSubmit(e) {
 }
 
 // Initial render
-render(); 
\ No newline at end of file
+doRender();
+
+// After devMode is set
+if (devMode) {
+  dev = initDevMode({
+    getState: () => state,
+    setState: s => { state = s; },
+    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();
+  }
+}
+
+function handleSliderChange(e) {
+  setHistoryPointer(Number(e.target.value));
+}
+
+function handleStepperChange(e) {
+  setHistoryPointer(Number(e.target.value));
+} 
\ No newline at end of file