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.js165
1 files changed, 165 insertions, 0 deletions
diff --git a/js/seed/src/app.js b/js/seed/src/app.js
new file mode 100644
index 0000000..34b4579
--- /dev/null
+++ b/js/seed/src/app.js
@@ -0,0 +1,165 @@
+// app.js
+// 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 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 }) {
+  const input = root.querySelector('#pokemon-query');
+  const error = root.querySelector('.error');
+  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 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();
+  }
+  doRender();
+}
+
+/**
+ * Handles input events by dispatching an update action.
+ *
+ * Why not update state directly? All state changes go through dispatch/update for consistency and traceability.
+ */
+function handleInput(e) {
+  dispatch({ type: 'UPDATE_QUERY', payload: 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 });
+  } catch (err) {
+    dispatch({ type: 'FETCH_ERROR', payload: err.message });
+  }
+}
+
+// Initial render
+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