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 {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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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
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";
|
||||
|
||||
|
||||
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}
|
||||
|
|
|
@ -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}
|
||||
|
|
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;
|
||||
}
|
||||
|
||||
input:invalid {
|
||||
border-color: #f00;
|
||||
}
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
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