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…
	
	Add table
		Add a link
		
	
		Reference in a new issue