about summary refs log tree commit diff stats
path: root/js/seed
diff options
context:
space:
mode:
Diffstat (limited to 'js/seed')
-rw-r--r--js/seed/README.md56
-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
-rw-r--r--js/seed/style.css4
7 files changed, 232 insertions, 41 deletions
diff --git a/js/seed/README.md b/js/seed/README.md
index 7d0ecec..8159cb3 100644
--- a/js/seed/README.md
+++ b/js/seed/README.md
@@ -20,10 +20,66 @@ This is an opinionated, minimal starting point for browser-native web apps using
 - No dependencies
 - Encourages functional, declarative code
 
+## How to Extend and Use This Template
+
+This template is designed to be a flexible, opinionated starting point for any browser-native app.
+
+### Key Files to Extend
+- **src/state.js**: Define the app's state shape and any helper functions for cloning or initializing state.
+- **src/update.js**: Add new action/message types and update logic. This is where you handle all state transitions.
+- **src/view.js**: Build your UI as a pure function of state. Add new components or views here.
+- **src/api.js**: Add or replace API calls as needed for your app's data fetching.
+- **src/app.js**: Wire up events, use the generalized `render` function, and add any app-specific logic (e.g., focus management, custom event handling).
+
+### Using the Generalized `render` Function
+The `render` function in `app.js` is designed to be reusable for any app. It takes a config object:
+
+```js
+render({
+  root,                // DOM element to render into
+  state,               // Current app state
+  view,                // View function: (state) => html
+  events: [            // Array of event bindings
+    { selector, event, handler },
+    // ...
+  ],
+  postRender           // Optional: function({ root, state }) for custom logic (e.g., focus)
+});
+```
+
+#### Example: Adding a New Feature
+Suppose you want to add a button that increments a counter:
+
+1. **state.js**: Add `count` to your state.
+2. **update.js**: Handle an `INCREMENT` action.
+3. **view.js**: Add a button and display the count.
+4. **app.js**:
+   - Add an event handler for the button:
+     ```js
+     function handleIncrement() {
+       dispatch({ type: 'INCREMENT' });
+     }
+     ```
+   - Add to the `events` array:
+     ```js
+     events: [
+       { selector: '#increment-btn', event: 'click', handler: handleIncrement },
+       // ...other events
+     ]
+     ```
+
+### Tips
+- Keep all state transitions in `update.js` for predictability.
+- Keep all DOM rendering in `view.js` for clarity.
+- Use the `postRender` hook for accessibility or focus management.
+- Add new features by extending state, update, view, and wiring up events in `app.js`.
+
 ---
 
 Inspired by the [Elm Architecture](https://guide.elm-lang.org/architecture/), but using only browser APIs and ES modules. 
 
+---
+
 ## MIT License
 
 Copyright 2025 eli_oat
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>
   `;
 }
diff --git a/js/seed/style.css b/js/seed/style.css
index 1d70c7f..da35a7a 100644
--- a/js/seed/style.css
+++ b/js/seed/style.css
@@ -67,13 +67,13 @@ button:disabled {
   border: 1.5px solid var(--color-result-border);
   border-radius: 0.3em;
   background: var(--color-result-bg);
-  text-align: center;
 }
 .pokemon-sprite {
   width: 120px;
   height: 120px;
   object-fit: contain;
-  margin-bottom: 0.5em;
+  margin: 0 auto;
+  display: block;
 }
 .error {
   color: var(--color-error);