feature/react-bare #1

Merged
manuel merged 2 commits from feature/react-bare into main 2023-04-15 23:15:56 +02:00
8 changed files with 190 additions and 3696 deletions
Showing only changes of commit fee75ff8a7 - Show all commits

View file

@ -1,16 +1,21 @@
import React from 'react';
import React, {useState} from 'react';
import Maze from "./Maze";
import {maze1, maze2, maze3} from "./testdata";
import InputForm from "./InputForm";
export default function Square() {
const mazes = [{grid: [[]]}, maze1, maze2, maze3];
const renderedMazes = mazes.map(maze => (<div>
<h1>The Maze ({maze.width}x{maze.height}).</h1>
<Maze labyrinth={maze}/>
</div>))
export default function App() {
const [maze, setMaze] = useState({});
let title;
if (!!maze.grid) {
title = <h1>The Maze ({maze.width}x{maze.height})</h1>;
} else {
title = <span/>;
}
return (
<div>
{renderedMazes}
<InputForm handleResult={setMaze}/>
{title}
<Maze labyrinth={maze}
showSolution={true}/>
</div>
);
}

View file

@ -1,15 +1,14 @@
import React from 'react';
export default function Cell({spec, rowIndex, cellIndex}) {
export default function Cell({spec, rowIndex, cellIndex, showSolution}) {
let classes = "cell r" + rowIndex + " c" + cellIndex;
if (spec.top) classes += " top";
if (spec.right) classes += " right";
if (spec.bottom) classes += " bottom";
if (spec.left) classes += " left";
if (spec.solution) classes += " solution";
if (spec.solution && showSolution) classes += " solution";
if (spec.user) classes += " user";
return (
<div className={classes}>
</div>
<div className={classes}/>
);
}

111
src/InputForm.js Normal file
View file

@ -0,0 +1,111 @@
import React, {useState} from 'react';
import ValidatingInputNumberField from "./ValidatingInputNumberField";
export default function InputForm({handleResult}) {
const [width, setWidth] = useState(2);
const [height, setHeight] = useState(2);
const [id, setId] = useState(null);
const [status, setStatus] = useState("typing"); // "typing", "submitting"
if (status === "submitted") {
return <span/>;
}
const callAPI = () => {
let url = "https://manuel.friedli.info/labyrinth/create/json?w=" + width +
"&h=" + height;
if (!!id) {
url += "&id=" + id;
}
fetch(url)
.then(response => response.json())
.then(result => {
handleResult(result);
// FIXME doesn't update the contents of the text input field.
setId(_ => result.id);
})
.catch(reason => {
console.error("Failed to fetch maze data.", reason);
// FIXME alert is not user friendly
alert("Failed to fetch maze data: " + reason);
})
.finally(() => {
setStatus("typing");
});
};
const handleSubmit = (e) => {
e.preventDefault();
setStatus("submitting");
callAPI();
};
const validateWidthHeightInput = value => {
if (isNaN(value) || "" === value || (Math.floor(value) !== Number(value))) {
return {
valid: false,
message: "Must be an integer greater than 1.",
value
};
}
if (value < 1) {
return {
valid: false,
message: "Must be greater than 1.",
value
};
}
return {
valid: true,
value
};
};
const validateIdInput = 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))))) {
return {
valid: false,
message: "Must be empty or an integer",
value
};
}
return {
valid: true,
value
}
};
return (
<form onSubmit={handleSubmit}>
<ValidatingInputNumberField id={"width"}
label={"Width"}
defaultValue={width}
constraints={{
min: 2
}}
validatorFn={validateWidthHeightInput}
disabled={status === "submitting"}
onChange={setWidth}
/><br/>
<ValidatingInputNumberField id={"height"}
label={"Height"}
defaultValue={height}
constraints={{
min: 2
}}
validatorFn={validateWidthHeightInput}
disabled={status === "submitting"}
onChange={setHeight}
/><br/>
<ValidatingInputNumberField id={"id"}
label={"ID (optional)"}
defaultValue={id}
validatorFn={validateIdInput}
disabled={status === "submitting"}
onChange={setId}
/><br/>
<button type={"submit"}
disabled={status === "submitting"
|| isNaN(width)
|| isNaN(height)
}>GO!
</button>
</form>
);
}

View file

@ -2,9 +2,14 @@ import React from 'react';
import Row from "./Row";
export default function Maze({labyrinth}) {
export default function Maze({labyrinth, showSolution = false}) {
if (!labyrinth.grid) {
return <div>No valid maze.</div>
}
const maze = labyrinth.grid.map((row, rowIdx) => <Row key={"r" + rowIdx} spec={row}
index={rowIdx}/>);
index={rowIdx}
showSolution={showSolution}/>);
return (
<div className={"maze"}>
{maze}

View file

@ -1,11 +1,12 @@
import React from 'react';
import Cell from "./Cell";
export default function Row({spec, index}) {
export default function Row({spec, index, showSolution}) {
const cells = spec.map((cell, cellIdx) => <Cell key={"c" + index + "-" + cellIdx}
spec={cell}
rowIndex={index}
cellIndex={cellIdx}/>)
cellIndex={cellIdx}
showSolution={showSolution}/>)
return (
<div className={"row"}>
{cells}

View file

@ -0,0 +1,49 @@
import React, {useState} from 'react';
export default function ValidatingInputNumberField({
id,
label,
defaultValue = 0,
constraints = {},
validatorFn = (value) => {
return {valid: true, value};
},
disabled = false,
onChange = _ => {
}
}) {
const [error, setError] = useState(null);
const [value, setValue] = useState(defaultValue);
const handleValueChange = (e) => {
const value = e.target.value;
const validation = validatorFn(value);
if (!validation.valid) {
setError(validation.message);
} else {
setError(null);
onChange(validation.value);
}
setValue(validation.value);
};
let errorComponent;
if (!!error) {
errorComponent = <span>{error}</span>;
} else {
errorComponent = <span/>;
}
return (
<span>
<label htmlFor={id}>{label}: </label>
<input id={id}
type={"number"}
onChange={handleValueChange}
value={value || ""}
min={constraints.min || null}
max={constraints.max || null}
disabled={disabled}
/>
{errorComponent}
</span>
);
}

View file

@ -42,6 +42,9 @@ div.cell.left {
border-left-color: #000;
}
input:invalid {
border-color: #f00;
}
body {
font-family: sans-serif;
margin: 20px;

File diff suppressed because it is too large Load diff