about summary refs log tree commit diff stats
path: root/js/seed/src
diff options
context:
space:
mode:
Diffstat (limited to 'js/seed/src')
-rw-r--r--js/seed/src/api.js2
-rw-r--r--js/seed/src/app.js114
-rw-r--r--js/seed/src/dev.js74
-rw-r--r--js/seed/src/update.js2
-rw-r--r--js/seed/src/view.js21
5 files changed, 174 insertions, 39 deletions
diff --git a/js/seed/src/api.js b/js/seed/src/api.js
index 6cd862b..d50c644 100644
--- a/js/seed/src/api.js
+++ b/js/seed/src/api.js
@@ -1,5 +1,5 @@
 // api.js
-// PokéAPI fetch logic
+// API fetch logic
 
 /**
  * Fetch a Pokémon by name from the PokéAPI
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
diff --git a/js/seed/src/dev.js b/js/seed/src/dev.js
new file mode 100644
index 0000000..ee1a6e7
--- /dev/null
+++ b/js/seed/src/dev.js
@@ -0,0 +1,74 @@
+// devMode.js
+// Minimal, single-file dev mode with scriptable console API
+
+/**
+ * Initialize dev mode: exposes a scriptable API for stepping through state history.
+ * @param {object} opts
+ * @param {function} opts.getState - returns current app state
+ * @param {function} opts.setState - sets app state
+ * @param {function} opts.render - triggers app re-render
+ */
+export function initDevMode({ getState, setState, render }) {
+  let history = [];
+  let pointer = -1;
+  let firstLoad = true;
+
+  function pushState(state) {
+    if (pointer < history.length - 1) history = history.slice(0, pointer + 1);
+    history.push(clone(state));
+    pointer = history.length - 1;
+    logInstructions();
+  }
+  function goTo(idx) {
+    if (idx < 0 || idx >= history.length) return;
+    pointer = idx;
+    setState(clone(history[pointer]));
+    render();
+    logInstructions();
+  }
+  function next() {
+    if (pointer < history.length - 1) goTo(pointer + 1);
+  }
+  function prev() {
+    if (pointer > 0) goTo(pointer - 1);
+  }
+  function get() {
+    return history[pointer];
+  }
+  function clone(obj) {
+    return JSON.parse(JSON.stringify(obj));
+  }
+  function table(obj) {
+    console.table(dev.history);
+  }
+  function logInstructions() {
+    if (firstLoad) {
+      console.log('[DevMode] State history debugger');
+      console.log('Usage:');
+      console.log('- dev.next()   // step forward');
+      console.log('- dev.prev()   // step backward');
+      console.log('- dev.goTo(n)  // jump to state n (1-based)');
+      console.log('- dev.get()    // get current state');
+      console.log('- dev.table()  // display history as a table');
+      console.log('- dev.history  // array of all states');
+      console.log('- dev.pointer  // current pointer (0-based)');
+      firstLoad = false;
+    }
+  }
+
+  // Expose API globally for console use
+  window.dev = {
+    next,
+    prev,
+    goTo,
+    get,
+    table,
+    get pointer() { return pointer; },
+    get history() { return history.slice(); },
+  };
+
+  // Initial state
+  pushState(getState());
+
+  return { pushState };
+} 
\ No newline at end of file
diff --git a/js/seed/src/update.js b/js/seed/src/update.js
index cd2b892..2f6c13b 100644
--- a/js/seed/src/update.js
+++ b/js/seed/src/update.js
@@ -1,5 +1,5 @@
 // update.js
-// Pure update function for the app
+// Pure update function
 
 /**
  * @param {object} state - Current state
diff --git a/js/seed/src/view.js b/js/seed/src/view.js
index 56467cb..5feef6e 100644
--- a/js/seed/src/view.js
+++ b/js/seed/src/view.js
@@ -1,8 +1,8 @@
 // view.js
-// Pure view functions for the PokéAPI app
+// Pure view functions
 
 /**
- * Pure view functions for the PokéAPI app.
+ * Pure view functions for the application.
  *
  * Why pure functions returning HTML strings?
  * - Keeps rendering logic stateless and easy to test.
@@ -22,26 +22,29 @@
  */
 export function view(state) {
   return `
-    <main role="main">
+    <h1>PokéDex</h1>
+    <container>
       <form id="search-form" autocomplete="off">
-        <label for="pokemon-query">Pokémon Name</label>
+        <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) : ''}
-    </main>
+    </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" />
-      <div><strong>${capitalize(pokemon.name)}</strong> (#${pokemon.id})</div>
-      <div>Type: ${pokemon.types.map(t => escape(t.type.name)).join(', ')}</div>
-      <div>Height: ${pokemon.height / 10} m</div>
-      <div>Weight: ${pokemon.weight / 10} kg</div>
+      <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>
   `;
 }
ef='#n719'>719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880