Fix linting and build errors.

This commit is contained in:
Manuel Friedli 2025-01-08 21:30:02 +01:00
parent d8d05589e7
commit 36e1ea45d5
Signed by: manuel
GPG key ID: 41D08ABA75634DA1
9 changed files with 94 additions and 45 deletions

View file

@ -1,15 +1,23 @@
import {MazeCell} from "./model/maze.ts"; import {MazeCell} from "./model/maze.ts";
import Coordinates from "./model/coordinates.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 styles from "./cell.module.css";
import "./cell.css"; import "./cell.css";
import {State} from "./state/state.ts";
import {ActionDispatch} from "react";
function isMarked(x: number, y: number, marked: Coordinates[]): boolean { function isMarked(x: number, y: number, marked: Coordinates[]): boolean {
return !!marked.find(e => e.x === x && e.y === y); return !!marked.find(e => e.x === x && e.y === y);
} }
export default function Cell({x, y, state, dispatch}) { export default function Cell({x, y, state, dispatch}:
const cell: MazeCell = state.maze.grid[y][x]; {
x: number,
y: number,
state: State,
dispatch: ActionDispatch<[Action]>
}) {
const cell: MazeCell = state.maze!.grid[y][x];
let classes = " r" + y + " c" + x; let classes = " r" + y + " c" + x;
if (cell.top) classes += " top"; if (cell.top) classes += " top";
if (cell.right) classes += " right"; if (cell.right) classes += " right";
@ -26,7 +34,7 @@ export default function Cell({x, y, state, dispatch}) {
dispatch(actionClickedCell(x, y)); dispatch(actionClickedCell(x, y));
} }
}} }}
onClick={(e) => { onClick={() => {
dispatch(actionClickedCell(x, y)); dispatch(actionClickedCell(x, y));
}}> }}>
</div> </div>

View file

@ -1,16 +1,20 @@
import {useState} from 'react'; import {ActionDispatch, FormEvent, useState} from 'react';
import ValidatingInputNumberField from "./validating-input-number-field.tsx"; import ValidatingInputNumberField, {ValidatorFunction} from "./validating-input-number-field.tsx";
import {actionLoadedMaze, actionLoadingFailed, actionStartedLoading} from "./state/action.ts"; import {Action, actionLoadedMaze, actionLoadingFailed, actionStartedLoading} from "./state/action.ts";
import styles from "./input-form.module.css"; import styles from "./input-form.module.css";
import "./input-form.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 [width, setWidth] = useState(10);
const [height, setHeight] = 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 [algorithm, setAlgorithm] = useState('wilson');
const handleSubmit = (e) => { const handleSubmit = (e: FormEvent) => {
e.preventDefault(); e.preventDefault();
dispatch(actionStartedLoading()); dispatch(actionStartedLoading());
const url = `https://manuel.friedli.info/labyrinth/create/json?w=${width}&h=${height}&id=${id || ''}&algorithm=${algorithm}`; 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)); dispatch(actionLoadingFailed(reason));
}); });
}; };
const validateWidthHeightInput = value => { const validateWidthHeightInput: ValidatorFunction<string, number> = value => {
if (isNaN(value) || "" === value || (Math.floor(value) !== Number(value))) { const numberValue = Number(value);
if (isNaN(numberValue) || "" === value || (Math.floor(numberValue) !== numberValue)) {
return { return {
valid: false, valid: false,
message: "Must be an integer greater than 1.", message: "Must be an integer greater than 1."
value
}; };
} }
if (value < 1) { if (numberValue < 1) {
return { return {
valid: false, valid: false,
message: "Must be greater than 1.", message: "Must be greater than 1."
value
}; };
} }
return { return {
valid: true, 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"). // 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 { return {
valid: false, valid: false,
message: "Must be empty or an integer", message: "Must be empty or an integer"
value
}; };
} }
return { return {
valid: true, valid: true,
value value: numberValue
} }
}; };
return ( return (

View file

@ -1,14 +1,21 @@
import Cell from "./cell.tsx"; import Cell from "./cell.tsx";
import styles from "./maze.module.css"; 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) { if (!state.maze) {
return <div>No valid maze.</div> return <div>No valid maze.</div>
} }
let maze: JSX.Element[] = []; const maze: JSX.Element[] = [];
for (let y = 0; y < state.maze.height; y++) { 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++) { for (let x = 0; x < state.maze.width; x++) {
row.push(<Cell key={`${x}x${y}`} x={x} y={y} state={state} dispatch={dispatch}/>) row.push(<Cell key={`${x}x${y}`} x={x} y={y} state={state} dispatch={dispatch}/>)
} }

View file

@ -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 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() { function handleClose() {
dispatch(actionClosedMessageBanner()); dispatch(actionClosedMessageBanner());
} }

View file

@ -22,8 +22,8 @@ export default function Home() {
dispatch={dispatch}/> dispatch={dispatch}/>
{hasValidMaze && {hasValidMaze &&
<> <>
<h1>The Maze ({state.maze.width}x{state.maze.height}, Algorithm: {state.maze.algorithm}, <h1>The Maze ({state.maze!.width}x{state.maze!.height}, Algorithm: {state.maze!.algorithm},
ID: {state.maze.id})</h1> ID: {state.maze!.id})</h1>
<input type={"checkbox"} <input type={"checkbox"}
onChange={(e) => { onChange={(e) => {
dispatch(actionToggledShowSolution(e.target.checked)); dispatch(actionToggledShowSolution(e.target.checked));

View file

@ -3,7 +3,7 @@ import Maze from "../model/maze.ts";
export interface Action { export interface Action {
type: string, type: string,
[key: string]: any [key: string]: boolean | number | string | object | null | undefined;
} }
export const ID_ACTION_STARTED_LOADING = 'started_loading'; export const ID_ACTION_STARTED_LOADING = 'started_loading';

View file

@ -9,6 +9,7 @@ import {
ID_ACTION_STARTED_LOADING, ID_ACTION_STARTED_LOADING,
ID_ACTION_TOGGLED_SHOW_SOLUTION ID_ACTION_TOGGLED_SHOW_SOLUTION
} from "./action.ts"; } from "./action.ts";
import Maze from "@/app/model/maze.ts";
export default function reduce(state: State, action: Action): State { export default function reduce(state: State, action: Action): State {
switch (action.type) { switch (action.type) {
@ -24,7 +25,7 @@ export default function reduce(state: State, action: Action): State {
return { return {
...state, ...state,
loading: false, loading: false,
maze: action.maze, maze: action.maze as Maze,
userPath: [] userPath: []
} }
} }
@ -38,7 +39,7 @@ export default function reduce(state: State, action: Action): State {
case ID_ACTION_TOGGLED_SHOW_SOLUTION: { case ID_ACTION_TOGGLED_SHOW_SOLUTION: {
return { return {
...state, ...state,
showSolution: action.value showSolution: action.value as boolean
} }
} }
case ID_ACTION_CLOSED_MESSAGE_BANNER: { case ID_ACTION_CLOSED_MESSAGE_BANNER: {
@ -49,7 +50,7 @@ export default function reduce(state: State, action: Action): State {
} }
case ID_ACTION_CLICKED_CELL: { case ID_ACTION_CLICKED_CELL: {
// There's so much logic involved, externalize that into its own file. // 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: { default: {
throw new Error(`Unknown action: ${action.type}`); throw new Error(`Unknown action: ${action.type}`);

View file

@ -4,7 +4,7 @@ import {MazeCell} from "../model/maze.ts";
export default function handleUserClicked(state: State, x: number, y: number): State { export default function handleUserClicked(state: State, x: number, y: number): State {
if (isClickAllowed(x, y, 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) // 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. // and that's not blocked by a wall. Now let's see.
if (-1 === state.userPath.findIndex(step => step.x === x && step.y === y)) { if (-1 === state.userPath.findIndex(step => step.x === x && step.y === y)) {

View file

@ -1,28 +1,39 @@
import React, {useState} from 'react'; import React, {ChangeEventHandler, useState} from 'react';
export default function ValidatingInputNumberField({ export default function ValidatingInputNumberField({
id, id,
label, label,
value = 0, value = 0,
constraints = {}, constraints = undefined,
validatorFn = (value) => { validatorFn = (value) => {
return {valid: true, value}; return {valid: true, value: Number(value)};
}, },
disabled = false, 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 value = e.target.value;
const validation = validatorFn(value); const validation = validatorFn(value);
if (!validation.valid) { if (!validation.valid) {
setError(validation.message); setError(validation.message);
} else { } else {
setError(null); setError(undefined);
} }
if (validation.value) {
onChange(validation.value); onChange(validation.value);
}
}; };
return ( return (
<> <>
@ -31,11 +42,19 @@ export default function ValidatingInputNumberField({
type={"number"} type={"number"}
onChange={handleValueChange} onChange={handleValueChange}
value={value || ""} value={value || ""}
min={constraints.min || null} min={constraints?.min}
max={constraints.max || null} max={constraints?.max}
disabled={disabled} disabled={disabled}
/> />
<span>{error}</span> <span>{error}</span>
</> </>
); );
} }
export interface Validation<T> {
valid: boolean;
message?: string;
value?: T;
}
export type ValidatorFunction<I, T> = (v: I) => Validation<T>;