feature/react-bare #1
8 changed files with 190 additions and 3696 deletions
23
src/App.js
23
src/App.js
|
@ -1,16 +1,21 @@
|
||||||
import React from 'react';
|
import React, {useState} from 'react';
|
||||||
import Maze from "./Maze";
|
import Maze from "./Maze";
|
||||||
import {maze1, maze2, maze3} from "./testdata";
|
import InputForm from "./InputForm";
|
||||||
|
|
||||||
export default function Square() {
|
export default function App() {
|
||||||
const mazes = [{grid: [[]]}, maze1, maze2, maze3];
|
const [maze, setMaze] = useState({});
|
||||||
const renderedMazes = mazes.map(maze => (<div>
|
let title;
|
||||||
<h1>The Maze ({maze.width}x{maze.height}).</h1>
|
if (!!maze.grid) {
|
||||||
<Maze labyrinth={maze}/>
|
title = <h1>The Maze ({maze.width}x{maze.height})</h1>;
|
||||||
</div>))
|
} else {
|
||||||
|
title = <span/>;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{renderedMazes}
|
<InputForm handleResult={setMaze}/>
|
||||||
|
{title}
|
||||||
|
<Maze labyrinth={maze}
|
||||||
|
showSolution={true}/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
import React from 'react';
|
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;
|
let classes = "cell r" + rowIndex + " c" + cellIndex;
|
||||||
if (spec.top) classes += " top";
|
if (spec.top) classes += " top";
|
||||||
if (spec.right) classes += " right";
|
if (spec.right) classes += " right";
|
||||||
if (spec.bottom) classes += " bottom";
|
if (spec.bottom) classes += " bottom";
|
||||||
if (spec.left) classes += " left";
|
if (spec.left) classes += " left";
|
||||||
if (spec.solution) classes += " solution";
|
if (spec.solution && showSolution) classes += " solution";
|
||||||
if (spec.user) classes += " user";
|
if (spec.user) classes += " user";
|
||||||
return (
|
return (
|
||||||
<div className={classes}>
|
<div className={classes}/>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
111
src/InputForm.js
Normal file
111
src/InputForm.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -2,9 +2,14 @@ import React from 'react';
|
||||||
import Row from "./Row";
|
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}
|
const maze = labyrinth.grid.map((row, rowIdx) => <Row key={"r" + rowIdx} spec={row}
|
||||||
index={rowIdx}/>);
|
index={rowIdx}
|
||||||
|
showSolution={showSolution}/>);
|
||||||
return (
|
return (
|
||||||
<div className={"maze"}>
|
<div className={"maze"}>
|
||||||
{maze}
|
{maze}
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Cell from "./Cell";
|
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}
|
const cells = spec.map((cell, cellIdx) => <Cell key={"c" + index + "-" + cellIdx}
|
||||||
spec={cell}
|
spec={cell}
|
||||||
rowIndex={index}
|
rowIndex={index}
|
||||||
cellIndex={cellIdx}/>)
|
cellIndex={cellIdx}
|
||||||
|
showSolution={showSolution}/>)
|
||||||
return (
|
return (
|
||||||
<div className={"row"}>
|
<div className={"row"}>
|
||||||
{cells}
|
{cells}
|
||||||
|
|
49
src/ValidatingInputNumberField.js
Normal file
49
src/ValidatingInputNumberField.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -42,6 +42,9 @@ div.cell.left {
|
||||||
border-left-color: #000;
|
border-left-color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input:invalid {
|
||||||
|
border-color: #f00;
|
||||||
|
}
|
||||||
body {
|
body {
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
margin: 20px;
|
margin: 20px;
|
||||||
|
|
3679
src/testdata.js
3679
src/testdata.js
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue