From 36e1ea45d5e4cab2739c6cd09474b24413254862 Mon Sep 17 00:00:00 2001
From: Manuel Friedli <manuel@fritteli.ch>
Date: Wed, 8 Jan 2025 21:30:02 +0100
Subject: [PATCH] Fix linting and build errors.

---
 src/app/cell.tsx                          | 16 ++++++--
 src/app/input-form.tsx                    | 46 +++++++++++++----------
 src/app/maze.tsx                          | 13 +++++--
 src/app/message-banner.tsx                | 10 ++++-
 src/app/page.tsx                          |  4 +-
 src/app/state/action.ts                   |  2 +-
 src/app/state/reducer.ts                  |  7 ++--
 src/app/state/userpathhandler.ts          |  2 +-
 src/app/validating-input-number-field.tsx | 39 ++++++++++++++-----
 9 files changed, 94 insertions(+), 45 deletions(-)

diff --git a/src/app/cell.tsx b/src/app/cell.tsx
index 3a68019..22294a9 100644
--- a/src/app/cell.tsx
+++ b/src/app/cell.tsx
@@ -1,15 +1,23 @@
 import {MazeCell} from "./model/maze.ts";
 import Coordinates from "./model/coordinates.ts";
-import {actionClickedCell} from "./state/action.ts";
+import {Action, actionClickedCell} from "./state/action.ts";
 import styles from "./cell.module.css";
 import "./cell.css";
+import {State} from "./state/state.ts";
+import {ActionDispatch} from "react";
 
 function isMarked(x: number, y: number, marked: Coordinates[]): boolean {
     return !!marked.find(e => e.x === x && e.y === y);
 }
 
-export default function Cell({x, y, state, dispatch}) {
-    const cell: MazeCell = state.maze.grid[y][x];
+export default function Cell({x, y, state, dispatch}:
+                             {
+                                 x: number,
+                                 y: number,
+                                 state: State,
+                                 dispatch: ActionDispatch<[Action]>
+                             }) {
+    const cell: MazeCell = state.maze!.grid[y][x];
     let classes = " r" + y + " c" + x;
     if (cell.top) classes += " top";
     if (cell.right) classes += " right";
@@ -26,7 +34,7 @@ export default function Cell({x, y, state, dispatch}) {
                      dispatch(actionClickedCell(x, y));
                  }
              }}
-             onClick={(e) => {
+             onClick={() => {
                  dispatch(actionClickedCell(x, y));
              }}>
         </div>
diff --git a/src/app/input-form.tsx b/src/app/input-form.tsx
index e2d30ef..af11612 100644
--- a/src/app/input-form.tsx
+++ b/src/app/input-form.tsx
@@ -1,16 +1,20 @@
-import {useState} from 'react';
-import ValidatingInputNumberField from "./validating-input-number-field.tsx";
-import {actionLoadedMaze, actionLoadingFailed, actionStartedLoading} from "./state/action.ts";
+import {ActionDispatch, FormEvent, useState} from 'react';
+import ValidatingInputNumberField, {ValidatorFunction} from "./validating-input-number-field.tsx";
+import {Action, actionLoadedMaze, actionLoadingFailed, actionStartedLoading} from "./state/action.ts";
 import styles from "./input-form.module.css";
 import "./input-form.css";
+import {State} from "@/app/state/state.ts";
 
-export default function InputForm({state, dispatch}) {
+export default function InputForm({state, dispatch}: {
+    state: State,
+    dispatch: ActionDispatch<[Action]>
+}) {
     const [width, setWidth] = useState(10);
     const [height, setHeight] = useState(10);
-    const [id, setId] = useState(null as number);
+    const [id, setId] = useState<number>();
     const [algorithm, setAlgorithm] = useState('wilson');
 
-    const handleSubmit = (e) => {
+    const handleSubmit = (e: FormEvent) => {
         e.preventDefault();
         dispatch(actionStartedLoading());
         const url = `https://manuel.friedli.info/labyrinth/create/json?w=${width}&h=${height}&id=${id || ''}&algorithm=${algorithm}`;
@@ -25,38 +29,42 @@ export default function InputForm({state, dispatch}) {
                 dispatch(actionLoadingFailed(reason));
             });
     };
-    const validateWidthHeightInput = value => {
-        if (isNaN(value) || "" === value || (Math.floor(value) !== Number(value))) {
+    const validateWidthHeightInput: ValidatorFunction<string, number> = value => {
+        const numberValue = Number(value);
+        if (isNaN(numberValue) || "" === value || (Math.floor(numberValue) !== numberValue)) {
             return {
                 valid: false,
-                message: "Must be an integer greater than 1.",
-                value
+                message: "Must be an integer greater than 1."
             };
         }
-        if (value < 1) {
+        if (numberValue < 1) {
             return {
                 valid: false,
-                message: "Must be greater than 1.",
-                value
+                message: "Must be greater than 1."
             };
         }
         return {
             valid: true,
-            value
+            value: numberValue
         };
     };
-    const validateIdInput = value => {
+    const validateIdInput: ValidatorFunction<string, number> = value => {
+        if ("" === value) {
+            return {
+                valid: true
+            };
+        }
+        const numberValue = Number(value);
         // FIXME doesn't handle strings with characters correctly (e.g. "asdf" yields an empty value, due to "type=number").
-        if (isNaN(value) || ("" !== value && ((Math.floor(value) !== Number(value))))) {
+        if (isNaN(numberValue) || Math.floor(numberValue) !== numberValue) {
             return {
                 valid: false,
-                message: "Must be empty or an integer",
-                value
+                message: "Must be empty or an integer"
             };
         }
         return {
             valid: true,
-            value
+            value: numberValue
         }
     };
     return (
diff --git a/src/app/maze.tsx b/src/app/maze.tsx
index 5f55d8b..bab0423 100644
--- a/src/app/maze.tsx
+++ b/src/app/maze.tsx
@@ -1,14 +1,21 @@
 import Cell from "./cell.tsx";
 import styles from "./maze.module.css";
+import {State} from "@/app/state/state.ts";
+import {ActionDispatch, JSX} from "react";
+import {Action} from "@/app/state/action.ts";
 
-export default function Maze({state, dispatch}) {
+export default function Maze({state, dispatch}:
+                             {
+                                 state: State,
+                                 dispatch: ActionDispatch<[Action]>
+                             }) {
     if (!state.maze) {
         return <div>No valid maze.</div>
     }
 
-    let maze: JSX.Element[] = [];
+    const maze: JSX.Element[] = [];
     for (let y = 0; y < state.maze.height; y++) {
-        let row: JSX.Element[] = [];
+        const row: JSX.Element[] = [];
         for (let x = 0; x < state.maze.width; x++) {
             row.push(<Cell key={`${x}x${y}`} x={x} y={y} state={state} dispatch={dispatch}/>)
         }
diff --git a/src/app/message-banner.tsx b/src/app/message-banner.tsx
index 92fdd22..a33eaf8 100644
--- a/src/app/message-banner.tsx
+++ b/src/app/message-banner.tsx
@@ -1,7 +1,13 @@
-import {actionClosedMessageBanner} from "./state/action.ts";
+import {Action, actionClosedMessageBanner} from "./state/action.ts";
 import styles from "./message-banner.module.css";
+import {State} from "@/app/state/state.ts";
+import {ActionDispatch} from "react";
 
-export default function MessageBanner({state, dispatch}) {
+export default function MessageBanner({state, dispatch}:
+                                      {
+                                          state: State;
+                                          dispatch: ActionDispatch<[Action]>
+                                      }) {
     function handleClose() {
         dispatch(actionClosedMessageBanner());
     }
diff --git a/src/app/page.tsx b/src/app/page.tsx
index fab1311..a03b3a8 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -22,8 +22,8 @@ export default function Home() {
                        dispatch={dispatch}/>
             {hasValidMaze &&
                 <>
-                    <h1>The Maze ({state.maze.width}x{state.maze.height}, Algorithm: {state.maze.algorithm},
-                        ID: {state.maze.id})</h1>
+                    <h1>The Maze ({state.maze!.width}x{state.maze!.height}, Algorithm: {state.maze!.algorithm},
+                        ID: {state.maze!.id})</h1>
                     <input type={"checkbox"}
                            onChange={(e) => {
                                dispatch(actionToggledShowSolution(e.target.checked));
diff --git a/src/app/state/action.ts b/src/app/state/action.ts
index 2c08b17..0d41231 100644
--- a/src/app/state/action.ts
+++ b/src/app/state/action.ts
@@ -3,7 +3,7 @@ import Maze from "../model/maze.ts";
 export interface Action {
     type: string,
 
-    [key: string]: any
+    [key: string]: boolean | number | string | object | null | undefined;
 }
 
 export const ID_ACTION_STARTED_LOADING = 'started_loading';
diff --git a/src/app/state/reducer.ts b/src/app/state/reducer.ts
index 04257a0..da97b26 100644
--- a/src/app/state/reducer.ts
+++ b/src/app/state/reducer.ts
@@ -9,6 +9,7 @@ import {
     ID_ACTION_STARTED_LOADING,
     ID_ACTION_TOGGLED_SHOW_SOLUTION
 } from "./action.ts";
+import Maze from "@/app/model/maze.ts";
 
 export default function reduce(state: State, action: Action): State {
     switch (action.type) {
@@ -24,7 +25,7 @@ export default function reduce(state: State, action: Action): State {
             return {
                 ...state,
                 loading: false,
-                maze: action.maze,
+                maze: action.maze as Maze,
                 userPath: []
             }
         }
@@ -38,7 +39,7 @@ export default function reduce(state: State, action: Action): State {
         case ID_ACTION_TOGGLED_SHOW_SOLUTION: {
             return {
                 ...state,
-                showSolution: action.value
+                showSolution: action.value as boolean
             }
         }
         case ID_ACTION_CLOSED_MESSAGE_BANNER: {
@@ -49,7 +50,7 @@ export default function reduce(state: State, action: Action): State {
         }
         case ID_ACTION_CLICKED_CELL: {
             // There's so much logic involved, externalize that into its own file.
-            return handleUserClicked(state, action.x, action.y);
+            return handleUserClicked(state, action.x as number, action.y as number);
         }
         default: {
             throw new Error(`Unknown action: ${action.type}`);
diff --git a/src/app/state/userpathhandler.ts b/src/app/state/userpathhandler.ts
index 56de445..e6c5f8d 100644
--- a/src/app/state/userpathhandler.ts
+++ b/src/app/state/userpathhandler.ts
@@ -4,7 +4,7 @@ import {MazeCell} from "../model/maze.ts";
 
 export default function handleUserClicked(state: State, x: number, y: number): State {
     if (isClickAllowed(x, y, state)) {
-        let maze = state.maze!;
+        const maze = state.maze!;
         // Okay, we clicked a cell that's adjacent to the end of the userpath (or which IS the end of the userpath)
         // and that's not blocked by a wall. Now let's see.
         if (-1 === state.userPath.findIndex(step => step.x === x && step.y === y)) {
diff --git a/src/app/validating-input-number-field.tsx b/src/app/validating-input-number-field.tsx
index 08842b1..4ef6a66 100644
--- a/src/app/validating-input-number-field.tsx
+++ b/src/app/validating-input-number-field.tsx
@@ -1,28 +1,39 @@
-import React, {useState} from 'react';
+import React, {ChangeEventHandler, useState} from 'react';
 
 export default function ValidatingInputNumberField({
                                                        id,
                                                        label,
                                                        value = 0,
-                                                       constraints = {},
+                                                       constraints = undefined,
                                                        validatorFn = (value) => {
-                                                           return {valid: true, value};
+                                                           return {valid: true, value: Number(value)};
                                                        },
                                                        disabled = false,
-                                                       onChange = _ => {
+                                                       onChange = () => {
                                                        }
+                                                   }:
+                                                   {
+                                                       id: string;
+                                                       label: string;
+                                                       value?: number;
+                                                       constraints?: { min?: number; max?: number; };
+                                                       validatorFn: ValidatorFunction<string, number>;
+                                                       disabled: boolean;
+                                                       onChange: (v: number) => void;
                                                    }) {
-    const [error, setError] = useState(null);
+    const [error, setError] = useState<string>();
 
-    const handleValueChange = (e) => {
+    const handleValueChange: ChangeEventHandler<HTMLInputElement> = (e) => {
         const value = e.target.value;
         const validation = validatorFn(value);
         if (!validation.valid) {
             setError(validation.message);
         } else {
-            setError(null);
+            setError(undefined);
+        }
+        if (validation.value) {
+            onChange(validation.value);
         }
-        onChange(validation.value);
     };
     return (
         <>
@@ -31,11 +42,19 @@ export default function ValidatingInputNumberField({
                    type={"number"}
                    onChange={handleValueChange}
                    value={value || ""}
-                   min={constraints.min || null}
-                   max={constraints.max || null}
+                   min={constraints?.min}
+                   max={constraints?.max}
                    disabled={disabled}
             />
             <span>{error}</span>
         </>
     );
 }
+
+export interface Validation<T> {
+    valid: boolean;
+    message?: string;
+    value?: T;
+}
+
+export type ValidatorFunction<I, T> = (v: I) => Validation<T>;